diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json
index 9d61f1493..5b4f7579f 100644
--- a/public/locales/fr-fr.json
+++ b/public/locales/fr-fr.json
@@ -1385,8 +1385,8 @@
"enable_functions_desc_1": "Autorise l'utilisation",
"enable_functions_desc_2": "outils de fonction",
"enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
- "Show model thoughts": "Afficher les pensées du modèle",
- "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
+ "Request model reasoning": "Demander les pensées du modèle",
+ "Allows the model to return its thinking process.": "Permet au modèle de retourner son processus de réflexion.",
"Confirm token parsing with": "Confirmer l'analyse des tokens avec",
"openai_logit_bias_no_items": "Aucun élément",
"api_no_connection": "Pas de connection...",
diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json
index acc33916e..2835406d5 100644
--- a/public/locales/zh-cn.json
+++ b/public/locales/zh-cn.json
@@ -266,8 +266,8 @@
"Use system prompt": "使用系统提示词",
"Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
"Merges_all_system_messages_desc_2": "字段发送。",
- "Show model thoughts": "展示思维链",
- "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
+ "Request model reasoning": "请求思维链",
+ "Allows the model to return its thinking process.": "允许模型返回其思维过程。",
"Assistant Prefill": "AI预填",
"Expand the editor": "展开编辑器",
"Start Claude's answer with...": "以如下内容开始Claude的回答...",
diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json
index fa3ee5b3a..c4d9939af 100644
--- a/public/locales/zh-tw.json
+++ b/public/locales/zh-tw.json
@@ -2356,8 +2356,8 @@
"Forbid": "禁止",
"Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
"Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
- "Show model thoughts": "顯示模型思維鏈",
- "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
+ "Request model reasoning": "請求模型思維鏈",
+ "Allows the model to return its thinking process.": "讓模型回傳其思考過程。",
"Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
"Model ID (optional)": "模型 ID(可選)",
"DeepSeek API Key": "DeepSeek API 金鑰",
diff --git a/public/script.js b/public/script.js
index dc3d299da..e6aada176 100644
--- a/public/script.js
+++ b/public/script.js
@@ -95,6 +95,7 @@ import {
resetMovableStyles,
forceCharacterEditorTokenize,
applyPowerUserSettings,
+ generatedTextFiltered,
} from './scripts/power-user.js';
import {
@@ -169,6 +170,7 @@ import {
toggleDrawer,
isElementInViewport,
copyText,
+ escapeHtml,
} from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js';
@@ -267,6 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
import { initBulkEdit } from './scripts/bulk-edit.js';
import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
import { getContext } from './scripts/st-context.js';
+import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
// API OBJECT FOR EXTERNAL WIRING
globalThis.SillyTavern = {
@@ -982,6 +985,7 @@ async function firstLoadInit() {
initServerHistory();
initSettingsSearch();
initBulkEdit();
+ initReasoning();
await initScrapers();
doDailyExtensionUpdatesCheck();
await hideLoader();
@@ -1991,14 +1995,15 @@ export async function sendTextareaMessage() {
* @param {boolean} isUser If the message was sent by the user
* @param {number} messageId Message index in chat array
* @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
+ * @param {boolean} [isReasoning] If the message is reasoning output
* @returns {string} HTML string
*/
-export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {
+export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
if (!mes) {
return '';
}
- if (Number(messageId) === 0 && !isSystem && !isUser) {
+ if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
const mesBeforeReplace = mes;
const chatMessage = chat[messageId];
mes = substituteParams(mes, undefined, ch_name);
@@ -2027,6 +2032,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
if (!isSystem) {
function getRegexPlacement() {
try {
+ if (isReasoning) {
+ return regex_placement.REASONING;
+ }
if (isUser) {
return regex_placement.USER_INPUT;
} else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2060,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
mes = mes.replaceAll('<', '<').replaceAll('>', '>');
}
+ // Make sure reasoning strings are always shown, even if they include "<" or ">"
+ [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
+ if (!reasoningString || !reasoningString.trim().length) {
+ return;
+ }
+ // Only replace the first occurrence of the reasoning string
+ if (mes.includes(reasoningString)) {
+ mes = mes.replace(reasoningString, escapeHtml(reasoningString));
+ }
+ });
+
if (!isSystem) {
// Save double quotes in tags as a special character to prevent them from being encoded
if (!power_user.encode_tags) {
@@ -2200,6 +2219,7 @@ function getMessageFromTemplate({
isUser,
avatarImg,
bias,
+ reasoning,
isSystem,
title,
timerValue,
@@ -2224,6 +2244,7 @@ function getMessageFromTemplate({
mes.find('.avatar img').attr('src', avatarImg);
mes.find('.ch_name .name_text').text(characterName);
mes.find('.mes_bias').html(bias);
+ mes.find('.mes_reasoning').html(reasoning);
mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
mes.find('.mesIDDisplay').text(`#${mesId}`);
tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2238,10 +2259,16 @@ function getMessageFromTemplate({
return mes;
}
+/**
+ * Re-renders a message block with updated content.
+ * @param {number} messageId Message ID
+ * @param {object} message Message object
+ */
export function updateMessageBlock(messageId, message) {
const messageElement = $(`#chat [mesid="${messageId}"]`);
const text = message?.extra?.display_text ?? message.mes;
- messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
+ messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
+ messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
addCopyToCodeBlocks(messageElement);
appendMediaToMessage(message, messageElement);
}
@@ -2398,8 +2425,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
mes.is_user,
chat.indexOf(mes),
sanitizerOverrides,
+ false,
);
- const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
+ const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
+ const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
let params = {
@@ -2409,6 +2438,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
isUser: mes.is_user,
avatarImg: avatarImg,
bias: bias,
+ reasoning: reasoning,
isSystem: isSystem,
title: title,
bookmarkLink: bookmarkLink,
@@ -2468,6 +2498,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
swipeMessage.attr('swipeid', params.swipeId);
swipeMessage.find('.mes_text').html(messageText).attr('title', title);
+ swipeMessage.find('.mes_reasoning').html(reasoning);
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
appendMediaToMessage(mes, swipeMessage);
if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3044,7 +3075,7 @@ export function isStreamingEnabled() {
(main_api == 'openai' &&
oai_settings.stream_openai &&
!noStreamSources.includes(oai_settings.chat_completion_source) &&
- !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith('o1-')) &&
+ !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) &&
!(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
|| (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
|| (main_api == 'novel' && nai_settings.streaming_novel)
@@ -3078,6 +3109,7 @@ class StreamingProcessor {
this.messageTextDom = null;
this.messageTimerDom = null;
this.messageTokenCounterDom = null;
+ this.messageReasoningDom = null;
/** @type {HTMLTextAreaElement} */
this.sendTextarea = document.querySelector('#send_textarea');
this.type = type;
@@ -3093,6 +3125,7 @@ class StreamingProcessor {
/** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
this.messageLogprobs = [];
this.toolCalls = [];
+ this.reasoning = '';
}
#checkDomElements(messageId) {
@@ -3101,6 +3134,7 @@ class StreamingProcessor {
this.messageTextDom = this.messageDom?.querySelector('.mes_text');
this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
+ this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
}
}
@@ -3137,7 +3171,7 @@ class StreamingProcessor {
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
}
else {
- await saveReply(this.type, text, true);
+ await saveReply(this.type, text, true, '', [], '');
messageId = chat.length - 1;
this.#checkDomElements(messageId);
this.showMessageButtons(messageId);
@@ -3178,18 +3212,27 @@ class StreamingProcessor {
this.#checkDomElements(messageId);
this.#updateMessageBlockVisibility();
const currentTime = new Date();
- // Don't waste time calculating token count for streaming
- const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
- const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
chat[messageId]['mes'] = processedText;
chat[messageId]['gen_started'] = this.timeStarted;
chat[messageId]['gen_finished'] = currentTime;
- if (currentTokenCount) {
- if (!chat[messageId]['extra']) {
- chat[messageId]['extra'] = {};
- }
+ if (!chat[messageId]['extra']) {
+ chat[messageId]['extra'] = {};
+ }
+ if (this.reasoning) {
+ chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
+ if (this.messageReasoningDom instanceof HTMLElement) {
+ const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
+ this.messageReasoningDom.innerHTML = formattedReasoning;
+ }
+ }
+
+ // Don't waste time calculating token count for streaming
+ const tokenCountText = (this.reasoning || '') + processedText;
+ const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
+
+ if (currentTokenCount) {
chat[messageId]['extra']['token_count'] = currentTokenCount;
if (this.messageTokenCounterDom instanceof HTMLElement) {
this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3207,14 +3250,19 @@ class StreamingProcessor {
chat[messageId].is_system,
chat[messageId].is_user,
messageId,
+ {},
+ false,
);
if (this.messageTextDom instanceof HTMLElement) {
this.messageTextDom.innerHTML = formattedText;
}
+
+ const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
if (this.messageTimerDom instanceof HTMLElement) {
this.messageTimerDom.textContent = timePassed.timerValue;
this.messageTimerDom.title = timePassed.timerTitle;
}
+
this.setFirstSwipe(messageId);
}
@@ -3255,39 +3303,11 @@ class StreamingProcessor {
unblockGeneration();
generatedPromptCache = '';
- //console.log("Generated text size:", text.length, text)
-
const isAborted = this.abortController.signal.aborted;
- if (power_user.auto_swipe && !isAborted) {
- function containsBlacklistedWords(str, blacklist, threshold) {
- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
- const matches = str.match(regex) || [];
- return matches.length >= threshold;
- }
-
- const generatedTextFiltered = (text) => {
- if (text) {
- if (power_user.auto_swipe_minimum_length) {
- if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
- console.log('Generated text size too small');
- return true;
- }
- }
- if (power_user.auto_swipe_blacklist_threshold) {
- if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
- console.log('Generated text has blacklisted words');
- return true;
- }
- }
- }
- return false;
- };
-
- if (generatedTextFiltered(text)) {
- swipe_right();
- return;
- }
+ if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
+ return swipe_right();
}
+
playMessageSound();
}
@@ -3321,7 +3341,7 @@ class StreamingProcessor {
}
/**
- * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}
+ * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
*/
*nullStreamingGeneration() {
throw new Error('Generation function for streaming is not hooked up');
@@ -3343,9 +3363,9 @@ class StreamingProcessor {
try {
const sw = new Stopwatch(1000 / power_user.streaming_fps);
const timestamps = [];
- for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {
+ for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
timestamps.push(Date.now());
- if (this.isStopped) {
+ if (this.isStopped || this.abortController.signal.aborted) {
return;
}
@@ -3355,6 +3375,7 @@ class StreamingProcessor {
if (logprobs) {
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
}
+ this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
}
@@ -3840,6 +3861,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
};
}));
+ const reasoning = new PromptReasoning();
+ for (let i = coreChat.length - 1; i >= 0; i--) {
+ const depth = coreChat.length - i - 1;
+ const isPrefix = isContinue && i === coreChat.length - 1;
+ coreChat[i] = {
+ ...coreChat[i],
+ mes: reasoning.addToMessage(
+ coreChat[i].mes,
+ getRegexedString(
+ String(coreChat[i].extra?.reasoning ?? ''),
+ regex_placement.REASONING,
+ { isPrompt: true, depth: depth },
+ ),
+ isPrefix,
+ ),
+ };
+ if (reasoning.isLimitReached()) {
+ break;
+ }
+ }
+
// Determine token limit
let this_max_context = getMaxContextSize();
@@ -4742,11 +4784,17 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
//const getData = await response.json();
let getMessage = extractMessageFromData(data);
let title = extractTitleFromData(data);
+ let reasoning = extractReasoningFromData(data);
kobold_horde_model = title;
const swipes = extractMultiSwipes(data, type);
messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
+ reasoning = getRegexedString(reasoning, regex_placement.REASONING);
+
+ if (power_user.trim_spaces) {
+ reasoning = reasoning.trim();
+ }
if (isContinue) {
getMessage = continue_mag + getMessage;
@@ -4768,10 +4816,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
else {
// Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
if (originalType !== 'continue') {
- ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes));
+ ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
}
else {
- ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes));
+ ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
}
// This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -4805,32 +4853,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
const isAborted = abortController && abortController.signal.aborted;
- if (power_user.auto_swipe && !isAborted) {
- console.debug('checking for autoswipeblacklist on non-streaming message');
- function containsBlacklistedWords(getMessage, blacklist, threshold) {
- console.debug('checking blacklisted words');
- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
- const matches = getMessage.match(regex) || [];
- return matches.length >= threshold;
- }
-
- const generatedTextFiltered = (getMessage) => {
- if (power_user.auto_swipe_blacklist_threshold) {
- if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
- console.debug('Generated text has blacklisted words');
- return true;
- }
- }
-
- return false;
- };
- if (generatedTextFiltered(getMessage)) {
- console.debug('swiping right automatically');
- is_send_press = false;
- swipe_right();
- // TODO: do we want to resolve after an auto-swipe?
- return;
- }
+ if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
+ is_send_press = false;
+ return swipe_right();
}
console.debug('/api/chats/save called by /Generate');
@@ -5659,31 +5684,51 @@ function extractMessageFromData(data) {
return data;
}
- function getTextContext() {
- switch (main_api) {
- case 'kobold':
- return data.results[0].text;
- case 'koboldhorde':
- return data.text;
- case 'textgenerationwebui':
- return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';
- case 'novel':
- return data.output;
- case 'openai':
- return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
- default:
- return '';
- }
+ switch (main_api) {
+ case 'kobold':
+ return data.results[0].text;
+ case 'koboldhorde':
+ return data.text;
+ case 'textgenerationwebui':
+ return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';
+ case 'novel':
+ return data.output;
+ case 'openai':
+ return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
+ default:
+ return '';
+ }
+}
+
+/**
+ * Extracts the reasoning from the response data.
+ * @param {object} data Response data
+ * @returns {string} Extracted reasoning
+ */
+function extractReasoningFromData(data) {
+ switch (main_api) {
+ case 'textgenerationwebui':
+ switch (textgen_settings.type) {
+ case textgen_types.OPENROUTER:
+ return data?.choices?.[0]?.reasoning ?? '';
+ }
+ break;
+
+ case 'openai':
+ if (!oai_settings.show_thoughts) break;
+
+ switch (oai_settings.chat_completion_source) {
+ case chat_completion_sources.DEEPSEEK:
+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
+ case chat_completion_sources.OPENROUTER:
+ return data?.choices?.[0]?.message?.reasoning ?? '';
+ case chat_completion_sources.MAKERSUITE:
+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
+ }
+ break;
}
- const content = getTextContext();
-
- if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) {
- const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? '';
- return [thoughts, content].filter(x => x).join('\n\n');
- }
-
- return content;
+ return '';
}
/**
@@ -5866,7 +5911,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
return getMessage;
}
-export async function saveReply(type, getMessage, fromStreaming, title, swipes) {
+export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
chat[chat.length - 1]['is_user'])) {
type = 'normal';
@@ -5891,8 +5936,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
if (power_user.message_token_count_enabled) {
- chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
+ const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5911,8 +5958,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
if (power_user.message_token_count_enabled) {
- chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
+ const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5928,8 +5977,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
if (power_user.message_token_count_enabled) {
- chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
+ const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5945,6 +5996,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
if (power_user.trim_spaces) {
getMessage = getMessage.trim();
}
@@ -5954,7 +6006,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
chat[chat.length - 1]['gen_finished'] = generationFinished;
if (power_user.message_token_count_enabled) {
- chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
+ const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
if (selected_group) {
@@ -6019,6 +6072,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
return { type, getMessage };
}
+export function syncCurrentSwipeInfoExtras() {
+ if (!chat.length) {
+ return;
+ }
+ const currentMessage = chat[chat.length - 1];
+ if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
+ const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
+ if (swipeInfo && typeof swipeInfo === 'object') {
+ swipeInfo.extra = structuredClone(currentMessage.extra);
+ }
+ }
+}
+
function saveImageToMessage(img, mes) {
if (mes && img.image) {
if (!mes.extra || typeof mes.extra !== 'object') {
@@ -7122,9 +7188,11 @@ function messageEditAuto(div) {
mes.is_system,
mes.is_user,
this_edit_mes_id,
+ {},
+ false,
));
mesBlock.find('.mes_bias').empty();
- mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));
+ mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
saveChatDebounced();
}
@@ -7146,10 +7214,12 @@ async function messageEditDone(div) {
mes.is_system,
mes.is_user,
this_edit_mes_id,
+ {},
+ false,
),
);
mesBlock.find('.mes_bias').empty();
- mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));
+ mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
appendMediaToMessage(mes, div.closest('.mes'));
addCopyToCodeBlocks(div.closest('.mes'));
@@ -7997,9 +8067,23 @@ function updateEditArrowClasses() {
}
}
-function closeMessageEditor() {
- if (this_edit_mes_id) {
- $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
+/**
+ * Closes the message editor.
+ * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
+ */
+export function closeMessageEditor(what = 'all') {
+ if (what === 'message' || what === 'all') {
+ if (this_edit_mes_id) {
+ $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
+ }
+ }
+ if (what === 'reasoning' || what === 'all') {
+ document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
+ const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
+ if (cancelButton instanceof HTMLElement) {
+ cancelButton.click();
+ }
+ });
}
}
@@ -8432,6 +8516,9 @@ function swipe_left() { // when we swipe left..but no generation.
streamingProcessor.onStopStreaming();
}
+ // Make sure ad-hoc changes to extras are saved before swiping away
+ syncCurrentSwipeInfoExtras();
+
const swipe_duration = 120;
const swipe_range = '700px';
chat[chat.length - 1]['swipe_id']--;
@@ -8483,7 +8570,8 @@ function swipe_left() { // when we swipe left..but no generation.
}
const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
- const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);
+ const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
chat[chat.length - 1]['extra']['token_count'] = tokenCount;
swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
}
@@ -8566,6 +8654,9 @@ const swipe_right = () => {
return unblockGeneration();
}
+ // Make sure ad-hoc changes to extras are saved before swiping away
+ syncCurrentSwipeInfoExtras();
+
const swipe_duration = 200;
const swipe_range = 700;
//console.log(swipe_range);
@@ -8647,6 +8738,7 @@ const swipe_right = () => {
// resets the timer
swipeMessage.find('.mes_timer').html('');
swipeMessage.find('.tokenCounterDisplay').text('');
+ swipeMessage.find('.mes_reasoning').html('');
} else {
//console.log('showing previously generated swipe candidate, or "..."');
//console.log('onclick right swipe calling addOneMessage');
@@ -8657,7 +8749,8 @@ const swipe_right = () => {
chat[chat.length - 1].extra = {};
}
- const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);
+ const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
chat[chat.length - 1]['extra']['token_count'] = tokenCount;
swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
}
@@ -9459,7 +9552,8 @@ function addDebugFunctions() {
message.extra = {};
}
- message.extra.token_count = await getTokenCountAsync(message.mes, 0);
+ const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
+ message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
}
await saveChatConditional();
@@ -10768,6 +10862,8 @@ jQuery(async function () {
chat[this_edit_mes_id].is_system,
chat[this_edit_mes_id].is_user,
this_edit_mes_id,
+ {},
+ false,
));
appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
addCopyToCodeBlocks($(this).closest('.mes'));
@@ -11228,14 +11324,15 @@ jQuery(async function () {
$(document).keyup(function (e) {
if (e.key === 'Escape') {
- const isEditVisible = $('#curEditTextarea').is(':visible');
+ const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
if (isEditVisible && power_user.auto_save_msg_edits === false) {
- closeMessageEditor();
+ closeMessageEditor('all');
$('#send_textarea').focus();
return;
}
if (isEditVisible && power_user.auto_save_msg_edits === true) {
$(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
+ closeMessageEditor('reasoning');
$('#send_textarea').focus();
return;
}
diff --git a/public/scripts/authors-note.js b/public/scripts/authors-note.js
index 2bf0b254d..326de966a 100644
--- a/public/scripts/authors-note.js
+++ b/public/scripts/authors-note.js
@@ -566,7 +566,7 @@ export function initAuthorsNote() {
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
- 'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
+ 'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
),
],
helpString: `
diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js
index f83d4e044..3deef929a 100644
--- a/public/scripts/backgrounds.js
+++ b/public/scripts/backgrounds.js
@@ -96,8 +96,13 @@ function highlightLockedBackground() {
});
}
+/**
+ * Locks the background for the current chat
+ * @param {Event} e Click event
+ * @returns {string} Empty string
+ */
function onLockBackgroundClick(e) {
- e.stopPropagation();
+ e?.stopPropagation();
const chatName = getCurrentChatId();
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
return '';
}
- const relativeBgImage = getUrlParameter(this);
+ const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
return '';
}
+/**
+ * Locks the background for the current chat
+ * @param {Event} e Click event
+ * @returns {string} Empty string
+ */
function onUnlockBackgroundClick(e) {
- e.stopPropagation();
+ e?.stopPropagation();
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
@@ -482,10 +492,10 @@ function highlightNewBackground(bg) {
*/
function setFittingClass(fitting) {
const backgrounds = $('#bg1, #bg_custom');
- backgrounds.toggleClass('cover', fitting === 'cover');
- backgrounds.toggleClass('contain', fitting === 'contain');
- backgrounds.toggleClass('stretch', fitting === 'stretch');
- backgrounds.toggleClass('center', fitting === 'center');
+ for (const option of ['cover', 'contain', 'stretch', 'center']) {
+ backgrounds.toggleClass(option, option === fitting);
+ }
+ background_settings.fitting = fitting;
}
function onBackgroundFilterInput() {
@@ -513,12 +523,12 @@ export function initBackgrounds() {
$('#add_bg_button').on('change', onBackgroundUploadSelected);
$('#bg-filter').on('input', onBackgroundFilterInput);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
- callback: onLockBackgroundClick,
+ callback: () => onLockBackgroundClick(new CustomEvent('click')),
aliases: ['bglock'],
helpString: 'Locks a background for the currently selected chat',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
- callback: onUnlockBackgroundClick,
+ callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
aliases: ['bgunlock'],
helpString: 'Unlocks a background for the currently selected chat',
}));
diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js
index e9d0dd2fe..f31e4b495 100644
--- a/public/scripts/extensions/connection-manager/index.js
+++ b/public/scripts/extensions/connection-manager/index.js
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
'api-url',
'model',
'proxy',
+ 'stop-strings',
];
const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
'context',
'instruct-state',
'tokenizer',
+ 'stop-strings',
];
const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
'instruct': 'Instruct Template',
'context': 'Context Template',
'tokenizer': 'Tokenizer',
+ 'stop-strings': 'Custom Stopping Strings',
};
/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
* @property {string} [context] Context Template
* @property {string} [instruct-state] Instruct Mode
* @property {string} [tokenizer] Tokenizer
+ * @property {string} [stop-strings] Custom Stopping Strings
* @property {string[]} [exclude] Commands to exclude
*/
diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
index 15f7524a4..b131e9668 100644
--- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
+++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
@@ -883,6 +883,10 @@ export class SlashCommandHandler {
}
}
getQuickReply(args) {
+ if (!args.id && !args.label) {
+ toastr.error('Please provide a valid id or label.');
+ return '';
+ }
try {
return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
} catch (ex) {
diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html
index 9e699a622..8cfe9e008 100644
--- a/public/scripts/extensions/regex/editor.html
+++ b/public/scripts/extensions/regex/editor.html
@@ -94,6 +94,12 @@
World Info
+
+
+
+ Reasoning
+
+
diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js
index 8c28d01f3..f6e22d249 100644
--- a/public/scripts/extensions/regex/engine.js
+++ b/public/scripts/extensions/regex/engine.js
@@ -20,6 +20,7 @@ const regex_placement = {
SLASH_COMMAND: 3,
// 4 - sendAs (legacy)
WORLD_INFO: 5,
+ REASONING: 6,
};
export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
// Script applies to Generate and input is Generate
(script.promptOnly && isPrompt) ||
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
- (!script.markdownOnly && !script.promptOnly && !isMarkdown)
+ (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
) {
if (isEdit && !script.runOnEdit) {
console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js
index 73538e4ed..8ac6ae6d6 100644
--- a/public/scripts/extensions/regex/index.js
+++ b/public/scripts/extensions/regex/index.js
@@ -18,7 +18,7 @@ import { t } from '../../i18n.js';
* @property {string} replaceString - The replace string
* @property {string[]} trimStrings - The trim strings
* @property {string?} findRegex - The find regex
- * @property {string?} substituteRegex - The substitute regex
+ * @property {number?} substituteRegex - The substitute regex
*/
/**
diff --git a/public/scripts/extensions/tts/alltalk.js b/public/scripts/extensions/tts/alltalk.js
index 593ff3eac..0c97caff3 100644
--- a/public/scripts/extensions/tts/alltalk.js
+++ b/public/scripts/extensions/tts/alltalk.js
@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
}
async fetchRvcVoiceObjects() {
- if (this.settings.server_version == 'v2') {
+ if (this.settings.server_version == 'v1') {
console.log('Skipping RVC voices fetch for V1 server');
return [];
}
@@ -1031,14 +1031,18 @@ class AllTalkTtsProvider {
console.error('fetchTtsGeneration Error Response Text:', errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
+
const data = await response.json();
- // Handle V1/V2 URL differences
- const outputUrl = this.settings.server_version === 'v1'
- ? data.output_file_url // V1 returns full URL
- : `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path
+ // V1 returns a complete URL, V2 returns a relative path
+ if (this.settings.server_version === 'v1') {
+ // V1: Use the complete URL directly from the response
+ return data.output_file_url;
+ } else {
+ // V2: Combine the endpoint with the relative path
+ return `${this.settings.provider_endpoint}${data.output_file_url}`;
+ }
- return outputUrl;
} catch (error) {
console.error('[fetchTtsGeneration] Exception caught:', error);
throw error;
diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js
index 53cdda83e..18919d779 100644
--- a/public/scripts/extensions/tts/index.js
+++ b/public/scripts/extensions/tts/index.js
@@ -29,6 +29,7 @@ import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { GoogleTranslateTtsProvider } from './google-translate.js';
const UPDATE_INTERVAL = 1000;
+const wrapper = new ModuleWorkerWrapper(moduleWorker);
let voiceMapEntries = [];
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -118,7 +119,7 @@ async function onNarrateOneMessage() {
}
resetTtsPlayback();
- ttsJobQueue.push(message);
+ processAndQueueTtsMessage(message);
moduleWorker();
}
@@ -145,7 +146,7 @@ async function onNarrateText(args, text) {
}
resetTtsPlayback();
- ttsJobQueue.push({ mes: text, name: name });
+ processAndQueueTtsMessage({ mes: text, name: name });
await moduleWorker();
// Return back to the chat voices
@@ -197,6 +198,36 @@ function isTtsProcessing() {
return processing;
}
+/**
+ * Splits a message into lines and adds each non-empty line to the TTS job queue.
+ * @param {Object} message - The message object to be processed.
+ * @param {string} message.mes - The text of the message to be split into lines.
+ * @param {string} message.name - The name associated with the message.
+ * @returns {void}
+ */
+function processAndQueueTtsMessage(message) {
+ if (!extension_settings.tts.narrate_by_paragraphs) {
+ ttsJobQueue.push(message);
+ return;
+ }
+
+ const lines = message.mes.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ if (line.length === 0) {
+ continue;
+ }
+
+ ttsJobQueue.push(
+ Object.assign({}, message, {
+ mes: line,
+ }),
+ );
+ }
+}
+
function debugTtsPlayback() {
console.log(JSON.stringify(
{
@@ -326,7 +357,7 @@ function onAudioControlClicked() {
resetTtsPlayback();
} else {
// Default play behavior if not processing or playing is to play the last message.
- ttsJobQueue.push(context.chat[context.chat.length - 1]);
+ processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
}
updateUiAudioPlayState();
}
@@ -351,6 +382,7 @@ function completeCurrentAudioJob() {
audioQueueProcessorReady = true;
currentAudioJob = null;
// updateUiPlayState();
+ wrapper.update();
}
/**
@@ -440,7 +472,7 @@ async function processTtsQueue() {
}
if (extension_settings.tts.skip_tags) {
- text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();
+ text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
}
if (!extension_settings.tts.pass_asterisks) {
@@ -543,6 +575,7 @@ function loadSettings() {
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
$('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
$('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
+ $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
$('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
$('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -612,6 +645,11 @@ function onPeriodicAutoGenerationClick() {
saveSettingsDebounced();
}
+function onNarrateByParagraphsClick() {
+ extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
+ saveSettingsDebounced();
+}
+
function onNarrateDialoguesClick() {
extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -790,7 +828,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
lastChatId = context.chatId;
console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
- ttsJobQueue.push(message);
+
+ if (extension_settings.tts.periodic_auto_generation) {
+ ttsJobQueue.push(message);
+ } else {
+ processAndQueueTtsMessage(message);
+ }
}
async function onMessageDeleted() {
@@ -1130,6 +1173,7 @@ jQuery(async function () {
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
$('#tts_auto_generation').on('click', onAutoGenerationClick);
$('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
+ $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
$('#tts_narrate_user').on('click', onNarrateUserClick);
$('#playback_rate').on('input', function () {
@@ -1151,7 +1195,6 @@ jQuery(async function () {
loadSettings(); // Depends on Extension Controls and loadTtsProvider
loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
addAudioControl(); // Depends on Extension Controls
- const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
diff --git a/public/scripts/extensions/tts/settings.html b/public/scripts/extensions/tts/settings.html
index dc005a406..5fb5b6895 100644
--- a/public/scripts/extensions/tts/settings.html
+++ b/public/scripts/extensions/tts/settings.html
@@ -30,6 +30,10 @@
Narrate by paragraphs (when streaming)
+
+
+ Narrate by paragraphs (when not streaming)
+
Only narrate "quotes"
diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js
index 6efadce87..65d47fc4b 100644
--- a/public/scripts/kai-settings.js
+++ b/public/scripts/kai-settings.js
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
if (data?.token) {
text += data.token;
}
- yield { text, swipes: [], toolCalls: [] };
+ yield { text, swipes: [], toolCalls: [], state: {} };
}
};
}
diff --git a/public/scripts/loader.js b/public/scripts/loader.js
index 2df5f6edf..c812ca651 100644
--- a/public/scripts/loader.js
+++ b/public/scripts/loader.js
@@ -27,24 +27,45 @@ export async function hideLoader() {
}
return new Promise((resolve) => {
- // Spinner blurs/fades out
- $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
+ const spinner = $('#load-spinner');
+ if (!spinner.length) {
+ console.warn('Spinner element not found, skipping animation');
+ cleanup();
+ return;
+ }
+
+ // Check if transitions are enabled
+ const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
+ const hasTransitions = parseFloat(transitionDuration) > 0;
+
+ if (hasTransitions) {
+ Promise.race([
+ new Promise((r) => setTimeout(r, 500)), // Fallback timeout
+ new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
+ ]).finally(cleanup);
+ } else {
+ cleanup();
+ }
+
+ function cleanup() {
$('#loader').remove();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
// If it's present, we remove it once and then it's gone.
yoinkPreloader();
- loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
- loaderPopup = null;
- resolve();
- });
- });
+ loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
+ .catch((err) => console.error('Error completing loaderPopup:', err))
+ .finally(() => {
+ loaderPopup = null;
+ resolve();
+ });
+ }
- $('#load-spinner')
- .css({
- 'filter': 'blur(15px)',
- 'opacity': '0',
- });
+ // Apply the styles
+ spinner.css({
+ 'filter': 'blur(15px)',
+ 'opacity': '0',
+ });
});
}
diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js
index f95e7d9f6..91ff09ef6 100644
--- a/public/scripts/nai-settings.js
+++ b/public/scripts/nai-settings.js
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
text += data.token;
}
- yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };
+ yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
}
};
}
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index d715377fb..2357e780f 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -258,7 +258,7 @@ const default_settings = {
ai21_model: 'jamba-1.5-large',
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
- perplexity_model: 'llama-3.1-70b-instruct',
+ perplexity_model: 'sonar-pro',
groq_model: 'llama-3.1-70b-versatile',
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
@@ -298,7 +298,7 @@ const default_settings = {
names_behavior: character_names_behavior.DEFAULT,
continue_postfix: continue_postfix_types.SPACE,
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
- show_thoughts: false,
+ show_thoughts: true,
seed: -1,
n: 1,
};
@@ -337,7 +337,7 @@ const oai_settings = {
ai21_model: 'jamba-1.5-large',
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
- perplexity_model: 'llama-3.1-70b-instruct',
+ perplexity_model: 'sonar-pro',
groq_model: 'llama-3.1-70b-versatile',
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
@@ -377,7 +377,7 @@ const oai_settings = {
names_behavior: character_names_behavior.DEFAULT,
continue_postfix: continue_postfix_types.SPACE,
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
- show_thoughts: false,
+ show_thoughts: true,
seed: -1,
n: 1,
};
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
// Unordered prompts without marker
{ role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
{ role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
- { role: 'system', content: bias, identifier: 'bias' },
{ role: 'system', content: groupNudge, identifier: 'groupNudge' },
+ { role: 'assistant', content: bias, identifier: 'bias' },
];
// Tavern Extras - Summary
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
- const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && oai_settings.openai_model.startsWith('o1-'));
+ const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3')));
const useLogprobs = !!power_user.request_token_probabilities;
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
@@ -1913,7 +1913,7 @@ async function sendOpenAIRequest(type, messages, signal) {
'user_name': name1,
'char_name': name2,
'group_names': getGroupNames(),
- 'show_thoughts': Boolean(oai_settings.show_thoughts),
+ 'include_reasoning': Boolean(oai_settings.show_thoughts),
};
// Empty array will produce a validation error
@@ -2050,7 +2050,7 @@ async function sendOpenAIRequest(type, messages, signal) {
await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
- if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
+ if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
generate_data.messages.forEach((msg) => {
if (msg.role === 'system') {
msg.role = 'user';
@@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
let text = '';
const swipes = [];
const toolCalls = [];
- const state = {};
+ const state = { reasoning: '' };
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) {
ToolManager.parseToolCalls(toolCalls, parsed);
- yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };
+ yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
}
};
}
@@ -2150,17 +2150,23 @@ function getStreamingReply(data, state) {
if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
return data?.delta?.text || '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
- return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';
+ if (oai_settings.show_thoughts) {
+ state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
+ }
+ return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
- const hadThoughts = state.hadThoughts;
- const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '';
- const content = data.choices?.[0]?.delta?.content || '';
- state.hadThoughts = !!thoughts;
- const separator = hadThoughts && !thoughts ? '\n\n' : '';
- return [thoughts, separator, content].filter(x => x).join('\n\n');
- } else {
+ if (oai_settings.show_thoughts) {
+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
+ }
+ return data.choices?.[0]?.delta?.content || '';
+ } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
+ if (oai_settings.show_thoughts) {
+ state.reasoning += (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 {
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
}
}
@@ -4021,7 +4027,7 @@ function getMaxContextOpenAI(value) {
if (oai_settings.max_context_unlocked) {
return unlocked_max;
}
- else if (value.startsWith('o1-')) {
+ else if (value.startsWith('o1') || value.startsWith('o3')) {
return max_128k;
}
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')) {
@@ -4374,28 +4380,19 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
+ else if (['sonar', 'sonar-reasoning'].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 if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
- $('#openai_max_context').attr('max', max_32k);
- }
- else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
- $('#openai_max_context').attr('max', 28000);
- }
- else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
- $('#openai_max_context').attr('max', max_16k);
- }
- else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
- $('#openai_max_context').attr('max', max_8k);
- }
- else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
- $('#openai_max_context').attr('max', 12000);
- }
else {
- $('#openai_max_context').attr('max', max_4k);
+ $('#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');
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index f4fe60f7b..59adaec00 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -253,6 +253,15 @@ let power_user = {
content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
},
+ reasoning: {
+ auto_parse: false,
+ add_to_prompts: false,
+ prefix: '\n',
+ suffix: '\n ',
+ separator: '\n\n',
+ max_additions: 1,
+ },
+
personas: {},
default_persona: null,
persona_descriptions: {},
@@ -2909,6 +2918,46 @@ export function flushEphemeralStoppingStrings() {
EPHEMERAL_STOPPING_STRINGS.splice(0, EPHEMERAL_STOPPING_STRINGS.length);
}
+/**
+ * Checks if the generated text should be filtered based on the auto-swipe settings.
+ * @param {string} text The text to check
+ * @returns {boolean} If the generated text should be filtered
+ */
+export function generatedTextFiltered(text) {
+ /**
+ * Checks if the given text contains any of the blacklisted words.
+ * @param {string} text The text to check
+ * @param {string[]} blacklist The list of blacklisted words
+ * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
+ * @returns {boolean} Whether the text contains blacklisted words
+ */
+ function containsBlacklistedWords(text, blacklist, threshold) {
+ const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
+ const matches = text.match(regex) || [];
+ return matches.length >= threshold;
+ }
+
+ // Make sure a generated text is non-empty
+ // Otherwise we might get in a loop with a broken API
+ text = text.trim();
+ if (text.length > 0) {
+ if (power_user.auto_swipe_minimum_length) {
+ if (text.length < power_user.auto_swipe_minimum_length) {
+ console.log('Generated text size too small');
+ return true;
+ }
+ }
+ if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
+ if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
+ console.log('Generated text has blacklisted words');
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/**
* Gets the custom stopping strings from the power user settings.
* @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
@@ -3880,9 +3929,9 @@ $(document).ready(() => {
helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
- name: 'delmode',
+ name: 'del',
callback: doDelMode,
- aliases: ['del'],
+ aliases: ['delete', 'delmode'],
unnamedArgumentList: [
new SlashCommandArgument(
'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4065,4 +4114,45 @@ $(document).ready(() => {
],
helpString: 'activates a movingUI preset by name',
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'stop-strings',
+ aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
+ helpString: `
+
+ Sets a list of custom stopping strings. Gets the list if no value is provided.
+
+
+ Examples:
+
+
+ `,
+ returns: ARGUMENT_TYPE.LIST,
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'list of strings',
+ typeList: [ARGUMENT_TYPE.LIST],
+ acceptsMultiple: false,
+ isRequired: false,
+ }),
+ ],
+ callback: (_, value) => {
+ if (String(value ?? '').trim()) {
+ const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
+ if (!parsedValue || !Array.isArray(parsedValue)) {
+ throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
+ }
+ parsedValue.forEach((item, index) => {
+ parsedValue[index] = String(item);
+ });
+ power_user.custom_stopping_strings = JSON.stringify(parsedValue);
+ $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
+ saveSettingsDebounced();
+ }
+
+ return power_user.custom_stopping_strings;
+ },
+ }));
});
diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js
index 0b6f3dcec..0d7d3c133 100644
--- a/public/scripts/preset-manager.js
+++ b/public/scripts/preset-manager.js
@@ -586,6 +586,7 @@ class PresetManager {
'tabby_model',
'derived',
'generic_model',
+ 'include_reasoning',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js
new file mode 100644
index 000000000..421eb65b1
--- /dev/null
+++ b/public/scripts/reasoning.js
@@ -0,0 +1,448 @@
+import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
+import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
+import { t } from './i18n.js';
+import { MacrosParser } from './macros.js';
+import { Popup } from './popup.js';
+import { power_user } from './power-user.js';
+import { SlashCommand } from './slash-commands/SlashCommand.js';
+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
+import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
+
+/**
+ * Gets a message from a jQuery element.
+ * @param {Element} element
+ * @returns {{messageId: number, message: object, messageBlock: JQuery}}
+ */
+function getMessageFromJquery(element) {
+ const messageBlock = $(element).closest('.mes');
+ const messageId = Number(messageBlock.attr('mesid'));
+ const message = chat[messageId];
+ return { messageId: messageId, message, messageBlock };
+}
+
+/**
+ * Helper class for adding reasoning to messages.
+ * Keeps track of the number of reasoning additions.
+ */
+export class PromptReasoning {
+ static REASONING_PLACEHOLDER = '\u200B';
+ static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
+
+ constructor() {
+ this.counter = 0;
+ }
+
+ /**
+ * Checks if the limit of reasoning additions has been reached.
+ * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
+ */
+ isLimitReached() {
+ if (!power_user.reasoning.add_to_prompts) {
+ return true;
+ }
+
+ return this.counter >= power_user.reasoning.max_additions;
+ }
+
+ /**
+ * Add reasoning to a message according to the power user settings.
+ * @param {string} content Message content
+ * @param {string} reasoning Message reasoning
+ * @param {boolean} isPrefix Whether this is the last message prefix
+ * @returns {string} Message content with reasoning
+ */
+ addToMessage(content, reasoning, isPrefix) {
+ // Disabled or reached limit of additions
+ if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
+ return content;
+ }
+
+ // No reasoning provided or a placeholder
+ if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
+ return content;
+ }
+
+ // Increment the counter
+ this.counter++;
+
+ // Substitute macros in variable parts
+ const prefix = substituteParams(power_user.reasoning.prefix || '');
+ const separator = substituteParams(power_user.reasoning.separator || '');
+ const suffix = substituteParams(power_user.reasoning.suffix || '');
+
+ // Combine parts with reasoning only
+ if (isPrefix && !content) {
+ return `${prefix}${reasoning}`;
+ }
+
+ // Combine parts with reasoning and content
+ return `${prefix}${reasoning}${suffix}${separator}${content}`;
+ }
+}
+
+function loadReasoningSettings() {
+ $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
+ $('#reasoning_add_to_prompts').on('change', function () {
+ power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
+ saveSettingsDebounced();
+ });
+
+ $('#reasoning_prefix').val(power_user.reasoning.prefix);
+ $('#reasoning_prefix').on('input', function () {
+ power_user.reasoning.prefix = String($(this).val());
+ saveSettingsDebounced();
+ });
+
+ $('#reasoning_suffix').val(power_user.reasoning.suffix);
+ $('#reasoning_suffix').on('input', function () {
+ power_user.reasoning.suffix = String($(this).val());
+ saveSettingsDebounced();
+ });
+
+ $('#reasoning_separator').val(power_user.reasoning.separator);
+ $('#reasoning_separator').on('input', function () {
+ power_user.reasoning.separator = String($(this).val());
+ saveSettingsDebounced();
+ });
+
+ $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
+ $('#reasoning_max_additions').on('input', function () {
+ power_user.reasoning.max_additions = Number($(this).val());
+ saveSettingsDebounced();
+ });
+
+ $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
+ $('#reasoning_auto_parse').on('change', function () {
+ power_user.reasoning.auto_parse = !!$(this).prop('checked');
+ saveSettingsDebounced();
+ });
+}
+
+function registerReasoningSlashCommands() {
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'reasoning-get',
+ aliases: ['get-reasoning'],
+ returns: ARGUMENT_TYPE.STRING,
+ helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Message ID. If not provided, the message ID of the last message is used.',
+ typeList: ARGUMENT_TYPE.NUMBER,
+ enumProvider: commonEnumProviders.messages(),
+ }),
+ ],
+ callback: (_args, value) => {
+ const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
+ const message = chat[messageId];
+ const reasoning = String(message?.extra?.reasoning ?? '');
+ return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
+ },
+ }));
+
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'reasoning-set',
+ aliases: ['set-reasoning'],
+ returns: ARGUMENT_TYPE.STRING,
+ helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'at',
+ description: 'Message ID. If not provided, the message ID of the last message is used.',
+ typeList: ARGUMENT_TYPE.NUMBER,
+ enumProvider: commonEnumProviders.messages(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Reasoning block content.',
+ typeList: ARGUMENT_TYPE.STRING,
+ }),
+ ],
+ callback: async (args, value) => {
+ const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
+ const message = chat[messageId];
+ if (!message?.extra) {
+ return '';
+ }
+
+ message.extra.reasoning = String(value ?? '');
+ await saveChatConditional();
+
+ closeMessageEditor('reasoning');
+ updateMessageBlock(messageId, message);
+ return message.extra.reasoning;
+ },
+ }));
+
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'reasoning-parse',
+ aliases: ['parse-reasoning'],
+ returns: 'reasoning string',
+ helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'regex',
+ description: 'Whether to apply regex scripts to the reasoning content.',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ isRequired: false,
+ enumProvider: commonEnumProviders.boolean('trueFalse'),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'input string',
+ typeList: [ARGUMENT_TYPE.STRING],
+ }),
+ ],
+ callback: (args, value) => {
+ if (!value) {
+ return '';
+ }
+
+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
+ toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
+ return String(value);
+ }
+
+ const parsedReasoning = parseReasoningFromString(String(value));
+
+ if (!parsedReasoning) {
+ return '';
+ }
+
+ const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
+ return applyRegex
+ ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
+ : parsedReasoning.reasoning;
+ },
+ }));
+}
+
+function registerReasoningMacros() {
+ MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
+ MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
+ MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
+}
+
+function setReasoningEventHandlers() {
+ $(document).on('click', '.mes_reasoning_copy', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ });
+
+ $(document).on('click', '.mes_reasoning_edit', function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ const { message, messageBlock } = getMessageFromJquery(this);
+ if (!message?.extra) {
+ return;
+ }
+
+ const reasoning = String(message?.extra?.reasoning ?? '');
+ const chatElement = document.getElementById('chat');
+ const textarea = document.createElement('textarea');
+ const reasoningBlock = messageBlock.find('.mes_reasoning');
+ textarea.classList.add('reasoning_edit_textarea');
+ textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
+ $(textarea).insertBefore(reasoningBlock);
+
+ if (!CSS.supports('field-sizing', 'content')) {
+ const resetHeight = function () {
+ const scrollTop = chatElement.scrollTop;
+ textarea.style.height = '0px';
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ chatElement.scrollTop = scrollTop;
+ };
+
+ textarea.addEventListener('input', resetHeight);
+ resetHeight();
+ }
+
+ textarea.focus();
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+
+ const textareaRect = textarea.getBoundingClientRect();
+ const chatRect = chatElement.getBoundingClientRect();
+
+ // Scroll if textarea bottom is below visible area
+ if (textareaRect.bottom > chatRect.bottom) {
+ const scrollOffset = textareaRect.bottom - chatRect.bottom;
+ chatElement.scrollTop += scrollOffset;
+ }
+ });
+
+ $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ const { message, messageId, messageBlock } = getMessageFromJquery(this);
+ if (!message?.extra) {
+ return;
+ }
+
+ const textarea = messageBlock.find('.reasoning_edit_textarea');
+ const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
+ message.extra.reasoning = reasoning;
+ await saveChatConditional();
+ updateMessageBlock(messageId, message);
+ textarea.remove();
+ });
+
+ $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const { messageBlock } = getMessageFromJquery(this);
+ const textarea = messageBlock.find('.reasoning_edit_textarea');
+ textarea.remove();
+ });
+
+ $(document).on('click', '.mes_edit_add_reasoning', async function () {
+ const { message, messageId } = getMessageFromJquery(this);
+ if (!message?.extra) {
+ return;
+ }
+
+ if (message.extra.reasoning) {
+ toastr.info(t`Reasoning already exists.`, t`Edit Message`);
+ return;
+ }
+
+ message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
+ await saveChatConditional();
+ closeMessageEditor();
+ updateMessageBlock(messageId, message);
+ });
+
+ $(document).on('click', '.mes_reasoning_delete', async function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
+
+ if (!confirm) {
+ return;
+ }
+
+ const { message, messageId } = getMessageFromJquery(this);
+ if (!message?.extra) {
+ return;
+ }
+ message.extra.reasoning = '';
+ await saveChatConditional();
+ updateMessageBlock(messageId, message);
+ });
+
+ $(document).on('pointerup', '.mes_reasoning_copy', async function () {
+ const { message } = getMessageFromJquery(this);
+ const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
+
+ if (!reasoning) {
+ return;
+ }
+
+ await copyText(reasoning);
+ toastr.info(t`Copied!`, '', { timeOut: 2000 });
+ });
+}
+
+/**
+ * Parses reasoning from a string using the power user reasoning settings.
+ * @typedef {Object} ParsedReasoning
+ * @property {string} reasoning Reasoning block
+ * @property {string} content Message content
+ * @param {string} str Content of the message
+ * @returns {ParsedReasoning|null} Parsed reasoning block and message content
+ */
+function parseReasoningFromString(str) {
+ // Both prefix and suffix must be defined
+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
+ return null;
+ }
+
+ try {
+ const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
+
+ let didReplace = false;
+ let reasoning = '';
+ let content = String(str).replace(regex, (_match, captureGroup) => {
+ didReplace = true;
+ reasoning = captureGroup;
+ return '';
+ });
+
+ if (didReplace && power_user.trim_spaces) {
+ reasoning = reasoning.trim();
+ content = content.trim();
+ }
+
+ return { reasoning, content };
+ } catch (error) {
+ console.error('[Reasoning] Error parsing reasoning block', error);
+ return null;
+ }
+}
+
+function registerReasoningAppEvents() {
+ eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
+ if (!power_user.reasoning.auto_parse) {
+ return;
+ }
+
+ console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
+ const message = chat[idx];
+
+ if (!message) {
+ console.warn('[Reasoning] Message not found', idx);
+ return null;
+ }
+
+ if (!message.mes || message.mes === '...') {
+ console.debug('[Reasoning] Message content is empty or a placeholder', idx);
+ return null;
+ }
+
+ const parsedReasoning = parseReasoningFromString(message.mes);
+
+ // No reasoning block found
+ if (!parsedReasoning) {
+ return;
+ }
+
+ // Make sure the message has an extra object
+ if (!message.extra || typeof message.extra !== 'object') {
+ message.extra = {};
+ }
+
+ const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
+
+ // If reasoning was found, add it to the message
+ if (parsedReasoning.reasoning) {
+ message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
+ }
+
+ // Update the message text if it was changed
+ if (parsedReasoning.content !== message.mes) {
+ message.mes = parsedReasoning.content;
+ }
+
+ // Find if a message already exists in DOM and must be updated
+ if (contentUpdated) {
+ const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
+ if (messageRendered) {
+ console.debug('[Reasoning] Updating message block', idx);
+ updateMessageBlock(idx, message);
+ }
+ }
+ });
+}
+
+export function initReasoning() {
+ loadReasoningSettings();
+ setReasoningEventHandlers();
+ registerReasoningSlashCommands();
+ registerReasoningMacros();
+ registerReasoningAppEvents();
+}
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 39b3db71e..42f9c40d0 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -42,6 +42,7 @@ import {
showMoreMessages,
stopGeneration,
substituteParams,
+ syncCurrentSwipeInfoExtras,
system_avatar,
system_message_types,
this_chid,
@@ -58,7 +59,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
-import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
+import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -1902,7 +1903,7 @@ export function initDefaultSlashCommands() {
returns: 'uppercase string',
unnamedArgumentList: [
new SlashCommandArgument(
- 'string', [ARGUMENT_TYPE.STRING], true, false,
+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
),
],
helpString: 'Converts the provided string to uppercase.',
@@ -1914,7 +1915,7 @@ export function initDefaultSlashCommands() {
returns: 'lowercase string',
unnamedArgumentList: [
new SlashCommandArgument(
- 'string', [ARGUMENT_TYPE.STRING], true, false,
+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
),
],
helpString: 'Converts the provided string to lowercase.',
@@ -1934,7 +1935,7 @@ export function initDefaultSlashCommands() {
],
unnamedArgumentList: [
new SlashCommandArgument(
- 'string', [ARGUMENT_TYPE.STRING], true, false,
+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
),
],
helpString: `
@@ -1998,6 +1999,62 @@ export function initDefaultSlashCommands() {
return '';
},
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'replace',
+ aliases: ['re'],
+ callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
+ if (pattern === '')
+ throw new Error('Argument of \'pattern=\' cannot be empty');
+ switch (mode) {
+ case 'literal':
+ return text.replaceAll(pattern, replacer);
+ case 'regex':
+ return text.replace(regexFromString(pattern), replacer);
+ default:
+ throw new Error('Invalid \'/replace mode=\' argument specified!');
+ }
+ }),
+ returns: 'replaced text',
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'mode',
+ description: 'Replaces occurrence(s) of a pattern',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'literal',
+ enumList: ['literal', 'regex'],
+ }),
+ new SlashCommandNamedArgument(
+ 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
+ ),
+ new SlashCommandNamedArgument(
+ 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
+ ),
+ ],
+ unnamedArgumentList: [
+ new SlashCommandArgument(
+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
+ ),
+ ],
+ helpString: `
+
+ Replaces text within the provided string based on the pattern.
+
+
+ If mode
is literal
(or omitted), pattern
is a literal search string (case-sensitive).
+ If mode
is regex
, pattern
is parsed as an ECMAScript Regular Expression.
+ The replacer
replaces based on the pattern
in the input text.
+ If replacer
is omitted, the replacement(s) will be an empty string.
+
+
+
Example:
+
/let x Blue house and blue car ||
+
/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||
+
/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||
+
/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||
+
/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||
+
+ `,
+ }));
registerVariableCommands();
}
@@ -2814,8 +2871,11 @@ async function addSwipeCallback(args, value) {
const newSwipeId = lastMessage.swipes.length - 1;
if (isTrueBoolean(args.switch)) {
+ // Make sure ad-hoc changes to extras are saved before swiping away
+ syncCurrentSwipeInfoExtras();
lastMessage.swipe_id = newSwipeId;
lastMessage.mes = lastMessage.swipes[newSwipeId];
+ lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
}
await saveChatConditional();
@@ -3081,11 +3141,6 @@ async function removeGroupMemberCallback(_, arg) {
return '';
}
- if (is_group_generating) {
- toastr.warning('Cannot run /memberremove command while the group reply is generating.');
- return '';
- }
-
const chid = findGroupMemberId(arg);
if (chid === undefined) {
@@ -3190,12 +3245,7 @@ function findPersonaByName(name) {
}
async function sendUserMessageCallback(args, text) {
- if (!text) {
- toastr.warning('You must specify text to send');
- return;
- }
-
- text = text.trim();
+ text = String(text ?? '').trim();
const compact = isTrueBoolean(args?.compact);
const bias = extractMessageBias(text);
@@ -3504,13 +3554,7 @@ export function getNameAndAvatarForMessage(character, name = null) {
}
export async function sendMessageAs(args, text) {
- if (!text) {
- toastr.warning('You must specify text to send as');
- return '';
- }
-
let name = args.name?.trim();
- let mesText;
if (!name) {
const namelessWarningKey = 'sendAsNamelessWarningShown';
@@ -3521,7 +3565,7 @@ export async function sendMessageAs(args, text) {
name = name2;
}
- mesText = text.trim();
+ let mesText = String(text ?? '').trim();
// Requires a regex check after the slash command is pushed to output
mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3599,11 +3643,7 @@ export async function sendMessageAs(args, text) {
}
export async function sendNarratorMessage(args, text) {
- if (!text) {
- toastr.warning('You must specify text to send');
- return '';
- }
-
+ text = String(text ?? '');
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
// Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias(text);
@@ -3694,18 +3734,13 @@ export async function promptQuietForLoudResponse(who, text) {
}
async function sendCommentMessage(args, text) {
- if (!text) {
- toastr.warning('You must specify text to send');
- return '';
- }
-
const compact = isTrueBoolean(args?.compact);
const message = {
name: COMMENT_NAME_DEFAULT,
is_user: false,
is_system: true,
send_date: getMessageTimeStamp(),
- mes: substituteParams(text.trim()),
+ mes: substituteParams(String(text ?? '').trim()),
force_avatar: comment_avatar,
extra: {
type: system_message_types.COMMENT,
diff --git a/public/scripts/sse-stream.js b/public/scripts/sse-stream.js
index 3921e7d58..17a31b567 100644
--- a/public/scripts/sse-stream.js
+++ b/public/scripts/sse-stream.js
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
}
return;
}
+ else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
+ for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
+ const str = json.choices[0].delta.reasoning[j];
+ const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
+ const choiceClone = structuredClone(json.choices[0]);
+ choiceClone.delta.reasoning = str;
+ choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
+ const choices = [choiceClone];
+ yield {
+ data: { ...json, choices },
+ chunk: str,
+ };
+ }
+ return;
+ }
else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
for (let j = 0; j < json.choices[0].delta.content.length; j++) {
const str = json.choices[0].delta.content[j];
diff --git a/public/scripts/st-context.js b/public/scripts/st-context.js
index a02fd5492..f69a1bd8c 100644
--- a/public/scripts/st-context.js
+++ b/public/scripts/st-context.js
@@ -1,6 +1,7 @@
import {
activateSendButtons,
addOneMessage,
+ appendMediaToMessage,
callPopup,
characters,
chat,
@@ -41,6 +42,7 @@ import {
substituteParamsExtended,
this_chid,
updateChatMetadata,
+ updateMessageBlock,
} from '../script.js';
import {
extension_settings,
@@ -67,6 +69,7 @@ import { textgenerationwebui_settings } from './textgen-settings.js';
import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
import { ToolManager } from './tool-calling.js';
import { timestampToMoment, uuidv4 } from './utils.js';
+import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
export function getContext() {
return {
@@ -171,6 +174,18 @@ export function getContext() {
getCharacters,
uuidv4,
humanizedDateTime,
+ updateMessageBlock,
+ appendMediaToMessage,
+ variables: {
+ local: {
+ get: getLocalVariable,
+ set: setLocalVariable,
+ },
+ global: {
+ get: getGlobalVariable,
+ set: setGlobalVariable,
+ },
+ },
};
}
diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js
index 824d5eec4..9523cfba5 100644
--- a/public/scripts/textgen-models.js
+++ b/public/scripts/textgen-models.js
@@ -54,6 +54,12 @@ const OPENROUTER_PROVIDERS = [
'xAI',
'Cloudflare',
'SF Compute',
+ 'Minimax',
+ 'Nineteen',
+ 'Liquid',
+ 'Nebius',
+ 'Chutes',
+ 'Kluster',
'01.AI',
'HuggingFace',
'Mancer',
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js
index cd1004991..29bce0858 100644
--- a/public/scripts/textgen-settings.js
+++ b/public/scripts/textgen-settings.js
@@ -172,6 +172,7 @@ const settings = {
//truncation_length: 2048,
ban_eos_token: false,
skip_special_tokens: true,
+ include_reasoning: true,
streaming: false,
mirostat_mode: 0,
mirostat_tau: 5,
@@ -263,6 +264,7 @@ export const setting_names = [
'add_bos_token',
'ban_eos_token',
'skip_special_tokens',
+ 'include_reasoning',
'streaming',
'mirostat_mode',
'mirostat_tau',
@@ -501,7 +503,7 @@ export function loadTextGenSettings(data, loadedSettings) {
for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
const control = $(selector);
control.val(settings.server_urls[type] ?? '').on('input', function () {
- settings.server_urls[type] = String($(this).val());
+ settings.server_urls[type] = String($(this).val()).trim();
saveSettingsDebounced();
});
}
@@ -740,6 +742,7 @@ jQuery(function () {
'add_bos_token_textgenerationwebui': true,
'temperature_last_textgenerationwebui': true,
'skip_special_tokens_textgenerationwebui': true,
+ 'include_reasoning_textgenerationwebui': true,
'top_a_textgenerationwebui': 0,
'top_a_counter_textgenerationwebui': 0,
'mirostat_mode_textgenerationwebui': 0,
@@ -986,6 +989,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
let logprobs = null;
const swipes = [];
const toolCalls = [];
+ const state = { reasoning: '' };
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -1002,9 +1006,10 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
const newText = data?.choices?.[0]?.text || data?.content || '';
text += newText;
logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
+ state.reasoning += data?.choices?.[0]?.reasoning ?? '';
}
- yield { text, swipes, logprobs, toolCalls };
+ yield { text, swipes, logprobs, toolCalls, state };
}
};
}
@@ -1265,6 +1270,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'truncation_length': max_context,
'ban_eos_token': settings.ban_eos_token,
'skip_special_tokens': settings.skip_special_tokens,
+ 'include_reasoning': settings.include_reasoning,
'top_a': settings.top_a,
'tfs': settings.tfs,
'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,
diff --git a/public/scripts/tokenizers.js b/public/scripts/tokenizers.js
index 59d55ba7f..9a4995206 100644
--- a/public/scripts/tokenizers.js
+++ b/public/scripts/tokenizers.js
@@ -679,6 +679,9 @@ export function getTokenizerModel() {
}
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
+ if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
+ return deepseekTokenizer;
+ }
if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
return llama3Tokenizer;
}
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index a479aee52..f49fe7b64 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
/**
* Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
- *
+ * The action will be executed after the animation ends or after the timeout, whichever comes first.
* @param {HTMLElement} control - The control element to listen for animation end event
* @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
+ * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
*/
-export function runAfterAnimation(control, callback) {
+export function runAfterAnimation(control, callback, timeout = 500) {
if (hasAnimation(control)) {
- const onAnimationEnd = () => {
- control.removeEventListener('animationend', onAnimationEnd);
- callback(control);
- };
- control.addEventListener('animationend', onAnimationEnd);
+ Promise.race([
+ new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
+ new Promise((r) => control.addEventListener('animationend', r, { once: true })),
+ ]).finally(() => callback(control));
} else {
callback(control);
}
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index fd533f656..0a8412ffd 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
const MAX_LOOPS = 100;
-function getLocalVariable(name, args = {}) {
+export function getLocalVariable(name, args = {}) {
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
}
-function setLocalVariable(name, value, args = {}) {
+export function setLocalVariable(name, value, args = {}) {
if (!name) {
throw new Error('Variable name cannot be empty or undefined.');
}
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
return value;
}
-function getGlobalVariable(name, args = {}) {
+export function getGlobalVariable(name, args = {}) {
let globalVariable = extension_settings.variables.global[args.key ?? name];
if (args.index !== undefined) {
try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
}
-function setGlobalVariable(name, value, args = {}) {
+export function setGlobalVariable(name, value, args = {}) {
if (!name) {
throw new Error('Variable name cannot be empty or undefined.');
}
diff --git a/public/style.css b/public/style.css
index 95dcd314c..fc2a4728a 100644
--- a/public/style.css
+++ b/public/style.css
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
filter: grayscale(25%);
}
-.mes_text table {
+.mes_text table,
+.mes_reasoning table {
border-spacing: 0;
border-collapse: collapse;
margin-bottom: 10px;
}
.mes_text td,
-.mes_text th {
+.mes_text th,
+.mes_reasoning td,
+.mes_reasoning th {
border: 1px solid;
border-collapse: collapse;
padding: 0.25em;
}
-.mes_text p {
+.mes_text p,
+.mes_reasoning p {
margin-top: 0;
margin-bottom: 10px;
}
-.mes_text li tt {
+.mes_text li tt,
+.mes_reasoning li tt {
display: inline-block;
}
.mes_text ol,
-.mes_text ul {
+.mes_text ul,
+.mes_reasoning ol,
+.mes_reasoning ul {
margin-top: 5px;
margin-bottom: 5px;
}
.mes_text br,
-.mes_bias br {
+.mes_bias br,
+.mes_reasoning br {
content: ' ';
}
@@ -332,25 +340,88 @@ input[type='checkbox']:focus-visible {
color: var(--SmartThemeQuoteColor);
}
+.mes_reasoning {
+ display: block;
+ border: 1px solid var(--SmartThemeBorderColor);
+ background-color: var(--black30a);
+ border-radius: 5px;
+ padding: 5px;
+ margin: 5px 0;
+ overflow-y: auto;
+}
+
+.mes_reasoning_summary {
+ cursor: pointer;
+ position: relative;
+ margin: 2px;
+}
+
+@supports not selector(:has(*)) {
+ .mes_reasoning_details {
+ display: none !important;
+ }
+}
+
+.mes_bias:empty,
+.mes_reasoning:empty,
+.mes_reasoning_details:has(.mes_reasoning:empty),
+.mes_block:has(.edit_textarea) .mes_reasoning_details,
+.mes_reasoning_details:not([open]) .mes_reasoning_actions,
+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
+ display: none;
+}
+
+.mes_reasoning_actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ display: flex;
+ gap: 4px;
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ transition: all 200ms;
+ overflow-x: hidden;
+ padding: 1px;
+}
+
+.mes_reasoning_summary>span {
+ margin-left: 0.5em;
+}
+
.mes_text i,
-.mes_text em {
+.mes_text em,
+.mes_reasoning i,
+.mes_reasoning em {
color: var(--SmartThemeEmColor);
}
-.mes_text u {
+.mes_text q i,
+.mes_text q em {
+ color: inherit;
+}
+
+.mes_text u,
+.mes_reasoning u {
color: var(--SmartThemeUnderlineColor);
}
-.mes_text q {
+.mes_text q,
+.mes_reasoning q {
color: var(--SmartThemeQuoteColor);
}
.mes_text font[color] em,
-.mes_text font[color] i {
- color: inherit;
-}
-
-.mes_text font[color] q {
+.mes_text font[color] i,
+.mes_text font[color] u,
+.mes_text font[color] q,
+.mes_reasoning font[color] em,
+.mes_reasoning font[color] i,
+.mes_reasoning font[color] u,
+.mes_reasoning font[color] q {
color: inherit;
}
@@ -358,7 +429,8 @@ input[type='checkbox']:focus-visible {
display: block;
}
-.mes_text blockquote {
+.mes_text blockquote,
+.mes_reasoning blockquote {
border-left: 3px solid var(--SmartThemeQuoteColor);
padding-left: 10px;
background-color: var(--black30a);
@@ -368,18 +440,24 @@ input[type='checkbox']:focus-visible {
.mes_text strong em,
.mes_text strong,
.mes_text h2,
-.mes_text h1 {
+.mes_text h1,
+.mes_reasoning strong em,
+.mes_reasoning strong,
+.mes_reasoning h2,
+.mes_reasoning h1 {
font-weight: bold;
}
-.mes_text pre code {
+.mes_text pre code,
+.mes_reasoning pre code {
position: relative;
display: block;
overflow-x: auto;
padding: 1em;
}
-.mes_text img:not(.mes_img) {
+.mes_text img:not(.mes_img),
+.mes_reasoning img:not(.mes_img) {
max-width: 100%;
max-height: var(--doc-height);
}
@@ -1022,6 +1100,11 @@ body .panelControlBar {
/*only affects bubblechat to make it sit nicely at the bottom*/
}
+.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
+ margin-bottom: 30px;
+}
+
+.last_mes .mes_reasoning,
.last_mes .mes_text {
padding-right: 30px;
}
@@ -1235,14 +1318,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
overflow-y: clip;
}
-.mes_text {
+.mes_text,
+.mes_reasoning {
font-weight: 500;
line-height: calc(var(--mainFontSize) + .5rem);
+ max-width: 100%;
+ overflow-wrap: anywhere;
+}
+
+.mes_text {
padding-left: 0;
padding-top: 5px;
padding-bottom: 5px;
- max-width: 100%;
- overflow-wrap: anywhere;
}
br {
@@ -4150,10 +4237,12 @@ input[type="range"]::-webkit-slider-thumb {
align-items: center;
}
+.mes_reasoning_edit_cancel,
.mes_edit_cancel.menu_button {
background-color: var(--crimson70a);
}
+.mes_reasoning_edit_done,
.mes_edit_done.menu_button {
background-color: var(--okGreen70a);
}
@@ -4162,6 +4251,7 @@ input[type="range"]::-webkit-slider-thumb {
opacity: 1;
}
+.reasoning_edit_textarea,
.edit_textarea {
padding: 5px;
margin: 0;
diff --git a/src/constants.js b/src/constants.js
index 35118a04b..afb00cd5b 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -369,6 +369,7 @@ export const OPENROUTER_KEYS = [
'prompt',
'stop',
'provider',
+ 'include_reasoning',
];
// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index 884a95c65..5c9f878e5 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -288,7 +288,8 @@ async function sendMakerSuiteRequest(request, response) {
const model = String(request.body.model);
const stream = Boolean(request.body.stream);
- const showThoughts = Boolean(request.body.show_thoughts);
+ const showThoughts = Boolean(request.body.include_reasoning);
+ const isThinking = model.includes('thinking');
const generationConfig = {
stopSequences: request.body.stop,
@@ -329,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
body.systemInstruction = prompt.system_instruction;
}
+ if (isThinking && showThoughts) {
+ generationConfig.thinkingConfig = {
+ includeThoughts: true,
+ };
+ }
+
return body;
}
@@ -342,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
controller.abort();
});
- const isThinking = model.includes('thinking');
const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
@@ -387,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
const responseContent = candidates[0].content ?? candidates[0].output;
console.log('Google AI Studio response:', responseContent);
- if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {
- responseContent.parts = responseContent.parts.filter(part => !part.thought);
- }
-
- const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
+ const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
if (!responseText) {
let message = 'Google AI Studio Candidate text empty';
console.log(message, generateResponseJson);
@@ -399,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
}
// Wrap it back to OAI format
- const reply = { choices: [{ 'message': { 'content': responseText } }] };
+ const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
return response.send(reply);
}
} catch (error) {
@@ -996,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
bodyParams['route'] = 'fallback';
}
+ if (request.body.include_reasoning) {
+ bodyParams['include_reasoning'] = true;
+ }
+
let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
diff --git a/src/endpoints/search.js b/src/endpoints/search.js
index c999f8264..1ba9977cf 100644
--- a/src/endpoints/search.js
+++ b/src/endpoints/search.js
@@ -162,7 +162,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
router.post('/searxng', jsonParser, async (request, response) => {
try {
- const { baseUrl, query, preferences } = request.body;
+ const { baseUrl, query, preferences, categories } = request.body;
if (!baseUrl || !query) {
console.log('Missing required parameters for /searxng');
@@ -193,6 +193,9 @@ router.post('/searxng', jsonParser, async (request, response) => {
if (preferences) {
searchParams.append('preferences', preferences);
}
+ if (categories) {
+ searchParams.append('categories', categories);
+ }
searchUrl.search = searchParams.toString();
const searchResult = await fetch(searchUrl, { headers: visitHeaders });
@@ -220,7 +223,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
return response.sendStatus(400);
}
- const { query } = request.body;
+ const { query, include_images } = request.body;
const body = {
query: query,
@@ -229,7 +232,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
topic: 'general',
include_answer: true,
include_raw_content: false,
- include_images: false,
+ include_images: !!include_images,
include_image_descriptions: false,
include_domains: [],
max_results: 10,
@@ -297,6 +300,7 @@ router.post('/koboldcpp', jsonParser, async (request, response) => {
router.post('/visit', jsonParser, async (request, response) => {
try {
const url = request.body.url;
+ const html = Boolean(request.body.html ?? true);
if (!url) {
console.log('No url provided for /visit');
@@ -340,13 +344,20 @@ router.post('/visit', jsonParser, async (request, response) => {
}
const contentType = String(result.headers.get('content-type'));
- if (!contentType.includes('text/html')) {
- console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
- return response.sendStatus(500);
+
+ if (html) {
+ if (!contentType.includes('text/html')) {
+ console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
+ return response.sendStatus(500);
+ }
+
+ const text = await result.text();
+ return response.send(text);
}
- const text = await result.text();
- return response.send(text);
+ response.setHeader('Content-Type', contentType);
+ const buffer = await result.arrayBuffer();
+ return response.send(Buffer.from(buffer));
} catch (error) {
console.log(error);
return response.sendStatus(500);
diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js
index 94e64d1fd..902d06727 100644
--- a/src/endpoints/stable-diffusion.js
+++ b/src/endpoints/stable-diffusion.js
@@ -533,6 +533,7 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => {
comfy.post('/generate', jsonParser, async (request, response) => {
try {
+ let item;
const url = new URL(urlJoin(request.body.url, '/prompt'));
const controller = new AbortController();
@@ -557,7 +558,6 @@ comfy.post('/generate', jsonParser, async (request, response) => {
/** @type {any} */
const data = await promptResult.json();
const id = data.prompt_id;
- let item;
const historyUrl = new URL(urlJoin(request.body.url, '/history'));
while (true) {
const result = await fetch(historyUrl);
diff --git a/src/endpoints/tokenizers.js b/src/endpoints/tokenizers.js
index 5f693b751..bb7358f91 100644
--- a/src/endpoints/tokenizers.js
+++ b/src/endpoints/tokenizers.js
@@ -398,7 +398,7 @@ function getWebTokenizersChunks(tokenizer, ids) {
* @returns {string} Tokenizer model to use
*/
export function getTokenizerModel(requestModel) {
- if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini')) {
+ if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini') || requestModel.includes('o3-mini')) {
return 'gpt-4o';
}