Merge branch 'staging' into tc-global-banlist

3d492523e2172a9661b4a54356d88f7dcaf21719

Cohee <18619528+Cohee1207@users.noreply.github.com>

17 files changed, +305 -60Ignore whitespace
default/config.yaml+8 -0
@@ -6,6 +6,10 @@ cardsCacheCapacity: 100
6# -- SERVER CONFIGURATION --6# -- SERVER CONFIGURATION --
7# Listen for incoming connections7# Listen for incoming connections
8listen: false8listen: false
9# Listen on a specific address, supports IPv4 and IPv6
10listenAddress:
11 ipv4: 0.0.0.0
12 ipv6: '[::]'
9# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!13# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
10# - Use option "auto" to automatically detect support14# - Use option "auto" to automatically detect support
11# - Use true or false (no qoutes) to enable or disable each protocol15# - Use true or false (no qoutes) to enable or disable each protocol
@@ -183,6 +187,10 @@ ollama:
183 # * 0: Unload the model immediately after the request187 # * 0: Unload the model immediately after the request
184 # * N (any positive number): Keep the model loaded for N seconds after the request.188 # * N (any positive number): Keep the model loaded for N seconds after the request.
185 keepAlive: -1189 keepAlive: -1
190 # Controls the "num_batch" (batch size) parameter of the generation request
191 # * -1: Use the default value of the model
192 # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
193 batchSize: -1
186# -- ANTHROPIC CLAUDE API CONFIGURATION --194# -- ANTHROPIC CLAUDE API CONFIGURATION --
187claude:195claude:
188 # Enables caching of the system prompt (if supported).196 # Enables caching of the system prompt (if supported).
default/content/index.json+0 -4
@@ -672,10 +672,6 @@
672 "type": "moving_ui"672 "type": "moving_ui"
673 },673 },
674 {674 {
675 "filename": "presets/moving-ui/Black Magic Time.json",
676 "type": "moving_ui"
677 },
678 {
679 "filename": "presets/quick-replies/Default.json",675 "filename": "presets/quick-replies/Default.json",
680 "type": "quick_replies"676 "type": "quick_replies"
681 },677 },
default/content/presets/moving-ui/Black Magic Time.json+0 -45
@@ -1,45 +0,0 @@
1{
2 "name": "Black Magic Time",
3 "movingUIState": {
4 "sheld": {
5 "top": 488,
6 "left": 1407,
7 "right": 1,
8 "bottom": 4,
9 "margin": "unset",
10 "width": 471,
11 "height": 439
12 },
13 "floatingPrompt": {
14 "width": 369,
15 "height": 441
16 },
17 "right-nav-panel": {
18 "top": 0,
19 "left": 1400,
20 "right": 111,
21 "bottom": 446,
22 "margin": "unset",
23 "width": 479,
24 "height": 487
25 },
26 "WorldInfo": {
27 "top": 41,
28 "left": 369,
29 "right": 642,
30 "bottom": 51,
31 "margin": "unset",
32 "width": 1034,
33 "height": 858
34 },
35 "left-nav-panel": {
36 "top": 442,
37 "left": 0,
38 "right": 1546,
39 "bottom": 25,
40 "margin": "unset",
41 "width": 368,
42 "height": 483
43 }
44 }
45}
45 \ No newline at end of file \ No newline at end of file
package-lock.json+13 -0
@@ -41,6 +41,7 @@
41 "html-entities": "^2.5.2",41 "html-entities": "^2.5.2",
42 "iconv-lite": "^0.6.3",42 "iconv-lite": "^0.6.3",
43 "ip-matching": "^2.1.2",43 "ip-matching": "^2.1.2",
44 "ip-regex": "^5.0.0",
44 "ipaddr.js": "^2.0.1",45 "ipaddr.js": "^2.0.1",
45 "jimp": "^0.22.10",46 "jimp": "^0.22.10",
46 "localforage": "^1.10.0",47 "localforage": "^1.10.0",
@@ -4628,6 +4629,18 @@
4628 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",4629 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
4629 "license": "LGPL-3.0-only"4630 "license": "LGPL-3.0-only"
4630 },4631 },
4632 "node_modules/ip-regex": {
4633 "version": "5.0.0",
4634 "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
4635 "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
4636 "license": "MIT",
4637 "engines": {
4638 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4639 },
4640 "funding": {
4641 "url": "https://github.com/sponsors/sindresorhus"
4642 }
4643 },
4631 "node_modules/ipaddr.js": {4644 "node_modules/ipaddr.js": {
4632 "version": "2.1.0",4645 "version": "2.1.0",
4633 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",4646 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
package.json+2 -0
@@ -31,6 +31,7 @@
31 "html-entities": "^2.5.2",31 "html-entities": "^2.5.2",
32 "iconv-lite": "^0.6.3",32 "iconv-lite": "^0.6.3",
33 "ip-matching": "^2.1.2",33 "ip-matching": "^2.1.2",
34 "ip-regex": "^5.0.0",
34 "ipaddr.js": "^2.0.1",35 "ipaddr.js": "^2.0.1",
35 "jimp": "^0.22.10",36 "jimp": "^0.22.10",
36 "localforage": "^1.10.0",37 "localforage": "^1.10.0",
@@ -89,6 +90,7 @@
89 "version": "1.12.11",90 "version": "1.12.11",
90 "scripts": {91 "scripts": {
91 "start": "node server.js",92 "start": "node server.js",
93 "debug": "node server.js --inspect",
92 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",94 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
93 "start:bun": "bun server.js",95 "start:bun": "bun server.js",
94 "start:no-csrf": "node server.js --disableCsrf",96 "start:no-csrf": "node server.js --disableCsrf",
public/scripts/extensions/stable-diffusion/index.js+67 -0
@@ -81,6 +81,7 @@ const sources = {
81 huggingface: 'huggingface',81 huggingface: 'huggingface',
82 nanogpt: 'nanogpt',82 nanogpt: 'nanogpt',
83 bfl: 'bfl',83 bfl: 'bfl',
84 falai: 'falai',
84};85};
8586
86const initiators = {87const initiators = {
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
1169 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);1170 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1170}1171}
11711172
1173async function onFalaiKeyClick() {
1174 return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175}
1176
1172function onBflUpsamplingInput() {1177function onBflUpsamplingInput() {
1173 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');1178 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
1174 saveSettingsDebounced();1179 saveSettingsDebounced();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
1299 sources.huggingface,1304 sources.huggingface,
1300 sources.nanogpt,1305 sources.nanogpt,
1301 sources.bfl,1306 sources.bfl,
1307 sources.falai,
1302 ];1308 ];
13031309
1304 if (cloudSources.includes(extension_settings.sd.source)) {1310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
1707 case sources.bfl:1713 case sources.bfl:
1708 models = await loadBflModels();1714 models = await loadBflModels();
1709 break;1715 break;
1716 case sources.falai:
1717 models = await loadFalaiModels();
1718 break;
1710 }1719 }
17111720
1712 for (const model of models) {1721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
1744 ];1753 ];
1745}1754}
17461755
1756async function loadFalaiModels() {
1757 $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
1758
1759 const result = await fetch('/api/sd/falai/models', {
1760 method: 'POST',
1761 headers: getRequestHeaders(),
1762 });
1763
1764 if (result.ok) {
1765 return await result.json();
1766 }
1767
1768 return [];
1769}
1770
1747async function loadPollinationsModels() {1771async function loadPollinationsModels() {
1748 const result = await fetch('/api/sd/pollinations/models', {1772 const result = await fetch('/api/sd/pollinations/models', {
1749 method: 'POST',1773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
2081 case sources.bfl:2105 case sources.bfl:
2082 schedulers = ['N/A'];2106 schedulers = ['N/A'];
2083 break;2107 break;
2108 case sources.falai:
2109 schedulers = ['N/A'];
2110 break;
2084 }2111 }
20852112
2086 for (const scheduler of schedulers) {2113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2735 case sources.bfl:2762 case sources.bfl:
2736 result = await generateBflImage(prefixedPrompt, signal);2763 result = await generateBflImage(prefixedPrompt, signal);
2737 break;2764 break;
2765 case sources.falai:
2766 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767 break;
2738 }2768 }
27392769
2740 if (!result.data) {2770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
3496 }3526 }
3497}3527}
34983528
3529/**
3530 * Generates an image using the FAL.AI API.
3531 * @param {string} prompt - The main instruction used to guide the image generation.
3532 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
3533 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3534 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3535 */
3536async function generateFalaiImage(prompt, negativePrompt, signal) {
3537 const result = await fetch('/api/sd/falai/generate', {
3538 method: 'POST',
3539 headers: getRequestHeaders(),
3540 signal: signal,
3541 body: JSON.stringify({
3542 prompt: prompt,
3543 negative_prompt: negativePrompt,
3544 model: extension_settings.sd.model,
3545 steps: clamp(extension_settings.sd.steps, 1, 50),
3546 guidance: clamp(extension_settings.sd.scale, 1.5, 5),
3547 width: clamp(extension_settings.sd.width, 256, 1440),
3548 height: clamp(extension_settings.sd.height, 256, 1440),
3549 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3550 }),
3551 });
3552
3553 if (result.ok) {
3554 const data = await result.json();
3555 return { format: 'jpg', data: data.image };
3556 } else {
3557 const text = await result.text();
3558 console.log(text);
3559 throw new Error(text);
3560 }
3561}
3562
3499async function onComfyOpenWorkflowEditorClick() {3563async function onComfyOpenWorkflowEditorClick() {
3500 let workflow = await (await fetch('/api/sd/comfy/workflow', {3564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3501 method: 'POST',3565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
3782 return secret_state[SECRET_KEYS.NANOGPT];3846 return secret_state[SECRET_KEYS.NANOGPT];
3783 case sources.bfl:3847 case sources.bfl:
3784 return secret_state[SECRET_KEYS.BFL];3848 return secret_state[SECRET_KEYS.BFL];
3849 case sources.falai:
3850 return secret_state[SECRET_KEYS.FALAI];
3785 }3851 }
3786}3852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
4443 $('#sd_function_tool').on('input', onFunctionToolInput);4509 $('#sd_function_tool').on('input', onFunctionToolInput);
4444 $('#sd_bfl_key').on('click', onBflKeyClick);4510 $('#sd_bfl_key').on('click', onBflKeyClick);
4445 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512 $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
4447 if (!CSS.supports('field-sizing', 'content')) {4514 if (!CSS.supports('field-sizing', 'content')) {
4448 $('.sd_settings .inline-drawer-toggle').on('click', function () {4515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -42,6 +42,7 @@
42 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
43 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="extras">Extras API (deprecated)</option>44 <option value="extras">Extras API (deprecated)</option>
45 <option value="falai">FAL.AI</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>46 <option value="huggingface">HuggingFace Inference API (serverless)</option>
46 <option value="nanogpt">NanoGPT</option>47 <option value="nanogpt">NanoGPT</option>
47 <option value="novel">NovelAI Diffusion</option>48 <option value="novel">NovelAI Diffusion</option>
@@ -256,6 +257,20 @@
256 </label>257 </label>
257 </div>258 </div>
258259
260 <div data-sd-source="falai">
261 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
262 <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
263 <strong data-i18n="API Key">API Key</strong>
264 <i class="fa-solid fa-share-from-square"></i>
265 </a>
266 <span class="expander"></span>
267 <div id="sd_falai_key" class="menu_button menu_button_icon">
268 <i class="fa-fw fa-solid fa-key"></i>
269 <span data-i18n="Click to set">Click to set</span>
270 </div>
271 </div>
272 </div>
273
259 <div class="flex-container">274 <div class="flex-container">
260 <div class="flex1">275 <div class="flex1">
261 <label for="sd_model" data-i18n="Model">Model</label>276 <label for="sd_model" data-i18n="Model">Model</label>
public/scripts/reasoning.js+4 -0
@@ -215,6 +215,10 @@ export class ReasoningHandler {
215 }215 }
216216
217 this.updateDom(messageId);217 this.updateDom(messageId);
218
219 if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
220 this.messageReasoningDetailsDom.open = true;
221 }
218 }222 }
219223
220 /**224 /**
public/scripts/secrets.js+1 -0
@@ -41,6 +41,7 @@ export const SECRET_KEYS = {
41 GENERIC: 'api_key_generic',41 GENERIC: 'api_key_generic',
42 DEEPSEEK: 'api_key_deepseek',42 DEEPSEEK: 'api_key_deepseek',
43 SERPER: 'api_key_serper',43 SERPER: 'api_key_serper',
44 FALAI: 'api_key_falai',
44};45};
4546
46const INPUT_MAP = {47const INPUT_MAP = {
public/style.css+7 -2
@@ -362,9 +362,14 @@ input[type='checkbox']:focus-visible {
362}362}
363363
364.mes_reasoning_details .mes_reasoning_summary {364.mes_reasoning_details .mes_reasoning_summary {
365 list-style: none;
365 margin-right: calc(var(--mes-right-spacing) * -1);366 margin-right: calc(var(--mes-right-spacing) * -1);
366}367}
367368
369.mes_reasoning_details summary::-webkit-details-marker {
370 display: none;
371}
372
368.mes_reasoning *:last-child {373.mes_reasoning *:last-child {
369 margin-bottom: 0;374 margin-bottom: 0;
370}375}
@@ -378,7 +383,7 @@ input[type='checkbox']:focus-visible {
378}383}
379384
380.mes_reasoning_details .mes_reasoning em {385.mes_reasoning_details .mes_reasoning em {
381 color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%)386 color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
382}387}
383388
384.mes_reasoning_header_block {389.mes_reasoning_header_block {
@@ -403,7 +408,7 @@ input[type='checkbox']:focus-visible {
403}408}
404409
405/* TWIMC: Remove with custom CSS to show the icon */410/* TWIMC: Remove with custom CSS to show the icon */
406.mes_reasoning_header > .icon-svg {411.mes_reasoning_header>.icon-svg {
407 display: none;412 display: none;
408}413}
409414
server.js+25 -9
@@ -30,6 +30,7 @@ import bodyParser from 'body-parser';
3030
31// net related library imports31// net related library imports
32import fetch from 'node-fetch';32import fetch from 'node-fetch';
33import ipRegex from 'ip-regex';
3334
34// Unrestrict console logs display limit35// Unrestrict console logs display limit
35util.inspect.defaultOptions.maxArrayLength = null;36util.inspect.defaultOptions.maxArrayLength = null;
@@ -59,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js';
59import whitelistMiddleware from './src/middleware/whitelist.js';60import whitelistMiddleware from './src/middleware/whitelist.js';
60import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';61import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
61import initRequestProxy from './src/request-proxy.js';62import initRequestProxy from './src/request-proxy.js';
63import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
62import {64import {
63 getVersion,65 getVersion,
64 getConfigValue,66 getConfigValue,
@@ -130,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
130const DEFAULT_PORT = 8000;132const DEFAULT_PORT = 8000;
131const DEFAULT_AUTORUN = false;133const DEFAULT_AUTORUN = false;
132const DEFAULT_LISTEN = false;134const DEFAULT_LISTEN = false;
135const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
136const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
133const DEFAULT_CORS_PROXY = false;137const DEFAULT_CORS_PROXY = false;
134const DEFAULT_WHITELIST = true;138const DEFAULT_WHITELIST = true;
135const DEFAULT_ACCOUNTS = false;139const DEFAULT_ACCOUNTS = false;
@@ -185,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
185 type: 'boolean',189 type: 'boolean',
186 default: null,190 default: null,
187 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,191 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
192 }).option('listenAddressIPv6', {
193 type: 'string',
194 default: null,
195 describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
196 }).option('listenAddressIPv4', {
197 type: 'string',
198 default: null,
199 describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]',
188 }).option('corsProxy', {200 }).option('corsProxy', {
189 type: 'boolean',201 type: 'boolean',
190 default: null,202 default: null,
@@ -254,6 +266,10 @@ const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getCon
254const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
255/** @type {boolean} */267/** @type {boolean} */
256const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
269/** @type {string} */
270const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271/** @type {string} */
272const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
257/** @type {boolean} */273/** @type {boolean} */
258const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
259const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
@@ -500,7 +516,7 @@ if (!disableCsrf) {
500516
501// Static files517// Static files
502// Host index page518// Host index page
503app.get('/', (request, response) => {519app.get('/', getCacheBusterMiddleware(), (request, response) => {
504 if (shouldRedirectToLogin(request)) {520 if (shouldRedirectToLogin(request)) {
505 const query = request.url.split('?')[1];521 const query = request.url.split('?')[1];
506 const redirectUrl = query ? `/login?${query}` : '/login';522 const redirectUrl = query ? `/login?${query}` : '/login';
@@ -708,13 +724,13 @@ app.use('/api/azure', azureRouter);
708724
709const tavernUrlV6 = new URL(725const tavernUrlV6 = new URL(
710 (cliArguments.ssl ? 'https://' : 'http://') +726 (cliArguments.ssl ? 'https://' : 'http://') +
711 (listen ? '[::]' : '[::1]') +727 (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
712 (':' + server_port),728 (':' + server_port),
713);729);
714730
715const tavernUrl = new URL(731const tavernUrl = new URL(
716 (cliArguments.ssl ? 'https://' : 'http://') +732 (cliArguments.ssl ? 'https://' : 'http://') +
717 (listen ? '0.0.0.0' : '127.0.0.1') +733 (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
718 (':' + server_port),734 (':' + server_port),
719);735);
720736
@@ -837,15 +853,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
837 const plainGoToLog = removeColorFormatting(goToLog);853 const plainGoToLog = removeColorFormatting(goToLog);
838854
839 console.log(logListen);855 console.log(logListen);
856 if (listen) {
857 console.log();
858 console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
859 console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.');
860 }
840 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');861 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
841 console.log(goToLog);862 console.log(goToLog);
842 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');863 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
843864
844 if (listen) {
845 console.log(
846 '[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n',
847 );
848 }
849865
850 if (basicAuthMode) {866 if (basicAuthMode) {
851 if (perUserBasicAuth && !enableAccounts) {867 if (perUserBasicAuth && !enableAccounts) {
@@ -1083,7 +1099,7 @@ async function verifySecuritySettings() {
1083 }1099 }
10841100
1085 if (!enableAccounts) {1101 if (!enableAccounts) {
1086 logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');1102 logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
1087 }1103 }
10881104
1089 const users = await getAllEnabledUsers();1105 const users = await getAllEnabledUsers();
src/constants.js+1 -0
@@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [
304export const OLLAMA_KEYS = [304export const OLLAMA_KEYS = [
305 'num_predict',305 'num_predict',
306 'num_ctx',306 'num_ctx',
307 'num_batch',
307 'stop',308 'stop',
308 'temperature',309 'temperature',
309 'repeat_penalty',310 'repeat_penalty',
src/endpoints/backends/text-completions.js+4 -0
@@ -373,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) {
373373
374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
375 const keepAlive = getConfigValue('ollama.keepAlive', -1);375 const keepAlive = getConfigValue('ollama.keepAlive', -1);
376 const numBatch = getConfigValue('ollama.batchSize', -1);
377 if (numBatch > 0) {
378 request.body['num_batch'] = numBatch;
379 }
376 args.body = JSON.stringify({380 args.body = JSON.stringify({
377 model: request.body.model,381 model: request.body.model,
378 prompt: request.body.prompt,382 prompt: request.body.prompt,
src/endpoints/characters.js+3 -0
@@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
841 fs.unlinkSync(newAvatarPath);841 fs.unlinkSync(newAvatarPath);
842
843 // Bust cache to reload the new avatar
844 response.setHeader('Clear-Site-Data', '"cache"');
842 }845 }
843846
844 return response.sendStatus(200);847 return response.sendStatus(200);
src/endpoints/secrets.js+1 -0
@@ -50,6 +50,7 @@ export const SECRET_KEYS = {
50 TAVILY: 'api_key_tavily',50 TAVILY: 'api_key_tavily',
51 NANOGPT: 'api_key_nanogpt',51 NANOGPT: 'api_key_nanogpt',
52 BFL: 'api_key_bfl',52 BFL: 'api_key_bfl',
53 FALAI: 'api_key_falai',
53 GENERIC: 'api_key_generic',54 GENERIC: 'api_key_generic',
54 DEEPSEEK: 'api_key_deepseek',55 DEEPSEEK: 'api_key_deepseek',
55 SERPER: 'api_key_serper',56 SERPER: 'api_key_serper',
src/endpoints/stable-diffusion.js+126 -0
@@ -1228,6 +1228,131 @@ bfl.post('/generate', jsonParser, async (request, response) => {
1228 }1228 }
1229});1229});
12301230
1231const falai = express.Router();
1232
1233falai.post('/models', jsonParser, async (_request, response) => {
1234 try {
1235 const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image');
1236 const result = await fetch(modelsUrl);
1237
1238 if (!result.ok) {
1239 console.warn('FAL.AI returned an error.', result.status, result.statusText);
1240 throw new Error('FAL.AI request failed.');
1241 }
1242
1243 const data = await result.json();
1244
1245 if (!Array.isArray(data)) {
1246 console.warn('FAL.AI returned invalid data.');
1247 throw new Error('FAL.AI request failed.');
1248 }
1249
1250 const models = data
1251 .filter(x => !x.title.toLowerCase().includes('inpainting') &&
1252 !x.title.toLowerCase().includes('control') &&
1253 !x.title.toLowerCase().includes('upscale'))
1254 .sort((a, b) => a.title.localeCompare(b.title))
1255 .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }));
1256 return response.send(models);
1257 } catch (error) {
1258 console.error(error);
1259 return response.sendStatus(500);
1260 }
1261});
1262
1263falai.post('/generate', jsonParser, async (request, response) => {
1264 try {
1265 const key = readSecret(request.user.directories, SECRET_KEYS.FALAI);
1266
1267 if (!key) {
1268 console.warn('FAL.AI key not found.');
1269 return response.sendStatus(400);
1270 }
1271
1272 const requestBody = {
1273 prompt: request.body.prompt,
1274 image_size: { 'width': request.body.width, 'height': request.body.height },
1275 num_inference_steps: request.body.steps,
1276 seed: request.body.seed ?? null,
1277 guidance_scale: request.body.guidance,
1278 enable_safety_checker: false,
1279 };
1280
1281 console.debug('FAL.AI request:', requestBody);
1282
1283 const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, {
1284 method: 'POST',
1285 body: JSON.stringify(requestBody),
1286 headers: {
1287 'Content-Type': 'application/json',
1288 'Authorization': `Key ${key}`,
1289 },
1290 });
1291
1292 if (!result.ok) {
1293 console.warn('FAL.AI returned an error.');
1294 return response.sendStatus(500);
1295 }
1296
1297 /** @type {any} */
1298 const taskData = await result.json();
1299 const { status_url } = taskData;
1300
1301 const MAX_ATTEMPTS = 100;
1302 for (let i = 0; i < MAX_ATTEMPTS; i++) {
1303 await delay(2500);
1304
1305 const statusResult = await fetch(status_url, {
1306 headers: {
1307 'Authorization': `Key ${key}`,
1308 },
1309 });
1310
1311 if (!statusResult.ok) {
1312 const text = await statusResult.text();
1313 console.warn('FAL.AI returned an error.', text);
1314 return response.sendStatus(500);
1315 }
1316
1317 /** @type {any} */
1318 const statusData = await statusResult.json();
1319
1320 if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') {
1321 continue;
1322 }
1323
1324 if (statusData?.status === 'COMPLETED') {
1325 const resultFetch = await fetch(statusData?.response_url, {
1326 method: 'GET',
1327 headers: {
1328 'Authorization': `Key ${key}`,
1329 },
1330 });
1331 const resultData = await resultFetch.json();
1332
1333 if (resultData.detail !== null && resultData.detail !== undefined) {
1334 throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` });
1335 }
1336
1337 const imageFetch = await fetch(resultData?.images[0].url, {
1338 headers: {
1339 'Authorization': `Key ${key}`,
1340 },
1341 });
1342
1343 const fetchData = await imageFetch.arrayBuffer();
1344 const image = Buffer.from(fetchData).toString('base64');
1345 return response.send({ image: image });
1346 }
1347
1348 throw new Error('FAL.AI failed to generate image.', { cause: statusData });
1349 }
1350 } catch (error) {
1351 console.error(error);
1352 return response.status(500).send(error.cause || error.message);
1353 }
1354});
1355
1231router.use('/comfy', comfy);1356router.use('/comfy', comfy);
1232router.use('/together', together);1357router.use('/together', together);
1233router.use('/drawthings', drawthings);1358router.use('/drawthings', drawthings);
@@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy);
1237router.use('/huggingface', huggingface);1362router.use('/huggingface', huggingface);
1238router.use('/nanogpt', nanogpt);1363router.use('/nanogpt', nanogpt);
1239router.use('/bfl', bfl);1364router.use('/bfl', bfl);
1365router.use('/falai', falai);
src/middleware/cacheBuster.js+28 -0
@@ -0,0 +1,28 @@
1import crypto from 'node:crypto';
2import { DEFAULT_USER } from '../constants.js';
3
4/**
5 * Middleware to bust the browser cache for the current user.
6 * @returns {import('express').RequestHandler}
7 */
8export default function getCacheBusterMiddleware() {
9 /**
10 * @type {Set<string>} Handles/User-Agents that have already been busted.
11 */
12 const keys = new Set();
13
14 return (request, response, next) => {
15 const handle = request.user?.profile?.handle || DEFAULT_USER.handle;
16 const userAgent = request.headers['user-agent'] || '';
17 const hash = crypto.createHash('sha256').update(userAgent).digest('hex');
18 const key = `${handle}-${hash}`;
19
20 if (keys.has(key)) {
21 return next();
22 }
23
24 keys.add(key);
25 response.setHeader('Clear-Site-Data', '"cache"');
26 next();
27 };
28}