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)
| @@ -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'; |
| 117 | 118 | ||
| 118 | import { | 119 | import { |
| @@ -269,7 +270,7 @@ import { initSettingsSearch } from './scripts/setting-search.js'; | |||
| 269 | import { initBulkEdit } from './scripts/bulk-edit.js'; | 270 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 270 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; | 271 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; |
| 271 | import { getContext } from './scripts/st-context.js'; | 272 | import { getContext } from './scripts/st-context.js'; |
| 272 | import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI } from './scripts/reasoning.js'; | 273 | import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI, updateReasoningUI } from './scripts/reasoning.js'; |
| 273 | 274 | ||
| 274 | // API OBJECT FOR EXTERNAL WIRING | 275 | // API OBJECT FOR EXTERNAL WIRING |
| 275 | globalThis.SillyTavern = { | 276 | globalThis.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); |
| 2259 | 2259 | ||
| 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 | } | ||
| 2266 | 2261 | ||
| 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 | } |
| 3154 | 3156 | ||
| 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 | } |
| 3172 | 3177 | ||
| 3173 | #updateMessageBlockVisibility() { | 3178 | #updateMessageBlockVisibility() { |
| @@ -3254,16 +3259,17 @@ class StreamingProcessor { | |||
| 3254 | chat[messageId]['extra'] = {}; | 3259 | chat[messageId]['extra'] = {}; |
| 3255 | } | 3260 | } |
| 3256 | 3261 | ||
| 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; |
| 3261 | 3266 | ||
| 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); |
| 3269 | 3275 | ||
| @@ -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 | } |
| 3328 | 3333 | ||
| 3329 | async onFinishStreaming(messageId, text) { | 3334 | async onFinishStreaming(messageId, text) { |
| @@ -3333,7 +3338,8 @@ class StreamingProcessor { | |||
| 3333 | 3338 | ||
| 3334 | // Ensure reasoning finish time is recorded if not already | 3339 | // 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 | } |
| 3339 | 3345 | ||
| @@ -8785,7 +8791,7 @@ const swipe_right = () => { | |||
| 8785 | // resets the timer | 8791 | // 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'); |
| @@ -4995,6 +4995,53 @@ export function isImageInliningSupported() { | |||
| 4995 | } | 4995 | } |
| 4996 | } | 4996 | } |
| 4997 | 4997 | ||
| 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 | |||
| 4998 | /** | 5045 | /** |
| 4999 | * Proxy stuff | 5046 | * Proxy stuff |
| 5000 | */ | 5047 | */ |
| @@ -1,11 +1,11 @@ | |||
| 1 | import { | 1 | import { |
| 2 | moment, | 2 | moment, |
| 3 | } from '../lib.js'; | 3 | } from '../lib.js'; |
| 4 | import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; | 4 | import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; |
| 5 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 5 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 6 | import { getCurrentLocale, t } from './i18n.js'; | 6 | import { getCurrentLocale, t } from './i18n.js'; |
| 7 | import { MacrosParser } from './macros.js'; | 7 | import { MacrosParser } from './macros.js'; |
| 8 | import { chat_completion_sources, oai_settings } from './openai.js'; | 8 | import { chat_completion_sources, isHiddenReasoningModel, oai_settings } from './openai.js'; |
| 9 | import { Popup } from './popup.js'; | 9 | import { Popup } from './popup.js'; |
| 10 | import { power_user } from './power-user.js'; | 10 | import { power_user } from './power-user.js'; |
| 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -71,6 +71,34 @@ export function extractReasoningFromData(data) { | |||
| 71 | } | 71 | } |
| 72 | 72 | ||
| 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 | */ | ||
| 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 | /** | ||
| 74 | * Updates the Reasoning controls | 102 | * Updates the Reasoning controls |
| 75 | * @param {HTMLElement} element The element to update | 103 | * @param {HTMLElement} element The element to update |
| 76 | * @param {number?} duration The duration of the reasoning in milliseconds | 104 | * @param {number?} duration The duration of the reasoning in milliseconds |
| @@ -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 | } |
| 419 | 422 | ||
| 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 | } |
| 4317 | 4327 | ||
| 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 | } |