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

Enable function calling @@ -2035,7 +2035,7 @@
-
+
-
+
-
+
+
+

Cloudflare Workers AI API Key

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you click 'Connect'. +
+

Cloudflare Account ID

+
+ +
+
+

Workers AI Model

+ +
+

DeepSeek API Key

diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 9fde3f205..d056c13fe 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -407,6 +407,7 @@ function RA_autoconnect(PrevApi) { || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI) || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) + || (secret_state[SECRET_KEYS.WORKERS_AI] && oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI) || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) ) { diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 89bc4f8d6..5571bf7f9 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -196,6 +196,7 @@ export const chat_completion_sources = { AZURE_OPENAI: 'azure_openai', ZAI: 'zai', SILICONFLOW: 'siliconflow', + WORKERS_AI: 'workers_ai', }; const character_names_behavior = { @@ -279,6 +280,7 @@ const sensitiveFields = [ 'vertexai_express_project_id', 'azure_base_url', 'azure_deployment_name', + 'workers_ai_account_id', ]; /** @@ -337,6 +339,8 @@ export const settingsToUpdate = { vertexai_model: ['#model_vertexai_select', 'vertexai_model', false, true], zai_model: ['#model_zai_select', 'zai_model', false, true], zai_endpoint: ['#zai_endpoint', 'zai_endpoint', false, true], + workers_ai_model: ['#model_workers_ai_select', 'workers_ai_model', false, true], + workers_ai_account_id: ['#workers_ai_account_id', 'workers_ai_account_id', false, true], openai_max_context: ['#openai_max_context', 'openai_max_context', false, false], openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false, false], names_behavior: ['#names_behavior', 'names_behavior', false, false], @@ -439,6 +443,8 @@ const default_settings = { fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct', zai_model: 'glm-4.6', zai_endpoint: ZAI_ENDPOINT.COMMON, + workers_ai_model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast', + workers_ai_account_id: '', azure_base_url: '', azure_deployment_name: '', azure_api_version: '2024-02-15-preview', @@ -1727,6 +1733,8 @@ export function getChatCompletionModel(settings = null) { return settings.azure_openai_model; case chat_completion_sources.ZAI: return settings.zai_model; + case chat_completion_sources.WORKERS_AI: + return settings.workers_ai_model; default: console.error(`Unknown chat completion source: ${source}`); return ''; @@ -2193,6 +2201,24 @@ function saveModelList(data) { $('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change'); } + if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) { + $('#model_workers_ai_select').empty(); + model_list.forEach((model) => { + $('#model_workers_ai_select').append( + $('
+