Merge pull request #3345 from SillyTavern/woo-yeah Better reasoning

3b8fd6f62f1afc410cb451acbe7e52dfc85724e1

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
15 files changed, +616 -75Showing whitespace changes
public/index.html+51 -6
@@ -1977,11 +1977,11 @@
1977 </span>1977 </span>
1978 </div>1978 </div>
1979 </div>1979 </div>
1980 <div class="range-block" data-source="makersuite,deepseek">1980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1982 <input id="openai_show_thoughts" type="checkbox" />1982 <input id="openai_show_thoughts" type="checkbox" />
1983 <span>1983 <span>
1984 <span data-i18n="Show model thoughts">Show model thoughts</span>1984 <span data-i18n="Show model reasoning">Show model reasoning</span>
1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
1986 </span>1986 </span>
1987 </label>1987 </label>
@@ -3800,6 +3800,39 @@
3800 </div>3800 </div>
3801 </div>3801 </div>
3802 <div>3802 <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>
3803 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>3836 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
3804 <div>3837 <div>
3805 <small>3838 <small>
@@ -6221,14 +6254,26 @@
6221 <div class="mes_edit_buttons">6254 <div class="mes_edit_buttons">
6222 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>6255 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
6223 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>6256 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
6224 <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message">6257 <div class="mes_edit_add_reasoning menu_button fa-solid fa-lightbulb" title="Add a reasoning block" data-i18n="[title]Add a reasoning block"></div>
6225 </div>6258 <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
6226 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>6259 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
6227 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down">6260 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down"></div>
6228 </div>
6229 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>6261 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
6230 </div>6262 </div>
6231 </div>6263 </div>
6264 <details class="mes_reasoning_details">
6265 <summary class="mes_reasoning_summary">
6266 <span data-i18n="Reasoning">Reasoning</span>
6267 <div class="mes_reasoning_actions">
6268 <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6269 <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6270 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6271 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6272 <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6273 </div>
6274 </summary>
6275 <div class="mes_reasoning"></div>
6276 </details>
6232 <div class="mes_text"></div>6277 <div class="mes_text"></div>
6233 <div class="mes_img_container">6278 <div class="mes_img_container">
6234 <div class="mes_img_controls">6279 <div class="mes_img_controls">
public/locales/fr-fr.json+1 -1
@@ -1385,7 +1385,7 @@
1385 "enable_functions_desc_1": "Autorise l'utilisation",1385 "enable_functions_desc_1": "Autorise l'utilisation",
1386 "enable_functions_desc_2": "outils de fonction",1386 "enable_functions_desc_2": "outils de fonction",
1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
1388 "Show model thoughts": "Afficher les pensées du modèle",1388 "Show model reasoning": "Afficher les pensées du modèle",
1389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",1389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
1391 "openai_logit_bias_no_items": "Aucun élément",1391 "openai_logit_bias_no_items": "Aucun élément",
public/locales/zh-cn.json+1 -1
@@ -266,7 +266,7 @@
266 "Use system prompt": "使用系统提示词",266 "Use system prompt": "使用系统提示词",
267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268 "Merges_all_system_messages_desc_2": "字段发送。",268 "Merges_all_system_messages_desc_2": "字段发送。",
269 "Show model thoughts": "展示思维链",269 "Show model reasoning": "展示思维链",
270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
271 "Assistant Prefill": "AI预填",271 "Assistant Prefill": "AI预填",
272 "Expand the editor": "展开编辑器",272 "Expand the editor": "展开编辑器",
public/locales/zh-tw.json+1 -1
@@ -2357,7 +2357,7 @@
2357 "Forbid": "禁止",2357 "Forbid": "禁止",
2358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",2358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
2359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",2359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
2360 "Show model thoughts": "顯示模型思維鏈",2360 "Show model reasoning": "顯示模型思維鏈",
2361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",2361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
2362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",2362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
2363 "Model ID (optional)": "模型 ID(可選)",2363 "Model ID (optional)": "模型 ID(可選)",
public/script.js+100 -26
@@ -267,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267import { initBulkEdit } from './scripts/bulk-edit.js';267import { initBulkEdit } from './scripts/bulk-edit.js';
268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269import { getContext } from './scripts/st-context.js';269import { getContext } from './scripts/st-context.js';
270import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
270271
271// API OBJECT FOR EXTERNAL WIRING272// API OBJECT FOR EXTERNAL WIRING
272globalThis.SillyTavern = {273globalThis.SillyTavern = {
@@ -982,6 +983,7 @@ async function firstLoadInit() {
982 initServerHistory();983 initServerHistory();
983 initSettingsSearch();984 initSettingsSearch();
984 initBulkEdit();985 initBulkEdit();
986 initReasoning();
985 await initScrapers();987 await initScrapers();
986 doDailyExtensionUpdatesCheck();988 doDailyExtensionUpdatesCheck();
987 await hideLoader();989 await hideLoader();
@@ -2200,6 +2202,7 @@ function getMessageFromTemplate({
2200 isUser,2202 isUser,
2201 avatarImg,2203 avatarImg,
2202 bias,2204 bias,
2205 reasoning,
2203 isSystem,2206 isSystem,
2204 title,2207 title,
2205 timerValue,2208 timerValue,
@@ -2224,6 +2227,7 @@ function getMessageFromTemplate({
2224 mes.find('.avatar img').attr('src', avatarImg);2227 mes.find('.avatar img').attr('src', avatarImg);
2225 mes.find('.ch_name .name_text').text(characterName);2228 mes.find('.ch_name .name_text').text(characterName);
2226 mes.find('.mes_bias').html(bias);2229 mes.find('.mes_bias').html(bias);
2230 mes.find('.mes_reasoning').html(reasoning);
2227 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2231 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2228 mes.find('.mesIDDisplay').text(`#${mesId}`);2232 mes.find('.mesIDDisplay').text(`#${mesId}`);
2229 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2233 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2238,10 +2242,16 @@ function getMessageFromTemplate({
2238 return mes;2242 return mes;
2239}2243}
22402244
2245/**
2246 * Re-renders a message block with updated content.
2247 * @param {number} messageId Message ID
2248 * @param {object} message Message object
2249 */
2241export function updateMessageBlock(messageId, message) {2250export function updateMessageBlock(messageId, message) {
2242 const messageElement = $(`#chat [mesid="${messageId}"]`);2251 const messageElement = $(`#chat [mesid="${messageId}"]`);
2243 const text = message?.extra?.display_text ?? message.mes;2252 const text = message?.extra?.display_text ?? message.mes;
2244 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));2253 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
2254 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));
2245 addCopyToCodeBlocks(messageElement);2255 addCopyToCodeBlocks(messageElement);
2246 appendMediaToMessage(message, messageElement);2256 appendMediaToMessage(message, messageElement);
2247}2257}
@@ -2400,6 +2410,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2400 sanitizerOverrides,2410 sanitizerOverrides,
2401 );2411 );
2402 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2412 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
2413 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
2403 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2414 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24042415
2405 let params = {2416 let params = {
@@ -2409,6 +2420,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2409 isUser: mes.is_user,2420 isUser: mes.is_user,
2410 avatarImg: avatarImg,2421 avatarImg: avatarImg,
2411 bias: bias,2422 bias: bias,
2423 reasoning: reasoning,
2412 isSystem: isSystem,2424 isSystem: isSystem,
2413 title: title,2425 title: title,
2414 bookmarkLink: bookmarkLink,2426 bookmarkLink: bookmarkLink,
@@ -2468,6 +2480,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2468 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);2480 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
2469 swipeMessage.attr('swipeid', params.swipeId);2481 swipeMessage.attr('swipeid', params.swipeId);
2470 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2482 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2483 swipeMessage.find('.mes_reasoning').html(reasoning);
2471 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2484 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2472 appendMediaToMessage(mes, swipeMessage);2485 appendMediaToMessage(mes, swipeMessage);
2473 if (power_user.timestamp_model_icon && params.extra?.api) {2486 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3078,6 +3091,7 @@ class StreamingProcessor {
3078 this.messageTextDom = null;3091 this.messageTextDom = null;
3079 this.messageTimerDom = null;3092 this.messageTimerDom = null;
3080 this.messageTokenCounterDom = null;3093 this.messageTokenCounterDom = null;
3094 this.messageReasoningDom = null;
3081 /** @type {HTMLTextAreaElement} */3095 /** @type {HTMLTextAreaElement} */
3082 this.sendTextarea = document.querySelector('#send_textarea');3096 this.sendTextarea = document.querySelector('#send_textarea');
3083 this.type = type;3097 this.type = type;
@@ -3093,6 +3107,7 @@ class StreamingProcessor {
3093 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3107 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3094 this.messageLogprobs = [];3108 this.messageLogprobs = [];
3095 this.toolCalls = [];3109 this.toolCalls = [];
3110 this.reasoning = '';
3096 }3111 }
30973112
3098 #checkDomElements(messageId) {3113 #checkDomElements(messageId) {
@@ -3101,6 +3116,7 @@ class StreamingProcessor {
3101 this.messageTextDom = this.messageDom?.querySelector('.mes_text');3116 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
3102 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3117 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3103 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3118 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3119 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3104 }3120 }
3105 }3121 }
31063122
@@ -3178,18 +3194,27 @@ class StreamingProcessor {
3178 this.#checkDomElements(messageId);3194 this.#checkDomElements(messageId);
3179 this.#updateMessageBlockVisibility();3195 this.#updateMessageBlockVisibility();
3180 const currentTime = new Date();3196 const currentTime = new Date();
3181 // Don't waste time calculating token count for streaming
3182 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
3183 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
3184 chat[messageId]['mes'] = processedText;3197 chat[messageId]['mes'] = processedText;
3185 chat[messageId]['gen_started'] = this.timeStarted;3198 chat[messageId]['gen_started'] = this.timeStarted;
3186 chat[messageId]['gen_finished'] = currentTime;3199 chat[messageId]['gen_finished'] = currentTime;
31873200
3188 if (currentTokenCount) {
3189 if (!chat[messageId]['extra']) {3201 if (!chat[messageId]['extra']) {
3190 chat[messageId]['extra'] = {};3202 chat[messageId]['extra'] = {};
3191 }3203 }
31923204
3205 if (this.reasoning) {
3206 chat[messageId]['extra']['reasoning'] = this.reasoning;
3207 if (this.messageReasoningDom instanceof HTMLElement) {
3208 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);
3209 this.messageReasoningDom.innerHTML = formattedReasoning;
3210 }
3211 }
3212
3213 // Don't waste time calculating token count for streaming
3214 const tokenCountText = (this.reasoning || '') + processedText;
3215 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3216
3217 if (currentTokenCount) {
3193 chat[messageId]['extra']['token_count'] = currentTokenCount;3218 chat[messageId]['extra']['token_count'] = currentTokenCount;
3194 if (this.messageTokenCounterDom instanceof HTMLElement) {3219 if (this.messageTokenCounterDom instanceof HTMLElement) {
3195 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;3220 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3211,10 +3236,13 @@ class StreamingProcessor {
3211 if (this.messageTextDom instanceof HTMLElement) {3236 if (this.messageTextDom instanceof HTMLElement) {
3212 this.messageTextDom.innerHTML = formattedText;3237 this.messageTextDom.innerHTML = formattedText;
3213 }3238 }
3239
3240 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
3214 if (this.messageTimerDom instanceof HTMLElement) {3241 if (this.messageTimerDom instanceof HTMLElement) {
3215 this.messageTimerDom.textContent = timePassed.timerValue;3242 this.messageTimerDom.textContent = timePassed.timerValue;
3216 this.messageTimerDom.title = timePassed.timerTitle;3243 this.messageTimerDom.title = timePassed.timerTitle;
3217 }3244 }
3245
3218 this.setFirstSwipe(messageId);3246 this.setFirstSwipe(messageId);
3219 }3247 }
32203248
@@ -3321,7 +3349,7 @@ class StreamingProcessor {
3321 }3349 }
33223350
3323 /**3351 /**
3324 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}3352 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
3325 */3353 */
3326 *nullStreamingGeneration() {3354 *nullStreamingGeneration() {
3327 throw new Error('Generation function for streaming is not hooked up');3355 throw new Error('Generation function for streaming is not hooked up');
@@ -3343,7 +3371,7 @@ class StreamingProcessor {
3343 try {3371 try {
3344 const sw = new Stopwatch(1000 / power_user.streaming_fps);3372 const sw = new Stopwatch(1000 / power_user.streaming_fps);
3345 const timestamps = [];3373 const timestamps = [];
3346 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {3374 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
3347 timestamps.push(Date.now());3375 timestamps.push(Date.now());
3348 if (this.isStopped) {3376 if (this.isStopped) {
3349 return;3377 return;
@@ -3355,6 +3383,7 @@ class StreamingProcessor {
3355 if (logprobs) {3383 if (logprobs) {
3356 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3384 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3357 }3385 }
3386 this.reasoning = state?.reasoning ?? '';
3358 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3387 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3359 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3388 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
3360 }3389 }
@@ -3821,6 +3850,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3821 coreChat.pop();3850 coreChat.pop();
3822 }3851 }
38233852
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
3824 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {3861 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
3825 let message = chatItem.mes;3862 let message = chatItem.mes;
3826 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;3863 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -4742,6 +4779,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4742 //const getData = await response.json();4779 //const getData = await response.json();
4743 let getMessage = extractMessageFromData(data);4780 let getMessage = extractMessageFromData(data);
4744 let title = extractTitleFromData(data);4781 let title = extractTitleFromData(data);
4782 let reasoning = extractReasoningFromData(data);
4745 kobold_horde_model = title;4783 kobold_horde_model = title;
47464784
4747 const swipes = extractMultiSwipes(data, type);4785 const swipes = extractMultiSwipes(data, type);
@@ -4768,10 +4806,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4768 else {4806 else {
4769 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.4807 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
4770 if (originalType !== 'continue') {4808 if (originalType !== 'continue') {
4771 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes));4809 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
4772 }4810 }
4773 else {4811 else {
4774 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes));4812 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
4775 }4813 }
47764814
4777 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.4815 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -5659,7 +5697,6 @@ function extractMessageFromData(data) {
5659 return data;5697 return data;
5660 }5698 }
56615699
5662 function getTextContext() {
5663 switch (main_api) {5700 switch (main_api) {
5664 case 'kobold':5701 case 'kobold':
5665 return data.results[0].text;5702 return data.results[0].text;
@@ -5676,14 +5713,24 @@ function extractMessageFromData(data) {
5676 }5713 }
5677}5714}
56785715
5679 const content = getTextContext();5716/**
56805717 * Extracts the reasoning from the response data.
5681 if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) {5718 * @param {object} data Response data
5682 const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? '';5719 * @returns {string} Extracted reasoning
5683 return [thoughts, content].filter(x => x).join('\n\n');5720 */
5721function extractReasoningFromData(data) {
5722 if (main_api === 'openai' && oai_settings.show_thoughts) {
5723 switch (oai_settings.chat_completion_source) {
5724 case chat_completion_sources.DEEPSEEK:
5725 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5726 case chat_completion_sources.OPENROUTER:
5727 return data?.choices?.[0]?.message?.reasoning ?? '';
5728 case chat_completion_sources.MAKERSUITE:
5729 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5730 }
5684 }5731 }
56855732
5686 return content;5733 return '';
5687}5734}
56885735
5689/**5736/**
@@ -5866,7 +5913,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5866 return getMessage;5913 return getMessage;
5867}5914}
58685915
5869export async function saveReply(type, getMessage, fromStreaming, title, swipes) {5916export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
5870 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||5917 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
5871 chat[chat.length - 1]['is_user'])) {5918 chat[chat.length - 1]['is_user'])) {
5872 type = 'normal';5919 type = 'normal';
@@ -5891,8 +5938,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5891 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5938 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5892 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5939 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5893 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5940 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5941 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5894 if (power_user.message_token_count_enabled) {5942 if (power_user.message_token_count_enabled) {
5895 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5943 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5944 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5896 }5945 }
5897 const chat_id = (chat.length - 1);5946 const chat_id = (chat.length - 1);
5898 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5947 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5911,8 +5960,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5911 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5960 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5912 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5961 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5913 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5962 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5963 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5914 if (power_user.message_token_count_enabled) {5964 if (power_user.message_token_count_enabled) {
5915 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5965 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5966 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5916 }5967 }
5917 const chat_id = (chat.length - 1);5968 const chat_id = (chat.length - 1);
5918 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5969 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5928,8 +5979,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5928 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5979 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5929 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5980 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5930 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5981 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5982 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5931 if (power_user.message_token_count_enabled) {5983 if (power_user.message_token_count_enabled) {
5932 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5984 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5985 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5933 }5986 }
5934 const chat_id = (chat.length - 1);5987 const chat_id = (chat.length - 1);
5935 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5988 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5945,6 +5998,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5945 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5998 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5946 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5999 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5947 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6000 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6001 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5948 if (power_user.trim_spaces) {6002 if (power_user.trim_spaces) {
5949 getMessage = getMessage.trim();6003 getMessage = getMessage.trim();
5950 }6004 }
@@ -5954,7 +6008,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5954 chat[chat.length - 1]['gen_finished'] = generationFinished;6008 chat[chat.length - 1]['gen_finished'] = generationFinished;
59556009
5956 if (power_user.message_token_count_enabled) {6010 if (power_user.message_token_count_enabled) {
5957 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);6011 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
6012 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5958 }6013 }
59596014
5960 if (selected_group) {6015 if (selected_group) {
@@ -7997,11 +8052,25 @@ function updateEditArrowClasses() {
7997 }8052 }
7998}8053}
79998054
8000function closeMessageEditor() {8055/**
8056 * Closes the message editor.
8057 * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8058 */
8059export function closeMessageEditor(what = 'all') {
8060 if (what === 'message' || what === 'all') {
8001 if (this_edit_mes_id) {8061 if (this_edit_mes_id) {
8002 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();8062 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8003 }8063 }
8004 }8064 }
8065 if (what === 'reasoning' || what === 'all') {
8066 document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8067 const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8068 if (cancelButton instanceof HTMLElement) {
8069 cancelButton.click();
8070 }
8071 });
8072 }
8073}
80058074
8006export function setGenerationProgress(progress) {8075export function setGenerationProgress(progress) {
8007 if (!progress) {8076 if (!progress) {
@@ -8483,7 +8552,8 @@ function swipe_left() { // when we swipe left..but no generation.
8483 }8552 }
84848553
8485 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);8554 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
8486 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8555 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8556 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8487 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8557 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8488 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8558 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8489 }8559 }
@@ -8647,6 +8717,7 @@ const swipe_right = () => {
8647 // resets the timer8717 // resets the timer
8648 swipeMessage.find('.mes_timer').html('');8718 swipeMessage.find('.mes_timer').html('');
8649 swipeMessage.find('.tokenCounterDisplay').text('');8719 swipeMessage.find('.tokenCounterDisplay').text('');
8720 swipeMessage.find('.mes_reasoning').html('');
8650 } else {8721 } else {
8651 //console.log('showing previously generated swipe candidate, or "..."');8722 //console.log('showing previously generated swipe candidate, or "..."');
8652 //console.log('onclick right swipe calling addOneMessage');8723 //console.log('onclick right swipe calling addOneMessage');
@@ -8657,7 +8728,8 @@ const swipe_right = () => {
8657 chat[chat.length - 1].extra = {};8728 chat[chat.length - 1].extra = {};
8658 }8729 }
86598730
8660 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8731 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8732 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8661 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8733 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8662 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8734 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8663 }8735 }
@@ -9459,7 +9531,8 @@ function addDebugFunctions() {
9459 message.extra = {};9531 message.extra = {};
9460 }9532 }
94619533
9462 message.extra.token_count = await getTokenCountAsync(message.mes, 0);9534 const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9535 message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
9463 }9536 }
94649537
9465 await saveChatConditional();9538 await saveChatConditional();
@@ -11228,14 +11301,15 @@ jQuery(async function () {
1122811301
11229 $(document).keyup(function (e) {11302 $(document).keyup(function (e) {
11230 if (e.key === 'Escape') {11303 if (e.key === 'Escape') {
11231 const isEditVisible = $('#curEditTextarea').is(':visible');11304 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
11232 if (isEditVisible && power_user.auto_save_msg_edits === false) {11305 if (isEditVisible && power_user.auto_save_msg_edits === false) {
11233 closeMessageEditor();11306 closeMessageEditor('all');
11234 $('#send_textarea').focus();11307 $('#send_textarea').focus();
11235 return;11308 return;
11236 }11309 }
11237 if (isEditVisible && power_user.auto_save_msg_edits === true) {11310 if (isEditVisible && power_user.auto_save_msg_edits === true) {
11238 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();11311 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11312 closeMessageEditor('reasoning');
11239 $('#send_textarea').focus();11313 $('#send_textarea').focus();
11240 return;11314 return;
11241 }11315 }
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188 if (data?.token) {188 if (data?.token) {
189 text += data.token;189 text += data.token;
190 }190 }
191 yield { text, swipes: [], toolCalls: [] };191 yield { text, swipes: [], toolCalls: [], state: {} };
192 }192 }
193 };193 };
194}194}
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746 text += data.token;746 text += data.token;
747 }747 }
748748
749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750 }750 }
751 };751 };
752}752}
public/scripts/openai.js+15 -9
@@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2095 let text = '';2095 let text = '';
2096 const swipes = [];2096 const swipes = [];
2097 const toolCalls = [];2097 const toolCalls = [];
2098 const state = {};2098 const state = { reasoning: '' };
2099 while (true) {2099 while (true) {
2100 const { done, value } = await reader.read();2100 const { done, value } = await reader.read();
2101 if (done) return;2101 if (done) return;
@@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21132113
2114 ToolManager.parseToolCalls(toolCalls, parsed);2114 ToolManager.parseToolCalls(toolCalls, parsed);
21152115
2116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };2116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
2117 }2117 }
2118 };2118 };
2119 }2119 }
@@ -2150,16 +2150,22 @@ function getStreamingReply(data, state) {
2150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2151 return data?.delta?.text || '';2151 return data?.delta?.text || '';
2152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2152 } 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] || '';
2154 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {2157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
2155 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';2158 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2156 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {2159 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2157 const hadThoughts = state.hadThoughts;2160 if (oai_settings.show_thoughts) {
2158 const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '';2161 state.reasoning += (data.choices?.filter(x => 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 ?? '';
2163 } else {2169 } else {
2164 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2165 }2171 }
public/scripts/power-user.js+8 -0
@@ -253,6 +253,14 @@ let power_user = {
253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254 },254 },
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
256 personas: {},264 personas: {},
257 default_persona: null,265 default_persona: null,
258 persona_descriptions: {},266 persona_descriptions: {},
public/scripts/reasoning.js+297 -0
@@ -0,0 +1,297 @@
1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { t } from './i18n.js';
3import { MacrosParser } from './macros.js';
4import { Popup } from './popup.js';
5import { power_user } from './power-user.js';
6import { SlashCommand } from './slash-commands/SlashCommand.js';
7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
9import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10import { 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 */
17function 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 */
28export 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
78function 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
110function 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
165function 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
171function 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
292export function initReasoning() {
293 loadReasoningSettings();
294 setReasoningEventHandlers();
295 registerReasoningSlashCommands();
296 registerReasoningMacros();
297}
public/scripts/sse-stream.js+15 -0
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
235 }235 }
236 return;236 return;
237 }237 }
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 }
238 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
239 for (let j = 0; j < json.choices[0].delta.content.length; j++) {254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
240 const str = json.choices[0].delta.content[j];255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+4 -0
@@ -1,6 +1,7 @@
1import {1import {
2 activateSendButtons,2 activateSendButtons,
3 addOneMessage,3 addOneMessage,
4 appendMediaToMessage,
4 callPopup,5 callPopup,
5 characters,6 characters,
6 chat,7 chat,
@@ -41,6 +42,7 @@ import {
41 substituteParamsExtended,42 substituteParamsExtended,
42 this_chid,43 this_chid,
43 updateChatMetadata,44 updateChatMetadata,
45 updateMessageBlock,
44} from '../script.js';46} from '../script.js';
45import {47import {
46 extension_settings,48 extension_settings,
@@ -171,6 +173,8 @@ export function getContext() {
171 getCharacters,173 getCharacters,
172 uuidv4,174 uuidv4,
173 humanizedDateTime,175 humanizedDateTime,
176 updateMessageBlock,
177 appendMediaToMessage,
174 };178 };
175}179}
176180
public/scripts/textgen-settings.js+2 -1
@@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986 let logprobs = null;986 let logprobs = null;
987 const swipes = [];987 const swipes = [];
988 const toolCalls = [];988 const toolCalls = [];
989 const state = {};
989 while (true) {990 while (true) {
990 const { done, value } = await reader.read();991 const { done, value } = await reader.read();
991 if (done) return;992 if (done) return;
@@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
1004 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);1005 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1005 }1006 }
10061007
1007 yield { text, swipes, logprobs, toolCalls };1008 yield { text, swipes, logprobs, toolCalls, state };
1008 }1009 }
1009 };1010 };
1010}1011}
public/style.css+106 -21
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292 filter: grayscale(25%);292 filter: grayscale(25%);
293}293}
294294
295.mes_text table {295.mes_text table,
296.mes_reasoning table {
296 border-spacing: 0;297 border-spacing: 0;
297 border-collapse: collapse;298 border-collapse: collapse;
298 margin-bottom: 10px;299 margin-bottom: 10px;
299}300}
300301
301.mes_text td,302.mes_text td,
302.mes_text th {303.mes_text th,
304.mes_reasoning td,
305.mes_reasoning th {
303 border: 1px solid;306 border: 1px solid;
304 border-collapse: collapse;307 border-collapse: collapse;
305 padding: 0.25em;308 padding: 0.25em;
306}309}
307310
308.mes_text p {311.mes_text p,
312.mes_reasoning p {
309 margin-top: 0;313 margin-top: 0;
310 margin-bottom: 10px;314 margin-bottom: 10px;
311}315}
312316
313.mes_text li tt {317.mes_text li tt,
318.mes_reasoning li tt {
314 display: inline-block;319 display: inline-block;
315}320}
316321
317.mes_text ol,322.mes_text ol,
318.mes_text ul {323.mes_text ul,
324.mes_reasoning ol,
325.mes_reasoning ul {
319 margin-top: 5px;326 margin-top: 5px;
320 margin-bottom: 5px;327 margin-bottom: 5px;
321}328}
322329
323.mes_text br,330.mes_text br,
324.mes_bias br {331.mes_bias br,
332.mes_reasoning br {
325 content: ' ';333 content: ' ';
326}334}
327335
@@ -332,25 +340,83 @@ input[type='checkbox']:focus-visible {
332 color: var(--SmartThemeQuoteColor);340 color: var(--SmartThemeQuoteColor);
333}341}
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
335.mes_text i,395.mes_text i,
336.mes_text em {396.mes_text em,
397.mes_reasoning i,
398.mes_reasoning em {
337 color: var(--SmartThemeEmColor);399 color: var(--SmartThemeEmColor);
338}400}
339401
340.mes_text u {402.mes_text u,
403.mes_reasoning u {
341 color: var(--SmartThemeUnderlineColor);404 color: var(--SmartThemeUnderlineColor);
342}405}
343406
344.mes_text q {407.mes_text q,
408.mes_reasoning q {
345 color: var(--SmartThemeQuoteColor);409 color: var(--SmartThemeQuoteColor);
346}410}
347411
348.mes_text font[color] em,412.mes_text font[color] em,
349.mes_text font[color] i {413.mes_text font[color] i,
350 color: inherit;414.mes_text font[color] u,
351}415.mes_text font[color] q,
352416.mes_reasoning font[color] em,
353.mes_text font[color] q {417.mes_reasoning font[color] i,
418.mes_reasoning font[color] u,
419.mes_reasoning font[color] q {
354 color: inherit;420 color: inherit;
355}421}
356422
@@ -358,7 +424,8 @@ input[type='checkbox']:focus-visible {
358 display: block;424 display: block;
359}425}
360426
361.mes_text blockquote {427.mes_text blockquote,
428.mes_reasoning blockquote {
362 border-left: 3px solid var(--SmartThemeQuoteColor);429 border-left: 3px solid var(--SmartThemeQuoteColor);
363 padding-left: 10px;430 padding-left: 10px;
364 background-color: var(--black30a);431 background-color: var(--black30a);
@@ -368,18 +435,24 @@ input[type='checkbox']:focus-visible {
368.mes_text strong em,435.mes_text strong em,
369.mes_text strong,436.mes_text strong,
370.mes_text h2,437.mes_text h2,
371.mes_text h1 {438.mes_text h1,
439.mes_reasoning strong em,
440.mes_reasoning strong,
441.mes_reasoning h2,
442.mes_reasoning h1 {
372 font-weight: bold;443 font-weight: bold;
373}444}
374445
375.mes_text pre code {446.mes_text pre code,
447.mes_reasoning pre code {
376 position: relative;448 position: relative;
377 display: block;449 display: block;
378 overflow-x: auto;450 overflow-x: auto;
379 padding: 1em;451 padding: 1em;
380}452}
381453
382.mes_text img:not(.mes_img) {454.mes_text img:not(.mes_img),
455.mes_reasoning img:not(.mes_img) {
383 max-width: 100%;456 max-width: 100%;
384 max-height: var(--doc-height);457 max-height: var(--doc-height);
385}458}
@@ -1022,6 +1095,11 @@ body .panelControlBar {
1022 /*only affects bubblechat to make it sit nicely at the bottom*/1095 /*only affects bubblechat to make it sit nicely at the bottom*/
1023}1096}
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,
1025.last_mes .mes_text {1103.last_mes .mes_text {
1026 padding-right: 30px;1104 padding-right: 30px;
1027}1105}
@@ -1235,14 +1313,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1235 overflow-y: clip;1313 overflow-y: clip;
1236}1314}
12371315
1238.mes_text {1316.mes_text,
1317.mes_reasoning {
1239 font-weight: 500;1318 font-weight: 500;
1240 line-height: calc(var(--mainFontSize) + .5rem);1319 line-height: calc(var(--mainFontSize) + .5rem);
1320 max-width: 100%;
1321 overflow-wrap: anywhere;
1322}
1323
1324.mes_text {
1241 padding-left: 0;1325 padding-left: 0;
1242 padding-top: 5px;1326 padding-top: 5px;
1243 padding-bottom: 5px;1327 padding-bottom: 5px;
1244 max-width: 100%;
1245 overflow-wrap: anywhere;
1246}1328}
12471329
1248br {1330br {
@@ -4150,10 +4232,12 @@ input[type="range"]::-webkit-slider-thumb {
4150 align-items: center;4232 align-items: center;
4151}4233}
41524234
4235.mes_reasoning_edit_cancel,
4153.mes_edit_cancel.menu_button {4236.mes_edit_cancel.menu_button {
4154 background-color: var(--crimson70a);4237 background-color: var(--crimson70a);
4155}4238}
41564239
4240.mes_reasoning_edit_done,
4157.mes_edit_done.menu_button {4241.mes_edit_done.menu_button {
4158 background-color: var(--okGreen70a);4242 background-color: var(--okGreen70a);
4159}4243}
@@ -4162,6 +4246,7 @@ input[type="range"]::-webkit-slider-thumb {
4162 opacity: 1;4246 opacity: 1;
4163}4247}
41644248
4249.reasoning_edit_textarea,
4165.edit_textarea {4250.edit_textarea {
4166 padding: 5px;4251 padding: 5px;
4167 margin: 0;4252 margin: 0;
src/endpoints/backends/chat-completions.js+13 -7
@@ -289,6 +289,7 @@ async function sendMakerSuiteRequest(request, response) {
289 const model = String(request.body.model);289 const model = String(request.body.model);
290 const stream = Boolean(request.body.stream);290 const stream = Boolean(request.body.stream);
291 const showThoughts = Boolean(request.body.show_thoughts);291 const showThoughts = Boolean(request.body.show_thoughts);
292 const isThinking = model.includes('thinking');
292293
293 const generationConfig = {294 const generationConfig = {
294 stopSequences: request.body.stop,295 stopSequences: request.body.stop,
@@ -329,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
329 body.systemInstruction = prompt.system_instruction;330 body.systemInstruction = prompt.system_instruction;
330 }331 }
331332
333 if (isThinking && showThoughts) {
334 generationConfig.thinkingConfig = {
335 includeThoughts: true,
336 };
337 }
338
332 return body;339 return body;
333 }340 }
334341
@@ -342,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
342 controller.abort();349 controller.abort();
343 });350 });
344351
345 const isThinking = model.includes('thinking');
346 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
347 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
348354
@@ -387,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
387 const responseContent = candidates[0].content ?? candidates[0].output;393 const responseContent = candidates[0].content ?? candidates[0].output;
388 console.log('Google AI Studio response:', responseContent);394 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');
395 if (!responseText) {397 if (!responseText) {
396 let message = 'Google AI Studio Candidate text empty';398 let message = 'Google AI Studio Candidate text empty';
397 console.log(message, generateResponseJson);399 console.log(message, generateResponseJson);
@@ -399,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
399 }401 }
400402
401 // Wrap it back to OAI format403 // Wrap it back to OAI format
402 const reply = { choices: [{ 'message': { 'content': responseText } }] };404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
403 return response.send(reply);405 return response.send(reply);
404 }406 }
405 } catch (error) {407 } catch (error) {
@@ -996,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
996 bodyParams['route'] = 'fallback';998 bodyParams['route'] = 'fallback';
997 }999 }
9981000
1001 if (request.body.show_thoughts) {
1002 bodyParams['include_reasoning'] = true;
1003 }
1004
999 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);1005 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
1000 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {1006 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
1001 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);1007 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);