Add thinking time for hidden reasoning models - Streamline reasoning UI update functionality - Add helper function to identify hidden reasoning models - Fix/update reasoning time calculation to actually utilize start gen time - Fix reasoning UI update on swipe - add CSS class for hidden reasoning blocks (to make it possible to hide for users)

d94ac48b6595310281e244d4e89e79cc96e47e65

Wolfsblvt <wolfsblvt@gmail.com>

4 files changed, +114 -27Showing whitespace changes
public/script.js+26 -20
@@ -113,6 +113,7 @@ import {
113 loadProxyPresets,113 loadProxyPresets,
114 selected_proxy,114 selected_proxy,
115 initOpenAI,115 initOpenAI,
116 isHiddenReasoningModel,
116} from './scripts/openai.js';117} from './scripts/openai.js';
117118
118import {119import {
@@ -269,7 +270,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
269import { initBulkEdit } from './scripts/bulk-edit.js';270import { initBulkEdit } from './scripts/bulk-edit.js';
270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';271import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271import { getContext } from './scripts/st-context.js';272import { getContext } from './scripts/st-context.js';
272import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI } from './scripts/reasoning.js';273import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI, updateReasoningUI } from './scripts/reasoning.js';
273274
274// API OBJECT FOR EXTERNAL WIRING275// API OBJECT FOR EXTERNAL WIRING
275globalThis.SillyTavern = {276globalThis.SillyTavern = {
@@ -2249,7 +2250,6 @@ function getMessageFromTemplate({
2249 mes.find('.avatar img').attr('src', avatarImg);2250 mes.find('.avatar img').attr('src', avatarImg);
2250 mes.find('.ch_name .name_text').text(characterName);2251 mes.find('.ch_name .name_text').text(characterName);
2251 mes.find('.mes_bias').html(bias);2252 mes.find('.mes_bias').html(bias);
2252 mes.find('.mes_reasoning').html(reasoning);
2253 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2253 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2254 mes.find('.mesIDDisplay').text(`#${mesId}`);2254 mes.find('.mesIDDisplay').text(`#${mesId}`);
2255 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2255 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2257,12 +2257,7 @@ function getMessageFromTemplate({
2257 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);2257 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2258 bookmarkLink && updateBookmarkDisplay(mes);2258 bookmarkLink && updateBookmarkDisplay(mes);
22592259
2260 if (reasoning) {2260 updateReasoningUI(mes, reasoning, reasoningDuration, { forceEnd: true });
2261 mes.addClass('reasoning');
2262 }
2263 if (reasoningDuration) {
2264 updateReasoningTimeUI(mes.find('.mes_reasoning_header_title')[0], reasoningDuration, { forceEnd: true });
2265 }
22662261
2267 if (power_user.timestamp_model_icon && extra?.api) {2262 if (power_user.timestamp_model_icon && extra?.api) {
2268 insertSVGIcon(mes, extra);2263 insertSVGIcon(mes, extra);
@@ -2284,8 +2279,9 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
2284 const text = message?.extra?.display_text ?? message.mes;2279 const text = message?.extra?.display_text ?? message.mes;
2285 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));2280 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2286 }2281 }
2287 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));2282
2288 messageElement.toggleClass('reasoning', !!message.extra?.reasoning);2283 updateReasoningUI(messageElement, message?.extra?.reasoning, message?.extra?.reasoning_duration, { forceEnd: true });
2284
2289 addCopyToCodeBlocks(messageElement);2285 addCopyToCodeBlocks(messageElement);
2290 appendMediaToMessage(message, messageElement);2286 appendMediaToMessage(message, messageElement);
2291}2287}
@@ -3123,8 +3119,11 @@ class StreamingProcessor {
3123 constructor(type, forceName2, timeStarted, continueMessage) {3119 constructor(type, forceName2, timeStarted, continueMessage) {
3124 this.result = '';3120 this.result = '';
3125 this.messageId = -1;3121 this.messageId = -1;
3122 /** @type {HTMLElement} */
3126 this.messageDom = null;3123 this.messageDom = null;
3124 /** @type {HTMLElement} */
3127 this.messageTextDom = null;3125 this.messageTextDom = null;
3126 /** @type {HTMLElement} */
3128 this.messageTimerDom = null;3127 this.messageTimerDom = null;
3129 /** @type {HTMLElement} */3128 /** @type {HTMLElement} */
3130 this.messageTokenCounterDom = null;3129 this.messageTokenCounterDom = null;
@@ -3148,13 +3147,17 @@ class StreamingProcessor {
3148 this.messageLogprobs = [];3147 this.messageLogprobs = [];
3149 this.toolCalls = [];3148 this.toolCalls = [];
3150 this.reasoning = '';3149 this.reasoning = '';
3150 /** @type {Date} */
3151 this.reasoningStartTime = null;3151 this.reasoningStartTime = null;
3152 /** @type {Date} */
3152 this.reasoningEndTime = null;3153 this.reasoningEndTime = null;
3154 this.isHiddenReasoning = isHiddenReasoningModel();
3153 }3155 }
31543156
3157 /** @type {() => number} Reasoning duration in milliseconds */
3155 #reasoningDuration() {3158 #reasoningDuration() {
3156 if (this.reasoningStartTime && this.reasoningEndTime) {3159 if (this.reasoningStartTime && this.reasoningEndTime) {
3157 return (this.reasoningEndTime - this.reasoningStartTime);3160 return (this.reasoningEndTime.getTime() - this.reasoningStartTime.getTime());
3158 }3161 }
3159 return null;3162 return null;
3160 }3163 }
@@ -3168,6 +3171,8 @@ class StreamingProcessor {
3168 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');3171 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3169 this.messageReasoningHeaderDom = this.messageDom?.querySelector('.mes_reasoning_header_title');3172 this.messageReasoningHeaderDom = this.messageDom?.querySelector('.mes_reasoning_header_title');
3170 }3173 }
3174
3175 this.messageDom.classList.toggle('reasoning_hidden', this.isHiddenReasoning);
3171 }3176 }
31723177
3173 #updateMessageBlockVisibility() {3178 #updateMessageBlockVisibility() {
@@ -3254,16 +3259,17 @@ class StreamingProcessor {
3254 chat[messageId]['extra'] = {};3259 chat[messageId]['extra'] = {};
3255 }3260 }
32563261
3257 if (this.reasoning) {3262 if (this.reasoning || this.isHiddenReasoning) {
3258 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;3263 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
3259 const reasoningChanged = chat[messageId]['extra']['reasoning'] !== reasoning;3264 const reasoningChanged = chat[messageId]['extra']['reasoning'] !== reasoning;
3260 chat[messageId]['extra']['reasoning'] = reasoning;3265 chat[messageId]['extra']['reasoning'] = reasoning;
32613266
3262 if (reasoningChanged && this.reasoningStartTime === null) {3267 if ((this.isHiddenReasoning || reasoningChanged) && this.reasoningStartTime === null) {
3263 this.reasoningStartTime = Date.now();3268 this.reasoningStartTime = this.timeStarted;
3264 }3269 }
3265 if (!reasoningChanged && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {3270 if ((this.isHiddenReasoning || !reasoningChanged) && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {
3266 this.reasoningEndTime = Date.now();3271 this.reasoningEndTime = currentTime;
3272 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.#reasoningDuration);
3267 }3273 }
3268 await this.#updateReasoningTime(messageId);3274 await this.#updateReasoningTime(messageId);
32693275
@@ -3322,8 +3328,7 @@ class StreamingProcessor {
3322 async #updateReasoningTime(messageId, { forceEnd = false } = {}) {3328 async #updateReasoningTime(messageId, { forceEnd = false } = {}) {
3323 const duration = this.#reasoningDuration();3329 const duration = this.#reasoningDuration();
3324 chat[messageId]['extra']['reasoning_duration'] = duration;3330 chat[messageId]['extra']['reasoning_duration'] = duration;
3325 updateReasoningTimeUI(this.messageReasoningHeaderDom, duration, { forceEnd: forceEnd });3331 updateReasoningUI(this.messageDom, this.reasoning, duration, { forceEnd: forceEnd });
3326 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, duration);
3327 }3332 }
33283333
3329 async onFinishStreaming(messageId, text) {3334 async onFinishStreaming(messageId, text) {
@@ -3333,7 +3338,8 @@ class StreamingProcessor {
33333338
3334 // Ensure reasoning finish time is recorded if not already3339 // Ensure reasoning finish time is recorded if not already
3335 if (this.reasoningStartTime !== null && this.reasoningEndTime === null) {3340 if (this.reasoningStartTime !== null && this.reasoningEndTime === null) {
3336 this.reasoningEndTime = Date.now();3341 this.reasoningEndTime = new Date();
3342 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.#reasoningDuration);
3337 await this.#updateReasoningTime(messageId, { forceEnd: true });3343 await this.#updateReasoningTime(messageId, { forceEnd: true });
3338 }3344 }
33393345
@@ -8785,7 +8791,7 @@ const swipe_right = () => {
8785 // resets the timer8791 // resets the timer
8786 swipeMessage.find('.mes_timer').html('');8792 swipeMessage.find('.mes_timer').html('');
8787 swipeMessage.find('.tokenCounterDisplay').text('');8793 swipeMessage.find('.tokenCounterDisplay').text('');
8788 swipeMessage.find('.mes_reasoning').html('');8794 updateReasoningUI(swipeMessage, null);
8789 } else {8795 } else {
8790 //console.log('showing previously generated swipe candidate, or "..."');8796 //console.log('showing previously generated swipe candidate, or "..."');
8791 //console.log('onclick right swipe calling addOneMessage');8797 //console.log('onclick right swipe calling addOneMessage');
public/scripts/openai.js+47 -0
@@ -4995,6 +4995,53 @@ export function isImageInliningSupported() {
4995 }4995 }
4996}4996}
49974997
4998
4999
5000/**
5001 * Check if the model supports reasoning, but does not send back the reasoning
5002 * @returns {boolean} True if the model supports reasoning
5003 */
5004export function isHiddenReasoningModel() {
5005 if (main_api !== 'openai') {
5006 return false;
5007 }
5008
5009 /** @typedef {Object.<chat_completion_sources, { currentModel: string; models: ({ name: string; startsWith: boolean?; matchingFunc: (model: string) => boolean?; }|string)[]; }>} */
5010 const hiddenReasoningModels = {
5011 [chat_completion_sources.OPENAI]: {
5012 currentModel: oai_settings.openai_model,
5013 models: [
5014 { name: 'o1', startsWith: true },
5015 { name: 'o3', startsWith: true },
5016 ],
5017 },
5018 [chat_completion_sources.MAKERSUITE]: {
5019 currentModel: oai_settings.google_model,
5020 models: [
5021 { name: 'gemini-2.0-flash-thinking-exp', startsWith: true },
5022 ],
5023 },
5024 };
5025
5026 const sourceConfig = hiddenReasoningModels[oai_settings.chat_completion_source];
5027 if (!sourceConfig) {
5028 return false;
5029 }
5030
5031 return sourceConfig.models.some(model => {
5032 if (typeof model === 'string') {
5033 return sourceConfig.currentModel === model;
5034 }
5035 if (model.startsWith) {
5036 return (sourceConfig.currentModel).startsWith(model.name);
5037 }
5038 if (model.matchingFunc) {
5039 return model.matchingFunc(sourceConfig.currentModel);
5040 }
5041 return false;
5042 });
5043}
5044
4998/**5045/**
4999 * Proxy stuff5046 * Proxy stuff
5000 */5047 */
public/scripts/reasoning.js+30 -2
@@ -1,11 +1,11 @@
1import {1import {
2 moment,2 moment,
3} from '../lib.js';3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
6import { getCurrentLocale, t } from './i18n.js';6import { getCurrentLocale, t } from './i18n.js';
7import { MacrosParser } from './macros.js';7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, oai_settings } from './openai.js';8import { chat_completion_sources, isHiddenReasoningModel, oai_settings } from './openai.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
10import { power_user } from './power-user.js';10import { power_user } from './power-user.js';
11import { SlashCommand } from './slash-commands/SlashCommand.js';11import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -71,6 +71,34 @@ export function extractReasoningFromData(data) {
71}71}
7272
73/**73/**
74 * Updates the Reasoning UI.
75 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element.
76 * @param {string|null} [reasoning=null] The reasoning content.
77 * @param {number|null} [reasoningDuration=null] The duration of the reasoning in milliseconds.
78 * @param {object} [options={}] Options for the function.
79 * @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists.
80 */
81export function updateReasoningUI(messageIdOrElement, reasoning = null, reasoningDuration = null, { forceEnd = false } = {}) {
82 const messageElement = typeof messageIdOrElement === 'number'
83 ? $(`#chat [mesid="${messageIdOrElement}"]`)
84 : $(messageIdOrElement);
85 const mesReasoningElement = messageElement.find('.mes_reasoning');
86 const mesReasoningHeaderTitle = messageElement.find('.mes_reasoning_header_title');
87 const mesId = Number(messageElement.attr('mesid'));
88
89 mesReasoningElement.html(messageFormatting(reasoning ?? '', '', false, false, mesId, {}, true));
90 const reasoningText = mesReasoningElement.text().trim();
91
92 const hasReasoningText = !!reasoningText;
93 const isReasoningHidden = (!!reasoningDuration && !hasReasoningText) || (!forceEnd && isHiddenReasoningModel());
94 const isReasoning = hasReasoningText || isReasoningHidden;
95
96 messageElement.toggleClass('reasoning', isReasoning);
97 messageElement.toggleClass('reasoning_hidden', isReasoningHidden);
98 updateReasoningTimeUI(mesReasoningHeaderTitle[0], reasoningDuration, { forceEnd });
99}
100
101/**
74 * Updates the Reasoning controls102 * Updates the Reasoning controls
75 * @param {HTMLElement} element The element to update103 * @param {HTMLElement} element The element to update
76 * @param {number?} duration The duration of the reasoning in milliseconds104 * @param {number?} duration The duration of the reasoning in milliseconds
public/style.css+11 -5
@@ -413,10 +413,20 @@ input[type='checkbox']:focus-visible {
413.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,413.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
414.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,414.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
415.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),415.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
416.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions {416.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
417.mes.reasoning:not(.reasoning_hidden) .mes_edit_add_reasoning,
418.mes.reasoning_hidden .mes_reasoning_arrow,
419.mes.reasoning_hidden .mes_reasoning_actions {
417 display: none;420 display: none;
418}421}
419422
423.mes.reasoning_hidden .mes_reasoning_details {
424 display: block;
425}
426.mes.reasoning_hidden .mes_reasoning_header {
427 cursor: initial;
428}
429
420.mes_reasoning_details .mes_reasoning_arrow {430.mes_reasoning_details .mes_reasoning_arrow {
421 position: absolute;431 position: absolute;
422 top: 50%;432 top: 50%;
@@ -4315,10 +4325,6 @@ input[type="range"]::-webkit-slider-thumb {
4315 field-sizing: content;4325 field-sizing: content;
4316}4326}
43174327
4318.mes.reasoning .mes_edit_add_reasoning {
4319 display: none;
4320}
4321
4322#anchor_order {4328#anchor_order {
4323 margin-bottom: 15px;4329 margin-bottom: 15px;
4324}4330}