From b4c721d7bc58bb1f572b0ec67081e892d3841034 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Sat, 28 Mar 2026 19:59:42 +0200
Subject: [PATCH 001/102] Fix npm audit in /tests (#5370)
---
tests/package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/package-lock.json b/tests/package-lock.json
index c35d72ea0..9c8244a2c 100644
--- a/tests/package-lock.json
+++ b/tests/package-lock.json
@@ -1407,9 +1407,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -3581,9 +3581,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
From ff1ca1412a0a39157727a476eb67af2515d27563 Mon Sep 17 00:00:00 2001
From: lunar sheep <102860538+dylenyedc@users.noreply.github.com>
Date: Tue, 31 Mar 2026 03:30:45 +0800
Subject: [PATCH 002/102] feat(secrets): update readSecret function to accept
optional secret ID (#5356)
* feat(secrets): update readSecret function to accept optional secret ID
* add secret_id to ConnectionManagerRequestService payload
* fix: pass secret_id for Text Completion types
---------
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
---
public/scripts/extensions/shared.js | 2 +
src/additional-headers.js | 79 ++++++++++++--------
src/endpoints/backends/chat-completions.js | 86 +++++++++++-----------
src/endpoints/secrets.js | 5 +-
4 files changed, 95 insertions(+), 77 deletions(-)
diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js
index 420495161..dc8491afa 100644
--- a/public/scripts/extensions/shared.js
+++ b/public/scripts/extensions/shared.js
@@ -435,6 +435,7 @@ export class ConnectionManagerRequestService {
max_tokens: maxTokens,
model: profile.model,
chat_completion_source: selectedApiMap.source,
+ secret_id: profile['secret-id'],
custom_url: profile['api-url'],
vertexai_region: profile['api-url'],
zai_endpoint: profile['api-url'],
@@ -459,6 +460,7 @@ export class ConnectionManagerRequestService {
model: profile.model,
api_type: selectedApiMap.type,
api_server: profile['api-url'],
+ secret_id: profile['secret-id'],
...overridePayload,
}, {
instructName: includeInstruct ? profile.instruct : undefined,
diff --git a/src/additional-headers.js b/src/additional-headers.js
index 1ea5d035f..8914435ea 100644
--- a/src/additional-headers.js
+++ b/src/additional-headers.js
@@ -5,10 +5,11 @@ import { getConfigValue } from './util.js';
/**
* Gets the headers for the Mancer API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getMancerHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.MANCER);
+function getMancerHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.MANCER, secretId);
return apiKey ? ({
'X-API-KEY': apiKey,
@@ -19,10 +20,11 @@ function getMancerHeaders(directories) {
/**
* Gets the headers for the TogetherAI API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getTogetherAIHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.TOGETHERAI);
+function getTogetherAIHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.TOGETHERAI, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -32,10 +34,11 @@ function getTogetherAIHeaders(directories) {
/**
* Gets the headers for the InfermaticAI API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getInfermaticAIHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.INFERMATICAI);
+function getInfermaticAIHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.INFERMATICAI, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -45,10 +48,11 @@ function getInfermaticAIHeaders(directories) {
/**
* Gets the headers for the DreamGen API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getDreamGenHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.DREAMGEN);
+function getDreamGenHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.DREAMGEN, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -58,10 +62,11 @@ function getDreamGenHeaders(directories) {
/**
* Gets the headers for the OpenRouter API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getOpenRouterHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.OPENROUTER);
+function getOpenRouterHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.OPENROUTER, secretId);
const baseHeaders = { ...OPENROUTER_HEADERS };
return apiKey ? Object.assign(baseHeaders, { 'Authorization': `Bearer ${apiKey}` }) : baseHeaders;
@@ -70,10 +75,11 @@ function getOpenRouterHeaders(directories) {
/**
* Gets the headers for the vLLM API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getVllmHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.VLLM);
+function getVllmHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.VLLM, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -83,10 +89,11 @@ function getVllmHeaders(directories) {
/**
* Gets the headers for the Aphrodite API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getAphroditeHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.APHRODITE);
+function getAphroditeHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.APHRODITE, secretId);
return apiKey ? ({
'X-API-KEY': apiKey,
@@ -97,10 +104,11 @@ function getAphroditeHeaders(directories) {
/**
* Gets the headers for the Tabby API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getTabbyHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.TABBY);
+function getTabbyHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.TABBY, secretId);
return apiKey ? ({
'x-api-key': apiKey,
@@ -111,10 +119,11 @@ function getTabbyHeaders(directories) {
/**
* Gets the headers for the LlamaCPP API.
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getLlamaCppHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.LLAMACPP);
+function getLlamaCppHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.LLAMACPP, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -123,11 +132,12 @@ function getLlamaCppHeaders(directories) {
/**
* Gets the headers for the Ooba API.
- * @param {import('./users.js').UserDirectoryList} directories
+ * @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getOobaHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.OOBA);
+function getOobaHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.OOBA, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -137,10 +147,11 @@ function getOobaHeaders(directories) {
/**
* Gets the headers for the KoboldCpp API.
* @param {import('./users.js').UserDirectoryList} directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getKoboldCppHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.KOBOLDCPP);
+function getKoboldCppHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.KOBOLDCPP, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -150,10 +161,11 @@ function getKoboldCppHeaders(directories) {
/**
* Gets the headers for the Featherless API.
* @param {import('./users.js').UserDirectoryList} directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getFeatherlessHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.FEATHERLESS);
+function getFeatherlessHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.FEATHERLESS, secretId);
const baseHeaders = { ...FEATHERLESS_HEADERS };
return apiKey ? Object.assign(baseHeaders, { 'Authorization': `Bearer ${apiKey}` }) : baseHeaders;
@@ -162,10 +174,11 @@ function getFeatherlessHeaders(directories) {
/**
* Gets the headers for the HuggingFace API.
* @param {import('./users.js').UserDirectoryList} directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getHuggingFaceHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.HUGGINGFACE);
+function getHuggingFaceHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.HUGGINGFACE, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -175,10 +188,11 @@ function getHuggingFaceHeaders(directories) {
/**
* Gets the headers for the Generic text completion API.
* @param {import('./users.js').UserDirectoryList} directories
+ * @param {string|null} secretId Secret ID for the request (optional, used to determine which secret to use)
* @returns {object} Headers for the request
*/
-function getGenericHeaders(directories) {
- const apiKey = readSecret(directories, SECRET_KEYS.GENERIC);
+function getGenericHeaders(directories, secretId = null) {
+ const apiKey = readSecret(directories, SECRET_KEYS.GENERIC, secretId);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
@@ -202,7 +216,7 @@ export function getOverrideHeaders(urlHost) {
* @param {string|null} server API server for new request
*/
export function setAdditionalHeaders(request, args, server) {
- setAdditionalHeadersByType(args.headers, request.body.api_type, server, request.user.directories);
+ setAdditionalHeadersByType(args.headers, request.body.api_type, server, request.user.directories, request.body.secret_id);
}
/**
@@ -211,8 +225,9 @@ export function setAdditionalHeaders(request, args, server) {
* @param {string} type API type
* @param {string|null} server API server for new request
* @param {import('./users.js').UserDirectoryList} directories User directories
+ * @param {string|null} secretId Secret ID for the request (optional, used for some API types to determine which secret to use)
*/
-export function setAdditionalHeadersByType(requestHeaders, type, server, directories) {
+export function setAdditionalHeadersByType(requestHeaders, type, server, directories, secretId = null) {
const headerGetters = {
[TEXTGEN_TYPES.MANCER]: getMancerHeaders,
[TEXTGEN_TYPES.VLLM]: getVllmHeaders,
@@ -231,7 +246,7 @@ export function setAdditionalHeadersByType(requestHeaders, type, server, directo
};
const getHeaders = headerGetters[type];
- const headers = getHeaders ? getHeaders(directories) : {};
+ const headers = getHeaders ? getHeaders(directories, secretId) : {};
if (typeof server === 'string' && server.length > 0) {
try {
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index d294a3c1f..a39974e16 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -208,7 +208,7 @@ function setJsonObjectFormat(bodyParams, messages, jsonSchema) {
*/
async function sendClaudeRequest(request, response) {
const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
- const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE, request.body.secret_id);
const divider = '-'.repeat(process.stdout.columns);
if (!apiKey) {
@@ -425,7 +425,7 @@ async function sendMakerSuiteRequest(request, response) {
}
} else {
apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE, request.body.secret_id);
if (!request.body.reverse_proxy && !apiKey) {
console.warn(`${apiName} API key is missing.`);
@@ -641,7 +641,7 @@ async function sendMakerSuiteRequest(request, response) {
} else if (authType === 'full') {
// For Full mode (service account authentication), use project-specific URL
// Get project ID from Service Account JSON
- const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
+ const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, request.body.secret_id);
if (!serviceAccountJson) {
console.warn('Vertex AI Service Account JSON is missing.');
return response.status(400).send({ error: true });
@@ -742,7 +742,7 @@ async function sendMakerSuiteRequest(request, response) {
async function sendAI21Request(request, response) {
if (!request.body) return response.sendStatus(400);
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.AI21);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.AI21, request.body.secret_id);
if (!apiKey) {
console.warn('AI21 API key is missing.');
return response.status(400).send({ error: true });
@@ -822,7 +822,7 @@ async function sendAI21Request(request, response) {
*/
async function sendMistralAIRequest(request, response) {
const apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString();
- const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI, request.body.secret_id);
if (!apiKey) {
console.warn('MistralAI API key is missing.');
@@ -911,7 +911,7 @@ async function sendMistralAIRequest(request, response) {
* @param {express.Response} response Express response
*/
async function sendCohereRequest(request, response) {
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE, request.body.secret_id);
const controller = new AbortController();
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
@@ -1012,7 +1012,7 @@ async function sendCohereRequest(request, response) {
*/
async function sendDeepSeekRequest(request, response) {
const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
- const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK, request.body.secret_id);
if (!apiKey && !request.body.reverse_proxy) {
console.warn('DeepSeek API key is missing.');
@@ -1122,7 +1122,7 @@ async function sendDeepSeekRequest(request, response) {
*/
async function sendXaiRequest(request, response) {
const apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();
- const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI, request.body.secret_id);
if (!apiKey && !request.body.reverse_proxy) {
console.warn('xAI API key is missing.');
@@ -1228,7 +1228,7 @@ async function sendXaiRequest(request, response) {
*/
async function sendAimlapiRequest(request, response) {
const apiUrl = API_AIMLAPI;
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI, request.body.secret_id);
if (!apiKey) {
console.warn('AI/ML API key is missing.');
@@ -1333,7 +1333,7 @@ async function sendAimlapiRequest(request, response) {
*/
async function sendElectronHubRequest(request, response) {
const apiUrl = API_ELECTRONHUB;
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB, request.body.secret_id);
if (!apiKey) {
console.warn('Electron Hub key is missing.');
@@ -1445,7 +1445,7 @@ async function sendElectronHubRequest(request, response) {
*/
async function sendChutesRequest(request, response) {
const apiUrl = API_CHUTES;
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES, request.body.secret_id);
if (!apiKey) {
console.warn('Chutes key is missing.');
@@ -1547,7 +1547,7 @@ async function sendChutesRequest(request, response) {
async function sendAzureOpenAIRequest(request, response) {
// 1. GATHER & VALIDATE SETTINGS
const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI, request.body.secret_id);
if (!azure_base_url || !azure_deployment_name || !azure_api_version || !apiKey) {
return response.status(400).send({
error: {
@@ -1646,74 +1646,74 @@ router.post('/status', async function (request, statusResponse) {
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
apiUrl = 'https://openrouter.ai/api/v1';
- apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER, request.body.secret_id);
// OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests
headers = { ...OPENROUTER_HEADERS };
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
apiUrl = request.body.custom_url;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM, request.body.secret_id);
headers = {};
mergeObjectWithYaml(headers, request.body.custom_include_headers);
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE) {
apiUrl = API_COHERE_V1;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES) {
apiUrl = API_CHUTES;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {
apiUrl = API_ELECTRONHUB;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {
apiUrl = API_NANOGPT;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT, request.body.secret_id);
headers = {};
queryParams = { detailed: true };
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', '')).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {
apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AIMLAPI) {
apiUrl = API_AIMLAPI;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI, request.body.secret_id);
headers = { ...AIMLAPI_HEADERS };
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
apiUrl = 'https://gen.pollinations.ai/text';
- apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
apiUrl = API_GROQ;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {
apiUrl = API_COMETAPI;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI, request.body.secret_id);
headers = {};
throw new Error('This provider is temporarily disabled.');
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {
apiUrl = new URL(request.body.reverse_proxy || API_MOONSHOT).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MOONSHOT, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {
apiUrl = API_FIREWORKS;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS, request.body.secret_id);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MAKERSUITE) {
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE, request.body.secret_id);
apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE);
const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta');
const modelsUrl = !apiKey && request.body.reverse_proxy
@@ -1750,7 +1750,7 @@ router.post('/status', async function (request, statusResponse) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AZURE_OPENAI) {
const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;
- const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);
+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI, request.body.secret_id);
// 1) Validate configuration from the frontend
if (!apiKey || !azure_base_url || !azure_deployment_name || !azure_api_version) {
@@ -1830,7 +1830,7 @@ router.post('/status', async function (request, statusResponse) {
const defaultApiUrl = request.body.siliconflow_endpoint === SILICONFLOW_ENDPOINT.CN
? API_SILICONFLOW_CN : API_SILICONFLOW;
apiUrl = defaultApiUrl;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW, request.body.secret_id);
headers = {};
queryParams = { type: 'text', sub_type: 'chat' };
} else {
@@ -2053,7 +2053,7 @@ router.post('/generate', async function (request, response) {
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI, request.body.secret_id);
headers = {};
bodyParams = {
logprobs: request.body.logprobs,
@@ -2073,7 +2073,7 @@ router.post('/generate', async function (request, response) {
embedOpenRouterMedia(request.body.messages, { audio: true, video: false });
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
apiUrl = 'https://openrouter.ai/api/v1';
- apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER, request.body.secret_id);
// OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests
headers = { ...OPENROUTER_HEADERS };
const includeReasoning = Boolean(request.body.include_reasoning);
@@ -2161,7 +2161,7 @@ router.post('/generate', async function (request, response) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
apiUrl = request.body.custom_url;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM, request.body.secret_id);
headers = {};
bodyParams = {
logprobs: request.body.logprobs,
@@ -2179,7 +2179,7 @@ router.post('/generate', async function (request, response) {
embedOpenRouterMedia(request.body.messages, { audio: true, video: false });
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) {
apiUrl = API_PERPLEXITY;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY, request.body.secret_id);
headers = {};
bodyParams = {
reasoning_effort: request.body.reasoning_effort,
@@ -2195,7 +2195,7 @@ router.post('/generate', async function (request, response) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
apiUrl = API_GROQ;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ, request.body.secret_id);
headers = {};
bodyParams = {};
if (request.body.json_schema) {
@@ -2211,7 +2211,7 @@ router.post('/generate', async function (request, response) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {
apiUrl = API_FIREWORKS;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS, request.body.secret_id);
headers = {};
bodyParams = {};
if (request.body.json_schema) {
@@ -2227,7 +2227,7 @@ router.post('/generate', async function (request, response) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {
apiUrl = API_NANOGPT;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT, request.body.secret_id);
headers = {};
bodyParams = {};
if (request.body.enable_web_search && !/:online$/.test(request.body.model)) {
@@ -2256,7 +2256,7 @@ router.post('/generate', async function (request, response) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
apiUrl = API_POLLINATIONS;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS, request.body.secret_id);
headers = {};
bodyParams = {
reasoning_effort: request.body.reasoning_effort,
@@ -2272,7 +2272,7 @@ router.post('/generate', async function (request, response) {
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {
apiUrl = new URL(request.body.reverse_proxy || API_MOONSHOT).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MOONSHOT, request.body.secret_id);
headers = {};
bodyParams = {
thinking: {
@@ -2284,7 +2284,7 @@ router.post('/generate', async function (request, response) {
: addAssistantPrefix(request.body.messages, [], 'partial');
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {
apiUrl = API_COMETAPI;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI, request.body.secret_id);
headers = {};
bodyParams = {
reasoning_effort: request.body.reasoning_effort,
@@ -2293,7 +2293,7 @@ router.post('/generate', async function (request, response) {
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZAI) {
const defaultApiUrl = request.body.zai_endpoint === ZAI_ENDPOINT.CODING ? API_ZAI_CODING : API_ZAI_COMMON;
apiUrl = new URL(request.body.reverse_proxy || defaultApiUrl).toString();
- apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.ZAI);
+ apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.ZAI, request.body.secret_id);
headers = {
'Accept-Language': 'en-US,en',
};
@@ -2309,7 +2309,7 @@ router.post('/generate', async function (request, response) {
const defaultApiUrl = request.body.siliconflow_endpoint === SILICONFLOW_ENDPOINT.CN
? API_SILICONFLOW_CN : API_SILICONFLOW;
apiUrl = defaultApiUrl;
- apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW);
+ apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW, request.body.secret_id);
headers = {};
bodyParams = {};
if (request.body.json_schema) {
diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js
index 35e1c57bc..d909d639b 100644
--- a/src/endpoints/secrets.js
+++ b/src/endpoints/secrets.js
@@ -441,10 +441,11 @@ export function deleteSecret(directories, key) {
* Reads a secret from the secrets file
* @param {import('../users.js').UserDirectoryList} directories User directories
* @param {string} key Secret key
+ * @param {string?} id Secret ID (optional)
* @returns {string} Secret value
*/
-export function readSecret(directories, key) {
- return new SecretManager(directories).readSecret(key, null);
+export function readSecret(directories, key, id = null) {
+ return new SecretManager(directories).readSecret(key, id);
}
/**
From b89293418d085f0e1522892dd65a9ca7da218177 Mon Sep 17 00:00:00 2001
From: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Date: Tue, 31 Mar 2026 04:26:48 +0800
Subject: [PATCH 003/102] fix: return Error objects from invokeFunctionTool and
create error invocations (#5351)
* fix: return Error objects from invokeFunctionTool and create error invocations
invokeFunctionTool previously called .toString() on caught errors,
converting them to plain strings. This made the instanceof Error
check in invokeFunctionTools dead code.
Changes:
- Return Error objects directly from invokeFunctionTool
- Create error invocations with error: true flag when tools fail
- Record failed stealth tools in stealthCalls
- Preserve signature/reasoning on error invocations
- Add error field to ToolInvocation typedef
* fix: use Error.toString to avoid behavioral changes
---------
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
---
public/scripts/tool-calling.js | 24 ++++++++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index 1e16e9a95..702f28298 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -21,11 +21,12 @@ import { isTrueBoolean } from './utils.js';
* @property {string} result - The result of the tool invocation.
* @property {string?} signature - The thought signature associated with the tool invocation.
* @property {string?} reasoning - The plaintext reasoning associated with this tool call turn.
+ * @property {boolean} [error] - Whether the tool invocation failed.
*/
/**
* @typedef {object} ToolInvocationResult
- * @property {ToolInvocation[]} invocations Successful tool invocations
+ * @property {ToolInvocation[]} invocations Tool invocations (both successful and failed)
* @property {Error[]} errors Errors that occurred during tool invocation
* @property {string[]} stealthCalls Names of stealth tools that were invoked
*/
@@ -336,10 +337,10 @@ export class ToolManager {
if (error instanceof Error) {
error.cause = name;
- return error.toString();
+ return error;
}
- return new Error('Unknown error occurred while invoking the tool.', { cause: name }).toString();
+ return new Error('Unknown error occurred while invoking the tool.', { cause: name });
}
}
@@ -796,9 +797,23 @@ export class ToolManager {
toastr.clear(toast);
console.log('[ToolManager] Function tool result:', result);
- // Save a successful invocation
+ // Handle tool errors — still create an invocation so the LLM sees the failure
if (toolResult instanceof Error) {
result.errors.push(toolResult);
+ if (isStealth) {
+ result.stealthCalls.push(name);
+ } else {
+ result.invocations.push({
+ id,
+ displayName,
+ name,
+ parameters: stringify(parameters),
+ result: toolResult.toString(),
+ error: true,
+ signature: toolCall.signature || null,
+ reasoning: reasoningText || null,
+ });
+ }
continue;
}
@@ -814,6 +829,7 @@ export class ToolManager {
name,
parameters: stringify(parameters),
result: toolResult,
+ error: false,
signature: toolCall.signature || null,
reasoning: reasoningText || null,
};
From c918f4f36d6d28519ce60e722f4f72f062b204d0 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 23:39:19 +0300
Subject: [PATCH 004/102] Add opt-in toggle to keep hidden messages in chat
vector index (#5378)
* Initial plan
* Add opt-in toggle to keep hidden messages in chat vector index
Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/eadc80a1-a417-40df-a374-76d7c4a46ce3
Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
* Revert package-lock changes
* Fix: Remove 'vectorized' class from chat messages before adding it
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
---
public/scripts/extensions/vectors/index.js | 11 +++++++++--
public/scripts/extensions/vectors/settings.html | 4 ++++
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js
index f60425888..173ed0433 100644
--- a/public/scripts/extensions/vectors/index.js
+++ b/public/scripts/extensions/vectors/index.js
@@ -81,6 +81,7 @@ const settings = {
// For chats
enabled_chats: false,
+ keep_hidden: false,
template: 'Past events:\n{{text}}',
depth: 2,
position: extension_prompt_types.IN_PROMPT,
@@ -347,7 +348,7 @@ async function synchronizeChat(batchSize = 5) {
return -1;
}
- const hashedMessages = context.chat.filter(x => !x.is_system).map(x => ({ text: String(substituteParams(x.mes)), hash: getStringHash(substituteParams(x.mes)), index: context.chat.indexOf(x) }));
+ const hashedMessages = context.chat.filter(x => settings.keep_hidden || !x.is_system).map(x => ({ text: String(substituteParams(x.mes)), hash: getStringHash(substituteParams(x.mes)), index: context.chat.indexOf(x) }));
const hashesInCollection = await getSavedHashes(chatId);
let newVectorItems = hashedMessages.filter(x => !hashesInCollection.includes(x.hash));
@@ -1496,10 +1497,11 @@ async function onViewStatsClick() {
{ timeOut: 10000, escapeHtml: false },
);
+ $('#chat .mes.vectorized').removeClass('vectorized');
const chat = getContext().chat;
for (const message of chat) {
if (hashesInCollection.includes(getStringHash(substituteParams(message.mes)))) {
- const messageElement = $(`.mes[mesid="${chat.indexOf(message)}"]`);
+ const messageElement = $(`#chat .mes[mesid="${chat.indexOf(message)}"]`);
messageElement.addClass('vectorized');
}
}
@@ -1726,6 +1728,11 @@ jQuery(async () => {
saveSettingsDebounced();
toggleSettings();
});
+ $('#vectors_keep_hidden').prop('checked', settings.keep_hidden).on('input', () => {
+ settings.keep_hidden = !!$('#vectors_keep_hidden').prop('checked');
+ Object.assign(extension_settings.vectors, settings);
+ saveSettingsDebounced();
+ });
$('#vectors_enabled_files').prop('checked', settings.enabled_files).on('input', () => {
settings.enabled_files = $('#vectors_enabled_files').prop('checked');
Object.assign(extension_settings.vectors, settings);
diff --git a/public/scripts/extensions/vectors/settings.html b/public/scripts/extensions/vectors/settings.html
index 1d86a3922..0678b9aad 100644
--- a/public/scripts/extensions/vectors/settings.html
+++ b/public/scripts/extensions/vectors/settings.html
@@ -408,6 +408,10 @@
+
-
+
diff --git a/public/scripts/swipe-picker.js b/public/scripts/swipe-picker.js
index 2116973ee..783f5007f 100644
--- a/public/scripts/swipe-picker.js
+++ b/public/scripts/swipe-picker.js
@@ -4,7 +4,7 @@ import { t } from './i18n.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { power_user } from './power-user.js';
import { getTokenCountAsync } from './tokenizers.js';
-import { clamp, timestampToMoment } from './utils.js';
+import { clamp, copyText, timestampToMoment } from './utils.js';
import { chat, deleteSwipe, ensureSwipes, isMessageSwipeable, isSwipingAllowed, swipe, syncMesToSwipe } from '/script.js';
/**
@@ -132,6 +132,7 @@ async function openSwipePicker(messageId) {
const template = $('#past_chat_template .select_chat_block_wrapper').clone();
const block = template.find('.select_chat_block');
block.removeClass('select_chat_block').addClass('swipe_picker_block');
+ block.find('.select_chat_actions').removeClass('gap10px');
const branchButton = template.find('.exportRawChatButton');
const deleteButton = template.find('.PastChat_cross');
const swipeInfo = Array.isArray(message.swipe_info) ? message.swipe_info[index] : null;
@@ -162,7 +163,7 @@ async function openSwipePicker(messageId) {
'data-i18n': '[title]Create Branch',
})
.removeClass('exportRawChatButton fa-solid fa-file-export')
- .addClass('swipe_picker_branch mes_button fa-regular fa-code-branch')
+ .addClass('swipe_picker_branch mes_button fa-fw fa-regular fa-code-branch')
.on('click', async (event) => {
event.preventDefault();
event.stopPropagation();
@@ -174,7 +175,7 @@ async function openSwipePicker(messageId) {
.removeAttr('file_name')
.attr('aria-disabled', String(!canDeleteSwipe))
.removeClass('fa-skull')
- .addClass('swipe_picker_delete fa-trash-can')
+ .addClass('swipe_picker_delete fa-fw fa-trash-can')
.toggleClass('hoverglow', canDeleteSwipe)
.toggleClass('disabled', !canDeleteSwipe)
.each(function () {
@@ -229,11 +230,42 @@ async function openSwipePicker(messageId) {
await renderSwipeList();
});
+
+ // Add expand/collapse toggle
+ const expandCheckboxId = `swipe_picker_expand_${messageId}_${index}`;
+ const expandCheckbox = document.createElement('input');
+ expandCheckbox.type = 'checkbox';
+ expandCheckbox.id = expandCheckboxId;
+ expandCheckbox.classList.add('swipe_picker_expand_toggle');
+ block[0].prepend(expandCheckbox);
+
+ const expandLabel = document.createElement('label');
+ expandLabel.htmlFor = expandCheckboxId;
+ expandLabel.classList.add('swipe_picker_expand_label', 'fa-solid', 'fa-fw', 'fa-chevron-down');
+ expandLabel.title = t`Expand/Collapse`;
+ expandLabel.setAttribute('data-i18n', '[title]Expand/Collapse');
+ expandLabel.addEventListener('click', (event) => event.stopPropagation());
+
+ // Add copy button
+ const copyButton = document.createElement('div');
+ copyButton.classList.add('swipe_picker_copy', 'fa-solid', 'fa-fw', 'fa-copy');
+ copyButton.title = t`Copy`;
+ copyButton.setAttribute('data-i18n', '[title]Copy');
+ copyButton.addEventListener('click', async (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ await copyText(swipeText);
+ toastr.info(t`Copied!`, '', { timeOut: 2000 });
+ });
+
+ // Insert new buttons before the branch button
+ branchButton.before(expandLabel, copyButton);
+
template.find('.select_chat_block_filename').text(`#${index + 1}${index === Number(message.swipe_id ?? 0) ? ` ${t`[Current]`}` : ''}`);
template.find('.chat_messages_date').text(sendDate);
template.find('.chat_file_size').text(swipeDetails.length ? `(${swipeDetails[0]}${swipeDetails.length > 1 ? ',' : ')'}` : '');
template.find('.chat_messages_num').text(swipeDetails.length > 1 ? `${swipeDetails.slice(1).join(', ')})` : '');
- template.find('.select_chat_block_mes').text(previewText || t`(empty swipe)`);
+ template.find('.select_chat_block_mes').text(previewText ? swipeText : t`(empty swipe)`);
block.on('click', () => setSelectedSwipe(index));
block.on('dblclick', async () => {
@@ -269,6 +301,7 @@ async function openSwipePicker(messageId) {
defaultState: String(selectedSwipeId + 1),
tooltip: `1-${message.swipes.length}`,
}],
+ large: true,
wider: true,
allowVerticalScrolling: true,
onOpen: function () {
diff --git a/public/style.css b/public/style.css
index 8b9791dae..22b503c6d 100644
--- a/public/style.css
+++ b/public/style.css
@@ -4788,6 +4788,48 @@ h5 {
user-select: none;
}
+.swipe_picker_block .select_chat_block_mes {
+ text-align: left;
+}
+
+.swipe_picker_block .select_chat_actions {
+ align-items: baseline;
+}
+
+.swipe_picker_expand_toggle {
+ display: none !important;
+}
+
+.swipe_picker_block:has(.swipe_picker_expand_toggle:checked) .select_chat_block_mes {
+ display: block;
+ -webkit-line-clamp: unset;
+ line-clamp: unset;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
+.swipe_picker_expand_label {
+ cursor: pointer;
+ opacity: 0.5;
+}
+
+.swipe_picker_expand_label:hover {
+ opacity: 1;
+}
+
+.swipe_picker_copy {
+ cursor: pointer;
+ opacity: 0.5;
+}
+
+.swipe_picker_copy:hover {
+ opacity: 1;
+}
+
+.swipe_picker_block:has(.swipe_picker_expand_toggle:checked) .swipe_picker_expand_label {
+ transform: rotate(180deg);
+}
+
.select_chat_block .avatar {
grid-row: span 2;
}
From a8021ee6e17b9b0510cfa62ad5fb4af95d045954 Mon Sep 17 00:00:00 2001
From: Claude <242468646+Claude@users.noreply.github.com>
Date: Tue, 31 Mar 2026 01:02:31 +0300
Subject: [PATCH 006/102] Fix /genraw user instruct format not applied and
unwanted system newline (#5372)
* Initial plan
* Fix /genraw user instruct format and system newline bugs
- Fix Bug #1: Change default role for string prompts from 'system' to 'user'
to ensure user instruct formatting is properly applied
- Fix Bug #2: Remove unconditional newline after system prompt when using
instruct mode, respecting the actual system instruct format
Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/7bfd62eb-2898-468d-9ea9-42d694a394b9
Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
* fix: add runtime string type check for substituteParams content
* Add fallback wrap for story string
---------
Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
---
public/script.js | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/public/script.js b/public/script.js
index dad10064f..835a9a2fc 100644
--- a/public/script.js
+++ b/public/script.js
@@ -2907,6 +2907,11 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
export function substituteParams(content, options = {}) {
if (!content) return '';
+ if (typeof content !== 'string') {
+ console.warn('substituteParams: content will be coerced to string', content);
+ content = String(content);
+ }
+
// Handle legacy signature calls to substituteParams
// We'll simply re-route them to a temporary legacy function. In the future, we'll remove this and cleanly build the options object ourselves.
const isOptionsObject = options && typeof options === 'object' && !Array.isArray(options);
@@ -3847,9 +3852,7 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
// If the prompt was given as a string, convert to a message-style object assuming user role
if (typeof prompt === 'string') {
- const message = api === 'openai'
- ? { role: 'user', content: prompt.trim() }
- : { role: 'system', content: prompt };
+ const message = { role: 'user', content: prompt.trim() };
prompt = [message];
} else { // checks for message-style object
if (prompt.length === 0 && !systemPrompt) throw Error('No messages provided');
@@ -3876,7 +3879,12 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
// prepend system prompt, if provided
if (systemPrompt) {
systemPrompt = substituteParams(systemPrompt);
- systemPrompt = isInstruct ? (formatInstructModeStoryString(systemPrompt) + '\n') : systemPrompt.trim();
+ systemPrompt = isInstruct ? formatInstructModeStoryString(systemPrompt) : systemPrompt.trim();
+ if (isInstruct && systemPrompt.length > 0 && !systemPrompt.endsWith('\n')) {
+ if (power_user.instruct.wrap && !power_user.instruct.story_string_suffix) {
+ systemPrompt += '\n';
+ }
+ }
prompt.unshift({ role: 'system', content: systemPrompt });
}
From d2b2b1b4a60eeea56b38f340e8f0d08faf4e240e Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 31 Mar 2026 20:08:56 +0300
Subject: [PATCH 007/102] fix: require long press to open swipe picker on
phones (#5382)
* fix: require long press to open swipe picker on phones
* fix: clarify parameter description in assignLorebookToChat function
* fix: update event parameter type in onSwipeCounterClick to include TouchEvent
* fix: update event parameter types in onSwipeCounterClick and addLongPressEvent
---
public/scripts/personas.js | 6 +++---
public/scripts/swipe-picker.js | 22 +++++++++++++++-------
public/scripts/utils.js | 3 ++-
public/scripts/world-info.js | 4 +---
4 files changed, 21 insertions(+), 14 deletions(-)
diff --git a/public/scripts/personas.js b/public/scripts/personas.js
index a7d1e2b96..57a3aa37c 100644
--- a/public/scripts/personas.js
+++ b/public/scripts/personas.js
@@ -1171,9 +1171,9 @@ function onPersonaDescriptionDepthRoleInput() {
/**
* Opens a popup to set the lorebook for the current persona.
- * @param {JQuery.ClickEvent} event Click event
+ * @param {Pick
} event Click event
*/
-async function onPersonaLoreButtonClick(event) {
+async function onPersonaLoreButtonClick({ shiftKey, altKey }) {
const personaName = power_user.personas[user_avatar];
const selectedLorebook = power_user.persona_description_lorebook;
@@ -1182,7 +1182,7 @@ async function onPersonaLoreButtonClick(event) {
return;
}
- if (selectedLorebook && !event.shiftKey && !event.altKey) {
+ if (selectedLorebook && !shiftKey && !altKey) {
openWorldInfoEditor(selectedLorebook);
return;
}
diff --git a/public/scripts/swipe-picker.js b/public/scripts/swipe-picker.js
index 783f5007f..843cc45d8 100644
--- a/public/scripts/swipe-picker.js
+++ b/public/scripts/swipe-picker.js
@@ -3,8 +3,9 @@ import { SWIPE_DIRECTION, SWIPE_SOURCE } from './constants.js';
import { t } from './i18n.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { power_user } from './power-user.js';
+import { isMobile } from './RossAscends-mods.js';
import { getTokenCountAsync } from './tokenizers.js';
-import { clamp, copyText, timestampToMoment } from './utils.js';
+import { addLongPressEvent, clamp, copyText, timestampToMoment } from './utils.js';
import { chat, deleteSwipe, ensureSwipes, isMessageSwipeable, isSwipingAllowed, swipe, syncMesToSwipe } from '/script.js';
/**
@@ -409,22 +410,29 @@ async function openSwipePicker(messageId) {
}
export function initSwipePicker() {
- $(document).on('click', '.swipes-counter.swipe-picker-enabled', async function (e) {
+ /**
+ * Click handler for opening the swipe picker when clicking on the swipe counter.
+ * @param {JQuery.Event | Event} e Event object
+ */
+ async function onSwipeCounterClick(e) {
e.preventDefault();
e.stopPropagation();
const mesId = Number($(this).closest('.mes').attr('mesid'));
await openSwipePicker(mesId);
- });
+ }
+
+ if (isMobile()) {
+ addLongPressEvent('.swipes-counter.swipe-picker-enabled', onSwipeCounterClick);
+ } else {
+ $(document).on('click', '.swipes-counter.swipe-picker-enabled', onSwipeCounterClick);
+ }
$(document).on('keydown', '.swipes-counter.swipe-picker-enabled', async function (e) {
if (e.key !== ' ') {
return;
}
- e.preventDefault();
- e.stopPropagation();
- const mesId = Number($(this).closest('.mes').attr('mesid'));
- await openSwipePicker(mesId);
+ onSwipeCounterClick.call(this, e);
});
$(document).on('click', '.mes_swipe_picker', async function (e) {
e.preventDefault();
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index 1d8464d1c..8d6f4c5ad 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -2955,7 +2955,7 @@ export function createTimeout(ms, errorMessage = '') {
* Registers a long-press (touch hold) event as an alternative to modifier+click.
* Supports event delegation for dynamically created elements.
* @param {string} selector CSS selector for target elements
- * @param {function} callback Callback to invoke on long-press, `this` is the matched element
+ * @param {(e: TouchEvent) => void} callback Callback to invoke on long-press, `this` is the matched element
* @param {number} [delay=500] Long-press duration in ms
*/
export function addLongPressEvent(selector, callback, delay = 500) {
@@ -2964,6 +2964,7 @@ export function addLongPressEvent(selector, callback, delay = 500) {
let target = null;
document.addEventListener('touchstart', function (event) {
+ if (!(event.target instanceof Element)) return;
const el = event.target.closest(selector);
if (!el) return;
target = el;
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js
index 2af83ca7b..d6a09907c 100644
--- a/public/scripts/world-info.js
+++ b/public/scripts/world-info.js
@@ -5808,9 +5808,7 @@ export function openWorldInfoEditor(worldName) {
/**
* Assigns a lorebook to the current chat.
- * @param {Object} options - The options for assigning the lorebook.
- * @param {boolean} options.shiftKey - Whether the Shift key is pressed.
- * @param {boolean} options.altKey - Whether the Alt key is pressed.
+ * @param {Pick} event Click event
* @returns {Promise}
*/
export async function assignLorebookToChat({ shiftKey, altKey }) {
From 04ef0632ee631d89705a6f4f5d278bf5accc92a5 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 1 Apr 2026 23:19:56 +0300
Subject: [PATCH 008/102] Save chat before emitting event for user message
(#5389)
Fixes #5388
---
public/script.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/script.js b/public/script.js
index 835a9a2fc..797ad3770 100644
--- a/public/script.js
+++ b/public/script.js
@@ -5831,11 +5831,11 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
} else {
chat.push(message);
+ await saveChatConditional();
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_SENT, chat_id);
addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
- await saveChatConditional();
}
return message;
From e2d8c0200f7a649b684832fea4e7b29b697bd352 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 1 Apr 2026 23:34:00 +0300
Subject: [PATCH 009/102] Use custom init script instead of postinstall (#5384)
* Use custom init script instead of postinstall
* Revert changes to start scripts in src\electron
* feat: add --ignore-scripts flag to npm install commands in batch and shell scripts
* feat: add --ignore-scripts flag to npm ci in Dockerfile
---
.npmrc | 2 ++
Dockerfile | 2 +-
Start.bat | 3 ++-
UpdateAndStart.bat | 3 ++-
UpdateForkAndStart.bat | 3 ++-
docker/docker-entrypoint.sh | 6 +++---
package-lock.json | 1 -
package.json | 2 +-
src/electron/Start.bat | 2 +-
post-install.js => src/server-init.js | 4 ++--
start.sh | 3 ++-
11 files changed, 18 insertions(+), 13 deletions(-)
create mode 100644 .npmrc
rename post-install.js => src/server-init.js (95%)
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..2143f3df2
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+ignore-scripts=true
+min-release-age=7
diff --git a/Dockerfile b/Dockerfile
index 999164f40..31dab85c2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -19,7 +19,7 @@ COPY --chown=node:node . ./
RUN \
echo "*** Install npm packages ***" && \
- npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force
+ npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts && npm cache clean --force
# Create config directory and link config.yaml. Added hardcoded dirs(constants.js?)
# that must be present for Non-Root Mode and volumeless docker runs.
diff --git a/Start.bat b/Start.bat
index 9be1a133e..f86f4333a 100644
--- a/Start.bat
+++ b/Start.bat
@@ -1,7 +1,8 @@
@echo off
pushd %~dp0
set NODE_ENV=production
-call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
+call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
+call npm run init
node server.js %*
pause
popd
diff --git a/UpdateAndStart.bat b/UpdateAndStart.bat
index 766d52e5a..b47d32b92 100644
--- a/UpdateAndStart.bat
+++ b/UpdateAndStart.bat
@@ -20,7 +20,8 @@ if %errorlevel% neq 0 (
)
)
set NODE_ENV=production
-call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
+call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
+call npm run init
node server.js %*
:end
pause
diff --git a/UpdateForkAndStart.bat b/UpdateForkAndStart.bat
index 68a9885a6..301e114cd 100644
--- a/UpdateForkAndStart.bat
+++ b/UpdateForkAndStart.bat
@@ -102,7 +102,8 @@ if %errorlevel% neq 0 (
echo Installing npm packages and starting server
set NODE_ENV=production
-call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
+call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
+call npm run init
node server.js %*
:end
diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh
index b4b744266..54e6d79b1 100644
--- a/docker/docker-entrypoint.sh
+++ b/docker/docker-entrypoint.sh
@@ -1,6 +1,6 @@
#!/bin/sh
-# Function to handle startup logic (Config check + Postinstall + Start)
+# Function to handle startup logic (Config check + init + Start)
start_sillytavern() {
local PREFIX="$1"
shift # Remove the first argument (PREFIX) so $@ contains the rest
@@ -11,8 +11,8 @@ start_sillytavern() {
$PREFIX cp "default/config.yaml" "config/config.yaml"
fi
- # Execute postinstall to auto-populate config.yaml with missing values
- $PREFIX npm run postinstall
+ # Execute init script to auto-populate config.yaml with missing values
+ $PREFIX npm run init
# Start the server
exec $PREFIX node server.js --listen "$@"
diff --git a/package-lock.json b/package-lock.json
index 1f7b82749..c93fcc4b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,6 @@
"": {
"name": "sillytavern",
"version": "1.17.0",
- "hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@adobe/css-tools": "^4.4.4",
diff --git a/package.json b/package.json
index 6e86a216d..786668480 100644
--- a/package.json
+++ b/package.json
@@ -115,6 +115,7 @@
},
"version": "1.17.0",
"scripts": {
+ "init": "node src/server-init.js",
"start": "node server.js",
"debug": "node --inspect server.js",
"start:global": "node server.js --global",
@@ -122,7 +123,6 @@
"start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
"start:bun": "bun server.js",
"start:no-csrf": "node server.js --disableCsrf",
- "postinstall": "node post-install.js",
"lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
"lint:fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix",
"plugins:update": "node plugins update",
diff --git a/src/electron/Start.bat b/src/electron/Start.bat
index d26dea4df..2854b42af 100644
--- a/src/electron/Start.bat
+++ b/src/electron/Start.bat
@@ -1,6 +1,6 @@
@echo off
pushd %~dp0
-call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
+call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
npm run start server.js %*
pause
popd
diff --git a/post-install.js b/src/server-init.js
similarity index 95%
rename from post-install.js
rename to src/server-init.js
index 4908e950e..cc7e0da0a 100644
--- a/post-install.js
+++ b/src/server-init.js
@@ -7,7 +7,7 @@ import process from 'node:process';
import yaml from 'yaml';
import chalk from 'chalk';
import { createRequire } from 'node:module';
-import { addMissingConfigValues } from './src/config-init.js';
+import { addMissingConfigValues } from './config-init.js';
/**
* Colorizes console output.
@@ -88,7 +88,7 @@ function createDefaultFiles() {
);
} else {
throw new Error(
- 'FATAL: Unexpected default file format in `post-install.js#createDefaultFiles()`.',
+ 'FATAL: Unexpected default file format in `server-init.js#createDefaultFiles()`.',
);
}
} catch (error) {
diff --git a/start.sh b/start.sh
index ab2f9035a..8dbd26584 100755
--- a/start.sh
+++ b/start.sh
@@ -10,7 +10,8 @@ fi
echo "Installing Node Modules..."
export NODE_ENV=production
-npm i --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
+npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
+npm run init
echo "Entering SillyTavern..."
node "server.js" "$@"
From 21e8cf9060c5d9c97454f4c308ddde474613bc9a Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Thu, 2 Apr 2026 20:19:01 +0300
Subject: [PATCH 010/102] glm-5v-turbo (#5393)
* glm-5v-turbo
* Add support for image and video inlining
---
public/index.html | 1 +
public/scripts/extensions/caption/settings.html | 1 +
public/scripts/openai.js | 3 +++
3 files changed, 5 insertions(+)
diff --git a/public/index.html b/public/index.html
index 8a5a3780d..6a6371101 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3924,6 +3924,7 @@
Z.AI Model
-
+
Temperature
@@ -710,7 +710,7 @@
-
+
Frequency Penalty
@@ -723,7 +723,7 @@
-
+
Presence Penalty
@@ -736,7 +736,7 @@
-
+
Top K
@@ -749,7 +749,7 @@
-
+
Top P
@@ -762,7 +762,7 @@
-
+
Repetition Penalty
@@ -986,7 +986,7 @@
-
+
Seed
@@ -1996,7 +1996,7 @@
-
-
+
-
+
-
+