Merge branch 'staging' into persona-improvements

6081b9f0cee884abc95c8d0d1f0eb78dadba12fb

Wolfsblvt <wolfsblvt@gmail.com>

19 files changed, +646 -82Showing whitespace changes
.github/readme.md+1 -1
@@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2742741. Open your Command Line
2752752. Run the following command
276276
277277`docker createrun --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
278278
279279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
280280
public/index.html+51 -6
@@ -1977,11 +1977,11 @@
19771977 </span>
19781978 </div>
19791979 </div>
19801980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
19811981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19821982 <input id="openai_show_thoughts" type="checkbox" />
19831983 <span>
19841984 <span data-i18n="Show model thoughtsreasoning">Show model thoughtsreasoning</span>
19851985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
19861986 </span>
19871987 </label>
@@ -3800,6 +3800,39 @@
38003800 </div>
38013801 </div>
38023802 <div>
3803+ <h4 class="standoutHeader">
3804+ <span data-i18n="Reasoning">Reasoning</span>
3805+ </h4>
3806+ <div>
3807+ <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">
3808+ <input id="reasoning_add_to_prompts" type="checkbox" />
3809+ <small data-i18n="Add Reasoning to Prompts">
3810+ Add Reasoning to Prompts
3811+ </small>
3812+ </label>
3813+ <div class="flex-container">
3814+ <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3815+ <small data-i18n="Prefix">Prefix</small>
3816+ <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3817+ </div>
3818+ <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3819+ <small data-i18n="Suffix">Suffix</small>
3820+ <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3821+ </div>
3822+ </div>
3823+ <div class="flex-container">
3824+ <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3825+ <small data-i18n="Separator">Separator</small>
3826+ <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3827+ </div>
3828+ <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3829+ <small data-i18n="Max Additions">Max Additions</small>
3830+ <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3831+ </div>
3832+ </div>
3833+ </div>
3834+ </div>
3835+ <div>
38033836 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
38043837 <div>
38053838 <small>
@@ -6265,14 +6298,26 @@
62656298 <div class="mes_edit_buttons">
62666299 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
62676300 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
62686301 <div class="mes_edit_deletemes_edit_add_reasoning menu_button fa-solid fa-trash-canlightbulb" title="DeleteAdd thisa messagereasoning block" data-i18n="[title]DeleteAdd thisa messagereasoning block"></div>
6269- </div>
6302+ <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
62706303 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
62716304 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down"></div>
6272- </div>
62736305 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
62746306 </div>
62756307 </div>
6308+ <details class="mes_reasoning_details">
6309+ <summary class="mes_reasoning_summary">
6310+ <span data-i18n="Reasoning">Reasoning</span>
6311+ <div class="mes_reasoning_actions">
6312+ <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6313+ <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6314+ <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6315+ <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6316+ <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6317+ </div>
6318+ </summary>
6319+ <div class="mes_reasoning"></div>
6320+ </details>
62766321 <div class="mes_text"></div>
62776322 <div class="mes_img_container">
62786323 <div class="mes_img_controls">
public/locales/fr-fr.json+1 -1
@@ -1385,7 +1385,7 @@
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 "Show model thoughtsreasoning": "Afficher les pensées du modèle",
13891389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
13901390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
13911391 "openai_logit_bias_no_items": "Aucun élément",
public/locales/zh-cn.json+1 -1
@@ -266,7 +266,7 @@
266266 "Use system prompt": "使用系统提示词",
267267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268268 "Merges_all_system_messages_desc_2": "字段发送。",
269269 "Show model thoughtsreasoning": "展示思维链",
270270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
271271 "Assistant Prefill": "AI预填",
272272 "Expand the editor": "展开编辑器",
public/locales/zh-tw.json+1 -1
@@ -2357,7 +2357,7 @@
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 "Show model thoughtsreasoning": "顯示模型思維鏈",
23612361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
23622362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
23632363 "Model ID (optional)": "模型 ID(可選)",
public/script.js+119 -26
@@ -271,6 +271,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
271271import { initBulkEdit } from './scripts/bulk-edit.js';
272272import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
273273import { getContext } from './scripts/st-context.js';
274+import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
274275
275276// API OBJECT FOR EXTERNAL WIRING
276277globalThis.SillyTavern = {
@@ -986,6 +987,7 @@ async function firstLoadInit() {
986987 initServerHistory();
987988 initSettingsSearch();
988989 initBulkEdit();
990+ initReasoning();
989991 await initScrapers();
990992 doDailyExtensionUpdatesCheck();
991993 await hideLoader();
@@ -2213,6 +2215,7 @@ function getMessageFromTemplate({
22132215 isUser,
22142216 avatarImg,
22152217 bias,
2218+ reasoning,
22162219 isSystem,
22172220 title,
22182221 timerValue,
@@ -2237,6 +2240,7 @@ function getMessageFromTemplate({
22372240 mes.find('.avatar img').attr('src', avatarImg);
22382241 mes.find('.ch_name .name_text').text(characterName);
22392242 mes.find('.mes_bias').html(bias);
2243+ mes.find('.mes_reasoning').html(reasoning);
22402244 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
22412245 mes.find('.mesIDDisplay').text(`#${mesId}`);
22422246 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2251,10 +2255,16 @@ function getMessageFromTemplate({
22512255 return mes;
22522256}
22532257
2258+/**
2259+ * Re-renders a message block with updated content.
2260+ * @param {number} messageId Message ID
2261+ * @param {object} message Message object
2262+ */
22542263export function updateMessageBlock(messageId, message) {
22552264 const messageElement = $(`#chat [mesid="${messageId}"]`);
22562265 const text = message?.extra?.display_text ?? message.mes;
22572266 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
2267+ messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));
22582268 addCopyToCodeBlocks(messageElement);
22592269 appendMediaToMessage(message, messageElement);
22602270}
@@ -2413,6 +2423,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24132423 sanitizerOverrides,
24142424 );
24152425 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
2426+ const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
24162427 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24172428
24182429 let params = {
@@ -2422,6 +2433,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24222433 isUser: mes.is_user,
24232434 avatarImg: avatarImg,
24242435 bias: bias,
2436+ reasoning: reasoning,
24252437 isSystem: isSystem,
24262438 title: title,
24272439 bookmarkLink: bookmarkLink,
@@ -2481,6 +2493,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24812493 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
24822494 swipeMessage.attr('swipeid', params.swipeId);
24832495 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2496+ swipeMessage.find('.mes_reasoning').html(reasoning);
24842497 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
24852498 appendMediaToMessage(mes, swipeMessage);
24862499 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3091,6 +3104,7 @@ class StreamingProcessor {
30913104 this.messageTextDom = null;
30923105 this.messageTimerDom = null;
30933106 this.messageTokenCounterDom = null;
3107+ this.messageReasoningDom = null;
30943108 /** @type {HTMLTextAreaElement} */
30953109 this.sendTextarea = document.querySelector('#send_textarea');
30963110 this.type = type;
@@ -3106,6 +3120,7 @@ class StreamingProcessor {
31063120 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
31073121 this.messageLogprobs = [];
31083122 this.toolCalls = [];
3123+ this.reasoning = '';
31093124 }
31103125
31113126 #checkDomElements(messageId) {
@@ -3114,6 +3129,7 @@ class StreamingProcessor {
31143129 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
31153130 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
31163131 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3132+ this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
31173133 }
31183134 }
31193135
@@ -3191,18 +3207,27 @@ class StreamingProcessor {
31913207 this.#checkDomElements(messageId);
31923208 this.#updateMessageBlockVisibility();
31933209 const currentTime = new Date();
3194- // Don't waste time calculating token count for streaming
3195- const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
3196- const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
31973210 chat[messageId]['mes'] = processedText;
31983211 chat[messageId]['gen_started'] = this.timeStarted;
31993212 chat[messageId]['gen_finished'] = currentTime;
32003213
3201- if (currentTokenCount) {
32023214 if (!chat[messageId]['extra']) {
32033215 chat[messageId]['extra'] = {};
32043216 }
32053217
3218+ if (this.reasoning) {
3219+ chat[messageId]['extra']['reasoning'] = this.reasoning;
3220+ if (this.messageReasoningDom instanceof HTMLElement) {
3221+ const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);
3222+ this.messageReasoningDom.innerHTML = formattedReasoning;
3223+ }
3224+ }
3225+
3226+ // Don't waste time calculating token count for streaming
3227+ const tokenCountText = (this.reasoning || '') + processedText;
3228+ const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3229+
3230+ if (currentTokenCount) {
32063231 chat[messageId]['extra']['token_count'] = currentTokenCount;
32073232 if (this.messageTokenCounterDom instanceof HTMLElement) {
32083233 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3224,10 +3249,13 @@ class StreamingProcessor {
32243249 if (this.messageTextDom instanceof HTMLElement) {
32253250 this.messageTextDom.innerHTML = formattedText;
32263251 }
3252+
3253+ const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
32273254 if (this.messageTimerDom instanceof HTMLElement) {
32283255 this.messageTimerDom.textContent = timePassed.timerValue;
32293256 this.messageTimerDom.title = timePassed.timerTitle;
32303257 }
3258+
32313259 this.setFirstSwipe(messageId);
32323260 }
32333261
@@ -3334,7 +3362,7 @@ class StreamingProcessor {
33343362 }
33353363
33363364 /**
33373365 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
33383366 */
33393367 *nullStreamingGeneration() {
33403368 throw new Error('Generation function for streaming is not hooked up');
@@ -3356,7 +3384,7 @@ class StreamingProcessor {
33563384 try {
33573385 const sw = new Stopwatch(1000 / power_user.streaming_fps);
33583386 const timestamps = [];
33593387 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33603388 timestamps.push(Date.now());
33613389 if (this.isStopped) {
33623390 return;
@@ -3368,6 +3396,7 @@ class StreamingProcessor {
33683396 if (logprobs) {
33693397 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33703398 }
3399+ this.reasoning = state?.reasoning ?? '';
33713400 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33723401 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
33733402 }
@@ -3834,6 +3863,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38343863 coreChat.pop();
38353864 }
38363865
3866+ const reasoning = new PromptReasoning();
3867+ for (let i = coreChat.length - 1; i >= 0; i--) {
3868+ if (reasoning.isLimitReached()) {
3869+ break;
3870+ }
3871+ coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
3872+ }
3873+
38373874 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
38383875 let message = chatItem.mes;
38393876 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -4755,6 +4792,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47554792 //const getData = await response.json();
47564793 let getMessage = extractMessageFromData(data);
47574794 let title = extractTitleFromData(data);
4795+ let reasoning = extractReasoningFromData(data);
47584796 kobold_horde_model = title;
47594797
47604798 const swipes = extractMultiSwipes(data, type);
@@ -4781,10 +4819,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47814819 else {
47824820 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
47834821 if (originalType !== 'continue') {
47844822 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
47854823 }
47864824 else {
47874825 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
47884826 }
47894827
47904828 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -5672,7 +5710,6 @@ function extractMessageFromData(data) {
56725710 return data;
56735711 }
56745712
5675- function getTextContext() {
56765713 switch (main_api) {
56775714 case 'kobold':
56785715 return data.results[0].text;
@@ -5689,14 +5726,24 @@ function extractMessageFromData(data) {
56895726 }
56905727}
56915728
5692- const content = getTextContext();
5729+/**
5693-
5730+ * Extracts the reasoning from the response data.
5694- if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) {
5731+ * @param {object} data Response data
5695- const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? '';
5732+ * @returns {string} Extracted reasoning
5696- return [thoughts, content].filter(x => x).join('\n\n');
5733+ */
5734+function extractReasoningFromData(data) {
5735+ if (main_api === 'openai' && oai_settings.show_thoughts) {
5736+ switch (oai_settings.chat_completion_source) {
5737+ case chat_completion_sources.DEEPSEEK:
5738+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
5739+ case chat_completion_sources.OPENROUTER:
5740+ return data?.choices?.[0]?.message?.reasoning ?? '';
5741+ case chat_completion_sources.MAKERSUITE:
5742+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5743+ }
56975744 }
56985745
56995746 return content'';
57005747}
57015748
57025749/**
@@ -5879,7 +5926,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
58795926 return getMessage;
58805927}
58815928
58825929export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
58835930 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
58845931 chat[chat.length - 1]['is_user'])) {
58855932 type = 'normal';
@@ -5904,8 +5951,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59045951 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59055952 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59065953 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5954+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
59075955 if (power_user.message_token_count_enabled) {
59085956 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5957+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59095958 }
59105959 const chat_id = (chat.length - 1);
59115960 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5924,8 +5973,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59245973 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59255974 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59265975 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5976+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
59275977 if (power_user.message_token_count_enabled) {
59285978 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5979+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59295980 }
59305981 const chat_id = (chat.length - 1);
59315982 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5941,8 +5992,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59415992 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59425993 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59435994 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5995+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
59445996 if (power_user.message_token_count_enabled) {
59455997 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5998+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59465999 }
59476000 const chat_id = (chat.length - 1);
59486001 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5958,6 +6011,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59586011 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59596012 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59606013 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6014+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
59616015 if (power_user.trim_spaces) {
59626016 getMessage = getMessage.trim();
59636017 }
@@ -5967,7 +6021,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59676021 chat[chat.length - 1]['gen_finished'] = generationFinished;
59686022
59696023 if (power_user.message_token_count_enabled) {
59706024 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
6025+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59716026 }
59726027
59736028 if (selected_group) {
@@ -6032,6 +6087,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
60326087 return { type, getMessage };
60336088}
60346089
6090+export function syncCurrentSwipeInfoExtras() {
6091+ if (!chat.length) {
6092+ return;
6093+ }
6094+ const currentMessage = chat[chat.length - 1];
6095+ if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6096+ const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6097+ if (swipeInfo && typeof swipeInfo === 'object') {
6098+ swipeInfo.extra = structuredClone(currentMessage.extra);
6099+ }
6100+ }
6101+}
6102+
60356103function saveImageToMessage(img, mes) {
60366104 if (mes && img.image) {
60376105 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -8016,11 +8084,25 @@ function updateEditArrowClasses() {
80168084 }
80178085}
80188086
8019-function closeMessageEditor() {
8087+/**
8088+ * Closes the message editor.
8089+ * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8090+ */
8091+export function closeMessageEditor(what = 'all') {
8092+ if (what === 'message' || what === 'all') {
80208093 if (this_edit_mes_id) {
80218094 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
80228095 }
80238096 }
8097+ if (what === 'reasoning' || what === 'all') {
8098+ document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8099+ const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8100+ if (cancelButton instanceof HTMLElement) {
8101+ cancelButton.click();
8102+ }
8103+ });
8104+ }
8105+}
80248106
80258107export function setGenerationProgress(progress) {
80268108 if (!progress) {
@@ -8451,6 +8533,9 @@ function swipe_left() { // when we swipe left..but no generation.
84518533 streamingProcessor.onStopStreaming();
84528534 }
84538535
8536+ // Make sure ad-hoc changes to extras are saved before swiping away
8537+ syncCurrentSwipeInfoExtras();
8538+
84548539 const swipe_duration = 120;
84558540 const swipe_range = '700px';
84568541 chat[chat.length - 1]['swipe_id']--;
@@ -8502,7 +8587,8 @@ function swipe_left() { // when we swipe left..but no generation.
85028587 }
85038588
85048589 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
85058590 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8591+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
85068592 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
85078593 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
85088594 }
@@ -8585,6 +8671,9 @@ const swipe_right = () => {
85858671 return unblockGeneration();
85868672 }
85878673
8674+ // Make sure ad-hoc changes to extras are saved before swiping away
8675+ syncCurrentSwipeInfoExtras();
8676+
85888677 const swipe_duration = 200;
85898678 const swipe_range = 700;
85908679 //console.log(swipe_range);
@@ -8666,6 +8755,7 @@ const swipe_right = () => {
86668755 // resets the timer
86678756 swipeMessage.find('.mes_timer').html('');
86688757 swipeMessage.find('.tokenCounterDisplay').text('');
8758+ swipeMessage.find('.mes_reasoning').html('');
86698759 } else {
86708760 //console.log('showing previously generated swipe candidate, or "..."');
86718761 //console.log('onclick right swipe calling addOneMessage');
@@ -8676,7 +8766,8 @@ const swipe_right = () => {
86768766 chat[chat.length - 1].extra = {};
86778767 }
86788768
86798769 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8770+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
86808771 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
86818772 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
86828773 }
@@ -9478,7 +9569,8 @@ function addDebugFunctions() {
94789569 message.extra = {};
94799570 }
94809571
9481- message.extra.token_count = await getTokenCountAsync(message.mes, 0);
9572+ const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9573+ message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
94829574 }
94839575
94849576 await saveChatConditional();
@@ -11290,14 +11382,15 @@ jQuery(async function () {
1129011382
1129111383 $(document).keyup(function (e) {
1129211384 if (e.key === 'Escape') {
1129311385 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
1129411386 if (isEditVisible && power_user.auto_save_msg_edits === false) {
1129511387 closeMessageEditor('all');
1129611388 $('#send_textarea').focus();
1129711389 return;
1129811390 }
1129911391 if (isEditVisible && power_user.auto_save_msg_edits === true) {
1130011392 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11393+ closeMessageEditor('reasoning');
1130111394 $('#send_textarea').focus();
1130211395 return;
1130311396 }
public/scripts/backgrounds.js+4 -4
@@ -482,10 +482,10 @@ function highlightNewBackground(bg) {
482482 */
483483function setFittingClass(fitting) {
484484 const backgrounds = $('#bg1, #bg_custom');
485- backgrounds.toggleClass('cover', fitting === 'cover');
485+ for (const option of ['cover', 'contain', 'stretch', 'center']) {
486486 backgrounds.toggleClass('contain'option, fittingoption === 'contain'fitting);
487- backgrounds.toggleClass('stretch', fitting === 'stretch');
487+ }
488- backgrounds.toggleClass('center', fitting === 'center');
488+ background_settings.fitting = fitting;
489489}
490490
491491function onBackgroundFilterInput() {
public/scripts/extensions/tts/index.js+1 -1
@@ -466,7 +466,7 @@ async function processTtsQueue() {
466466 }
467467
468468 if (extension_settings.tts.skip_tags) {
469469 text = text.replace(/<.*?>.[\s\S]*?<\/.*?>/g, '').trim();
470470 }
471471
472472 if (!extension_settings.tts.pass_asterisks) {
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188188 if (data?.token) {
189189 text += data.token;
190190 }
191191 yield { text, swipes: [], toolCalls: [], state: {} };
192192 }
193193 };
194194}
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746746 text += data.token;
747747 }
748748
749749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750750 }
751751 };
752752}
public/scripts/openai.js+16 -10
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
10961096 // Unordered prompts without marker
10971097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
10981098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099- { role: 'system', content: bias, identifier: 'bias' },
11001099 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1100+ { role: 'assistant', content: bias, identifier: 'bias' },
11011101 ];
11021102
11031103 // Tavern Extras - Summary
@@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20952095 let text = '';
20962096 const swipes = [];
20972097 const toolCalls = [];
20982098 const state = { reasoning: '' };
20992099 while (true) {
21002100 const { done, value } = await reader.read();
21012101 if (done) return;
@@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21132113
21142114 ToolManager.parseToolCalls(toolCalls, parsed);
21152115
21162116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
21172117 }
21182118 };
21192119 }
@@ -2150,16 +2150,22 @@ function getStreamingReply(data, state) {
21502150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
21512151 return data?.delta?.text || '';
21522152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2153- return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';
2153+ if (oai_settings.show_thoughts) {
2154+ state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2155+ }
2156+ return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
21542157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
21552158 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
21562159 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2157- const hadThoughts = state.hadThoughts;
2160+ if (oai_settings.show_thoughts) {
21582161 const thoughts state.reasoning += (data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2159- const content = data.choices?.[0]?.delta?.content || '';
2162+ }
2160- state.hadThoughts = !!thoughts;
2163+ return data.choices?.[0]?.delta?.content || '';
2161- const separator = hadThoughts && !thoughts ? '\n\n' : '';
2164+ } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2162- return [thoughts, separator, content].filter(x => x).join('\n\n');
2165+ if (oai_settings.show_thoughts) {
2166+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167+ }
2168+ return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21632169 } else {
21642170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21652171 }
public/scripts/power-user.js+8 -0
@@ -253,6 +253,14 @@ let power_user = {
253253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254254 },
255255
256+ reasoning: {
257+ add_to_prompts: false,
258+ prefix: '<think>\n',
259+ suffix: '\n</think>',
260+ separator: '\n\n',
261+ max_additions: 1,
262+ },
263+
256264 personas: {},
257265 default_persona: null,
258266 persona_descriptions: {},
public/scripts/reasoning.js+297 -0
@@ -0,0 +1,297 @@
1+import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2+import { t } from './i18n.js';
3+import { MacrosParser } from './macros.js';
4+import { Popup } from './popup.js';
5+import { power_user } from './power-user.js';
6+import { SlashCommand } from './slash-commands/SlashCommand.js';
7+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
9+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10+import { copyText } from './utils.js';
11+
12+/**
13+ * Gets a message from a jQuery element.
14+ * @param {Element} element
15+ * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
16+ */
17+function getMessageFromJquery(element) {
18+ const messageBlock = $(element).closest('.mes');
19+ const messageId = Number(messageBlock.attr('mesid'));
20+ const message = chat[messageId];
21+ return { messageId: messageId, message, messageBlock };
22+}
23+
24+/**
25+ * Helper class for adding reasoning to messages.
26+ * Keeps track of the number of reasoning additions.
27+ */
28+export class PromptReasoning {
29+ static REASONING_PLACEHOLDER = '\u200B';
30+ static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
31+
32+ constructor() {
33+ this.counter = 0;
34+ }
35+
36+ /**
37+ * Checks if the limit of reasoning additions has been reached.
38+ * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
39+ */
40+ isLimitReached() {
41+ if (!power_user.reasoning.add_to_prompts) {
42+ return true;
43+ }
44+
45+ return this.counter >= power_user.reasoning.max_additions;
46+ }
47+
48+ /**
49+ * Add reasoning to a message according to the power user settings.
50+ * @param {string} content Message content
51+ * @param {string} reasoning Message reasoning
52+ * @returns {string} Message content with reasoning
53+ */
54+ addToMessage(content, reasoning) {
55+ // Disabled or reached limit of additions
56+ if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
57+ return content;
58+ }
59+
60+ // No reasoning provided or a placeholder
61+ if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
62+ return content;
63+ }
64+
65+ // Increment the counter
66+ this.counter++;
67+
68+ // Substitute macros in variable parts
69+ const prefix = substituteParams(power_user.reasoning.prefix || '');
70+ const separator = substituteParams(power_user.reasoning.separator || '');
71+ const suffix = substituteParams(power_user.reasoning.suffix || '');
72+
73+ // Combine parts with reasoning and content
74+ return `${prefix}${reasoning}${suffix}${separator}${content}`;
75+ }
76+}
77+
78+function loadReasoningSettings() {
79+ $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
80+ $('#reasoning_add_to_prompts').on('change', function () {
81+ power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
82+ saveSettingsDebounced();
83+ });
84+
85+ $('#reasoning_prefix').val(power_user.reasoning.prefix);
86+ $('#reasoning_prefix').on('input', function () {
87+ power_user.reasoning.prefix = String($(this).val());
88+ saveSettingsDebounced();
89+ });
90+
91+ $('#reasoning_suffix').val(power_user.reasoning.suffix);
92+ $('#reasoning_suffix').on('input', function () {
93+ power_user.reasoning.suffix = String($(this).val());
94+ saveSettingsDebounced();
95+ });
96+
97+ $('#reasoning_separator').val(power_user.reasoning.separator);
98+ $('#reasoning_separator').on('input', function () {
99+ power_user.reasoning.separator = String($(this).val());
100+ saveSettingsDebounced();
101+ });
102+
103+ $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
104+ $('#reasoning_max_additions').on('input', function () {
105+ power_user.reasoning.max_additions = Number($(this).val());
106+ saveSettingsDebounced();
107+ });
108+}
109+
110+function registerReasoningSlashCommands() {
111+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112+ name: 'reasoning-get',
113+ returns: ARGUMENT_TYPE.STRING,
114+ 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.`,
115+ unnamedArgumentList: [
116+ SlashCommandArgument.fromProps({
117+ description: 'Message ID. If not provided, the message ID of the last message is used.',
118+ typeList: ARGUMENT_TYPE.NUMBER,
119+ enumProvider: commonEnumProviders.messages(),
120+ }),
121+ ],
122+ callback: (_args, value) => {
123+ const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
124+ const message = chat[messageId];
125+ const reasoning = String(message?.extra?.reasoning ?? '');
126+ return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
127+ },
128+ }));
129+
130+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
131+ name: 'reasoning-set',
132+ returns: ARGUMENT_TYPE.STRING,
133+ helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
134+ namedArgumentList: [
135+ SlashCommandNamedArgument.fromProps({
136+ name: 'at',
137+ description: 'Message ID. If not provided, the message ID of the last message is used.',
138+ typeList: ARGUMENT_TYPE.NUMBER,
139+ enumProvider: commonEnumProviders.messages(),
140+ }),
141+ ],
142+ unnamedArgumentList: [
143+ SlashCommandArgument.fromProps({
144+ description: 'Reasoning block content.',
145+ typeList: ARGUMENT_TYPE.STRING,
146+ }),
147+ ],
148+ callback: async (args, value) => {
149+ const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1;
150+ const message = chat[messageId];
151+ if (!message?.extra) {
152+ return '';
153+ }
154+
155+ message.extra.reasoning = String(value ?? '');
156+ await saveChatConditional();
157+
158+ closeMessageEditor('reasoning');
159+ updateMessageBlock(messageId, message);
160+ return message.extra.reasoning;
161+ },
162+ }));
163+}
164+
165+function registerReasoningMacros() {
166+ MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
167+ MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
168+ MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
169+}
170+
171+function setReasoningEventHandlers(){
172+ $(document).on('click', '.mes_reasoning_copy', (e) => {
173+ e.stopPropagation();
174+ e.preventDefault();
175+ });
176+
177+ $(document).on('click', '.mes_reasoning_edit', function (e) {
178+ e.stopPropagation();
179+ e.preventDefault();
180+ const { message, messageBlock } = getMessageFromJquery(this);
181+ if (!message?.extra) {
182+ return;
183+ }
184+
185+ const reasoning = String(message?.extra?.reasoning ?? '');
186+ const chatElement = document.getElementById('chat');
187+ const textarea = document.createElement('textarea');
188+ const reasoningBlock = messageBlock.find('.mes_reasoning');
189+ textarea.classList.add('reasoning_edit_textarea');
190+ textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
191+ $(textarea).insertBefore(reasoningBlock);
192+
193+ if (!CSS.supports('field-sizing', 'content')) {
194+ const resetHeight = function () {
195+ const scrollTop = chatElement.scrollTop;
196+ textarea.style.height = '0px';
197+ textarea.style.height = `${textarea.scrollHeight}px`;
198+ chatElement.scrollTop = scrollTop;
199+ };
200+
201+ textarea.addEventListener('input', resetHeight);
202+ resetHeight();
203+ }
204+
205+ textarea.focus();
206+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
207+
208+ const textareaRect = textarea.getBoundingClientRect();
209+ const chatRect = chatElement.getBoundingClientRect();
210+
211+ // Scroll if textarea bottom is below visible area
212+ if (textareaRect.bottom > chatRect.bottom) {
213+ const scrollOffset = textareaRect.bottom - chatRect.bottom;
214+ chatElement.scrollTop += scrollOffset;
215+ }
216+ });
217+
218+ $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
219+ e.stopPropagation();
220+ e.preventDefault();
221+ const { message, messageId, messageBlock } = getMessageFromJquery(this);
222+ if (!message?.extra) {
223+ return;
224+ }
225+
226+ const textarea = messageBlock.find('.reasoning_edit_textarea');
227+ const reasoning = String(textarea.val());
228+ message.extra.reasoning = reasoning;
229+ await saveChatConditional();
230+ updateMessageBlock(messageId, message);
231+ textarea.remove();
232+ });
233+
234+ $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
235+ e.stopPropagation();
236+ e.preventDefault();
237+
238+ const { messageBlock } = getMessageFromJquery(this);
239+ const textarea = messageBlock.find('.reasoning_edit_textarea');
240+ textarea.remove();
241+ });
242+
243+ $(document).on('click', '.mes_edit_add_reasoning', async function () {
244+ const { message, messageId } = getMessageFromJquery(this);
245+ if (!message?.extra) {
246+ return;
247+ }
248+
249+ if (message.extra.reasoning) {
250+ toastr.info(t`Reasoning already exists.`, t`Edit Message`);
251+ return;
252+ }
253+
254+ message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
255+ await saveChatConditional();
256+ closeMessageEditor();
257+ updateMessageBlock(messageId, message);
258+ });
259+
260+ $(document).on('click', '.mes_reasoning_delete', async function (e) {
261+ e.stopPropagation();
262+ e.preventDefault();
263+
264+ const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
265+
266+ if (!confirm) {
267+ return;
268+ }
269+
270+ const { message, messageId } = getMessageFromJquery(this);
271+ if (!message?.extra) {
272+ return;
273+ }
274+ message.extra.reasoning = '';
275+ await saveChatConditional();
276+ updateMessageBlock(messageId, message);
277+ });
278+
279+ $(document).on('pointerup', '.mes_reasoning_copy', async function () {
280+ const { message } = getMessageFromJquery(this);
281+ const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
282+
283+ if (!reasoning) {
284+ return;
285+ }
286+
287+ await copyText(reasoning);
288+ toastr.info(t`Copied!`, '', { timeOut: 2000 });
289+ });
290+}
291+
292+export function initReasoning() {
293+ loadReasoningSettings();
294+ setReasoningEventHandlers();
295+ registerReasoningSlashCommands();
296+ registerReasoningMacros();
297+}
public/scripts/slash-commands.js+4 -0
@@ -42,6 +42,7 @@ import {
4242 showMoreMessages,
4343 stopGeneration,
4444 substituteParams,
45+ syncCurrentSwipeInfoExtras,
4546 system_avatar,
4647 system_message_types,
4748 this_chid,
@@ -2862,8 +2863,11 @@ async function addSwipeCallback(args, value) {
28622863 const newSwipeId = lastMessage.swipes.length - 1;
28632864
28642865 if (isTrueBoolean(args.switch)) {
2866+ // Make sure ad-hoc changes to extras are saved before swiping away
2867+ syncCurrentSwipeInfoExtras();
28652868 lastMessage.swipe_id = newSwipeId;
28662869 lastMessage.mes = lastMessage.swipes[newSwipeId];
2870+ lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
28672871 }
28682872
28692873 await saveChatConditional();
public/scripts/sse-stream.js+15 -0
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
235235 }
236236 return;
237237 }
238+ else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
239+ for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
240+ const str = json.choices[0].delta.reasoning[j];
241+ const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
242+ const choiceClone = structuredClone(json.choices[0]);
243+ choiceClone.delta.reasoning = str;
244+ choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
245+ const choices = [choiceClone];
246+ yield {
247+ data: { ...json, choices },
248+ chunk: str,
249+ };
250+ }
251+ return;
252+ }
238253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
239254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
240255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+4 -0
@@ -1,6 +1,7 @@
11import {
22 activateSendButtons,
33 addOneMessage,
4+ appendMediaToMessage,
45 callPopup,
56 characters,
67 chat,
@@ -41,6 +42,7 @@ import {
4142 substituteParamsExtended,
4243 this_chid,
4344 updateChatMetadata,
45+ updateMessageBlock,
4446} from '../script.js';
4547import {
4648 extension_settings,
@@ -171,6 +173,8 @@ export function getContext() {
171173 getCharacters,
172174 uuidv4,
173175 humanizedDateTime,
176+ updateMessageBlock,
177+ appendMediaToMessage,
174178 };
175179}
176180
public/scripts/textgen-settings.js+2 -1
@@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986986 let logprobs = null;
987987 const swipes = [];
988988 const toolCalls = [];
989+ const state = {};
989990 while (true) {
990991 const { done, value } = await reader.read();
991992 if (done) return;
@@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
10041005 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
10051006 }
10061007
10071008 yield { text, swipes, logprobs, toolCalls, state };
10081009 }
10091010 };
10101011}
public/style.css+106 -21
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292292 filter: grayscale(25%);
293293}
294294
295295.mes_text table {,
296+.mes_reasoning table {
296297 border-spacing: 0;
297298 border-collapse: collapse;
298299 margin-bottom: 10px;
299300}
300301
301302.mes_text td,
302303.mes_text th {,
304+.mes_reasoning td,
305+.mes_reasoning th {
303306 border: 1px solid;
304307 border-collapse: collapse;
305308 padding: 0.25em;
306309}
307310
308311.mes_text p {,
312+.mes_reasoning p {
309313 margin-top: 0;
310314 margin-bottom: 10px;
311315}
312316
313317.mes_text li tt {,
318+.mes_reasoning li tt {
314319 display: inline-block;
315320}
316321
317322.mes_text ol,
318323.mes_text ul {,
324+.mes_reasoning ol,
325+.mes_reasoning ul {
319326 margin-top: 5px;
320327 margin-bottom: 5px;
321328}
322329
323330.mes_text br,
324331.mes_bias br {,
332+.mes_reasoning br {
325333 content: ' ';
326334}
327335
@@ -332,25 +340,83 @@ input[type='checkbox']:focus-visible {
332340 color: var(--SmartThemeQuoteColor);
333341}
334342
343+.mes_reasoning {
344+ display: block;
345+ border: 1px solid var(--SmartThemeBorderColor);
346+ background-color: var(--black30a);
347+ border-radius: 5px;
348+ padding: 5px;
349+ margin: 5px 0;
350+ overflow-y: auto;
351+}
352+
353+.mes_reasoning_summary {
354+ cursor: pointer;
355+ position: relative;
356+ margin: 2px;
357+}
358+
359+@supports not selector(:has(*)) {
360+ .mes_reasoning_details {
361+ display: none !important;
362+ }
363+}
364+
365+.mes_bias:empty,
366+.mes_reasoning:empty,
367+.mes_reasoning_details:has(.mes_reasoning:empty),
368+.mes_block:has(.edit_textarea) .mes_reasoning_details,
369+.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
371+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
372+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
373+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
374+ display: none;
375+}
376+
377+.mes_reasoning_actions {
378+ position: absolute;
379+ right: 0;
380+ top: 0;
381+
382+ display: flex;
383+ gap: 4px;
384+ flex-wrap: nowrap;
385+ justify-content: flex-end;
386+ transition: all 200ms;
387+ overflow-x: hidden;
388+ padding: 1px;
389+}
390+
391+.mes_reasoning_summary>span {
392+ margin-left: 0.5em;
393+}
394+
335395.mes_text i,
336396.mes_text em {,
397+.mes_reasoning i,
398+.mes_reasoning em {
337399 color: var(--SmartThemeEmColor);
338400}
339401
340402.mes_text u {,
403+.mes_reasoning u {
341404 color: var(--SmartThemeUnderlineColor);
342405}
343406
344407.mes_text q {,
408+.mes_reasoning q {
345409 color: var(--SmartThemeQuoteColor);
346410}
347411
348412.mes_text font[color] em,
349413.mes_text font[color] i {,
350- color: inherit;
414+.mes_text font[color] u,
351-}
415+.mes_text font[color] q,
352-
416+.mes_reasoning font[color] em,
353417.mes_textmes_reasoning font[color] q {i,
418+.mes_reasoning font[color] u,
419+.mes_reasoning font[color] q {
354420 color: inherit;
355421}
356422
@@ -358,7 +424,8 @@ input[type='checkbox']:focus-visible {
358424 display: block;
359425}
360426
361427.mes_text blockquote {,
428+.mes_reasoning blockquote {
362429 border-left: 3px solid var(--SmartThemeQuoteColor);
363430 padding-left: 10px;
364431 background-color: var(--black30a);
@@ -368,18 +435,24 @@ input[type='checkbox']:focus-visible {
368435.mes_text strong em,
369436.mes_text strong,
370437.mes_text h2,
371438.mes_text h1 {,
439+.mes_reasoning strong em,
440+.mes_reasoning strong,
441+.mes_reasoning h2,
442+.mes_reasoning h1 {
372443 font-weight: bold;
373444}
374445
375446.mes_text pre code {,
447+.mes_reasoning pre code {
376448 position: relative;
377449 display: block;
378450 overflow-x: auto;
379451 padding: 1em;
380452}
381453
382454.mes_text img:not(.mes_img) {,
455+.mes_reasoning img:not(.mes_img) {
383456 max-width: 100%;
384457 max-height: var(--doc-height);
385458}
@@ -1022,6 +1095,11 @@ body .panelControlBar {
10221095 /*only affects bubblechat to make it sit nicely at the bottom*/
10231096}
10241097
1098+.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1099+ margin-bottom: 30px;
1100+}
1101+
1102+.last_mes .mes_reasoning,
10251103.last_mes .mes_text {
10261104 padding-right: 30px;
10271105}
@@ -1242,14 +1320,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12421320 overflow-y: clip;
12431321}
12441322
12451323.mes_text {,
1324+.mes_reasoning {
12461325 font-weight: 500;
12471326 line-height: calc(var(--mainFontSize) + .5rem);
1327+ max-width: 100%;
1328+ overflow-wrap: anywhere;
1329+}
1330+
1331+.mes_text {
12481332 padding-left: 0;
12491333 padding-top: 5px;
12501334 padding-bottom: 5px;
1251- max-width: 100%;
1252- overflow-wrap: anywhere;
12531335}
12541336
12551337br {
@@ -4169,10 +4251,12 @@ input[type="range"]::-webkit-slider-thumb {
41694251 align-items: center;
41704252}
41714253
4254+.mes_reasoning_edit_cancel,
41724255.mes_edit_cancel.menu_button {
41734256 background-color: var(--crimson70a);
41744257}
41754258
4259+.mes_reasoning_edit_done,
41764260.mes_edit_done.menu_button {
41774261 background-color: var(--okGreen70a);
41784262}
@@ -4181,6 +4265,7 @@ input[type="range"]::-webkit-slider-thumb {
41814265 opacity: 1;
41824266}
41834267
4268+.reasoning_edit_textarea,
41844269.edit_textarea {
41854270 padding: 5px;
41864271 margin: 0;
src/endpoints/backends/chat-completions.js+13 -7
@@ -289,6 +289,7 @@ async function sendMakerSuiteRequest(request, response) {
289289 const model = String(request.body.model);
290290 const stream = Boolean(request.body.stream);
291291 const showThoughts = Boolean(request.body.show_thoughts);
292+ const isThinking = model.includes('thinking');
292293
293294 const generationConfig = {
294295 stopSequences: request.body.stop,
@@ -329,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
329330 body.systemInstruction = prompt.system_instruction;
330331 }
331332
333+ if (isThinking && showThoughts) {
334+ generationConfig.thinkingConfig = {
335+ includeThoughts: true,
336+ };
337+ }
338+
332339 return body;
333340 }
334341
@@ -342,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
342349 controller.abort();
343350 });
344351
345- const isThinking = model.includes('thinking');
346352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
347353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
348354
@@ -387,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
387393 const responseContent = candidates[0].content ?? candidates[0].output;
388394 console.log('Google AI Studio response:', responseContent);
389395
390- if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {
396+ const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
391- responseContent.parts = responseContent.parts.filter(part => !part.thought);
392- }
393-
394- const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
395397 if (!responseText) {
396398 let message = 'Google AI Studio Candidate text empty';
397399 console.log(message, generateResponseJson);
@@ -399,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
399401 }
400402
401403 // Wrap it back to OAI format
402404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
403405 return response.send(reply);
404406 }
405407 } catch (error) {
@@ -996,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
996998 bodyParams['route'] = 'fallback';
997999 }
9981000
1001+ if (request.body.show_thoughts) {
1002+ bodyParams['include_reasoning'] = true;
1003+ }
1004+
9991005 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
10001006 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
10011007 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);