Merge branch 'staging' into staging-3071-v2

842b59605bd5f3fd5bb2f5bf54102171e1b5b151

Eradev <admin@eradev.com>

37 files changed, +775 -349Ignore whitespace
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+30 -47
@@ -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>
@@ -1981,13 +1985,13 @@
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 reasoning">ShowRequest model reasoning</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>
@@ -3151,34 +3155,22 @@
31513155 </div>
31523156 <h4 data-i18n="Groq Model">Groq Model</h4>
31533157 <select id="model_groq_select">
31543158 <optgroup label="LlamaProduction 3.3Models">
3159+ <option value="gemma2-9b-it">gemma2-9b-it</option>
31553160 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
3161+ <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3162+ <option value="llama3-70b-8192">llama3-70b-8192</option>
3163+ <option value="llama3-8b-8192">llama3-8b-8192</option>
3164+ <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
31563165 </optgroup>
31573166 <optgroup label="LlamaPreview 3.2Models">
3167+ <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3168+ <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
31583169 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
31593170 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
31603171 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
31613172 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
31623173 </optgroup>
3163- <optgroup label="Llama 3.1">
3164- <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3165- <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
3166- <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
3167- </optgroup>
3168- <optgroup label="Llama 3">
3169- <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
3170- <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
3171- <option value="llama3-8b-8192">llama3-8b-8192</option>
3172- <option value="llama3-70b-8192">llama3-70b-8192</option>
3173- </optgroup>
3174- <optgroup label="Gemma">
3175- <option value="gemma-7b-it">gemma-7b-it</option>
3176- <option value="gemma2-9b-it">gemma2-9b-it</option>
3177- </optgroup>
3178- <optgroup label="Other">
3179- <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3180- <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
3181- </optgroup>
31823174 </select>
31833175 </div>
31843176 <div id="nanogpt_form" data-source="nanogpt">
@@ -3227,32 +3219,19 @@
32273219 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
32283220 <select id="model_perplexity_select">
32293221 <optgroup label="Perplexity Sonar Models">
3222+ <option value="sonar">sonar</option>
3223+ <option value="sonar-pro">sonar-pro</option>
3224+ <option value="sonar-reasoning">sonar-reasoning</option>
3225+ </optgroup>
3226+ <optgroup label="Deprecated Models">
3227+ <!-- These are scheduled for deprecation after 2/22/2025 -->
32303228 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
32313229 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
32323230 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
3233- </optgroup>
3231+ <!-- These are not listed on the site anymore -->
3234- <optgroup label="Perplexity Chat Models">
32353232 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
32363233 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
32373234 </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>
32563235 </select>
32573236 </div>
32583237 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@@ -3804,6 +3783,12 @@
38043783 <span data-i18n="Reasoning">Reasoning</span>
38053784 </h4>
38063785 <div>
3786+ <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">
3787+ <input id="reasoning_auto_parse" type="checkbox" />
3788+ <small data-i18n="Auto-Parse Reasoning">
3789+ Auto-Parse Reasoning
3790+ </small>
3791+ </label>
38073792 <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">
38083793 <input id="reasoning_add_to_prompts" type="checkbox" />
38093794 <small data-i18n="Add Reasoning to Prompts">
@@ -4845,6 +4830,7 @@
48454830 </div>
48464831 <div id="extensions_settings" class="flex1 wide50p">
48474832 <div id="assets_container" class="extension_container"></div>
4833+ <div id="typing_indicator_container" class="extension_container"></div>
48484834 <div id="expressions_container" class="extension_container"></div>
48494835 <div id="sd_container" class="extension_container"></div>
48504836 <div id="tts_container" class="extension_container"></div>
@@ -6488,9 +6474,6 @@
64886474 </div>
64896475
64906476 <!-- chat and input bar -->
6491- <div id="typing_indicator_template" class="template_element">
6492- <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
6493- </div>
64946477 <div id="message_file_template" class="template_element">
64956478 <div class="mes_file_container">
64966479 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
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 reasoning": "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 reasoning": "展示思维链请求思维链",
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
@@ -2357,8 +2357,8 @@
23572357 "Forbid": "禁止",
23582358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
23592359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
23602360 "ShowRequest model reasoning": "顯示模型思維鏈請求模型思維鏈",
23612361 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "在回應中顯示模型的思維鏈(內部思考過程)讓模型回傳其思考過程。",
23622362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
23632363 "Model ID (optional)": "模型 ID(可選)",
23642364 "DeepSeek API Key": "DeepSeek API 金鑰",
public/script.js+108 -94
@@ -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
@@ -1993,14 +1995,15 @@ export async function sendTextareaMessage() {
19931995 * @param {boolean} isUser If the message was sent by the user
19941996 * @param {number} messageId Message index in chat array
19951997 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
1998+ * @param {boolean} [isReasoning] If the message is reasoning output
19961999 * @returns {string} HTML string
19972000 */
19982001export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
19992002 if (!mes) {
20002003 return '';
20012004 }
20022005
20032006 if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
20042007 const mesBeforeReplace = mes;
20052008 const chatMessage = chat[messageId];
20062009 mes = substituteParams(mes, undefined, ch_name);
@@ -2029,6 +2032,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20292032 if (!isSystem) {
20302033 function getRegexPlacement() {
20312034 try {
2035+ if (isReasoning) {
2036+ return regex_placement.REASONING;
2037+ }
20322038 if (isUser) {
20332039 return regex_placement.USER_INPUT;
20342040 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2062,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20622068 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
20632069 }
20642070
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+
20652082 if (!isSystem) {
20662083 // Save double quotes in tags as a special character to prevent them from being encoded
20672084 if (!power_user.encode_tags) {
@@ -2250,8 +2267,8 @@ function getMessageFromTemplate({
22502267export function updateMessageBlock(messageId, message) {
22512268 const messageElement = $(`#chat [mesid="${messageId}"]`);
22522269 const text = message?.extra?.display_text ?? message.mes;
22532270 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
22542271 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1messageId, {}, true));
22552272 addCopyToCodeBlocks(messageElement);
22562273 appendMediaToMessage(message, messageElement);
22572274}
@@ -2408,9 +2425,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24082425 mes.is_user,
24092426 chat.indexOf(mes),
24102427 sanitizerOverrides,
2428+ false,
24112429 );
24122430 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
24132431 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1chat.indexOf(mes), {}, true);
24142432 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24152433
24162434 let params = {
@@ -3057,7 +3075,7 @@ export function isStreamingEnabled() {
30573075 (main_api == 'openai' &&
30583076 oai_settings.stream_openai &&
30593077 !noStreamSources.includes(oai_settings.chat_completion_source) &&
30603078 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('o1-') || oai_settings.openai_model.startsWith('o3'))) &&
30613079 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
30623080 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
30633081 || (main_api == 'novel' && nai_settings.streaming_novel)
@@ -3153,7 +3171,7 @@ class StreamingProcessor {
31533171 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
31543172 }
31553173 else {
31563174 await saveReply(this.type, text, true, '', [], '');
31573175 messageId = chat.length - 1;
31583176 this.#checkDomElements(messageId);
31593177 this.showMessageButtons(messageId);
@@ -3203,9 +3221,9 @@ class StreamingProcessor {
32033221 }
32043222
32053223 if (this.reasoning) {
32063224 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
32073225 if (this.messageReasoningDom instanceof HTMLElement) {
32083226 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1messageId, {}, true);
32093227 this.messageReasoningDom.innerHTML = formattedReasoning;
32103228 }
32113229 }
@@ -3232,6 +3250,8 @@ class StreamingProcessor {
32323250 chat[messageId].is_system,
32333251 chat[messageId].is_user,
32343252 messageId,
3253+ {},
3254+ false,
32353255 );
32363256 if (this.messageTextDom instanceof HTMLElement) {
32373257 this.messageTextDom.innerHTML = formattedText;
@@ -3283,39 +3303,11 @@ class StreamingProcessor {
32833303 unblockGeneration();
32843304 generatedPromptCache = '';
32853305
3286- //console.log("Generated text size:", text.length, text)
3287-
32883306 const isAborted = this.abortController.signal.aborted;
32893307 if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(text)) {
3290- function containsBlacklistedWords(str, blacklist, threshold) {
3308+ return swipe_right();
3291- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3292- const matches = str.match(regex) || [];
3293- return matches.length >= threshold;
3294- }
3295-
3296- const generatedTextFiltered = (text) => {
3297- if (text) {
3298- if (power_user.auto_swipe_minimum_length) {
3299- if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
3300- console.log('Generated text size too small');
3301- return true;
3302- }
3303- }
3304- if (power_user.auto_swipe_blacklist_threshold) {
3305- if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3306- console.log('Generated text has blacklisted words');
3307- return true;
3308- }
3309- }
3310- }
3311- return false;
3312- };
3313-
3314- if (generatedTextFiltered(text)) {
3315- swipe_right();
3316- return;
3317- }
33183309 }
3310+
33193311 playMessageSound();
33203312 }
33213313
@@ -3373,7 +3365,7 @@ class StreamingProcessor {
33733365 const timestamps = [];
33743366 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33753367 timestamps.push(Date.now());
33763368 if (this.isStopped || this.abortController.signal.aborted) {
33773369 return;
33783370 }
33793371
@@ -3383,7 +3375,7 @@ class StreamingProcessor {
33833375 if (logprobs) {
33843376 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33853377 }
33863378 this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
33873379 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33883380 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
33893381 }
@@ -3850,14 +3842,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38503842 coreChat.pop();
38513843 }
38523844
3853- const reasoning = new PromptReasoning();
3854- for (let i = coreChat.length - 1; i >= 0; i--) {
3855- if (reasoning.isLimitReached()) {
3856- break;
3857- }
3858- coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
3859- }
3860-
38613845 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
38623846 let message = chatItem.mes;
38633847 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -3877,6 +3861,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38773861 };
38783862 }));
38793863
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+
38803885 // Determine token limit
38813886 let this_max_context = getMaxContextSize();
38823887
@@ -4462,7 +4467,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44624467 // TODO: Make all extension prompts use an array/splice method
44634468 const lengthDiff = mesSend.length - cfgPrompt.depth;
44644469 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
4465- finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4470+ const cfgMessage = finalMesSend[cfgDepth];
4471+ if (cfgMessage) {
4472+ if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
4473+ finalMesSend[cfgDepth].extensionPrompts = [];
4474+ }
4475+ finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4476+ }
44664477 }
44674478 }
44684479 }
@@ -4785,6 +4796,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47854796 const swipes = extractMultiSwipes(data, type);
47864797
47874798 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4799+ reasoning = getRegexedString(reasoning, regex_placement.REASONING);
4800+
4801+ if (power_user.trim_spaces) {
4802+ reasoning = reasoning.trim();
4803+ }
47884804
47894805 if (isContinue) {
47904806 getMessage = continue_mag + getMessage;
@@ -4843,32 +4859,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
48434859 }
48444860
48454861 const isAborted = abortController && abortController.signal.aborted;
48464862 if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(getMessage)) {
4847- console.debug('checking for autoswipeblacklist on non-streaming message');
4863+ is_send_press = false;
4848- function containsBlacklistedWords(getMessage, blacklist, threshold) {
4864+ return swipe_right();
4849- console.debug('checking blacklisted words');
4850- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
4851- const matches = getMessage.match(regex) || [];
4852- return matches.length >= threshold;
4853- }
4854-
4855- const generatedTextFiltered = (getMessage) => {
4856- if (power_user.auto_swipe_blacklist_threshold) {
4857- if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
4858- console.debug('Generated text has blacklisted words');
4859- return true;
4860- }
4861- }
4862-
4863- return false;
4864- };
4865- if (generatedTextFiltered(getMessage)) {
4866- console.debug('swiping right automatically');
4867- is_send_press = false;
4868- swipe_right();
4869- // TODO: do we want to resolve after an auto-swipe?
4870- return;
4871- }
48724865 }
48734866
48744867 console.debug('/api/chats/save called by /Generate');
@@ -5719,15 +5712,26 @@ function extractMessageFromData(data) {
57195712 * @returns {string} Extracted reasoning
57205713 */
57215714function extractReasoningFromData(data) {
5722- if (main_api === 'openai' && oai_settings.show_thoughts) {
5715+ switch (main_api) {
5723- switch (oai_settings.chat_completion_source) {
5716+ case 'textgenerationwebui':
5724- case chat_completion_sources.DEEPSEEK:
5717+ switch (textgen_settings.type) {
5725- return data?.choices?.[0]?.message?.reasoning_content ?? '';
5718+ case textgen_types.OPENROUTER:
5726- case chat_completion_sources.OPENROUTER:
5719+ return data?.choices?.[0]?.reasoning ?? '';
5727- return data?.choices?.[0]?.message?.reasoning ?? '';
5720+ }
5728- case chat_completion_sources.MAKERSUITE:
5721+ break;
5729- return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5722+
5730- }
5723+ case 'openai':
5724+ if (!oai_settings.show_thoughts) break;
5725+
5726+ switch (oai_settings.chat_completion_source) {
5727+ case chat_completion_sources.DEEPSEEK:
5728+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
5729+ case chat_completion_sources.OPENROUTER:
5730+ return data?.choices?.[0]?.message?.reasoning ?? '';
5731+ case chat_completion_sources.MAKERSUITE:
5732+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5733+ }
5734+ break;
57315735 }
57325736
57335737 return '';
@@ -5923,6 +5927,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
59235927 chat[chat.length - 1]['extra'] = {};
59245928 }
59255929
5930+ // Coerce null/undefined to empty string
5931+ if (!chat[chat.length - 1]['extra']['reasoning']) {
5932+ chat[chat.length - 1]['extra']['reasoning'] = '';
5933+ }
5934+
5935+ if (!reasoning) {
5936+ reasoning = '';
5937+ }
5938+
59265939 let oldMessage = '';
59275940 const generationFinished = new Date();
59285941 const img = extractImageFromMessage(getMessage);
@@ -7190,9 +7203,11 @@ function messageEditAuto(div) {
71907203 mes.is_system,
71917204 mes.is_user,
71927205 this_edit_mes_id,
7206+ {},
7207+ false,
71937208 ));
71947209 mesBlock.find('.mes_bias').empty();
71957210 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
71967211 saveChatDebounced();
71977212}
71987213
@@ -7214,10 +7229,12 @@ async function messageEditDone(div) {
72147229 mes.is_system,
72157230 mes.is_user,
72167231 this_edit_mes_id,
7232+ {},
7233+ false,
72177234 ),
72187235 );
72197236 mesBlock.find('.mes_bias').empty();
72207237 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
72217238 appendMediaToMessage(mes, div.closest('.mes'));
72227239 addCopyToCodeBlocks(div.closest('.mes'));
72237240
@@ -8721,11 +8738,6 @@ const swipe_right = () => {
87218738 easing: animation_easing,
87228739 queue: false,
87238740 complete: async function () {
8724- /*if (!selected_group) {
8725- var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
8726- typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
8727- } */
8728- /* $("#chat").append(typingIndicator); */
87298741 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
87308742 //console.log(parseInt(chat[chat.length-1]['swipe_id']));
87318743 //console.log(chat[chat.length-1]['swipes'].length);
@@ -10860,6 +10872,8 @@ jQuery(async function () {
1086010872 chat[this_edit_mes_id].is_system,
1086110873 chat[this_edit_mes_id].is_user,
1086210874 this_edit_mes_id,
10875+ {},
10876+ false,
1086310877 ));
1086410878 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
1086510879 addCopyToCodeBlocks($(this).closest('.mes'));
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+15 -5
@@ -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();
@@ -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/expressions/index.js+0 -1
@@ -2178,7 +2178,6 @@ function migrateSettings() {
21782178 typeList: [ARGUMENT_TYPE.STRING],
21792179 isRequired: true,
21802180 enumProvider: commonEnumProviders.characters('character'),
2181- forceEnum: true,
21822181 }),
21832182 ],
21842183 helpString: 'Returns the last set sprite / expression for the named character.',
public/scripts/extensions/gallery/index.js+0 -1
@@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441441 description: 'character name',
442442 typeList: [ARGUMENT_TYPE.STRING],
443443 enumProvider: commonEnumProviders.characters('character'),
444- forceEnum: true,
445444 }),
446445 SlashCommandNamedArgument.fromProps({
447446 name: 'group',
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+48 -5
@@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js';
3030export { talkingAnimation };
3131
3232const UPDATE_INTERVAL = 1000;
33+const wrapper = new ModuleWorkerWrapper(moduleWorker);
3334
3435let voiceMapEntries = [];
3536let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -120,7 +121,7 @@ async function onNarrateOneMessage() {
120121 }
121122
122123 resetTtsPlayback();
123124 ttsJobQueue.pushprocessAndQueueTtsMessage(message);
124125 moduleWorker();
125126}
126127
@@ -147,7 +148,7 @@ async function onNarrateText(args, text) {
147148 }
148149
149150 resetTtsPlayback();
150151 ttsJobQueue.pushprocessAndQueueTtsMessage({ mes: text, name: name });
151152 await moduleWorker();
152153
153154 // Return back to the chat voices
@@ -220,6 +221,36 @@ function isTtsProcessing() {
220221 return processing;
221222}
222223
224+/**
225+ * Splits a message into lines and adds each non-empty line to the TTS job queue.
226+ * @param {Object} message - The message object to be processed.
227+ * @param {string} message.mes - The text of the message to be split into lines.
228+ * @param {string} message.name - The name associated with the message.
229+ * @returns {void}
230+ */
231+function processAndQueueTtsMessage(message) {
232+ if (!extension_settings.tts.narrate_by_paragraphs) {
233+ ttsJobQueue.push(message);
234+ return;
235+ }
236+
237+ const lines = message.mes.split('\n');
238+
239+ for (let i = 0; i < lines.length; i++) {
240+ const line = lines[i];
241+
242+ if (line.length === 0) {
243+ continue;
244+ }
245+
246+ ttsJobQueue.push(
247+ Object.assign({}, message, {
248+ mes: line,
249+ }),
250+ );
251+ }
252+}
253+
223254function debugTtsPlayback() {
224255 console.log(JSON.stringify(
225256 {
@@ -350,7 +381,7 @@ function onAudioControlClicked() {
350381 talkingAnimation(false);
351382 } else {
352383 // Default play behavior if not processing or playing is to play the last message.
353384 ttsJobQueue.pushprocessAndQueueTtsMessage(context.chat[context.chat.length - 1]);
354385 }
355386 updateUiAudioPlayState();
356387}
@@ -376,6 +407,7 @@ function completeCurrentAudioJob() {
376407 currentAudioJob = null;
377408 talkingAnimation(false); //stop lip animation
378409 // updateUiPlayState();
410+ wrapper.update();
379411}
380412
381413/**
@@ -569,6 +601,7 @@ function loadSettings() {
569601 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
570602 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
571603 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
604+ $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
572605 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
573606 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
574607 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() {
638671 saveSettingsDebounced();
639672}
640673
674+function onNarrateByParagraphsClick() {
675+ extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
676+ saveSettingsDebounced();
677+}
678+
641679
642680function onNarrateDialoguesClick() {
643681 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
816854 lastChatId = context.chatId;
817855
818856 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
819- ttsJobQueue.push(message);
857+
858+ if (extension_settings.tts.periodic_auto_generation) {
859+ ttsJobQueue.push(message);
860+ } else {
861+ processAndQueueTtsMessage(message);
862+ }
820863}
821864
822865async function onMessageDeleted() {
@@ -1156,6 +1199,7 @@ jQuery(async function () {
11561199 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
11571200 $('#tts_auto_generation').on('click', onAutoGenerationClick);
11581201 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1202+ $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
11591203 $('#tts_narrate_user').on('click', onNarrateUserClick);
11601204
11611205 $('#playback_rate').on('input', function () {
@@ -1177,7 +1221,6 @@ jQuery(async function () {
11771221 loadSettings(); // Depends on Extension Controls and loadTtsProvider
11781222 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
11791223 addAudioControl(); // Depends on Extension Controls
1180- const wrapper = new ModuleWorkerWrapper(moduleWorker);
11811224 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
11821225 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
11831226 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/group-chats.js+27 -33
@@ -292,10 +292,11 @@ export function getGroupNames() {
292292
293293/**
294294 * Finds the character ID for a group member.
295295 * @param {number|string} arg 0-based member index or character name
296- * @returns {number} 0-based character ID
296+ * @param {Boolean} full Whether to return a key-value object containing extra data
297+ * @returns {number|Object} 0-based character ID or key-value object if full is true
297298 */
298299export function findGroupMemberId(arg, full = false) {
299300 arg = arg?.trim();
300301
301302 if (!arg) {
@@ -311,15 +312,19 @@ export function findGroupMemberId(arg) {
311312 }
312313
313314 const index = parseInt(arg);
314315 const searchByNamesearchByString = isNaN(index);
315316
316317 if (searchByNamesearchByString) {
317- const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) }));
318+ const memberNames = group.members.map(x => ({
318- const fuse = new Fuse(memberNames, { keys: ['name'] });
319+ avatar: x,
320+ name: characters.find(y => y.avatar === x)?.name,
321+ index: characters.findIndex(y => y.avatar === x),
322+ }));
323+ const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] });
319324 const result = fuse.search(arg);
320325
321326 if (!result.length) {
322327 console.warn(`WARN: No group member found withusing namestring ${arg}`);
323328 return;
324329 }
325330
@@ -330,9 +335,11 @@ export function findGroupMemberId(arg) {
330335 return;
331336 }
332337
333338 console.log(`TriggeringTargeting group member ${chid} (${arg}) from search result`, result[0]);
334- return chid;
339+
335- } else {
340+ return !full ? chid : { ...{ id: chid }, ...result[0].item };
341+ }
342+ else {
336343 const memberAvatar = group.members[index];
337344
338345 if (memberAvatar === undefined) {
@@ -347,8 +354,14 @@ export function findGroupMemberId(arg) {
347354 return;
348355 }
349356
350357 console.log(`TriggeringTargeting group member ${memberAvatar} at index ${index}`);
351- return chid;
358+
359+ return !full ? chid : {
360+ id: chid,
361+ avatar: memberAvatar,
362+ name: characters.find(y => y.avatar === memberAvatar)?.name,
363+ index: index,
364+ };
352365 }
353366}
354367
@@ -805,7 +818,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
805818
806819 /** @type {any} Caution: JS war crimes ahead */
807820 let textResult = '';
808- let typingIndicator = $('#chat .typing_indicator');
809821 const group = groups.find((x) => x.id === selected_group);
810822
811823 if (!group || !Array.isArray(group.members) || !group.members.length) {
@@ -821,14 +833,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
821833 setCharacterId(undefined);
822834 const userInput = String($('#send_textarea').val());
823835
824- if (typingIndicator.length === 0 && !isStreamingEnabled()) {
825- typingIndicator = $(
826- '#typing_indicator_template .typing_indicator',
827- ).clone();
828- typingIndicator.hide();
829- $('#chat').append(typingIndicator);
830- }
831-
832836 // id of this specific batch for regeneration purposes
833837 group_generation_id = Date.now();
834838 const lastMessage = chat[chat.length - 1];
@@ -906,14 +910,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
906910 }
907911 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
908912
909- if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
910- // update indicator and scroll down
911- typingIndicator
912- .find('.typing_indicator_name')
913- .text(characters[chId].name);
914- typingIndicator.show();
915- }
916-
917913 // Wait for generation to finish
918914 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
919915 let messageChunk = textResult?.messageChunk;
@@ -930,8 +926,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
930926 }
931927 }
932928 } finally {
933- typingIndicator.hide();
934-
935929 is_group_generating = false;
936930 setSendButtonState(false);
937931 setCharacterId(undefined);
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/openai.js+37 -40
@@ -258,8 +258,8 @@ 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.13-70b-versatile',
263263 nanogpt_model: 'gpt-4o-mini',
264264 zerooneai_model: 'yi-large',
265265 blockentropy_model: 'be-70b-base-llama3.1',
@@ -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};
@@ -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';
@@ -2151,7 +2151,7 @@ function getStreamingReply(data, state) {
21512151 return data?.delta?.text || '';
21522152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
21532153 if (oai_settings.show_thoughts) {
21542154 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
21552155 }
21562156 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
21572157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
@@ -2166,7 +2166,7 @@ function getStreamingReply(data, state) {
21662166 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
21672167 }
21682168 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21692169 } else {
21702170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21712171 }
21722172}
@@ -4027,7 +4027,7 @@ function getMaxContextOpenAI(value) {
40274027 if (oai_settings.max_context_unlocked) {
40284028 return unlocked_max;
40294029 }
40304030 else if (value.startsWith('o1-') || value.startsWith('o3')) {
40314031 return max_128k;
40324032 }
40334033 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')) {
@@ -4380,28 +4380,19 @@ async function onModelChange() {
43804380 if (oai_settings.max_context_unlocked) {
43814381 $('#openai_max_context').attr('max', unlocked_max);
43824382 }
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+ }
43834389 else if (oai_settings.perplexity_model.includes('llama-3.1')) {
43844390 const isOnline = oai_settings.perplexity_model.includes('online');
43854391 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
43864392 $('#openai_max_context').attr('max', contextSize);
43874393 }
4388- else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
4389- $('#openai_max_context').attr('max', max_32k);
4390- }
4391- else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
4392- $('#openai_max_context').attr('max', 28000);
4393- }
4394- 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)) {
4395- $('#openai_max_context').attr('max', max_16k);
4396- }
4397- else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
4398- $('#openai_max_context').attr('max', max_8k);
4399- }
4400- else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
4401- $('#openai_max_context').attr('max', 12000);
4402- }
44034394 else {
44044395 $('#openai_max_context').attr('max', max_4kmax_128k);
44054396 }
44064397 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
44074398 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4412,24 +4403,30 @@ async function onModelChange() {
44124403 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
44134404 if (oai_settings.max_context_unlocked) {
44144405 $('#openai_max_context').attr('max', unlocked_max);
4415- }
4406+ } else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
4416- else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
44174407 $('#openai_max_context').attr('max', max_8k);
4418- }
4408+ } else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
4419- else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
44204409 $('#openai_max_context').attr('max', max_128k);
4421- }
4410+ } else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
4422- else if (oai_settings.groq_model.includes('llama3-groq')) {
4411+ $('#openai_max_context').attr('max', max_128k);
4412+ } else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
44234413 $('#openai_max_context').attr('max', max_8k);
4424- }
4414+ } else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
4425- else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
44264415 $('#openai_max_context').attr('max', max_8k);
4427- }
4416+ } else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
4428- else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
44294417 $('#openai_max_context').attr('max', max_32k);
4430- }
4418+ } else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
4431- else {
4419+ $('#openai_max_context').attr('max', max_128k);
4432- $('#openai_max_context').attr('max', max_4k);
4420+ } else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
4421+ $('#openai_max_context').attr('max', max_8k);
4422+ } else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
4423+ $('#openai_max_context').attr('max', max_128k);
4424+ } else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
4425+ $('#openai_max_context').attr('max', max_128k);
4426+ } else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
4427+ $('#openai_max_context').attr('max', max_128k);
4428+ } else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
4429+ $('#openai_max_context').attr('max', max_128k);
44334430 }
44344431 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
44354432 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
public/scripts/power-user.js+84 -2
@@ -254,6 +254,7 @@ let power_user = {
254254 },
255255
256256 reasoning: {
257+ auto_parse: false,
257258 add_to_prompts: false,
258259 prefix: '<think>\n',
259260 suffix: '\n</think>',
@@ -2917,6 +2918,46 @@ export function flushEphemeralStoppingStrings() {
29172918}
29182919
29192920/**
2921+ * Checks if the generated text should be filtered based on the auto-swipe settings.
2922+ * @param {string} text The text to check
2923+ * @returns {boolean} If the generated text should be filtered
2924+ */
2925+export function generatedTextFiltered(text) {
2926+ /**
2927+ * Checks if the given text contains any of the blacklisted words.
2928+ * @param {string} text The text to check
2929+ * @param {string[]} blacklist The list of blacklisted words
2930+ * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
2931+ * @returns {boolean} Whether the text contains blacklisted words
2932+ */
2933+ function containsBlacklistedWords(text, blacklist, threshold) {
2934+ const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
2935+ const matches = text.match(regex) || [];
2936+ return matches.length >= threshold;
2937+ }
2938+
2939+ // Make sure a generated text is non-empty
2940+ // Otherwise we might get in a loop with a broken API
2941+ text = text.trim();
2942+ if (text.length > 0) {
2943+ if (power_user.auto_swipe_minimum_length) {
2944+ if (text.length < power_user.auto_swipe_minimum_length) {
2945+ console.log('Generated text size too small');
2946+ return true;
2947+ }
2948+ }
2949+ if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
2950+ if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
2951+ console.log('Generated text has blacklisted words');
2952+ return true;
2953+ }
2954+ }
2955+ }
2956+
2957+ return false;
2958+}
2959+
2960+/**
29202961 * Gets the custom stopping strings from the power user settings.
29212962 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
29222963 * @returns {string[]} An array of custom stopping strings
@@ -3887,9 +3928,9 @@ $(document).ready(() => {
38873928 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
38883929 }));
38893930 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
38903931 name: 'delmodedel',
38913932 callback: doDelMode,
38923933 aliases: ['deldelete', 'delmode'],
38933934 unnamedArgumentList: [
38943935 new SlashCommandArgument(
38953936 'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4072,4 +4113,45 @@ $(document).ready(() => {
40724113 ],
40734114 helpString: 'activates a movingUI preset by name',
40744115 }));
4116+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4117+ name: 'stop-strings',
4118+ aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4119+ helpString: `
4120+ <div>
4121+ Sets a list of custom stopping strings. Gets the list if no value is provided.
4122+ </div>
4123+ <div>
4124+ <strong>Examples:</strong>
4125+ </div>
4126+ <ul>
4127+ <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4128+ <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4129+ </ul>
4130+ `,
4131+ returns: ARGUMENT_TYPE.LIST,
4132+ unnamedArgumentList: [
4133+ SlashCommandArgument.fromProps({
4134+ description: 'list of strings',
4135+ typeList: [ARGUMENT_TYPE.LIST],
4136+ acceptsMultiple: false,
4137+ isRequired: false,
4138+ }),
4139+ ],
4140+ callback: (_, value) => {
4141+ if (String(value ?? '').trim()) {
4142+ const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4143+ if (!parsedValue || !Array.isArray(parsedValue)) {
4144+ throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4145+ }
4146+ parsedValue.forEach((item, index) => {
4147+ parsedValue[index] = String(item);
4148+ });
4149+ power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4150+ $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4151+ saveSettingsDebounced();
4152+ }
4153+
4154+ return power_user.custom_stopping_strings;
4155+ },
4156+ }));
40754157});
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+158 -7
@@ -1,4 +1,5 @@
11import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2+import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
23import { t } from './i18n.js';
34import { MacrosParser } from './macros.js';
45import { Popup } from './popup.js';
@@ -7,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
78import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
89import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
910import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1011import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
1112
1213/**
1314 * Gets a message from a jQuery element.
@@ -49,11 +50,12 @@ export class PromptReasoning {
4950 * Add reasoning to a message according to the power user settings.
5051 * @param {string} content Message content
5152 * @param {string} reasoning Message reasoning
53+ * @param {boolean} isPrefix Whether this is the last message prefix
5254 * @returns {string} Message content with reasoning
5355 */
5456 addToMessage(content, reasoning, isPrefix) {
5557 // Disabled or reached limit of additions
5658 if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
5759 return content;
5860 }
5961
@@ -70,6 +72,11 @@ export class PromptReasoning {
7072 const separator = substituteParams(power_user.reasoning.separator || '');
7173 const suffix = substituteParams(power_user.reasoning.suffix || '');
7274
75+ // Combine parts with reasoning only
76+ if (isPrefix && !content) {
77+ return `${prefix}${reasoning}`;
78+ }
79+
7380 // Combine parts with reasoning and content
7481 return `${prefix}${reasoning}${suffix}${separator}${content}`;
7582 }
@@ -105,11 +112,18 @@ function loadReasoningSettings() {
105112 power_user.reasoning.max_additions = Number($(this).val());
106113 saveSettingsDebounced();
107114 });
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+ });
108121}
109122
110123function registerReasoningSlashCommands() {
111124 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112125 name: 'reasoning-get',
126+ aliases: ['get-reasoning'],
113127 returns: ARGUMENT_TYPE.STRING,
114128 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.`,
115129 unnamedArgumentList: [
@@ -129,6 +143,7 @@ function registerReasoningSlashCommands() {
129143
130144 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
131145 name: 'reasoning-set',
146+ aliases: ['set-reasoning'],
132147 returns: ARGUMENT_TYPE.STRING,
133148 helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
134149 namedArgumentList: [
@@ -146,7 +161,7 @@ function registerReasoningSlashCommands() {
146161 }),
147162 ],
148163 callback: async (args, value) => {
149164 const messageId = !isNaN(Number(args[0].at)) ? Number(args[0].at) : chat.length - 1;
150165 const message = chat[messageId];
151166 if (!message?.extra) {
152167 return '';
@@ -160,6 +175,50 @@ function registerReasoningSlashCommands() {
160175 return message.extra.reasoning;
161176 },
162177 }));
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+ }));
163222}
164223
165224function registerReasoningMacros() {
@@ -168,7 +227,7 @@ function registerReasoningMacros() {
168227 MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
169228}
170229
171230function setReasoningEventHandlers() {
172231 $(document).on('click', '.mes_reasoning_copy', (e) => {
173232 e.stopPropagation();
174233 e.preventDefault();
@@ -224,7 +283,7 @@ function setReasoningEventHandlers(){
224283 }
225284
226285 const textarea = messageBlock.find('.reasoning_edit_textarea');
227286 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
228287 message.extra.reasoning = reasoning;
229288 await saveChatConditional();
230289 updateMessageBlock(messageId, message);
@@ -289,9 +348,101 @@ function setReasoningEventHandlers(){
289348 });
290349}
291350
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+
292442export function initReasoning() {
293443 loadReasoningSettings();
294444 setReasoningEventHandlers();
295445 registerReasoningSlashCommands();
296446 registerReasoningMacros();
447+ registerReasoningAppEvents();
297448}
public/scripts/slash-commands.js+124 -45
@@ -59,7 +59,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
5959import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
6060import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
6161import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
6262import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
6363import { registerVariableCommands, resolveVariable } from './variables.js';
6464import { background_settings } from './backgrounds.js';
6565import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -235,7 +235,6 @@ export function initDefaultSlashCommands() {
235235 description: 'Character name - or unique character identifier (avatar key)',
236236 typeList: [ARGUMENT_TYPE.STRING],
237237 enumProvider: commonEnumProviders.characters('character'),
238- forceEnum: false,
239238 }),
240239 ],
241240 helpString: `
@@ -274,7 +273,6 @@ export function initDefaultSlashCommands() {
274273 typeList: [ARGUMENT_TYPE.STRING],
275274 isRequired: true,
276275 enumProvider: commonEnumProviders.characters('character'),
277- forceEnum: false,
278276 }),
279277 SlashCommandNamedArgument.fromProps({
280278 name: 'avatar',
@@ -518,7 +516,6 @@ export function initDefaultSlashCommands() {
518516 typeList: [ARGUMENT_TYPE.STRING],
519517 isRequired: true,
520518 enumProvider: commonEnumProviders.characters('all'),
521- forceEnum: true,
522519 }),
523520 ],
524521 helpString: 'Opens up a chat with the character or group by its name',
@@ -734,6 +731,57 @@ export function initDefaultSlashCommands() {
734731 helpString: 'Unhides a message from the prompt.',
735732 }));
736733 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
734+ name: 'member-get',
735+ aliases: ['getmember', 'memberget'],
736+ callback: (async ({ field = 'name' }, arg) => {
737+ if (!selected_group) {
738+ toastr.warning('Cannot run /member-get command outside of a group chat.');
739+ return '';
740+ }
741+ if (field === '') {
742+ toastr.warning('\'/member-get field=\' argument required!');
743+ return '';
744+ }
745+ field = field.toString();
746+ arg = arg.toString();
747+ if (!['name', 'index', 'id', 'avatar'].includes(field)) {
748+ toastr.warning('\'/member-get field=\' argument required!');
749+ return '';
750+ }
751+ const isId = !isNaN(parseInt(arg));
752+ const groupMember = findGroupMemberId(arg, true);
753+ if (!groupMember) {
754+ toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
755+ return '';
756+ }
757+ return groupMember[field];
758+ }),
759+ namedArgumentList: [
760+ SlashCommandNamedArgument.fromProps({
761+ name: 'field',
762+ description: 'Whether to retrieve the name, index, id, or avatar.',
763+ typeList: [ARGUMENT_TYPE.STRING],
764+ isRequired: true,
765+ defaultValue: 'name',
766+ enumList: [
767+ new SlashCommandEnumValue('name', 'Character name'),
768+ new SlashCommandEnumValue('index', 'Group member index'),
769+ new SlashCommandEnumValue('avatar', 'Character avatar'),
770+ new SlashCommandEnumValue('id', 'Character index'),
771+ ],
772+ }),
773+ ],
774+ unnamedArgumentList: [
775+ SlashCommandArgument.fromProps({
776+ description: 'member index (starts with 0), name, or avatar',
777+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
778+ isRequired: true,
779+ enumProvider: commonEnumProviders.groupMembers(),
780+ }),
781+ ],
782+ helpString: 'Retrieves a group member\'s name, index, id, or avatar.',
783+ }));
784+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
737785 name: 'member-disable',
738786 callback: disableGroupMemberCallback,
739787 aliases: ['disable', 'disablemember', 'memberdisable'],
@@ -843,7 +891,8 @@ export function initDefaultSlashCommands() {
843891 helpString: 'Moves a group member down in the group chat list.',
844892 }));
845893 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
846894 name: 'member-peek',
895+ aliases: ['peek', 'memberpeek', 'peekmember'],
847896 callback: peekCallback,
848897 unnamedArgumentList: [
849898 SlashCommandArgument.fromProps({
@@ -1009,7 +1058,6 @@ export function initDefaultSlashCommands() {
10091058 typeList: [ARGUMENT_TYPE.STRING],
10101059 defaultValue: 'System',
10111060 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
1012- forceEnum: false,
10131061 }),
10141062 new SlashCommandNamedArgument(
10151063 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@@ -1903,7 +1951,7 @@ export function initDefaultSlashCommands() {
19031951 returns: 'uppercase string',
19041952 unnamedArgumentList: [
19051953 new SlashCommandArgument(
19061954 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19071955 ),
19081956 ],
19091957 helpString: 'Converts the provided string to uppercase.',
@@ -1915,7 +1963,7 @@ export function initDefaultSlashCommands() {
19151963 returns: 'lowercase string',
19161964 unnamedArgumentList: [
19171965 new SlashCommandArgument(
19181966 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19191967 ),
19201968 ],
19211969 helpString: 'Converts the provided string to lowercase.',
@@ -1935,7 +1983,7 @@ export function initDefaultSlashCommands() {
19351983 ],
19361984 unnamedArgumentList: [
19371985 new SlashCommandArgument(
19381986 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19391987 ),
19401988 ],
19411989 helpString: `
@@ -1999,6 +2047,62 @@ export function initDefaultSlashCommands() {
19992047 return '';
20002048 },
20012049 }));
2050+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2051+ name: 'replace',
2052+ aliases: ['re'],
2053+ callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2054+ if (pattern === '')
2055+ throw new Error('Argument of \'pattern=\' cannot be empty');
2056+ switch (mode) {
2057+ case 'literal':
2058+ return text.replaceAll(pattern, replacer);
2059+ case 'regex':
2060+ return text.replace(regexFromString(pattern), replacer);
2061+ default:
2062+ throw new Error('Invalid \'/replace mode=\' argument specified!');
2063+ }
2064+ }),
2065+ returns: 'replaced text',
2066+ namedArgumentList: [
2067+ SlashCommandNamedArgument.fromProps({
2068+ name: 'mode',
2069+ description: 'Replaces occurrence(s) of a pattern',
2070+ typeList: [ARGUMENT_TYPE.STRING],
2071+ defaultValue: 'literal',
2072+ enumList: ['literal', 'regex'],
2073+ }),
2074+ new SlashCommandNamedArgument(
2075+ 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
2076+ ),
2077+ new SlashCommandNamedArgument(
2078+ 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
2079+ ),
2080+ ],
2081+ unnamedArgumentList: [
2082+ new SlashCommandArgument(
2083+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
2084+ ),
2085+ ],
2086+ helpString: `
2087+ <div>
2088+ Replaces text within the provided string based on the pattern.
2089+ </div>
2090+ <div>
2091+ If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />
2092+ If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />
2093+ The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />
2094+ If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />
2095+ </div>
2096+ <div>
2097+ <strong>Example:</strong>
2098+ <pre>/let x Blue house and blue car || </pre>
2099+ <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
2100+ <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
2101+ <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
2102+ <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
2103+ </div>
2104+ `,
2105+ }));
20022106
20032107 registerVariableCommands();
20042108}
@@ -2991,7 +3095,7 @@ function performGroupMemberAction(chid, action) {
29913095
29923096async function disableGroupMemberCallback(_, arg) {
29933097 if (!selected_group) {
29943098 toastr.warning('Cannot run /member-disable command outside of a group chat.');
29953099 return '';
29963100 }
29973101
@@ -3008,7 +3112,7 @@ async function disableGroupMemberCallback(_, arg) {
30083112
30093113async function enableGroupMemberCallback(_, arg) {
30103114 if (!selected_group) {
30113115 toastr.warning('Cannot run /member-enable command outside of a group chat.');
30123116 return '';
30133117 }
30143118
@@ -3025,7 +3129,7 @@ async function enableGroupMemberCallback(_, arg) {
30253129
30263130async function moveGroupMemberUpCallback(_, arg) {
30273131 if (!selected_group) {
30283132 toastr.warning('Cannot run /memberupmember-up command outside of a group chat.');
30293133 return '';
30303134 }
30313135
@@ -3042,7 +3146,7 @@ async function moveGroupMemberUpCallback(_, arg) {
30423146
30433147async function moveGroupMemberDownCallback(_, arg) {
30443148 if (!selected_group) {
30453149 toastr.warning('Cannot run /memberdownmember-down command outside of a group chat.');
30463150 return '';
30473151 }
30483152
@@ -3059,12 +3163,12 @@ async function moveGroupMemberDownCallback(_, arg) {
30593163
30603164async function peekCallback(_, arg) {
30613165 if (!selected_group) {
30623166 toastr.warning('Cannot run /member-peek command outside of a group chat.');
30633167 return '';
30643168 }
30653169
30663170 if (is_group_generating) {
30673171 toastr.warning('Cannot run /member-peek command while the group reply is generating.');
30683172 return '';
30693173 }
30703174
@@ -3081,12 +3185,7 @@ async function peekCallback(_, arg) {
30813185
30823186async function removeGroupMemberCallback(_, arg) {
30833187 if (!selected_group) {
30843188 toastr.warning('Cannot run /memberremovemember-remove command outside of a group chat.');
3085- return '';
3086- }
3087-
3088- if (is_group_generating) {
3089- toastr.warning('Cannot run /memberremove command while the group reply is generating.');
30903189 return '';
30913190 }
30923191
@@ -3194,12 +3293,7 @@ function findPersonaByName(name) {
31943293}
31953294
31963295async function sendUserMessageCallback(args, text) {
3197- if (!text) {
3296+ text = String(text ?? '').trim();
3198- toastr.warning('You must specify text to send');
3199- return;
3200- }
3201-
3202- text = text.trim();
32033297 const compact = isTrueBoolean(args?.compact);
32043298 const bias = extractMessageBias(text);
32053299
@@ -3508,13 +3602,7 @@ export function getNameAndAvatarForMessage(character, name = null) {
35083602}
35093603
35103604export async function sendMessageAs(args, text) {
3511- if (!text) {
3512- toastr.warning('You must specify text to send as');
3513- return '';
3514- }
3515-
35163605 let name = args.name?.trim();
3517- let mesText;
35183606
35193607 if (!name) {
35203608 const namelessWarningKey = 'sendAsNamelessWarningShown';
@@ -3525,7 +3613,7 @@ export async function sendMessageAs(args, text) {
35253613 name = name2;
35263614 }
35273615
35283616 let mesText = String(text ?? '').trim();
35293617
35303618 // Requires a regex check after the slash command is pushed to output
35313619 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3603,11 +3691,7 @@ export async function sendMessageAs(args, text) {
36033691}
36043692
36053693export async function sendNarratorMessage(args, text) {
3606- if (!text) {
3694+ text = String(text ?? '');
3607- toastr.warning('You must specify text to send');
3608- return '';
3609- }
3610-
36113695 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
36123696 // Messages that do nothing but set bias will be hidden from the context
36133697 const bias = extractMessageBias(text);
@@ -3698,18 +3782,13 @@ export async function promptQuietForLoudResponse(who, text) {
36983782}
36993783
37003784async function sendCommentMessage(args, text) {
3701- if (!text) {
3702- toastr.warning('You must specify text to send');
3703- return '';
3704- }
3705-
37063785 const compact = isTrueBoolean(args?.compact);
37073786 const message = {
37083787 name: COMMENT_NAME_DEFAULT,
37093788 is_user: false,
37103789 is_system: true,
37113790 send_date: getMessageTimeStamp(),
37123791 mes: substituteParams(String(text ?? '').trim()),
37133792 force_avatar: comment_avatar,
37143793 extra: {
37153794 type: system_message_types.COMMENT,
public/scripts/st-context.js+11 -0
@@ -69,6 +69,7 @@ import { textgenerationwebui_settings } from './textgen-settings.js';
6969import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
7070import { ToolManager } from './tool-calling.js';
7171import { timestampToMoment, uuidv4 } from './utils.js';
72+import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
7273
7374export function getContext() {
7475 return {
@@ -175,6 +176,16 @@ export function getContext() {
175176 humanizedDateTime,
176177 updateMessageBlock,
177178 appendMediaToMessage,
179+ variables: {
180+ local: {
181+ get: getLocalVariable,
182+ set: setLocalVariable,
183+ },
184+ global: {
185+ get: getGlobalVariable,
186+ set: setGlobalVariable,
187+ },
188+ },
178189 };
179190}
180191
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+7 -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,7 +989,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986989 let logprobs = null;
987990 const swipes = [];
988991 const toolCalls = [];
989992 const state = { reasoning: '' };
990993 while (true) {
991994 const { done, value } = await reader.read();
992995 if (done) return;
@@ -1003,6 +1006,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
10031006 const newText = data?.choices?.[0]?.text || data?.content || '';
10041007 text += newText;
10051008 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1009+ state.reasoning += data?.choices?.[0]?.reasoning ?? '';
10061010 }
10071011
10081012 yield { text, swipes, logprobs, toolCalls, state };
@@ -1266,6 +1270,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12661270 'truncation_length': max_context,
12671271 'ban_eos_token': settings.ban_eos_token,
12681272 'skip_special_tokens': settings.skip_special_tokens,
1273+ 'include_reasoning': settings.include_reasoning,
12691274 'top_a': settings.top_a,
12701275 'tfs': settings.tfs,
12711276 '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+5 -17
@@ -399,6 +399,11 @@ input[type='checkbox']:focus-visible {
399399 color: var(--SmartThemeEmColor);
400400}
401401
402+.mes_text q i,
403+.mes_text q em {
404+ color: inherit;
405+}
406+
402407.mes_text u,
403408.mes_reasoning u {
404409 color: var(--SmartThemeUnderlineColor);
@@ -4597,23 +4602,6 @@ body .ui-widget-content li:hover {
45974602 opacity: 1;
45984603}
45994604
4600-.typing_indicator {
4601- position: sticky;
4602- bottom: 10px;
4603- margin: 10px;
4604- opacity: 0.85;
4605- text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
4606- order: 9999;
4607-}
4608-
4609-.typing_indicator:after {
4610- display: inline-block;
4611- vertical-align: bottom;
4612- animation: ellipsis steps(4, end) 1500ms infinite;
4613- content: "";
4614- width: 0px;
4615-}
4616-
46174605#group_avatar_preview .missing-avatar {
46184606 display: inline;
46194607 vertical-align: middle;
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+2 -2
@@ -288,7 +288,7 @@ 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);
292292 const isThinking = model.includes('thinking');
293293
294294 const generationConfig = {
@@ -1000,7 +1000,7 @@ router.post('/generate', jsonParser, function (request, response) {
10001000 bodyParams['route'] = 'fallback';
10011001 }
10021002
10031003 if (request.body.show_thoughtsinclude_reasoning) {
10041004 bodyParams['include_reasoning'] = true;
10051005 }
10061006
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.error('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.error('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.error(`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.error(`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.error(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