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)
This commit is contained in:
Wolfsblvt 2025-02-08 00:45:33 +01:00
parent 5886bb6b3a
commit d94ac48b65
4 changed files with 114 additions and 27 deletions

View File

@ -113,6 +113,7 @@ import {
loadProxyPresets,
selected_proxy,
initOpenAI,
isHiddenReasoningModel,
} from './scripts/openai.js';
import {
@ -269,7 +270,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
import { initBulkEdit } from './scripts/bulk-edit.js';
import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
import { getContext } from './scripts/st-context.js';
import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI } from './scripts/reasoning.js';
import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI, updateReasoningUI } from './scripts/reasoning.js';
// API OBJECT FOR EXTERNAL WIRING
globalThis.SillyTavern = {
@ -2249,7 +2250,6 @@ function getMessageFromTemplate({
mes.find('.avatar img').attr('src', avatarImg);
mes.find('.ch_name .name_text').text(characterName);
mes.find('.mes_bias').html(bias);
mes.find('.mes_reasoning').html(reasoning);
mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
mes.find('.mesIDDisplay').text(`#${mesId}`);
tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@ -2257,12 +2257,7 @@ function getMessageFromTemplate({
timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
bookmarkLink && updateBookmarkDisplay(mes);
if (reasoning) {
mes.addClass('reasoning');
}
if (reasoningDuration) {
updateReasoningTimeUI(mes.find('.mes_reasoning_header_title')[0], reasoningDuration, { forceEnd: true });
}
updateReasoningUI(mes, reasoning, reasoningDuration, { forceEnd: true });
if (power_user.timestamp_model_icon && extra?.api) {
insertSVGIcon(mes, extra);
@ -2284,8 +2279,9 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
const text = message?.extra?.display_text ?? message.mes;
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
}
messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
messageElement.toggleClass('reasoning', !!message.extra?.reasoning);
updateReasoningUI(messageElement, message?.extra?.reasoning, message?.extra?.reasoning_duration, { forceEnd: true });
addCopyToCodeBlocks(messageElement);
appendMediaToMessage(message, messageElement);
}
@ -3123,8 +3119,11 @@ class StreamingProcessor {
constructor(type, forceName2, timeStarted, continueMessage) {
this.result = '';
this.messageId = -1;
/** @type {HTMLElement} */
this.messageDom = null;
/** @type {HTMLElement} */
this.messageTextDom = null;
/** @type {HTMLElement} */
this.messageTimerDom = null;
/** @type {HTMLElement} */
this.messageTokenCounterDom = null;
@ -3148,13 +3147,17 @@ class StreamingProcessor {
this.messageLogprobs = [];
this.toolCalls = [];
this.reasoning = '';
/** @type {Date} */
this.reasoningStartTime = null;
/** @type {Date} */
this.reasoningEndTime = null;
this.isHiddenReasoning = isHiddenReasoningModel();
}
/** @type {() => number} Reasoning duration in milliseconds */
#reasoningDuration() {
if (this.reasoningStartTime && this.reasoningEndTime) {
return (this.reasoningEndTime - this.reasoningStartTime);
return (this.reasoningEndTime.getTime() - this.reasoningStartTime.getTime());
}
return null;
}
@ -3168,6 +3171,8 @@ class StreamingProcessor {
this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
this.messageReasoningHeaderDom = this.messageDom?.querySelector('.mes_reasoning_header_title');
}
this.messageDom.classList.toggle('reasoning_hidden', this.isHiddenReasoning);
}
#updateMessageBlockVisibility() {
@ -3254,16 +3259,17 @@ class StreamingProcessor {
chat[messageId]['extra'] = {};
}
if (this.reasoning) {
if (this.reasoning || this.isHiddenReasoning) {
const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
const reasoningChanged = chat[messageId]['extra']['reasoning'] !== reasoning;
chat[messageId]['extra']['reasoning'] = reasoning;
if (reasoningChanged && this.reasoningStartTime === null) {
this.reasoningStartTime = Date.now();
if ((this.isHiddenReasoning || reasoningChanged) && this.reasoningStartTime === null) {
this.reasoningStartTime = this.timeStarted;
}
if (!reasoningChanged && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {
this.reasoningEndTime = Date.now();
if ((this.isHiddenReasoning || !reasoningChanged) && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {
this.reasoningEndTime = currentTime;
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.#reasoningDuration);
}
await this.#updateReasoningTime(messageId);
@ -3322,8 +3328,7 @@ class StreamingProcessor {
async #updateReasoningTime(messageId, { forceEnd = false } = {}) {
const duration = this.#reasoningDuration();
chat[messageId]['extra']['reasoning_duration'] = duration;
updateReasoningTimeUI(this.messageReasoningHeaderDom, duration, { forceEnd: forceEnd });
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, duration);
updateReasoningUI(this.messageDom, this.reasoning, duration, { forceEnd: forceEnd });
}
async onFinishStreaming(messageId, text) {
@ -3333,7 +3338,8 @@ class StreamingProcessor {
// Ensure reasoning finish time is recorded if not already
if (this.reasoningStartTime !== null && this.reasoningEndTime === null) {
this.reasoningEndTime = Date.now();
this.reasoningEndTime = new Date();
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.#reasoningDuration);
await this.#updateReasoningTime(messageId, { forceEnd: true });
}
@ -8785,7 +8791,7 @@ const swipe_right = () => {
// resets the timer
swipeMessage.find('.mes_timer').html('');
swipeMessage.find('.tokenCounterDisplay').text('');
swipeMessage.find('.mes_reasoning').html('');
updateReasoningUI(swipeMessage, null);
} else {
//console.log('showing previously generated swipe candidate, or "..."');
//console.log('onclick right swipe calling addOneMessage');

View File

@ -4995,6 +4995,53 @@ export function isImageInliningSupported() {
}
}
/**
* Check if the model supports reasoning, but does not send back the reasoning
* @returns {boolean} True if the model supports reasoning
*/
export function isHiddenReasoningModel() {
if (main_api !== 'openai') {
return false;
}
/** @typedef {Object.<chat_completion_sources, { currentModel: string; models: ({ name: string; startsWith: boolean?; matchingFunc: (model: string) => boolean?; }|string)[]; }>} */
const hiddenReasoningModels = {
[chat_completion_sources.OPENAI]: {
currentModel: oai_settings.openai_model,
models: [
{ name: 'o1', startsWith: true },
{ name: 'o3', startsWith: true },
],
},
[chat_completion_sources.MAKERSUITE]: {
currentModel: oai_settings.google_model,
models: [
{ name: 'gemini-2.0-flash-thinking-exp', startsWith: true },
],
},
};
const sourceConfig = hiddenReasoningModels[oai_settings.chat_completion_source];
if (!sourceConfig) {
return false;
}
return sourceConfig.models.some(model => {
if (typeof model === 'string') {
return sourceConfig.currentModel === model;
}
if (model.startsWith) {
return (sourceConfig.currentModel).startsWith(model.name);
}
if (model.matchingFunc) {
return model.matchingFunc(sourceConfig.currentModel);
}
return false;
});
}
/**
* Proxy stuff
*/

View File

@ -1,11 +1,11 @@
import {
moment,
} from '../lib.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { getCurrentLocale, t } from './i18n.js';
import { MacrosParser } from './macros.js';
import { chat_completion_sources, oai_settings } from './openai.js';
import { chat_completion_sources, isHiddenReasoningModel, oai_settings } from './openai.js';
import { Popup } from './popup.js';
import { power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@ -70,6 +70,34 @@ export function extractReasoningFromData(data) {
return '';
}
/**
* Updates the Reasoning UI.
* @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element.
* @param {string|null} [reasoning=null] The reasoning content.
* @param {number|null} [reasoningDuration=null] The duration of the reasoning in milliseconds.
* @param {object} [options={}] Options for the function.
* @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists.
*/
export function updateReasoningUI(messageIdOrElement, reasoning = null, reasoningDuration = null, { forceEnd = false } = {}) {
const messageElement = typeof messageIdOrElement === 'number'
? $(`#chat [mesid="${messageIdOrElement}"]`)
: $(messageIdOrElement);
const mesReasoningElement = messageElement.find('.mes_reasoning');
const mesReasoningHeaderTitle = messageElement.find('.mes_reasoning_header_title');
const mesId = Number(messageElement.attr('mesid'));
mesReasoningElement.html(messageFormatting(reasoning ?? '', '', false, false, mesId, {}, true));
const reasoningText = mesReasoningElement.text().trim();
const hasReasoningText = !!reasoningText;
const isReasoningHidden = (!!reasoningDuration && !hasReasoningText) || (!forceEnd && isHiddenReasoningModel());
const isReasoning = hasReasoningText || isReasoningHidden;
messageElement.toggleClass('reasoning', isReasoning);
messageElement.toggleClass('reasoning_hidden', isReasoningHidden);
updateReasoningTimeUI(mesReasoningHeaderTitle[0], reasoningDuration, { forceEnd });
}
/**
* Updates the Reasoning controls
* @param {HTMLElement} element The element to update

View File

@ -413,10 +413,20 @@ input[type='checkbox']:focus-visible {
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions {
.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
.mes.reasoning:not(.reasoning_hidden) .mes_edit_add_reasoning,
.mes.reasoning_hidden .mes_reasoning_arrow,
.mes.reasoning_hidden .mes_reasoning_actions {
display: none;
}
.mes.reasoning_hidden .mes_reasoning_details {
display: block;
}
.mes.reasoning_hidden .mes_reasoning_header {
cursor: initial;
}
.mes_reasoning_details .mes_reasoning_arrow {
position: absolute;
top: 50%;
@ -4315,10 +4325,6 @@ input[type="range"]::-webkit-slider-thumb {
field-sizing: content;
}
.mes.reasoning .mes_edit_add_reasoning {
display: none;
}
#anchor_order {
margin-bottom: 15px;
}