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 {
113113 loadProxyPresets,
114114 selected_proxy,
115115 initOpenAI,
116+ isHiddenReasoningModel,
116117} from './scripts/openai.js';
117118
118119import {
@@ -269,7 +270,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
269270import { initBulkEdit } from './scripts/bulk-edit.js';
270271import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271272import { getContext } from './scripts/st-context.js';
272273import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI, updateReasoningUI } from './scripts/reasoning.js';
273274
274275// API OBJECT FOR EXTERNAL WIRING
275276globalThis.SillyTavern = {
@@ -2249,7 +2250,6 @@ function getMessageFromTemplate({
22492250 mes.find('.avatar img').attr('src', avatarImg);
22502251 mes.find('.ch_name .name_text').text(characterName);
22512252 mes.find('.mes_bias').html(bias);
2252- mes.find('.mes_reasoning').html(reasoning);
22532253 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
22542254 mes.find('.mesIDDisplay').text(`#${mesId}`);
22552255 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2257,12 +2257,7 @@ function getMessageFromTemplate({
22572257 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
22582258 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
22672262 if (power_user.timestamp_model_icon && extra?.api) {
22682263 insertSVGIcon(mes, extra);
@@ -2284,8 +2279,9 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
22842279 const text = message?.extra?.display_text ?? message.mes;
22852280 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
22862281 }
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+
22892285 addCopyToCodeBlocks(messageElement);
22902286 appendMediaToMessage(message, messageElement);
22912287}
@@ -3123,8 +3119,11 @@ class StreamingProcessor {
31233119 constructor(type, forceName2, timeStarted, continueMessage) {
31243120 this.result = '';
31253121 this.messageId = -1;
3122+ /** @type {HTMLElement} */
31263123 this.messageDom = null;
3124+ /** @type {HTMLElement} */
31273125 this.messageTextDom = null;
3126+ /** @type {HTMLElement} */
31283127 this.messageTimerDom = null;
31293128 /** @type {HTMLElement} */
31303129 this.messageTokenCounterDom = null;
@@ -3148,13 +3147,17 @@ class StreamingProcessor {
31483147 this.messageLogprobs = [];
31493148 this.toolCalls = [];
31503149 this.reasoning = '';
3150+ /** @type {Date} */
31513151 this.reasoningStartTime = null;
3152+ /** @type {Date} */
31523153 this.reasoningEndTime = null;
3154+ this.isHiddenReasoning = isHiddenReasoningModel();
31533155 }
31543156
3157+ /** @type {() => number} Reasoning duration in milliseconds */
31553158 #reasoningDuration() {
31563159 if (this.reasoningStartTime && this.reasoningEndTime) {
31573160 return (this.reasoningEndTime.getTime() - this.reasoningStartTime.getTime());
31583161 }
31593162 return null;
31603163 }
@@ -3168,6 +3171,8 @@ class StreamingProcessor {
31683171 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
31693172 this.messageReasoningHeaderDom = this.messageDom?.querySelector('.mes_reasoning_header_title');
31703173 }
3174+
3175+ this.messageDom.classList.toggle('reasoning_hidden', this.isHiddenReasoning);
31713176 }
31723177
31733178 #updateMessageBlockVisibility() {
@@ -3254,16 +3259,17 @@ class StreamingProcessor {
32543259 chat[messageId]['extra'] = {};
32553260 }
32563261
32573262 if (this.reasoning || this.isHiddenReasoning) {
32583263 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
32593264 const reasoningChanged = chat[messageId]['extra']['reasoning'] !== reasoning;
32603265 chat[messageId]['extra']['reasoning'] = reasoning;
32613266
32623267 if ((this.isHiddenReasoning || reasoningChanged) && this.reasoningStartTime === null) {
32633268 this.reasoningStartTime = Datethis.now()timeStarted;
32643269 }
32653270 if ((this.isHiddenReasoning || !reasoningChanged) && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {
32663271 this.reasoningEndTime = Date.now()currentTime;
3272+ await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.#reasoningDuration);
32673273 }
32683274 await this.#updateReasoningTime(messageId);
32693275
@@ -3322,8 +3328,7 @@ class StreamingProcessor {
33223328 async #updateReasoningTime(messageId, { forceEnd = false } = {}) {
33233329 const duration = this.#reasoningDuration();
33243330 chat[messageId]['extra']['reasoning_duration'] = duration;
33253331 updateReasoningTimeUIupdateReasoningUI(this.messageReasoningHeaderDommessageDom, this.reasoning, duration, { forceEnd: forceEnd });
3326- await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, duration);
33273332 }
33283333
33293334 async onFinishStreaming(messageId, text) {
@@ -3333,7 +3338,8 @@ class StreamingProcessor {
33333338
33343339 // Ensure reasoning finish time is recorded if not already
33353340 if (this.reasoningStartTime !== null && this.reasoningEndTime === null) {
33363341 this.reasoningEndTime = new Date.now();
3342+ await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.#reasoningDuration);
33373343 await this.#updateReasoningTime(messageId, { forceEnd: true });
33383344 }
33393345
@@ -8785,7 +8791,7 @@ const swipe_right = () => {
87858791 // resets the timer
87868792 swipeMessage.find('.mes_timer').html('');
87878793 swipeMessage.find('.tokenCounterDisplay').text('');
8788- swipeMessage.find('.mes_reasoning').html('');
8794+ updateReasoningUI(swipeMessage, null);
87898795 } else {
87908796 //console.log('showing previously generated swipe candidate, or "..."');
87918797 //console.log('onclick right swipe calling addOneMessage');
public/scripts/openai.js+47 -0
@@ -4995,6 +4995,53 @@ export function isImageInliningSupported() {
49954995 }
49964996}
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+ */
5004+export 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+
49985045/**
49995046 * Proxy stuff
50005047 */
public/scripts/reasoning.js+30 -2
@@ -1,11 +1,11 @@
11import {
22 moment,
33} from '../lib.js';
44import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
55import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
66import { getCurrentLocale, t } from './i18n.js';
77import { MacrosParser } from './macros.js';
88import { chat_completion_sources, isHiddenReasoningModel, oai_settings } from './openai.js';
99import { Popup } from './popup.js';
1010import { power_user } from './power-user.js';
1111import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -71,6 +71,34 @@ export function extractReasoningFromData(data) {
7171}
7272
7373/**
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+ */
81+export 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+/**
74102 * Updates the Reasoning controls
75103 * @param {HTMLElement} element The element to update
76104 * @param {number?} duration The duration of the reasoning in milliseconds
public/style.css+11 -5
@@ -413,10 +413,20 @@ input[type='checkbox']:focus-visible {
413413.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
414414.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
415415.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
416416.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 {
417420 display: none;
418421}
419422
423+.mes.reasoning_hidden .mes_reasoning_details {
424+ display: block;
425+}
426+.mes.reasoning_hidden .mes_reasoning_header {
427+ cursor: initial;
428+}
429+
420430.mes_reasoning_details .mes_reasoning_arrow {
421431 position: absolute;
422432 top: 50%;
@@ -4315,10 +4325,6 @@ input[type="range"]::-webkit-slider-thumb {
43154325 field-sizing: content;
43164326}
43174327
4318-.mes.reasoning .mes_edit_add_reasoning {
4319- display: none;
4320-}
4321-
43224328#anchor_order {
43234329 margin-bottom: 15px;
43244330}