Merge branch 'staging' into support-multiple-expressions

25ffc4ac4ae9ac54e877c25266aeec1e4bb64d73

Wolfsblvt <wolfsblvt@gmail.com>

38 files changed, +1269 -304Ignore whitespace
.github/readme.md+1 -1
@@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2742741. Open your Command Line
2752752. Run the following command
276276
277277`docker createrun --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
278278
279279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
280280
public/css/toggle-dependent.css+5 -0
@@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
472472 display: none;
473473}
474474
475+label[for="trim_spaces"]:not(:has(input:checked)) small {
476+ color: var(--warning);
477+ opacity: 1;
478+}
479+
475480#claude_function_prefill_warning {
476481 display: none;
477482 color: red;
public/index.html+70 -28
@@ -1587,6 +1587,10 @@
15871587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
15881588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
15891589 </label>
1590+ <label data-tg-type="openrouter" class="checkbox_label flexGrow flexShrink" for="include_reasoning_textgenerationwebui">
1591+ <input type="checkbox" id="include_reasoning_textgenerationwebui" />
1592+ <small data-i18n="Request Model Reasoning">Request Model Reasoning</small>
1593+ </label>
15901594 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
15911595 <input type="checkbox" id="temperature_last_textgenerationwebui" />
15921596 <label>
@@ -1977,17 +1981,17 @@
19771981 </span>
19781982 </div>
19791983 </div>
19801984 <div class="range-block" data-source="makersuite,deepseek,openrouter">
19811985 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19821986 <input id="openai_show_thoughts" type="checkbox" />
19831987 <span>
19841988 <span data-i18n="ShowRequest model thoughtsreasoning">ShowRequest model thoughtsreasoning</span>
19851989 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
19861990 </span>
19871991 </label>
19881992 <div class="toggle-description justifyLeft marginBot5">
19891993 <span data-i18n="DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.">
19901994 DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.
19911995 </span>
19921996 </div>
19931997 </div>
@@ -3227,32 +3231,19 @@
32273231 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
32283232 <select id="model_perplexity_select">
32293233 <optgroup label="Perplexity Sonar Models">
3234+ <option value="sonar">sonar</option>
3235+ <option value="sonar-pro">sonar-pro</option>
3236+ <option value="sonar-reasoning">sonar-reasoning</option>
3237+ </optgroup>
3238+ <optgroup label="Deprecated Models">
3239+ <!-- These are scheduled for deprecation after 2/22/2025 -->
32303240 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
32313241 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
32323242 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
3233- </optgroup>
3243+ <!-- These are not listed on the site anymore -->
3234- <optgroup label="Perplexity Chat Models">
32353244 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
32363245 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
32373246 </optgroup>
3238- <optgroup label="Open-Source Models">
3239- <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
3240- <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
3241- </optgroup>
3242- <optgroup label="Deprecated Models">
3243- <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
3244- <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
3245- <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
3246- <option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
3247- <option value="sonar-small-chat">sonar-small-chat</option>
3248- <option value="sonar-small-online">sonar-small-online</option>
3249- <option value="sonar-medium-chat">sonar-medium-chat</option>
3250- <option value="sonar-medium-online">sonar-medium-online</option>
3251- <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
3252- <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
3253- <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
3254- <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
3255- </optgroup>
32563247 </select>
32573248 </div>
32583249 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@@ -3800,6 +3791,45 @@
38003791 </div>
38013792 </div>
38023793 <div>
3794+ <h4 class="standoutHeader">
3795+ <span data-i18n="Reasoning">Reasoning</span>
3796+ </h4>
3797+ <div>
3798+ <label class="checkbox_label" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
3799+ <input id="reasoning_auto_parse" type="checkbox" />
3800+ <small data-i18n="Auto-Parse Reasoning">
3801+ Auto-Parse Reasoning
3802+ </small>
3803+ </label>
3804+ <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3805+ <input id="reasoning_add_to_prompts" type="checkbox" />
3806+ <small data-i18n="Add Reasoning to Prompts">
3807+ Add Reasoning to Prompts
3808+ </small>
3809+ </label>
3810+ <div class="flex-container">
3811+ <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3812+ <small data-i18n="Prefix">Prefix</small>
3813+ <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3814+ </div>
3815+ <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3816+ <small data-i18n="Suffix">Suffix</small>
3817+ <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3818+ </div>
3819+ </div>
3820+ <div class="flex-container">
3821+ <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3822+ <small data-i18n="Separator">Separator</small>
3823+ <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3824+ </div>
3825+ <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3826+ <small data-i18n="Max Additions">Max Additions</small>
3827+ <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3828+ </div>
3829+ </div>
3830+ </div>
3831+ </div>
3832+ <div>
38033833 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
38043834 <div>
38053835 <small>
@@ -6221,14 +6251,26 @@
62216251 <div class="mes_edit_buttons">
62226252 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
62236253 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
62246254 <div class="mes_edit_deletemes_edit_add_reasoning menu_button fa-solid fa-trash-canlightbulb" title="DeleteAdd thisa messagereasoning block" data-i18n="[title]DeleteAdd thisa messagereasoning block"></div>
6225- </div>
6255+ <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
62266256 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
62276257 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down"></div>
6228- </div>
62296258 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
62306259 </div>
62316260 </div>
6261+ <details class="mes_reasoning_details">
6262+ <summary class="mes_reasoning_summary">
6263+ <span data-i18n="Reasoning">Reasoning</span>
6264+ <div class="mes_reasoning_actions">
6265+ <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6266+ <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6267+ <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6268+ <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6269+ <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6270+ </div>
6271+ </summary>
6272+ <div class="mes_reasoning"></div>
6273+ </details>
62326274 <div class="mes_text"></div>
62336275 <div class="mes_img_container">
62346276 <div class="mes_img_controls">
public/locales/fr-fr.json+2 -2
@@ -1385,8 +1385,8 @@
13851385 "enable_functions_desc_1": "Autorise l'utilisation",
13861386 "enable_functions_desc_2": "outils de fonction",
13871387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
13881388 "ShowRequest model thoughtsreasoning": "AfficherDemander les pensées du modèle",
13891389 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "AfficherPermet lesau penséesmodèle internesde duretourner modèleson dansprocessus lade réponseréflexion.",
13901390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
13911391 "openai_logit_bias_no_items": "Aucun élément",
13921392 "api_no_connection": "Pas de connection...",
public/locales/zh-cn.json+2 -2
@@ -266,8 +266,8 @@
266266 "Use system prompt": "使用系统提示词",
267267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268268 "Merges_all_system_messages_desc_2": "字段发送。",
269269 "ShowRequest model thoughtsreasoning": "展示思维链请求思维链",
270270 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "展示模型在回复时的内部思维链允许模型返回其思维过程。",
271271 "Assistant Prefill": "AI预填",
272272 "Expand the editor": "展开编辑器",
273273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",
public/locales/zh-tw.json+2 -2
@@ -2356,8 +2356,8 @@
23562356 "Forbid": "禁止",
23572357 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
23582358 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
23592359 "ShowRequest model thoughtsreasoning": "顯示模型思維鏈請求模型思維鏈",
23602360 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "在回應中顯示模型的思維鏈(內部思考過程)讓模型回傳其思考過程。",
23612361 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
23622362 "Model ID (optional)": "模型 ID(可選)",
23632363 "DeepSeek API Key": "DeepSeek API 金鑰",
public/script.js+206 -109
@@ -95,6 +95,7 @@ import {
9595 resetMovableStyles,
9696 forceCharacterEditorTokenize,
9797 applyPowerUserSettings,
98+ generatedTextFiltered,
9899} from './scripts/power-user.js';
99100
100101import {
@@ -169,6 +170,7 @@ import {
169170 toggleDrawer,
170171 isElementInViewport,
171172 copyText,
173+ escapeHtml,
172174} from './scripts/utils.js';
173175import { debounce_timeout } from './scripts/constants.js';
174176
@@ -267,6 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267269import { initBulkEdit } from './scripts/bulk-edit.js';
268270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269271import { getContext } from './scripts/st-context.js';
272+import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
270273
271274// API OBJECT FOR EXTERNAL WIRING
272275globalThis.SillyTavern = {
@@ -982,6 +985,7 @@ async function firstLoadInit() {
982985 initServerHistory();
983986 initSettingsSearch();
984987 initBulkEdit();
988+ initReasoning();
985989 await initScrapers();
986990 doDailyExtensionUpdatesCheck();
987991 await hideLoader();
@@ -1991,14 +1995,15 @@ export async function sendTextareaMessage() {
19911995 * @param {boolean} isUser If the message was sent by the user
19921996 * @param {number} messageId Message index in chat array
19931997 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
1998+ * @param {boolean} [isReasoning] If the message is reasoning output
19941999 * @returns {string} HTML string
19952000 */
19962001export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
19972002 if (!mes) {
19982003 return '';
19992004 }
20002005
20012006 if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
20022007 const mesBeforeReplace = mes;
20032008 const chatMessage = chat[messageId];
20042009 mes = substituteParams(mes, undefined, ch_name);
@@ -2027,6 +2032,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20272032 if (!isSystem) {
20282033 function getRegexPlacement() {
20292034 try {
2035+ if (isReasoning) {
2036+ return regex_placement.REASONING;
2037+ }
20302038 if (isUser) {
20312039 return regex_placement.USER_INPUT;
20322040 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2060,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20602068 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
20612069 }
20622070
2071+ // Make sure reasoning strings are always shown, even if they include "<" or ">"
2072+ [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2073+ if (!reasoningString || !reasoningString.trim().length) {
2074+ return;
2075+ }
2076+ // Only replace the first occurrence of the reasoning string
2077+ if (mes.includes(reasoningString)) {
2078+ mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2079+ }
2080+ });
2081+
20632082 if (!isSystem) {
20642083 // Save double quotes in tags as a special character to prevent them from being encoded
20652084 if (!power_user.encode_tags) {
@@ -2200,6 +2219,7 @@ function getMessageFromTemplate({
22002219 isUser,
22012220 avatarImg,
22022221 bias,
2222+ reasoning,
22032223 isSystem,
22042224 title,
22052225 timerValue,
@@ -2224,6 +2244,7 @@ function getMessageFromTemplate({
22242244 mes.find('.avatar img').attr('src', avatarImg);
22252245 mes.find('.ch_name .name_text').text(characterName);
22262246 mes.find('.mes_bias').html(bias);
2247+ mes.find('.mes_reasoning').html(reasoning);
22272248 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
22282249 mes.find('.mesIDDisplay').text(`#${mesId}`);
22292250 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2238,10 +2259,16 @@ function getMessageFromTemplate({
22382259 return mes;
22392260}
22402261
2262+/**
2263+ * Re-renders a message block with updated content.
2264+ * @param {number} messageId Message ID
2265+ * @param {object} message Message object
2266+ */
22412267export function updateMessageBlock(messageId, message) {
22422268 const messageElement = $(`#chat [mesid="${messageId}"]`);
22432269 const text = message?.extra?.display_text ?? message.mes;
22442270 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2271+ messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
22452272 addCopyToCodeBlocks(messageElement);
22462273 appendMediaToMessage(message, messageElement);
22472274}
@@ -2398,8 +2425,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23982425 mes.is_user,
23992426 chat.indexOf(mes),
24002427 sanitizerOverrides,
2428+ false,
24012429 );
24022430 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2431+ const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
24032432 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24042433
24052434 let params = {
@@ -2409,6 +2438,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24092438 isUser: mes.is_user,
24102439 avatarImg: avatarImg,
24112440 bias: bias,
2441+ reasoning: reasoning,
24122442 isSystem: isSystem,
24132443 title: title,
24142444 bookmarkLink: bookmarkLink,
@@ -2468,6 +2498,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24682498 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
24692499 swipeMessage.attr('swipeid', params.swipeId);
24702500 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2501+ swipeMessage.find('.mes_reasoning').html(reasoning);
24712502 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
24722503 appendMediaToMessage(mes, swipeMessage);
24732504 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3044,7 +3075,7 @@ export function isStreamingEnabled() {
30443075 (main_api == 'openai' &&
30453076 oai_settings.stream_openai &&
30463077 !noStreamSources.includes(oai_settings.chat_completion_source) &&
30473078 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('o1-') || oai_settings.openai_model.startsWith('o3'))) &&
30483079 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
30493080 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
30503081 || (main_api == 'novel' && nai_settings.streaming_novel)
@@ -3078,6 +3109,7 @@ class StreamingProcessor {
30783109 this.messageTextDom = null;
30793110 this.messageTimerDom = null;
30803111 this.messageTokenCounterDom = null;
3112+ this.messageReasoningDom = null;
30813113 /** @type {HTMLTextAreaElement} */
30823114 this.sendTextarea = document.querySelector('#send_textarea');
30833115 this.type = type;
@@ -3093,6 +3125,7 @@ class StreamingProcessor {
30933125 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
30943126 this.messageLogprobs = [];
30953127 this.toolCalls = [];
3128+ this.reasoning = '';
30963129 }
30973130
30983131 #checkDomElements(messageId) {
@@ -3101,6 +3134,7 @@ class StreamingProcessor {
31013134 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
31023135 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
31033136 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3137+ this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
31043138 }
31053139 }
31063140
@@ -3137,7 +3171,7 @@ class StreamingProcessor {
31373171 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
31383172 }
31393173 else {
31403174 await saveReply(this.type, text, true, '', [], '');
31413175 messageId = chat.length - 1;
31423176 this.#checkDomElements(messageId);
31433177 this.showMessageButtons(messageId);
@@ -3178,18 +3212,27 @@ class StreamingProcessor {
31783212 this.#checkDomElements(messageId);
31793213 this.#updateMessageBlockVisibility();
31803214 const currentTime = new Date();
3181- // Don't waste time calculating token count for streaming
3182- const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
3183- const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
31843215 chat[messageId]['mes'] = processedText;
31853216 chat[messageId]['gen_started'] = this.timeStarted;
31863217 chat[messageId]['gen_finished'] = currentTime;
31873218
31883219 if (currentTokenCount!chat[messageId]['extra']) {
31893220 if (!chat[messageId]['extra']) = {};
3190- chat[messageId]['extra'] = {};
3221+ }
3222+
3223+ if (this.reasoning) {
3224+ chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
3225+ if (this.messageReasoningDom instanceof HTMLElement) {
3226+ const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3227+ this.messageReasoningDom.innerHTML = formattedReasoning;
31913228 }
3229+ }
3230+
3231+ // Don't waste time calculating token count for streaming
3232+ const tokenCountText = (this.reasoning || '') + processedText;
3233+ const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
31923234
3235+ if (currentTokenCount) {
31933236 chat[messageId]['extra']['token_count'] = currentTokenCount;
31943237 if (this.messageTokenCounterDom instanceof HTMLElement) {
31953238 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3207,14 +3250,19 @@ class StreamingProcessor {
32073250 chat[messageId].is_system,
32083251 chat[messageId].is_user,
32093252 messageId,
3253+ {},
3254+ false,
32103255 );
32113256 if (this.messageTextDom instanceof HTMLElement) {
32123257 this.messageTextDom.innerHTML = formattedText;
32133258 }
3259+
3260+ const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
32143261 if (this.messageTimerDom instanceof HTMLElement) {
32153262 this.messageTimerDom.textContent = timePassed.timerValue;
32163263 this.messageTimerDom.title = timePassed.timerTitle;
32173264 }
3265+
32183266 this.setFirstSwipe(messageId);
32193267 }
32203268
@@ -3255,39 +3303,11 @@ class StreamingProcessor {
32553303 unblockGeneration();
32563304 generatedPromptCache = '';
32573305
3258- //console.log("Generated text size:", text.length, text)
3259-
32603306 const isAborted = this.abortController.signal.aborted;
32613307 if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(text)) {
3262- function containsBlacklistedWords(str, blacklist, threshold) {
3308+ return swipe_right();
3263- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3264- const matches = str.match(regex) || [];
3265- return matches.length >= threshold;
3266- }
3267-
3268- const generatedTextFiltered = (text) => {
3269- if (text) {
3270- if (power_user.auto_swipe_minimum_length) {
3271- if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
3272- console.log('Generated text size too small');
3273- return true;
3274- }
3275- }
3276- if (power_user.auto_swipe_blacklist_threshold) {
3277- if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3278- console.log('Generated text has blacklisted words');
3279- return true;
3280- }
3281- }
3282- }
3283- return false;
3284- };
3285-
3286- if (generatedTextFiltered(text)) {
3287- swipe_right();
3288- return;
3289- }
32903309 }
3310+
32913311 playMessageSound();
32923312 }
32933313
@@ -3321,7 +3341,7 @@ class StreamingProcessor {
33213341 }
33223342
33233343 /**
33243344 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
33253345 */
33263346 *nullStreamingGeneration() {
33273347 throw new Error('Generation function for streaming is not hooked up');
@@ -3343,9 +3363,9 @@ class StreamingProcessor {
33433363 try {
33443364 const sw = new Stopwatch(1000 / power_user.streaming_fps);
33453365 const timestamps = [];
33463366 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33473367 timestamps.push(Date.now());
33483368 if (this.isStopped || this.abortController.signal.aborted) {
33493369 return;
33503370 }
33513371
@@ -3355,6 +3375,7 @@ class StreamingProcessor {
33553375 if (logprobs) {
33563376 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33573377 }
3378+ this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
33583379 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33593380 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
33603381 }
@@ -3840,6 +3861,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38403861 };
38413862 }));
38423863
3864+ const reasoning = new PromptReasoning();
3865+ for (let i = coreChat.length - 1; i >= 0; i--) {
3866+ const depth = coreChat.length - i - 1;
3867+ const isPrefix = isContinue && i === coreChat.length - 1;
3868+ coreChat[i] = {
3869+ ...coreChat[i],
3870+ mes: reasoning.addToMessage(
3871+ coreChat[i].mes,
3872+ getRegexedString(
3873+ String(coreChat[i].extra?.reasoning ?? ''),
3874+ regex_placement.REASONING,
3875+ { isPrompt: true, depth: depth },
3876+ ),
3877+ isPrefix,
3878+ ),
3879+ };
3880+ if (reasoning.isLimitReached()) {
3881+ break;
3882+ }
3883+ }
3884+
38433885 // Determine token limit
38443886 let this_max_context = getMaxContextSize();
38453887
@@ -4742,11 +4784,17 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47424784 //const getData = await response.json();
47434785 let getMessage = extractMessageFromData(data);
47444786 let title = extractTitleFromData(data);
4787+ let reasoning = extractReasoningFromData(data);
47454788 kobold_horde_model = title;
47464789
47474790 const swipes = extractMultiSwipes(data, type);
47484791
47494792 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4793+ reasoning = getRegexedString(reasoning, regex_placement.REASONING);
4794+
4795+ if (power_user.trim_spaces) {
4796+ reasoning = reasoning.trim();
4797+ }
47504798
47514799 if (isContinue) {
47524800 getMessage = continue_mag + getMessage;
@@ -4768,10 +4816,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47684816 else {
47694817 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
47704818 if (originalType !== 'continue') {
47714819 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
47724820 }
47734821 else {
47744822 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
47754823 }
47764824
47774825 // 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
48054853 }
48064854
48074855 const isAborted = abortController && abortController.signal.aborted;
48084856 if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(getMessage)) {
4809- console.debug('checking for autoswipeblacklist on non-streaming message');
4857+ is_send_press = false;
4810- function containsBlacklistedWords(getMessage, blacklist, threshold) {
4858+ return swipe_right();
4811- console.debug('checking blacklisted words');
4812- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
4813- const matches = getMessage.match(regex) || [];
4814- return matches.length >= threshold;
4815- }
4816-
4817- const generatedTextFiltered = (getMessage) => {
4818- if (power_user.auto_swipe_blacklist_threshold) {
4819- if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
4820- console.debug('Generated text has blacklisted words');
4821- return true;
4822- }
4823- }
4824-
4825- return false;
4826- };
4827- if (generatedTextFiltered(getMessage)) {
4828- console.debug('swiping right automatically');
4829- is_send_press = false;
4830- swipe_right();
4831- // TODO: do we want to resolve after an auto-swipe?
4832- return;
4833- }
48344859 }
48354860
48364861 console.debug('/api/chats/save called by /Generate');
@@ -5659,31 +5684,51 @@ function extractMessageFromData(data) {
56595684 return data;
56605685 }
56615686
56625687 functionswitch getTextContext(main_api) {
5663- switch (main_api) {
5688+ case 'kobold':
5664- case 'kobold':
5689+ return data.results[0].text;
5665- return data.results[0].text;
5690+ case 'koboldhorde':
5666- case 'koboldhorde':
5691+ return data.text;
5667- return data.text;
5692+ case 'textgenerationwebui':
5668- case 'textgenerationwebui':
5693+ return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';
5669- return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';
5694+ case 'novel':
5670- case 'novel':
5695+ return data.output;
5671- return data.output;
5696+ case 'openai':
5672- case 'openai':
5697+ return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
5673- return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
5698+ default:
5674- default:
5699+ return '';
5675- return '';
5676- }
56775700 }
5701+}
5702+
5703+/**
5704+ * Extracts the reasoning from the response data.
5705+ * @param {object} data Response data
5706+ * @returns {string} Extracted reasoning
5707+ */
5708+function extractReasoningFromData(data) {
5709+ switch (main_api) {
5710+ case 'textgenerationwebui':
5711+ switch (textgen_settings.type) {
5712+ case textgen_types.OPENROUTER:
5713+ return data?.choices?.[0]?.reasoning ?? '';
5714+ }
5715+ break;
56785716
5679- const content = getTextContext();
5717+ case 'openai':
5718+ if (!oai_settings.show_thoughts) break;
56805719
5681- if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) {
5720+ switch (oai_settings.chat_completion_source) {
5682- const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? '';
5721+ case chat_completion_sources.DEEPSEEK:
5683- return [thoughts, content].filter(x => x).join('\n\n');
5722+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
5723+ case chat_completion_sources.OPENROUTER:
5724+ return data?.choices?.[0]?.message?.reasoning ?? '';
5725+ case chat_completion_sources.MAKERSUITE:
5726+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5727+ }
5728+ break;
56845729 }
56855730
56865731 return content'';
56875732}
56885733
56895734/**
@@ -5866,7 +5911,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
58665911 return getMessage;
58675912}
58685913
58695914export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
58705915 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
58715916 chat[chat.length - 1]['is_user'])) {
58725917 type = 'normal';
@@ -5891,8 +5936,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58915936 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58925937 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58935938 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5939+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
58945940 if (power_user.message_token_count_enabled) {
58955941 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5942+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
58965943 }
58975944 const chat_id = (chat.length - 1);
58985945 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5911,8 +5958,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59115958 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59125959 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59135960 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5961+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
59145962 if (power_user.message_token_count_enabled) {
59155963 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5964+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59165965 }
59175966 const chat_id = (chat.length - 1);
59185967 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5928,8 +5977,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59285977 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59295978 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59305979 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5980+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
59315981 if (power_user.message_token_count_enabled) {
59325982 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5983+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59335984 }
59345985 const chat_id = (chat.length - 1);
59355986 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5945,6 +5996,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59455996 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59465997 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59475998 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5999+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
59486000 if (power_user.trim_spaces) {
59496001 getMessage = getMessage.trim();
59506002 }
@@ -5954,7 +6006,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59546006 chat[chat.length - 1]['gen_finished'] = generationFinished;
59556007
59566008 if (power_user.message_token_count_enabled) {
59576009 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
6010+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59586011 }
59596012
59606013 if (selected_group) {
@@ -6019,6 +6072,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
60196072 return { type, getMessage };
60206073}
60216074
6075+export function syncCurrentSwipeInfoExtras() {
6076+ if (!chat.length) {
6077+ return;
6078+ }
6079+ const currentMessage = chat[chat.length - 1];
6080+ if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6081+ const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6082+ if (swipeInfo && typeof swipeInfo === 'object') {
6083+ swipeInfo.extra = structuredClone(currentMessage.extra);
6084+ }
6085+ }
6086+}
6087+
60226088function saveImageToMessage(img, mes) {
60236089 if (mes && img.image) {
60246090 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -7122,9 +7188,11 @@ function messageEditAuto(div) {
71227188 mes.is_system,
71237189 mes.is_user,
71247190 this_edit_mes_id,
7191+ {},
7192+ false,
71257193 ));
71267194 mesBlock.find('.mes_bias').empty();
71277195 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
71287196 saveChatDebounced();
71297197}
71307198
@@ -7146,10 +7214,12 @@ async function messageEditDone(div) {
71467214 mes.is_system,
71477215 mes.is_user,
71487216 this_edit_mes_id,
7217+ {},
7218+ false,
71497219 ),
71507220 );
71517221 mesBlock.find('.mes_bias').empty();
71527222 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
71537223 appendMediaToMessage(mes, div.closest('.mes'));
71547224 addCopyToCodeBlocks(div.closest('.mes'));
71557225
@@ -7997,9 +8067,23 @@ function updateEditArrowClasses() {
79978067 }
79988068}
79998069
8000-function closeMessageEditor() {
8070+/**
8001- if (this_edit_mes_id) {
8071+ * Closes the message editor.
8002- $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8072+ * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8073+ */
8074+export function closeMessageEditor(what = 'all') {
8075+ if (what === 'message' || what === 'all') {
8076+ if (this_edit_mes_id) {
8077+ $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8078+ }
8079+ }
8080+ if (what === 'reasoning' || what === 'all') {
8081+ document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8082+ const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8083+ if (cancelButton instanceof HTMLElement) {
8084+ cancelButton.click();
8085+ }
8086+ });
80038087 }
80048088}
80058089
@@ -8432,6 +8516,9 @@ function swipe_left() { // when we swipe left..but no generation.
84328516 streamingProcessor.onStopStreaming();
84338517 }
84348518
8519+ // Make sure ad-hoc changes to extras are saved before swiping away
8520+ syncCurrentSwipeInfoExtras();
8521+
84358522 const swipe_duration = 120;
84368523 const swipe_range = '700px';
84378524 chat[chat.length - 1]['swipe_id']--;
@@ -8483,7 +8570,8 @@ function swipe_left() { // when we swipe left..but no generation.
84838570 }
84848571
84858572 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
84868573 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8574+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
84878575 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
84888576 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
84898577 }
@@ -8566,6 +8654,9 @@ const swipe_right = () => {
85668654 return unblockGeneration();
85678655 }
85688656
8657+ // Make sure ad-hoc changes to extras are saved before swiping away
8658+ syncCurrentSwipeInfoExtras();
8659+
85698660 const swipe_duration = 200;
85708661 const swipe_range = 700;
85718662 //console.log(swipe_range);
@@ -8647,6 +8738,7 @@ const swipe_right = () => {
86478738 // resets the timer
86488739 swipeMessage.find('.mes_timer').html('');
86498740 swipeMessage.find('.tokenCounterDisplay').text('');
8741+ swipeMessage.find('.mes_reasoning').html('');
86508742 } else {
86518743 //console.log('showing previously generated swipe candidate, or "..."');
86528744 //console.log('onclick right swipe calling addOneMessage');
@@ -8657,7 +8749,8 @@ const swipe_right = () => {
86578749 chat[chat.length - 1].extra = {};
86588750 }
86598751
86608752 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8753+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
86618754 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
86628755 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
86638756 }
@@ -9459,7 +9552,8 @@ function addDebugFunctions() {
94599552 message.extra = {};
94609553 }
94619554
9462- message.extra.token_count = await getTokenCountAsync(message.mes, 0);
9555+ const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9556+ message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
94639557 }
94649558
94659559 await saveChatConditional();
@@ -10768,6 +10862,8 @@ jQuery(async function () {
1076810862 chat[this_edit_mes_id].is_system,
1076910863 chat[this_edit_mes_id].is_user,
1077010864 this_edit_mes_id,
10865+ {},
10866+ false,
1077110867 ));
1077210868 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
1077310869 addCopyToCodeBlocks($(this).closest('.mes'));
@@ -11228,14 +11324,15 @@ jQuery(async function () {
1122811324
1122911325 $(document).keyup(function (e) {
1123011326 if (e.key === 'Escape') {
1123111327 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
1123211328 if (isEditVisible && power_user.auto_save_msg_edits === false) {
1123311329 closeMessageEditor('all');
1123411330 $('#send_textarea').focus();
1123511331 return;
1123611332 }
1123711333 if (isEditVisible && power_user.auto_save_msg_edits === true) {
1123811334 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11335+ closeMessageEditor('reasoning');
1123911336 $('#send_textarea').focus();
1124011337 return;
1124111338 }
public/scripts/authors-note.js+1 -1
@@ -566,7 +566,7 @@ export function initAuthorsNote() {
566566 namedArgumentList: [],
567567 unnamedArgumentList: [
568568 new SlashCommandArgument(
569569 'positionrole', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
570570 ),
571571 ],
572572 helpString: `
public/scripts/backgrounds.js+19 -9
@@ -96,8 +96,13 @@ function highlightLockedBackground() {
9696 });
9797}
9898
99+/**
100+ * Locks the background for the current chat
101+ * @param {Event} e Click event
102+ * @returns {string} Empty string
103+ */
99104function onLockBackgroundClick(e) {
100105 e?.stopPropagation();
101106
102107 const chatName = getCurrentChatId();
103108
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
106111 return '';
107112 }
108113
109114 const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
110115
111116 saveBackgroundMetadata(relativeBgImage);
112117 setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
114119 return '';
115120}
116121
122+/**
123+ * Locks the background for the current chat
124+ * @param {Event} e Click event
125+ * @returns {string} Empty string
126+ */
117127function onUnlockBackgroundClick(e) {
118128 e?.stopPropagation();
119129 removeBackgroundMetadata();
120130 unsetCustomBackground();
121131 highlightLockedBackground();
@@ -482,10 +492,10 @@ function highlightNewBackground(bg) {
482492 */
483493function setFittingClass(fitting) {
484494 const backgrounds = $('#bg1, #bg_custom');
485- backgrounds.toggleClass('cover', fitting === 'cover');
495+ for (const option of ['cover', 'contain', 'stretch', 'center']) {
486496 backgrounds.toggleClass('contain'option, fittingoption === 'contain'fitting);
487- backgrounds.toggleClass('stretch', fitting === 'stretch');
497+ }
488- backgrounds.toggleClass('center', fitting === 'center');
498+ background_settings.fitting = fitting;
489499}
490500
491501function onBackgroundFilterInput() {
@@ -513,12 +523,12 @@ export function initBackgrounds() {
513523 $('#add_bg_button').on('change', onBackgroundUploadSelected);
514524 $('#bg-filter').on('input', onBackgroundFilterInput);
515525 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
516- callback: onLockBackgroundClick,
526+ callback: () => onLockBackgroundClick(new CustomEvent('click')),
517527 aliases: ['bglock'],
518528 helpString: 'Locks a background for the currently selected chat',
519529 }));
520530 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
521- callback: onUnlockBackgroundClick,
531+ callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
522532 aliases: ['bgunlock'],
523533 helpString: 'Unlocks a background for the currently selected chat',
524534 }));
public/scripts/extensions/connection-manager/index.js+4 -0
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
3030 'api-url',
3131 'model',
3232 'proxy',
33+ 'stop-strings',
3334];
3435
3536const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
4344 'context',
4445 'instruct-state',
4546 'tokenizer',
47+ 'stop-strings',
4648];
4749
4850const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
5759 'instruct': 'Instruct Template',
5860 'context': 'Context Template',
5961 'tokenizer': 'Tokenizer',
62+ 'stop-strings': 'Custom Stopping Strings',
6063};
6164
6265/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
138141 * @property {string} [context] Context Template
139142 * @property {string} [instruct-state] Instruct Mode
140143 * @property {string} [tokenizer] Tokenizer
144+ * @property {string} [stop-strings] Custom Stopping Strings
141145 * @property {string[]} [exclude] Commands to exclude
142146 */
143147
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+4 -0
@@ -883,6 +883,10 @@ export class SlashCommandHandler {
883883 }
884884 }
885885 getQuickReply(args) {
886+ if (!args.id && !args.label) {
887+ toastr.error('Please provide a valid id or label.');
888+ return '';
889+ }
886890 try {
887891 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
888892 } catch (ex) {
public/scripts/extensions/regex/editor.html+6 -0
@@ -94,6 +94,12 @@
9494 <span data-i18n="World Info">World Info</span>
9595 </label>
9696 </div>
97+ <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
98+ <label class="checkbox flex-container">
99+ <input type="checkbox" name="replace_position" value="6">
100+ <span data-i18n="Reasoning">Reasoning</span>
101+ </label>
102+ </div>
97103 <div class="flex-container wide100p marginTop5">
98104 <div class="flex1 flex-container flexNoGap">
99105 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
public/scripts/extensions/regex/engine.js+2 -1
@@ -20,6 +20,7 @@ const regex_placement = {
2020 SLASH_COMMAND: 3,
2121 // 4 - sendAs (legacy)
2222 WORLD_INFO: 5,
23+ REASONING: 6,
2324};
2425
2526export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
9495 // Script applies to Generate and input is Generate
9596 (script.promptOnly && isPrompt) ||
9697 // 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
9798 (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
9899 ) {
99100 if (isEdit && !script.runOnEdit) {
100101 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
public/scripts/extensions/regex/index.js+1 -1
@@ -18,7 +18,7 @@ import { t } from '../../i18n.js';
1818 * @property {string} replaceString - The replace string
1919 * @property {string[]} trimStrings - The trim strings
2020 * @property {string?} findRegex - The find regex
2121 * @property {stringnumber?} substituteRegex - The substitute regex
2222 */
2323
2424/**
public/scripts/extensions/tts/alltalk.js+10 -6
@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
388388 }
389389
390390 async fetchRvcVoiceObjects() {
391391 if (this.settings.server_version == 'v2v1') {
392392 console.log('Skipping RVC voices fetch for V1 server');
393393 return [];
394394 }
@@ -1031,14 +1031,18 @@ class AllTalkTtsProvider {
10311031 console.error('fetchTtsGeneration Error Response Text:', errorText);
10321032 throw new Error(`HTTP ${response.status}: ${errorText}`);
10331033 }
1034+
10341035 const data = await response.json();
10351036
1036- // Handle V1/V2 URL differences
1037+ // V1 returns a complete URL, V2 returns a relative path
10371038 const outputUrl =if (this.settings.server_version === 'v1') {
1038- ? data.output_file_url // V1 returns full URL
1039+ // V1: Use the complete URL directly from the response
1039- : `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path
1040+ return data.output_file_url;
1041+ } else {
1042+ // V2: Combine the endpoint with the relative path
1043+ return `${this.settings.provider_endpoint}${data.output_file_url}`;
1044+ }
10401045
1041- return outputUrl;
10421046 } catch (error) {
10431047 console.error('[fetchTtsGeneration] Exception caught:', error);
10441048 throw error;
public/scripts/extensions/tts/index.js+49 -6
@@ -29,6 +29,7 @@ import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
2929import { GoogleTranslateTtsProvider } from './google-translate.js';
3030
3131const UPDATE_INTERVAL = 1000;
32+const wrapper = new ModuleWorkerWrapper(moduleWorker);
3233
3334let voiceMapEntries = [];
3435let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -118,7 +119,7 @@ async function onNarrateOneMessage() {
118119 }
119120
120121 resetTtsPlayback();
121122 ttsJobQueue.pushprocessAndQueueTtsMessage(message);
122123 moduleWorker();
123124}
124125
@@ -145,7 +146,7 @@ async function onNarrateText(args, text) {
145146 }
146147
147148 resetTtsPlayback();
148149 ttsJobQueue.pushprocessAndQueueTtsMessage({ mes: text, name: name });
149150 await moduleWorker();
150151
151152 // Return back to the chat voices
@@ -197,6 +198,36 @@ function isTtsProcessing() {
197198 return processing;
198199}
199200
201+/**
202+ * Splits a message into lines and adds each non-empty line to the TTS job queue.
203+ * @param {Object} message - The message object to be processed.
204+ * @param {string} message.mes - The text of the message to be split into lines.
205+ * @param {string} message.name - The name associated with the message.
206+ * @returns {void}
207+ */
208+function processAndQueueTtsMessage(message) {
209+ if (!extension_settings.tts.narrate_by_paragraphs) {
210+ ttsJobQueue.push(message);
211+ return;
212+ }
213+
214+ const lines = message.mes.split('\n');
215+
216+ for (let i = 0; i < lines.length; i++) {
217+ const line = lines[i];
218+
219+ if (line.length === 0) {
220+ continue;
221+ }
222+
223+ ttsJobQueue.push(
224+ Object.assign({}, message, {
225+ mes: line,
226+ }),
227+ );
228+ }
229+}
230+
200231function debugTtsPlayback() {
201232 console.log(JSON.stringify(
202233 {
@@ -326,7 +357,7 @@ function onAudioControlClicked() {
326357 resetTtsPlayback();
327358 } else {
328359 // Default play behavior if not processing or playing is to play the last message.
329360 ttsJobQueue.pushprocessAndQueueTtsMessage(context.chat[context.chat.length - 1]);
330361 }
331362 updateUiAudioPlayState();
332363}
@@ -351,6 +382,7 @@ function completeCurrentAudioJob() {
351382 audioQueueProcessorReady = true;
352383 currentAudioJob = null;
353384 // updateUiPlayState();
385+ wrapper.update();
354386}
355387
356388/**
@@ -440,7 +472,7 @@ async function processTtsQueue() {
440472 }
441473
442474 if (extension_settings.tts.skip_tags) {
443475 text = text.replace(/<.*?>.[\s\S]*?<\/.*?>/g, '').trim();
444476 }
445477
446478 if (!extension_settings.tts.pass_asterisks) {
@@ -543,6 +575,7 @@ function loadSettings() {
543575 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
544576 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
545577 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
578+ $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
546579 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
547580 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
548581 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -612,6 +645,11 @@ function onPeriodicAutoGenerationClick() {
612645 saveSettingsDebounced();
613646}
614647
648+function onNarrateByParagraphsClick() {
649+ extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
650+ saveSettingsDebounced();
651+}
652+
615653
616654function onNarrateDialoguesClick() {
617655 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -790,7 +828,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
790828 lastChatId = context.chatId;
791829
792830 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
793- ttsJobQueue.push(message);
831+
832+ if (extension_settings.tts.periodic_auto_generation) {
833+ ttsJobQueue.push(message);
834+ } else {
835+ processAndQueueTtsMessage(message);
836+ }
794837}
795838
796839async function onMessageDeleted() {
@@ -1130,6 +1173,7 @@ jQuery(async function () {
11301173 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
11311174 $('#tts_auto_generation').on('click', onAutoGenerationClick);
11321175 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1176+ $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
11331177 $('#tts_narrate_user').on('click', onNarrateUserClick);
11341178
11351179 $('#playback_rate').on('input', function () {
@@ -1151,7 +1195,6 @@ jQuery(async function () {
11511195 loadSettings(); // Depends on Extension Controls and loadTtsProvider
11521196 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
11531197 addAudioControl(); // Depends on Extension Controls
1154- const wrapper = new ModuleWorkerWrapper(moduleWorker);
11551198 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
11561199 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
11571200 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
public/scripts/extensions/tts/settings.html+4 -0
@@ -30,6 +30,10 @@
3030 <input type="checkbox" id="tts_periodic_auto_generation">
3131 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
3232 </label>
33+ <label class="checkbox_label" for="tts_narrate_by_paragraphs">
34+ <input type="checkbox" id="tts_narrate_by_paragraphs">
35+ <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
36+ </label>
3337 <label class="checkbox_label" for="tts_narrate_quoted">
3438 <input type="checkbox" id="tts_narrate_quoted">
3539 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188188 if (data?.token) {
189189 text += data.token;
190190 }
191191 yield { text, swipes: [], toolCalls: [], state: {} };
192192 }
193193 };
194194}
public/scripts/loader.js+33 -12
@@ -27,24 +27,45 @@ export async function hideLoader() {
2727 }
2828
2929 return new Promise((resolve) => {
30- // Spinner blurs/fades out
30+ const spinner = $('#load-spinner');
31- $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
31+ if (!spinner.length) {
32+ console.warn('Spinner element not found, skipping animation');
33+ cleanup();
34+ return;
35+ }
36+
37+ // Check if transitions are enabled
38+ const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
39+ const hasTransitions = parseFloat(transitionDuration) > 0;
40+
41+ if (hasTransitions) {
42+ Promise.race([
43+ new Promise((r) => setTimeout(r, 500)), // Fallback timeout
44+ new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
45+ ]).finally(cleanup);
46+ } else {
47+ cleanup();
48+ }
49+
50+ function cleanup() {
3251 $('#loader').remove();
3352 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
3453 // If it's present, we remove it once and then it's gone.
3554 yoinkPreloader();
3655
3756 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
38- loaderPopup = null;
57+ .catch((err) => console.error('Error completing loaderPopup:', err))
39- resolve();
58+ .finally(() => {
40- });
59+ loaderPopup = null;
41- });
60+ resolve();
61+ });
62+ }
4263
43- $('#load-spinner')
64+ // Apply the styles
4465 spinner.css({
4566 'filter': 'blur(15px)',
4667 'opacity': '0',
4768 });
4869 });
4970}
5071
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746746 text += data.token;
747747 }
748748
749749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750750 }
751751 };
752752}
public/scripts/openai.js+32 -35
@@ -258,7 +258,7 @@ const default_settings = {
258258 ai21_model: 'jamba-1.5-large',
259259 mistralai_model: 'mistral-large-latest',
260260 cohere_model: 'command-r-plus',
261261 perplexity_model: 'llama-3.1-70bsonar-instructpro',
262262 groq_model: 'llama-3.1-70b-versatile',
263263 nanogpt_model: 'gpt-4o-mini',
264264 zerooneai_model: 'yi-large',
@@ -298,7 +298,7 @@ const default_settings = {
298298 names_behavior: character_names_behavior.DEFAULT,
299299 continue_postfix: continue_postfix_types.SPACE,
300300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301301 show_thoughts: falsetrue,
302302 seed: -1,
303303 n: 1,
304304};
@@ -337,7 +337,7 @@ const oai_settings = {
337337 ai21_model: 'jamba-1.5-large',
338338 mistralai_model: 'mistral-large-latest',
339339 cohere_model: 'command-r-plus',
340340 perplexity_model: 'llama-3.1-70bsonar-instructpro',
341341 groq_model: 'llama-3.1-70b-versatile',
342342 nanogpt_model: 'gpt-4o-mini',
343343 zerooneai_model: 'yi-large',
@@ -377,7 +377,7 @@ const oai_settings = {
377377 names_behavior: character_names_behavior.DEFAULT,
378378 continue_postfix: continue_postfix_types.SPACE,
379379 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380380 show_thoughts: falsetrue,
381381 seed: -1,
382382 n: 1,
383383};
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
10961096 // Unordered prompts without marker
10971097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
10981098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099- { role: 'system', content: bias, identifier: 'bias' },
11001099 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1100+ { role: 'assistant', content: bias, identifier: 'bias' },
11011101 ];
11021102
11031103 // Tavern Extras - Summary
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
18691869 const isQuiet = type === 'quiet';
18701870 const isImpersonate = type === 'impersonate';
18711871 const isContinue = type === 'continue';
18721872 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')));
18731873 const useLogprobs = !!power_user.request_token_probabilities;
18741874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1913,7 +1913,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19131913 'user_name': name1,
19141914 'char_name': name2,
19151915 'group_names': getGroupNames(),
19161916 'show_thoughtsinclude_reasoning': Boolean(oai_settings.show_thoughts),
19171917 };
19181918
19191919 // Empty array will produce a validation error
@@ -2050,7 +2050,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20502050 await ToolManager.registerFunctionToolsOpenAI(generate_data);
20512051 }
20522052
20532053 if (isOAI && (oai_settings.openai_model.startsWith('o1-') || oai_settings.openai_model.startsWith('o3'))) {
20542054 generate_data.messages.forEach((msg) => {
20552055 if (msg.role === 'system') {
20562056 msg.role = 'user';
@@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20952095 let text = '';
20962096 const swipes = [];
20972097 const toolCalls = [];
20982098 const state = { reasoning: '' };
20992099 while (true) {
21002100 const { done, value } = await reader.read();
21012101 if (done) return;
@@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21132113
21142114 ToolManager.parseToolCalls(toolCalls, parsed);
21152115
21162116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
21172117 }
21182118 };
21192119 }
@@ -2150,17 +2150,23 @@ function getStreamingReply(data, state) {
21502150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
21512151 return data?.delta?.text || '';
21522152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2153- return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';
2153+ if (oai_settings.show_thoughts) {
2154+ state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2155+ }
2156+ return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
21542157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
21552158 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
21562159 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2157- const hadThoughts = state.hadThoughts;
2160+ if (oai_settings.show_thoughts) {
21582161 const thoughts state.reasoning += (data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2159- const content = data.choices?.[0]?.delta?.content || '';
2162+ }
2160- state.hadThoughts = !!thoughts;
2163+ return data.choices?.[0]?.delta?.content || '';
2161- const separator = hadThoughts && !thoughts ? '\n\n' : '';
2164+ } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2162- return [thoughts, separator, content].filter(x => x).join('\n\n');
2165+ if (oai_settings.show_thoughts) {
2163- } else {
2166+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167+ }
2168+ return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2169+ } else {
21642170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21652171 }
21662172}
@@ -4021,7 +4027,7 @@ function getMaxContextOpenAI(value) {
40214027 if (oai_settings.max_context_unlocked) {
40224028 return unlocked_max;
40234029 }
40244030 else if (value.startsWith('o1-') || value.startsWith('o3')) {
40254031 return max_128k;
40264032 }
40274033 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() {
43744380 if (oai_settings.max_context_unlocked) {
43754381 $('#openai_max_context').attr('max', unlocked_max);
43764382 }
4383+ else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
4384+ $('#openai_max_context').attr('max', 127000);
4385+ }
4386+ else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
4387+ $('#openai_max_context').attr('max', 200000);
4388+ }
43774389 else if (oai_settings.perplexity_model.includes('llama-3.1')) {
43784390 const isOnline = oai_settings.perplexity_model.includes('online');
43794391 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
43804392 $('#openai_max_context').attr('max', contextSize);
43814393 }
4382- else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
4383- $('#openai_max_context').attr('max', max_32k);
4384- }
4385- else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
4386- $('#openai_max_context').attr('max', 28000);
4387- }
4388- 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)) {
4389- $('#openai_max_context').attr('max', max_16k);
4390- }
4391- else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
4392- $('#openai_max_context').attr('max', max_8k);
4393- }
4394- else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
4395- $('#openai_max_context').attr('max', 12000);
4396- }
43974394 else {
43984395 $('#openai_max_context').attr('max', max_4kmax_128k);
43994396 }
44004397 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
44014398 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
public/scripts/power-user.js+92 -2
@@ -253,6 +253,15 @@ let power_user = {
253253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254254 },
255255
256+ reasoning: {
257+ auto_parse: false,
258+ add_to_prompts: false,
259+ prefix: '<think>\n',
260+ suffix: '\n</think>',
261+ separator: '\n\n',
262+ max_additions: 1,
263+ },
264+
256265 personas: {},
257266 default_persona: null,
258267 persona_descriptions: {},
@@ -2910,6 +2919,46 @@ export function flushEphemeralStoppingStrings() {
29102919}
29112920
29122921/**
2922+ * Checks if the generated text should be filtered based on the auto-swipe settings.
2923+ * @param {string} text The text to check
2924+ * @returns {boolean} If the generated text should be filtered
2925+ */
2926+export function generatedTextFiltered(text) {
2927+ /**
2928+ * Checks if the given text contains any of the blacklisted words.
2929+ * @param {string} text The text to check
2930+ * @param {string[]} blacklist The list of blacklisted words
2931+ * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
2932+ * @returns {boolean} Whether the text contains blacklisted words
2933+ */
2934+ function containsBlacklistedWords(text, blacklist, threshold) {
2935+ const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
2936+ const matches = text.match(regex) || [];
2937+ return matches.length >= threshold;
2938+ }
2939+
2940+ // Make sure a generated text is non-empty
2941+ // Otherwise we might get in a loop with a broken API
2942+ text = text.trim();
2943+ if (text.length > 0) {
2944+ if (power_user.auto_swipe_minimum_length) {
2945+ if (text.length < power_user.auto_swipe_minimum_length) {
2946+ console.log('Generated text size too small');
2947+ return true;
2948+ }
2949+ }
2950+ if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
2951+ if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
2952+ console.log('Generated text has blacklisted words');
2953+ return true;
2954+ }
2955+ }
2956+ }
2957+
2958+ return false;
2959+}
2960+
2961+/**
29132962 * Gets the custom stopping strings from the power user settings.
29142963 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
29152964 * @returns {string[]} An array of custom stopping strings
@@ -3880,9 +3929,9 @@ $(document).ready(() => {
38803929 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
38813930 }));
38823931 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
38833932 name: 'delmodedel',
38843933 callback: doDelMode,
38853934 aliases: ['deldelete', 'delmode'],
38863935 unnamedArgumentList: [
38873936 new SlashCommandArgument(
38883937 'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4065,4 +4114,45 @@ $(document).ready(() => {
40654114 ],
40664115 helpString: 'activates a movingUI preset by name',
40674116 }));
4117+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4118+ name: 'stop-strings',
4119+ aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4120+ helpString: `
4121+ <div>
4122+ Sets a list of custom stopping strings. Gets the list if no value is provided.
4123+ </div>
4124+ <div>
4125+ <strong>Examples:</strong>
4126+ </div>
4127+ <ul>
4128+ <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4129+ <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4130+ </ul>
4131+ `,
4132+ returns: ARGUMENT_TYPE.LIST,
4133+ unnamedArgumentList: [
4134+ SlashCommandArgument.fromProps({
4135+ description: 'list of strings',
4136+ typeList: [ARGUMENT_TYPE.LIST],
4137+ acceptsMultiple: false,
4138+ isRequired: false,
4139+ }),
4140+ ],
4141+ callback: (_, value) => {
4142+ if (String(value ?? '').trim()) {
4143+ const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4144+ if (!parsedValue || !Array.isArray(parsedValue)) {
4145+ throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4146+ }
4147+ parsedValue.forEach((item, index) => {
4148+ parsedValue[index] = String(item);
4149+ });
4150+ power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4151+ $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4152+ saveSettingsDebounced();
4153+ }
4154+
4155+ return power_user.custom_stopping_strings;
4156+ },
4157+ }));
40684158});
public/scripts/preset-manager.js+1 -0
@@ -586,6 +586,7 @@ class PresetManager {
586586 'tabby_model',
587587 'derived',
588588 'generic_model',
589+ 'include_reasoning',
589590 ];
590591 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
591592
public/scripts/reasoning.js+448 -0
@@ -0,0 +1,448 @@
1+import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2+import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
3+import { t } from './i18n.js';
4+import { MacrosParser } from './macros.js';
5+import { Popup } from './popup.js';
6+import { power_user } from './power-user.js';
7+import { SlashCommand } from './slash-commands/SlashCommand.js';
8+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
9+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
10+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
11+import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
12+
13+/**
14+ * Gets a message from a jQuery element.
15+ * @param {Element} element
16+ * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
17+ */
18+function getMessageFromJquery(element) {
19+ const messageBlock = $(element).closest('.mes');
20+ const messageId = Number(messageBlock.attr('mesid'));
21+ const message = chat[messageId];
22+ return { messageId: messageId, message, messageBlock };
23+}
24+
25+/**
26+ * Helper class for adding reasoning to messages.
27+ * Keeps track of the number of reasoning additions.
28+ */
29+export class PromptReasoning {
30+ static REASONING_PLACEHOLDER = '\u200B';
31+ static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
32+
33+ constructor() {
34+ this.counter = 0;
35+ }
36+
37+ /**
38+ * Checks if the limit of reasoning additions has been reached.
39+ * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
40+ */
41+ isLimitReached() {
42+ if (!power_user.reasoning.add_to_prompts) {
43+ return true;
44+ }
45+
46+ return this.counter >= power_user.reasoning.max_additions;
47+ }
48+
49+ /**
50+ * Add reasoning to a message according to the power user settings.
51+ * @param {string} content Message content
52+ * @param {string} reasoning Message reasoning
53+ * @param {boolean} isPrefix Whether this is the last message prefix
54+ * @returns {string} Message content with reasoning
55+ */
56+ addToMessage(content, reasoning, isPrefix) {
57+ // Disabled or reached limit of additions
58+ if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
59+ return content;
60+ }
61+
62+ // No reasoning provided or a placeholder
63+ if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
64+ return content;
65+ }
66+
67+ // Increment the counter
68+ this.counter++;
69+
70+ // Substitute macros in variable parts
71+ const prefix = substituteParams(power_user.reasoning.prefix || '');
72+ const separator = substituteParams(power_user.reasoning.separator || '');
73+ const suffix = substituteParams(power_user.reasoning.suffix || '');
74+
75+ // Combine parts with reasoning only
76+ if (isPrefix && !content) {
77+ return `${prefix}${reasoning}`;
78+ }
79+
80+ // Combine parts with reasoning and content
81+ return `${prefix}${reasoning}${suffix}${separator}${content}`;
82+ }
83+}
84+
85+function loadReasoningSettings() {
86+ $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
87+ $('#reasoning_add_to_prompts').on('change', function () {
88+ power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
89+ saveSettingsDebounced();
90+ });
91+
92+ $('#reasoning_prefix').val(power_user.reasoning.prefix);
93+ $('#reasoning_prefix').on('input', function () {
94+ power_user.reasoning.prefix = String($(this).val());
95+ saveSettingsDebounced();
96+ });
97+
98+ $('#reasoning_suffix').val(power_user.reasoning.suffix);
99+ $('#reasoning_suffix').on('input', function () {
100+ power_user.reasoning.suffix = String($(this).val());
101+ saveSettingsDebounced();
102+ });
103+
104+ $('#reasoning_separator').val(power_user.reasoning.separator);
105+ $('#reasoning_separator').on('input', function () {
106+ power_user.reasoning.separator = String($(this).val());
107+ saveSettingsDebounced();
108+ });
109+
110+ $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
111+ $('#reasoning_max_additions').on('input', function () {
112+ power_user.reasoning.max_additions = Number($(this).val());
113+ saveSettingsDebounced();
114+ });
115+
116+ $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
117+ $('#reasoning_auto_parse').on('change', function () {
118+ power_user.reasoning.auto_parse = !!$(this).prop('checked');
119+ saveSettingsDebounced();
120+ });
121+}
122+
123+function registerReasoningSlashCommands() {
124+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
125+ name: 'reasoning-get',
126+ aliases: ['get-reasoning'],
127+ returns: ARGUMENT_TYPE.STRING,
128+ 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.`,
129+ unnamedArgumentList: [
130+ SlashCommandArgument.fromProps({
131+ description: 'Message ID. If not provided, the message ID of the last message is used.',
132+ typeList: ARGUMENT_TYPE.NUMBER,
133+ enumProvider: commonEnumProviders.messages(),
134+ }),
135+ ],
136+ callback: (_args, value) => {
137+ const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
138+ const message = chat[messageId];
139+ const reasoning = String(message?.extra?.reasoning ?? '');
140+ return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
141+ },
142+ }));
143+
144+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
145+ name: 'reasoning-set',
146+ aliases: ['set-reasoning'],
147+ returns: ARGUMENT_TYPE.STRING,
148+ helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
149+ namedArgumentList: [
150+ SlashCommandNamedArgument.fromProps({
151+ name: 'at',
152+ description: 'Message ID. If not provided, the message ID of the last message is used.',
153+ typeList: ARGUMENT_TYPE.NUMBER,
154+ enumProvider: commonEnumProviders.messages(),
155+ }),
156+ ],
157+ unnamedArgumentList: [
158+ SlashCommandArgument.fromProps({
159+ description: 'Reasoning block content.',
160+ typeList: ARGUMENT_TYPE.STRING,
161+ }),
162+ ],
163+ callback: async (args, value) => {
164+ const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
165+ const message = chat[messageId];
166+ if (!message?.extra) {
167+ return '';
168+ }
169+
170+ message.extra.reasoning = String(value ?? '');
171+ await saveChatConditional();
172+
173+ closeMessageEditor('reasoning');
174+ updateMessageBlock(messageId, message);
175+ return message.extra.reasoning;
176+ },
177+ }));
178+
179+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
180+ name: 'reasoning-parse',
181+ aliases: ['parse-reasoning'],
182+ returns: 'reasoning string',
183+ helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
184+ namedArgumentList: [
185+ SlashCommandNamedArgument.fromProps({
186+ name: 'regex',
187+ description: 'Whether to apply regex scripts to the reasoning content.',
188+ typeList: [ARGUMENT_TYPE.BOOLEAN],
189+ defaultValue: 'true',
190+ isRequired: false,
191+ enumProvider: commonEnumProviders.boolean('trueFalse'),
192+ }),
193+ ],
194+ unnamedArgumentList: [
195+ SlashCommandArgument.fromProps({
196+ description: 'input string',
197+ typeList: [ARGUMENT_TYPE.STRING],
198+ }),
199+ ],
200+ callback: (args, value) => {
201+ if (!value) {
202+ return '';
203+ }
204+
205+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
206+ toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
207+ return String(value);
208+ }
209+
210+ const parsedReasoning = parseReasoningFromString(String(value));
211+
212+ if (!parsedReasoning) {
213+ return '';
214+ }
215+
216+ const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
217+ return applyRegex
218+ ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
219+ : parsedReasoning.reasoning;
220+ },
221+ }));
222+}
223+
224+function registerReasoningMacros() {
225+ MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
226+ MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
227+ MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
228+}
229+
230+function setReasoningEventHandlers() {
231+ $(document).on('click', '.mes_reasoning_copy', (e) => {
232+ e.stopPropagation();
233+ e.preventDefault();
234+ });
235+
236+ $(document).on('click', '.mes_reasoning_edit', function (e) {
237+ e.stopPropagation();
238+ e.preventDefault();
239+ const { message, messageBlock } = getMessageFromJquery(this);
240+ if (!message?.extra) {
241+ return;
242+ }
243+
244+ const reasoning = String(message?.extra?.reasoning ?? '');
245+ const chatElement = document.getElementById('chat');
246+ const textarea = document.createElement('textarea');
247+ const reasoningBlock = messageBlock.find('.mes_reasoning');
248+ textarea.classList.add('reasoning_edit_textarea');
249+ textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
250+ $(textarea).insertBefore(reasoningBlock);
251+
252+ if (!CSS.supports('field-sizing', 'content')) {
253+ const resetHeight = function () {
254+ const scrollTop = chatElement.scrollTop;
255+ textarea.style.height = '0px';
256+ textarea.style.height = `${textarea.scrollHeight}px`;
257+ chatElement.scrollTop = scrollTop;
258+ };
259+
260+ textarea.addEventListener('input', resetHeight);
261+ resetHeight();
262+ }
263+
264+ textarea.focus();
265+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
266+
267+ const textareaRect = textarea.getBoundingClientRect();
268+ const chatRect = chatElement.getBoundingClientRect();
269+
270+ // Scroll if textarea bottom is below visible area
271+ if (textareaRect.bottom > chatRect.bottom) {
272+ const scrollOffset = textareaRect.bottom - chatRect.bottom;
273+ chatElement.scrollTop += scrollOffset;
274+ }
275+ });
276+
277+ $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
278+ e.stopPropagation();
279+ e.preventDefault();
280+ const { message, messageId, messageBlock } = getMessageFromJquery(this);
281+ if (!message?.extra) {
282+ return;
283+ }
284+
285+ const textarea = messageBlock.find('.reasoning_edit_textarea');
286+ const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
287+ message.extra.reasoning = reasoning;
288+ await saveChatConditional();
289+ updateMessageBlock(messageId, message);
290+ textarea.remove();
291+ });
292+
293+ $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
294+ e.stopPropagation();
295+ e.preventDefault();
296+
297+ const { messageBlock } = getMessageFromJquery(this);
298+ const textarea = messageBlock.find('.reasoning_edit_textarea');
299+ textarea.remove();
300+ });
301+
302+ $(document).on('click', '.mes_edit_add_reasoning', async function () {
303+ const { message, messageId } = getMessageFromJquery(this);
304+ if (!message?.extra) {
305+ return;
306+ }
307+
308+ if (message.extra.reasoning) {
309+ toastr.info(t`Reasoning already exists.`, t`Edit Message`);
310+ return;
311+ }
312+
313+ message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
314+ await saveChatConditional();
315+ closeMessageEditor();
316+ updateMessageBlock(messageId, message);
317+ });
318+
319+ $(document).on('click', '.mes_reasoning_delete', async function (e) {
320+ e.stopPropagation();
321+ e.preventDefault();
322+
323+ const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
324+
325+ if (!confirm) {
326+ return;
327+ }
328+
329+ const { message, messageId } = getMessageFromJquery(this);
330+ if (!message?.extra) {
331+ return;
332+ }
333+ message.extra.reasoning = '';
334+ await saveChatConditional();
335+ updateMessageBlock(messageId, message);
336+ });
337+
338+ $(document).on('pointerup', '.mes_reasoning_copy', async function () {
339+ const { message } = getMessageFromJquery(this);
340+ const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
341+
342+ if (!reasoning) {
343+ return;
344+ }
345+
346+ await copyText(reasoning);
347+ toastr.info(t`Copied!`, '', { timeOut: 2000 });
348+ });
349+}
350+
351+/**
352+ * Parses reasoning from a string using the power user reasoning settings.
353+ * @typedef {Object} ParsedReasoning
354+ * @property {string} reasoning Reasoning block
355+ * @property {string} content Message content
356+ * @param {string} str Content of the message
357+ * @returns {ParsedReasoning|null} Parsed reasoning block and message content
358+ */
359+function parseReasoningFromString(str) {
360+ // Both prefix and suffix must be defined
361+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
362+ return null;
363+ }
364+
365+ try {
366+ const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
367+
368+ let didReplace = false;
369+ let reasoning = '';
370+ let content = String(str).replace(regex, (_match, captureGroup) => {
371+ didReplace = true;
372+ reasoning = captureGroup;
373+ return '';
374+ });
375+
376+ if (didReplace && power_user.trim_spaces) {
377+ reasoning = reasoning.trim();
378+ content = content.trim();
379+ }
380+
381+ return { reasoning, content };
382+ } catch (error) {
383+ console.error('[Reasoning] Error parsing reasoning block', error);
384+ return null;
385+ }
386+}
387+
388+function registerReasoningAppEvents() {
389+ eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
390+ if (!power_user.reasoning.auto_parse) {
391+ return;
392+ }
393+
394+ console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
395+ const message = chat[idx];
396+
397+ if (!message) {
398+ console.warn('[Reasoning] Message not found', idx);
399+ return null;
400+ }
401+
402+ if (!message.mes || message.mes === '...') {
403+ console.debug('[Reasoning] Message content is empty or a placeholder', idx);
404+ return null;
405+ }
406+
407+ const parsedReasoning = parseReasoningFromString(message.mes);
408+
409+ // No reasoning block found
410+ if (!parsedReasoning) {
411+ return;
412+ }
413+
414+ // Make sure the message has an extra object
415+ if (!message.extra || typeof message.extra !== 'object') {
416+ message.extra = {};
417+ }
418+
419+ const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
420+
421+ // If reasoning was found, add it to the message
422+ if (parsedReasoning.reasoning) {
423+ message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
424+ }
425+
426+ // Update the message text if it was changed
427+ if (parsedReasoning.content !== message.mes) {
428+ message.mes = parsedReasoning.content;
429+ }
430+
431+ // Find if a message already exists in DOM and must be updated
432+ if (contentUpdated) {
433+ const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
434+ if (messageRendered) {
435+ console.debug('[Reasoning] Updating message block', idx);
436+ updateMessageBlock(idx, message);
437+ }
438+ }
439+ });
440+}
441+
442+export function initReasoning() {
443+ loadReasoningSettings();
444+ setReasoningEventHandlers();
445+ registerReasoningSlashCommands();
446+ registerReasoningMacros();
447+ registerReasoningAppEvents();
448+}
public/scripts/slash-commands.js+68 -33
@@ -42,6 +42,7 @@ import {
4242 showMoreMessages,
4343 stopGeneration,
4444 substituteParams,
45+ syncCurrentSwipeInfoExtras,
4546 system_avatar,
4647 system_message_types,
4748 this_chid,
@@ -58,7 +59,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
5859import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5960import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
6061import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
6162import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
6263import { registerVariableCommands, resolveVariable } from './variables.js';
6364import { background_settings } from './backgrounds.js';
6465import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -1902,7 +1903,7 @@ export function initDefaultSlashCommands() {
19021903 returns: 'uppercase string',
19031904 unnamedArgumentList: [
19041905 new SlashCommandArgument(
19051906 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19061907 ),
19071908 ],
19081909 helpString: 'Converts the provided string to uppercase.',
@@ -1914,7 +1915,7 @@ export function initDefaultSlashCommands() {
19141915 returns: 'lowercase string',
19151916 unnamedArgumentList: [
19161917 new SlashCommandArgument(
19171918 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19181919 ),
19191920 ],
19201921 helpString: 'Converts the provided string to lowercase.',
@@ -1934,7 +1935,7 @@ export function initDefaultSlashCommands() {
19341935 ],
19351936 unnamedArgumentList: [
19361937 new SlashCommandArgument(
19371938 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19381939 ),
19391940 ],
19401941 helpString: `
@@ -1998,6 +1999,62 @@ export function initDefaultSlashCommands() {
19981999 return '';
19992000 },
20002001 }));
2002+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2003+ name: 'replace',
2004+ aliases: ['re'],
2005+ callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2006+ if (pattern === '')
2007+ throw new Error('Argument of \'pattern=\' cannot be empty');
2008+ switch (mode) {
2009+ case 'literal':
2010+ return text.replaceAll(pattern, replacer);
2011+ case 'regex':
2012+ return text.replace(regexFromString(pattern), replacer);
2013+ default:
2014+ throw new Error('Invalid \'/replace mode=\' argument specified!');
2015+ }
2016+ }),
2017+ returns: 'replaced text',
2018+ namedArgumentList: [
2019+ SlashCommandNamedArgument.fromProps({
2020+ name: 'mode',
2021+ description: 'Replaces occurrence(s) of a pattern',
2022+ typeList: [ARGUMENT_TYPE.STRING],
2023+ defaultValue: 'literal',
2024+ enumList: ['literal', 'regex'],
2025+ }),
2026+ new SlashCommandNamedArgument(
2027+ 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
2028+ ),
2029+ new SlashCommandNamedArgument(
2030+ 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
2031+ ),
2032+ ],
2033+ unnamedArgumentList: [
2034+ new SlashCommandArgument(
2035+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
2036+ ),
2037+ ],
2038+ helpString: `
2039+ <div>
2040+ Replaces text within the provided string based on the pattern.
2041+ </div>
2042+ <div>
2043+ If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />
2044+ If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />
2045+ The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />
2046+ If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />
2047+ </div>
2048+ <div>
2049+ <strong>Example:</strong>
2050+ <pre>/let x Blue house and blue car || </pre>
2051+ <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
2052+ <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
2053+ <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
2054+ <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
2055+ </div>
2056+ `,
2057+ }));
20012058
20022059 registerVariableCommands();
20032060}
@@ -2814,8 +2871,11 @@ async function addSwipeCallback(args, value) {
28142871 const newSwipeId = lastMessage.swipes.length - 1;
28152872
28162873 if (isTrueBoolean(args.switch)) {
2874+ // Make sure ad-hoc changes to extras are saved before swiping away
2875+ syncCurrentSwipeInfoExtras();
28172876 lastMessage.swipe_id = newSwipeId;
28182877 lastMessage.mes = lastMessage.swipes[newSwipeId];
2878+ lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
28192879 }
28202880
28212881 await saveChatConditional();
@@ -3081,11 +3141,6 @@ async function removeGroupMemberCallback(_, arg) {
30813141 return '';
30823142 }
30833143
3084- if (is_group_generating) {
3085- toastr.warning('Cannot run /memberremove command while the group reply is generating.');
3086- return '';
3087- }
3088-
30893144 const chid = findGroupMemberId(arg);
30903145
30913146 if (chid === undefined) {
@@ -3190,12 +3245,7 @@ function findPersonaByName(name) {
31903245}
31913246
31923247async function sendUserMessageCallback(args, text) {
3193- if (!text) {
3248+ text = String(text ?? '').trim();
3194- toastr.warning('You must specify text to send');
3195- return;
3196- }
3197-
3198- text = text.trim();
31993249 const compact = isTrueBoolean(args?.compact);
32003250 const bias = extractMessageBias(text);
32013251
@@ -3504,13 +3554,7 @@ export function getNameAndAvatarForMessage(character, name = null) {
35043554}
35053555
35063556export async function sendMessageAs(args, text) {
3507- if (!text) {
3508- toastr.warning('You must specify text to send as');
3509- return '';
3510- }
3511-
35123557 let name = args.name?.trim();
3513- let mesText;
35143558
35153559 if (!name) {
35163560 const namelessWarningKey = 'sendAsNamelessWarningShown';
@@ -3521,7 +3565,7 @@ export async function sendMessageAs(args, text) {
35213565 name = name2;
35223566 }
35233567
35243568 let mesText = String(text ?? '').trim();
35253569
35263570 // Requires a regex check after the slash command is pushed to output
35273571 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3599,11 +3643,7 @@ export async function sendMessageAs(args, text) {
35993643}
36003644
36013645export async function sendNarratorMessage(args, text) {
3602- if (!text) {
3646+ text = String(text ?? '');
3603- toastr.warning('You must specify text to send');
3604- return '';
3605- }
3606-
36073647 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
36083648 // Messages that do nothing but set bias will be hidden from the context
36093649 const bias = extractMessageBias(text);
@@ -3694,18 +3734,13 @@ export async function promptQuietForLoudResponse(who, text) {
36943734}
36953735
36963736async function sendCommentMessage(args, text) {
3697- if (!text) {
3698- toastr.warning('You must specify text to send');
3699- return '';
3700- }
3701-
37023737 const compact = isTrueBoolean(args?.compact);
37033738 const message = {
37043739 name: COMMENT_NAME_DEFAULT,
37053740 is_user: false,
37063741 is_system: true,
37073742 send_date: getMessageTimeStamp(),
37083743 mes: substituteParams(String(text ?? '').trim()),
37093744 force_avatar: comment_avatar,
37103745 extra: {
37113746 type: system_message_types.COMMENT,
public/scripts/sse-stream.js+15 -0
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
235235 }
236236 return;
237237 }
238+ else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
239+ for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
240+ const str = json.choices[0].delta.reasoning[j];
241+ const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
242+ const choiceClone = structuredClone(json.choices[0]);
243+ choiceClone.delta.reasoning = str;
244+ choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
245+ const choices = [choiceClone];
246+ yield {
247+ data: { ...json, choices },
248+ chunk: str,
249+ };
250+ }
251+ return;
252+ }
238253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
239254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
240255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+15 -0
@@ -1,6 +1,7 @@
11import {
22 activateSendButtons,
33 addOneMessage,
4+ appendMediaToMessage,
45 callPopup,
56 characters,
67 chat,
@@ -41,6 +42,7 @@ import {
4142 substituteParamsExtended,
4243 this_chid,
4344 updateChatMetadata,
45+ updateMessageBlock,
4446} from '../script.js';
4547import {
4648 extension_settings,
@@ -67,6 +69,7 @@ import { textgenerationwebui_settings } from './textgen-settings.js';
6769import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
6870import { ToolManager } from './tool-calling.js';
6971import { timestampToMoment, uuidv4 } from './utils.js';
72+import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
7073
7174export function getContext() {
7275 return {
@@ -171,6 +174,18 @@ export function getContext() {
171174 getCharacters,
172175 uuidv4,
173176 humanizedDateTime,
177+ updateMessageBlock,
178+ appendMediaToMessage,
179+ variables: {
180+ local: {
181+ get: getLocalVariable,
182+ set: setLocalVariable,
183+ },
184+ global: {
185+ get: getGlobalVariable,
186+ set: setGlobalVariable,
187+ },
188+ },
174189 };
175190}
176191
public/scripts/textgen-models.js+6 -0
@@ -54,6 +54,12 @@ const OPENROUTER_PROVIDERS = [
5454 'xAI',
5555 'Cloudflare',
5656 'SF Compute',
57+ 'Minimax',
58+ 'Nineteen',
59+ 'Liquid',
60+ 'Nebius',
61+ 'Chutes',
62+ 'Kluster',
5763 '01.AI',
5864 'HuggingFace',
5965 'Mancer',
public/scripts/textgen-settings.js+8 -2
@@ -172,6 +172,7 @@ const settings = {
172172 //truncation_length: 2048,
173173 ban_eos_token: false,
174174 skip_special_tokens: true,
175+ include_reasoning: true,
175176 streaming: false,
176177 mirostat_mode: 0,
177178 mirostat_tau: 5,
@@ -263,6 +264,7 @@ export const setting_names = [
263264 'add_bos_token',
264265 'ban_eos_token',
265266 'skip_special_tokens',
267+ 'include_reasoning',
266268 'streaming',
267269 'mirostat_mode',
268270 'mirostat_tau',
@@ -501,7 +503,7 @@ export function loadTextGenSettings(data, loadedSettings) {
501503 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
502504 const control = $(selector);
503505 control.val(settings.server_urls[type] ?? '').on('input', function () {
504506 settings.server_urls[type] = String($(this).val()).trim();
505507 saveSettingsDebounced();
506508 });
507509 }
@@ -740,6 +742,7 @@ jQuery(function () {
740742 'add_bos_token_textgenerationwebui': true,
741743 'temperature_last_textgenerationwebui': true,
742744 'skip_special_tokens_textgenerationwebui': true,
745+ 'include_reasoning_textgenerationwebui': true,
743746 'top_a_textgenerationwebui': 0,
744747 'top_a_counter_textgenerationwebui': 0,
745748 'mirostat_mode_textgenerationwebui': 0,
@@ -986,6 +989,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986989 let logprobs = null;
987990 const swipes = [];
988991 const toolCalls = [];
992+ const state = { reasoning: '' };
989993 while (true) {
990994 const { done, value } = await reader.read();
991995 if (done) return;
@@ -1002,9 +1006,10 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
10021006 const newText = data?.choices?.[0]?.text || data?.content || '';
10031007 text += newText;
10041008 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1009+ state.reasoning += data?.choices?.[0]?.reasoning ?? '';
10051010 }
10061011
10071012 yield { text, swipes, logprobs, toolCalls, state };
10081013 }
10091014 };
10101015}
@@ -1265,6 +1270,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12651270 'truncation_length': max_context,
12661271 'ban_eos_token': settings.ban_eos_token,
12671272 'skip_special_tokens': settings.skip_special_tokens,
1273+ 'include_reasoning': settings.include_reasoning,
12681274 'top_a': settings.top_a,
12691275 'tfs': settings.tfs,
12701276 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,
public/scripts/tokenizers.js+3 -0
@@ -679,6 +679,9 @@ export function getTokenizerModel() {
679679 }
680680
681681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
682+ if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
683+ return deepseekTokenizer;
684+ }
682685 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
683686 return llama3Tokenizer;
684687 }
public/scripts/utils.js+7 -7
@@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
17331733
17341734/**
17351735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
1736- *
1736+ * The action will be executed after the animation ends or after the timeout, whichever comes first.
17371737 * @param {HTMLElement} control - The control element to listen for animation end event
17381738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
1739+ * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
17391740 */
17401741export function runAfterAnimation(control, callback, timeout = 500) {
17411742 if (hasAnimation(control)) {
1742- const onAnimationEnd = () => {
1743+ Promise.race([
1743- control.removeEventListener('animationend', onAnimationEnd);
1744+ new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
1744- callback(control);
1745+ new Promise((r) => control.addEventListener('animationend', r, { once: true })),
1745- };
1746+ ]).finally(() => callback(control));
1746- control.addEventListener('animationend', onAnimationEnd);
17471747 } else {
17481748 callback(control);
17491749 }
public/scripts/variables.js+4 -4
@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1919
2020const MAX_LOOPS = 100;
2121
2222export function getLocalVariable(name, args = {}) {
2323 if (!chat_metadata.variables) {
2424 chat_metadata.variables = {};
2525 }
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
4545 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
4646}
4747
4848export function setLocalVariable(name, value, args = {}) {
4949 if (!name) {
5050 throw new Error('Variable name cannot be empty or undefined.');
5151 }
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
8080 return value;
8181}
8282
8383export function getGlobalVariable(name, args = {}) {
8484 let globalVariable = extension_settings.variables.global[args.key ?? name];
8585 if (args.index !== undefined) {
8686 try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
102102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
103103}
104104
105105export function setGlobalVariable(name, value, args = {}) {
106106 if (!name) {
107107 throw new Error('Variable name cannot be empty or undefined.');
108108 }
public/style.css+111 -21
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292292 filter: grayscale(25%);
293293}
294294
295295.mes_text table {,
296+.mes_reasoning table {
296297 border-spacing: 0;
297298 border-collapse: collapse;
298299 margin-bottom: 10px;
299300}
300301
301302.mes_text td,
302303.mes_text th {,
304+.mes_reasoning td,
305+.mes_reasoning th {
303306 border: 1px solid;
304307 border-collapse: collapse;
305308 padding: 0.25em;
306309}
307310
308311.mes_text p {,
312+.mes_reasoning p {
309313 margin-top: 0;
310314 margin-bottom: 10px;
311315}
312316
313317.mes_text li tt {,
318+.mes_reasoning li tt {
314319 display: inline-block;
315320}
316321
317322.mes_text ol,
318323.mes_text ul {,
324+.mes_reasoning ol,
325+.mes_reasoning ul {
319326 margin-top: 5px;
320327 margin-bottom: 5px;
321328}
322329
323330.mes_text br,
324331.mes_bias br {,
332+.mes_reasoning br {
325333 content: ' ';
326334}
327335
@@ -332,25 +340,88 @@ input[type='checkbox']:focus-visible {
332340 color: var(--SmartThemeQuoteColor);
333341}
334342
343+.mes_reasoning {
344+ display: block;
345+ border: 1px solid var(--SmartThemeBorderColor);
346+ background-color: var(--black30a);
347+ border-radius: 5px;
348+ padding: 5px;
349+ margin: 5px 0;
350+ overflow-y: auto;
351+}
352+
353+.mes_reasoning_summary {
354+ cursor: pointer;
355+ position: relative;
356+ margin: 2px;
357+}
358+
359+@supports not selector(:has(*)) {
360+ .mes_reasoning_details {
361+ display: none !important;
362+ }
363+}
364+
365+.mes_bias:empty,
366+.mes_reasoning:empty,
367+.mes_reasoning_details:has(.mes_reasoning:empty),
368+.mes_block:has(.edit_textarea) .mes_reasoning_details,
369+.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
371+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
372+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
373+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
374+ display: none;
375+}
376+
377+.mes_reasoning_actions {
378+ position: absolute;
379+ right: 0;
380+ top: 0;
381+
382+ display: flex;
383+ gap: 4px;
384+ flex-wrap: nowrap;
385+ justify-content: flex-end;
386+ transition: all 200ms;
387+ overflow-x: hidden;
388+ padding: 1px;
389+}
390+
391+.mes_reasoning_summary>span {
392+ margin-left: 0.5em;
393+}
394+
335395.mes_text i,
336396.mes_text em {,
397+.mes_reasoning i,
398+.mes_reasoning em {
337399 color: var(--SmartThemeEmColor);
338400}
339401
340402.mes_text uq {i,
403+.mes_text q em {
404+ color: inherit;
405+}
406+
407+.mes_text u,
408+.mes_reasoning u {
341409 color: var(--SmartThemeUnderlineColor);
342410}
343411
344412.mes_text q {,
413+.mes_reasoning q {
345414 color: var(--SmartThemeQuoteColor);
346415}
347416
348417.mes_text font[color] em,
349418.mes_text font[color] i {,
350- color: inherit;
419+.mes_text font[color] u,
351-}
420+.mes_text font[color] q,
352-
421+.mes_reasoning font[color] em,
353422.mes_textmes_reasoning font[color] q {i,
423+.mes_reasoning font[color] u,
424+.mes_reasoning font[color] q {
354425 color: inherit;
355426}
356427
@@ -358,7 +429,8 @@ input[type='checkbox']:focus-visible {
358429 display: block;
359430}
360431
361432.mes_text blockquote {,
433+.mes_reasoning blockquote {
362434 border-left: 3px solid var(--SmartThemeQuoteColor);
363435 padding-left: 10px;
364436 background-color: var(--black30a);
@@ -368,18 +440,24 @@ input[type='checkbox']:focus-visible {
368440.mes_text strong em,
369441.mes_text strong,
370442.mes_text h2,
371443.mes_text h1 {,
444+.mes_reasoning strong em,
445+.mes_reasoning strong,
446+.mes_reasoning h2,
447+.mes_reasoning h1 {
372448 font-weight: bold;
373449}
374450
375451.mes_text pre code {,
452+.mes_reasoning pre code {
376453 position: relative;
377454 display: block;
378455 overflow-x: auto;
379456 padding: 1em;
380457}
381458
382459.mes_text img:not(.mes_img) {,
460+.mes_reasoning img:not(.mes_img) {
383461 max-width: 100%;
384462 max-height: var(--doc-height);
385463}
@@ -1022,6 +1100,11 @@ body .panelControlBar {
10221100 /*only affects bubblechat to make it sit nicely at the bottom*/
10231101}
10241102
1103+.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1104+ margin-bottom: 30px;
1105+}
1106+
1107+.last_mes .mes_reasoning,
10251108.last_mes .mes_text {
10261109 padding-right: 30px;
10271110}
@@ -1235,14 +1318,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12351318 overflow-y: clip;
12361319}
12371320
12381321.mes_text {,
1322+.mes_reasoning {
12391323 font-weight: 500;
12401324 line-height: calc(var(--mainFontSize) + .5rem);
1325+ max-width: 100%;
1326+ overflow-wrap: anywhere;
1327+}
1328+
1329+.mes_text {
12411330 padding-left: 0;
12421331 padding-top: 5px;
12431332 padding-bottom: 5px;
1244- max-width: 100%;
1245- overflow-wrap: anywhere;
12461333}
12471334
12481335br {
@@ -4150,10 +4237,12 @@ input[type="range"]::-webkit-slider-thumb {
41504237 align-items: center;
41514238}
41524239
4240+.mes_reasoning_edit_cancel,
41534241.mes_edit_cancel.menu_button {
41544242 background-color: var(--crimson70a);
41554243}
41564244
4245+.mes_reasoning_edit_done,
41574246.mes_edit_done.menu_button {
41584247 background-color: var(--okGreen70a);
41594248}
@@ -4162,6 +4251,7 @@ input[type="range"]::-webkit-slider-thumb {
41624251 opacity: 1;
41634252}
41644253
4254+.reasoning_edit_textarea,
41654255.edit_textarea {
41664256 padding: 5px;
41674257 margin: 0;
src/constants.js+1 -0
@@ -369,6 +369,7 @@ export const OPENROUTER_KEYS = [
369369 'prompt',
370370 'stop',
371371 'provider',
372+ 'include_reasoning',
372373];
373374
374375// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
src/endpoints/backends/chat-completions.js+14 -8
@@ -288,7 +288,8 @@ async function sendMakerSuiteRequest(request, response) {
288288
289289 const model = String(request.body.model);
290290 const stream = Boolean(request.body.stream);
291291 const showThoughts = Boolean(request.body.show_thoughtsinclude_reasoning);
292+ const isThinking = model.includes('thinking');
292293
293294 const generationConfig = {
294295 stopSequences: request.body.stop,
@@ -329,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
329330 body.systemInstruction = prompt.system_instruction;
330331 }
331332
333+ if (isThinking && showThoughts) {
334+ generationConfig.thinkingConfig = {
335+ includeThoughts: true,
336+ };
337+ }
338+
332339 return body;
333340 }
334341
@@ -342,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
342349 controller.abort();
343350 });
344351
345- const isThinking = model.includes('thinking');
346352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
347353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
348354
@@ -387,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
387393 const responseContent = candidates[0].content ?? candidates[0].output;
388394 console.log('Google AI Studio response:', responseContent);
389395
390- if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {
396+ const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
391- responseContent.parts = responseContent.parts.filter(part => !part.thought);
392- }
393-
394- const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
395397 if (!responseText) {
396398 let message = 'Google AI Studio Candidate text empty';
397399 console.log(message, generateResponseJson);
@@ -399,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
399401 }
400402
401403 // Wrap it back to OAI format
402404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
403405 return response.send(reply);
404406 }
405407 } catch (error) {
@@ -996,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
996998 bodyParams['route'] = 'fallback';
997999 }
9981000
1001+ if (request.body.include_reasoning) {
1002+ bodyParams['include_reasoning'] = true;
1003+ }
1004+
9991005 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
10001006 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
10011007 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
src/endpoints/search.js+19 -8
@@ -162,7 +162,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
162162
163163router.post('/searxng', jsonParser, async (request, response) => {
164164 try {
165165 const { baseUrl, query, preferences, categories } = request.body;
166166
167167 if (!baseUrl || !query) {
168168 console.log('Missing required parameters for /searxng');
@@ -193,6 +193,9 @@ router.post('/searxng', jsonParser, async (request, response) => {
193193 if (preferences) {
194194 searchParams.append('preferences', preferences);
195195 }
196+ if (categories) {
197+ searchParams.append('categories', categories);
198+ }
196199 searchUrl.search = searchParams.toString();
197200
198201 const searchResult = await fetch(searchUrl, { headers: visitHeaders });
@@ -220,7 +223,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
220223 return response.sendStatus(400);
221224 }
222225
223226 const { query, include_images } = request.body;
224227
225228 const body = {
226229 query: query,
@@ -229,7 +232,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
229232 topic: 'general',
230233 include_answer: true,
231234 include_raw_content: false,
232235 include_images: false!!include_images,
233236 include_image_descriptions: false,
234237 include_domains: [],
235238 max_results: 10,
@@ -297,6 +300,7 @@ router.post('/koboldcpp', jsonParser, async (request, response) => {
297300router.post('/visit', jsonParser, async (request, response) => {
298301 try {
299302 const url = request.body.url;
303+ const html = Boolean(request.body.html ?? true);
300304
301305 if (!url) {
302306 console.log('No url provided for /visit');
@@ -340,13 +344,20 @@ router.post('/visit', jsonParser, async (request, response) => {
340344 }
341345
342346 const contentType = String(result.headers.get('content-type'));
343- if (!contentType.includes('text/html')) {
347+
344- console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
348+ if (html) {
345- return response.sendStatus(500);
349+ if (!contentType.includes('text/html')) {
350+ console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
351+ return response.sendStatus(500);
352+ }
353+
354+ const text = await result.text();
355+ return response.send(text);
346356 }
347357
348- const text = await result.text();
358+ response.setHeader('Content-Type', contentType);
349- return response.send(text);
359+ const buffer = await result.arrayBuffer();
360+ return response.send(Buffer.from(buffer));
350361 } catch (error) {
351362 console.log(error);
352363 return response.sendStatus(500);
src/endpoints/stable-diffusion.js+1 -1
@@ -533,6 +533,7 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => {
533533
534534comfy.post('/generate', jsonParser, async (request, response) => {
535535 try {
536+ let item;
536537 const url = new URL(urlJoin(request.body.url, '/prompt'));
537538
538539 const controller = new AbortController();
@@ -557,7 +558,6 @@ comfy.post('/generate', jsonParser, async (request, response) => {
557558 /** @type {any} */
558559 const data = await promptResult.json();
559560 const id = data.prompt_id;
560- let item;
561561 const historyUrl = new URL(urlJoin(request.body.url, '/history'));
562562 while (true) {
563563 const result = await fetch(historyUrl);
src/endpoints/tokenizers.js+1 -1
@@ -398,7 +398,7 @@ function getWebTokenizersChunks(tokenizer, ids) {
398398 * @returns {string} Tokenizer model to use
399399 */
400400export function getTokenizerModel(requestModel) {
401401 if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini') || requestModel.includes('o3-mini')) {
402402 return 'gpt-4o';
403403 }
404404