Implement Gemini thought signatures (#4886)

* Implement Gemini thought signatures

* Implement streaming support for Gemini thought signatures

* Implement OR support for Gemini thought signatures

* Remove unnecessary extraction of thought sigs from response parts

* Update thought sig comments to remove explicit Gemini mention

* Fix thought_signature naming convention in message.extra

* Add thought_signatures to ReasoningMessageExtra typedef

* Prevent thought sigs being sent to incompatible endpoints

* Move signatures to populateChatHistory, update for consistent casing

* Code clean-up

* Only send thought signatures if target model and API match original

* Implement content-hash thought signature mapping

* Change the data model + split for text/functions

* Don't include signature to invocations if the model doesn't match

* Fix function description

* Remove misleading comment

* Handle OpenRouter signatures

* Improve message extra types

* Prevent modifying original invocations when removing signatures

* Fix return of openrouter non-streaming signatures

* Remove redundant array check

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
mightytribble
2025-12-17 12:23:47 -08:00
committed by GitHub
parent 5accfbc5d8
commit 2cd2bd4a4d
7 changed files with 259 additions and 29 deletions
+20 -5
View File
@@ -267,7 +267,7 @@ import { initServerHistory } from './scripts/server-history.js';
import { initSettingsSearch } from './scripts/setting-search.js';
import { initBulkEdit } from './scripts/bulk-edit.js';
import { getContext } from './scripts/st-context.js';
import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
import { extractReasoningFromData, extractReasoningSignatureFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
import { accountStorage } from './scripts/util/AccountStorage.js';
import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
import { initDataMaid } from './scripts/data-maid.js';
@@ -3388,6 +3388,8 @@ class StreamingProcessor {
this.promptReasoning = promptReasoning;
/** @type {string[]} */
this.images = [];
/** @type {string?} */
this.reasoningSignature = null;
}
/**
@@ -3581,6 +3583,12 @@ class StreamingProcessor {
appendMediaToMessage(message, $(this.messageDom));
}
// Store reasoning signature for models that support multi-turn context
if (this.reasoningSignature) {
message.extra = message.extra || {};
message.extra.reasoning_signature = this.reasoningSignature;
}
this.markUIGenStopped();
if (this.type !== 'impersonate') {
@@ -3675,6 +3683,7 @@ class StreamingProcessor {
// Get the updated reasoning string into the handler
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
this.images = state?.images ?? [];
this.reasoningSignature = state?.signature ?? null;
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
}
@@ -5237,6 +5246,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
let title = extractTitleFromData(data);
let reasoning = extractReasoningFromData(data);
let imageUrls = extractImagesFromData(data);
const reasoningSignature = extractReasoningSignatureFromData(data);
kobold_horde_model = title;
const swipes = extractMultiSwipes(data, type);
@@ -5280,10 +5290,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
else {
// Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
if (originalType !== 'continue') {
({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls }));
({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
}
else {
({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls }));
({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrls, reasoningSignature }));
}
// This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -6332,16 +6342,17 @@ async function processImageAttachment(message, { imageUrls }) {
* @property {string[]} [swipes] Extra swipes
* @property {string} [reasoning] Message reasoning
* @property {string[]} [imageUrls] Links to images
* @property {string?} [reasoningSignature] Encrypted signature of the reasoning text
*
* @typedef {object} SaveReplyResult
* @property {string} type Type of generation
* @property {string} getMessage Generated message
*/
export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrls = [] }) {
export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrls = [], reasoningSignature = null }) {
// Backward compatibility
if (arguments.length > 1 && typeof arguments[0] !== 'object') {
console.trace('saveReply called with positional arguments. Please use an object instead.');
[type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls] = arguments;
[type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments;
}
if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
@@ -6377,6 +6388,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] = reasoning;
chat[chat.length - 1]['extra']['reasoning_duration'] = null;
chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
await processImageAttachment(chat[chat.length - 1], { imageUrls });
if (power_user.message_token_count_enabled) {
const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
@@ -6401,6 +6413,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] = reasoning;
chat[chat.length - 1]['extra']['reasoning_duration'] = null;
chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
await processImageAttachment(chat[chat.length - 1], { imageUrls });
if (power_user.message_token_count_enabled) {
const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
@@ -6421,6 +6434,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] += reasoning;
chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
await processImageAttachment(chat[chat.length - 1], { imageUrls });
// We don't know if the reasoning duration extended, so we don't update it here on purpose.
if (power_user.message_token_count_enabled) {
@@ -6443,6 +6457,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] = reasoning;
chat[chat.length - 1]['extra']['reasoning_duration'] = null;
chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;
if (power_user.trim_spaces) {
getMessage = getMessage.trim();
}