Merge branch 'staging' into patch-1

7f321c9cf64e3ae18fa6cde165bbbde9a68c78d0

Ashley Saleem-west <hello@ashleysw.com>

Signed
69 files changed, +1450 -252Ignore whitespace
.github/readme.md+1 -1
@@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2742741. Open your Command Line
2752752. Run the following command
276276
277277`docker createrun --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
278278
279279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
280280
.gitignore+3 -0
@@ -45,6 +45,7 @@ access.log
4545/vectors/
4646/cache/
4747public/css/user.css
48+public/error/
4849/plugins/
4950/data
5051/default/scaffold
@@ -52,3 +53,5 @@ public/scripts/extensions/third-party
5253/certs
5354.aider*
5455.env
56+/StartDev.bat
57+
default/config.yaml+18 -16
@@ -70,7 +70,7 @@ perUserBasicAuth: false
7070## Set to a positive number to expire session after a certain time of inactivity
7171## Set to 0 to expire session when the browser is closed
7272## Set to a negative number to disable session expiration
7373sessionTimeout: 86400-1
7474# Used to sign session cookies. Will be auto-generated if not set
7575cookieSecret: ''
7676# Disable CSRF protection - NOT RECOMMENDED
@@ -133,24 +133,26 @@ whitelistImportDomains:
133133## headers:
134134## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
135135requestOverrides: []
136-# -- EXTENSIONS CONFIGURATION --
136+
137137# Enable UIEXTENSIONS extensionsCONFIGURATION
138-enableExtensions: true
138+extensions:
139-# Automatically update extensions when a release version changes
139+ # Enable UI extensions
140140enableExtensionsAutoUpdate enabled: true
141+ # Automatically update extensions when a release version changes
142+ autoUpdate: true
143+ models:
144+ # Enables automatic model download from HuggingFace
145+ autoDownload: true
146+ # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
147+ classification: Cohee/distilbert-base-uncased-go-emotions-onnx
148+ captioning: Xenova/vit-gpt2-image-captioning
149+ embedding: Cohee/jina-embeddings-v2-base-en
150+ speechToText: Xenova/whisper-small
151+ textToSpeech: Xenova/speecht5_tts
152+
141153# Additional model tokenizers can be downloaded on demand.
142154# Disabling will fallback to another locally available tokenizer.
143155enableDownloadableTokenizers: true
144-# Extension settings
145-extras:
146- # Disables automatic model download from HuggingFace
147- disableAutoDownload: false
148- # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
149- classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
150- captioningModel: Xenova/vit-gpt2-image-captioning
151- embeddingModel: Cohee/jina-embeddings-v2-base-en
152- speechToTextModel: Xenova/whisper-small
153- textToSpeechModel: Xenova/speecht5_tts
154156# -- OPENAI CONFIGURATION --
155157# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
156158promptPlaceholder: "[Start a new chat]"
default/content/index.json+8 -0
@@ -782,5 +782,13 @@
782782 {
783783 "filename": "presets/context/Mistral V7.json",
784784 "type": "context"
785+ },
786+ {
787+ "filename": "presets/instruct/DeepSeek-V2.5.json",
788+ "type": "instruct"
789+ },
790+ {
791+ "filename": "presets/context/DeepSeek-V2.5.json",
792+ "type": "context"
785793 }
786794]
default/content/presets/context/DeepSeek-V2.5.json+11 -0
@@ -0,0 +1,11 @@
1+{
2+ "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}\n",
3+ "example_separator": "",
4+ "chat_start": "",
5+ "use_stop_strings": false,
6+ "allow_jailbreak": false,
7+ "always_force_name2": true,
8+ "trim_sentences": false,
9+ "single_line": false,
10+ "name": "DeepSeek-V2.5"
11+}
default/content/presets/instruct/DeepSeek-V2.5.json+22 -0
@@ -0,0 +1,22 @@
1+{
2+ "input_sequence": "<|User|>",
3+ "output_sequence": "<|Assistant|>",
4+ "first_output_sequence": "",
5+ "last_output_sequence": "",
6+ "system_sequence_prefix": "",
7+ "system_sequence_suffix": "",
8+ "stop_sequence": "",
9+ "wrap": false,
10+ "macro": true,
11+ "names_behavior": "force",
12+ "activation_regex": "",
13+ "skip_examples": false,
14+ "output_suffix": "<|end▁of▁sentence|>",
15+ "input_suffix": "",
16+ "system_sequence": "",
17+ "system_suffix": "",
18+ "user_alignment_message": "",
19+ "last_system_sequence": "",
20+ "system_same_as_user": true,
21+ "name": "DeepSeek-V2.5"
22+}
default/user.css → default/public/css/user.css+0 -0
default/public/error/forbidden-by-whitelist.html+22 -0
@@ -0,0 +1,22 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <title>Forbidden</title>
6+</head>
7+
8+<body>
9+ <h1>Forbidden</h1>
10+ <p>
11+ If you are the system administrator, add your IP address to the
12+ whitelist or disable whitelist mode by editing
13+ <code>config.yaml</code> in the root directory of your installation.
14+ </p>
15+ <hr />
16+ <p>
17+ <em>Connection from {{ipDetails}} has been blocked. This attempt
18+ has been logged.</em>
19+ </p>
20+</body>
21+
22+</html>
default/public/error/unauthorized.html+17 -0
@@ -0,0 +1,17 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <title>Unauthorized</title>
6+</head>
7+
8+<body>
9+ <h1>Unauthorized</h1>
10+ <p>
11+ If you are the system administrator, you can configure the
12+ <code>basicAuthUser</code> credentials by editing
13+ <code>config.yaml</code> in the root directory of your installation.
14+ </p>
15+</body>
16+
17+</html>
default/public/error/url-not-found.html+15 -0
@@ -0,0 +1,15 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <title>Not found</title>
6+</head>
7+
8+<body>
9+ <h1>Not found</h1>
10+ <p>
11+ The requested URL was not found on this server.
12+ </p>
13+</body>
14+
15+</html>
index.d.ts+18 -8
@@ -1,6 +1,24 @@
11import { UserDirectoryList, User } from "./src/users";
2+import { CsrfSyncedToken } from "csrf-sync";
23
34declare global {
5+ declare namespace CookieSessionInterfaces {
6+ export interface CookieSessionObject {
7+ /**
8+ * The CSRF token for the session.
9+ */
10+ csrfToken: CsrfSyncedToken;
11+ /**
12+ * Authenticated user handle.
13+ */
14+ handle: string;
15+ /**
16+ * Last time the session was extended.
17+ */
18+ touch: number;
19+ }
20+ }
21+
422 namespace Express {
523 export interface Request {
624 user: {
@@ -15,11 +33,3 @@ declare global {
1533 */
1634 var DATA_ROOT: string;
1735}
18-
19-declare module 'express-session' {
20- export interface SessionData {
21- handle: string;
22- touch: number;
23- // other properties...
24- }
25- }
jsconfig.json+1 -1
@@ -15,7 +15,7 @@
1515 "**/node_modules/**",
1616 "**/dist/**",
1717 "**/.git/**",
1818 "public/lib/**",
1919 "backups/**",
2020 "data/**",
2121 "cache/**",
package-lock.json+5 -5
@@ -26,7 +26,7 @@
2626 "cookie-parser": "^1.4.6",
2727 "cookie-session": "^2.1.0",
2828 "cors": "^2.8.5",
2929 "csrf-csrfsync": "^24.20.3",
3030 "diff-match-patch": "^1.0.5",
3131 "dompurify": "^3.1.7",
3232 "droll": "^0.2.1",
@@ -2987,10 +2987,10 @@
29872987 "node": "*"
29882988 }
29892989 },
29902990 "node_modules/csrf-csrfsync": {
29912991 "version": "24.20.43",
29922992 "resolved": "https://registry.npmjs.org/csrf-csrfsync/-/csrf-csrfsync-24.20.43.tgz",
29932993 "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRTwXzltBBzt/YMl1Njo7imzDt6ZT7G/yKh2L9AYsIwSlTPnx2AaxQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==",
29942994 "license": "ISC",
29952995 "dependencies": {
29962996 "http-errors": "^2.0.0"
package.json+1 -1
@@ -16,7 +16,7 @@
1616 "cookie-parser": "^1.4.6",
1717 "cookie-session": "^2.1.0",
1818 "cors": "^2.8.5",
1919 "csrf-csrfsync": "^24.20.3",
2020 "diff-match-patch": "^1.0.5",
2121 "dompurify": "^3.1.7",
2222 "droll": "^0.2.1",
post-install.js+91 -11
@@ -64,6 +64,46 @@ const keyMigrationMap = [
6464 newKey: 'backups.chat.throttleInterval',
6565 migrate: (value) => value,
6666 },
67+ {
68+ oldKey: 'enableExtensions',
69+ newKey: 'extensions.enabled',
70+ migrate: (value) => value,
71+ },
72+ {
73+ oldKey: 'enableExtensionsAutoUpdate',
74+ newKey: 'extensions.autoUpdate',
75+ migrate: (value) => value,
76+ },
77+ {
78+ oldKey: 'extras.disableAutoDownload',
79+ newKey: 'extensions.models.autoDownload',
80+ migrate: (value) => !value,
81+ },
82+ {
83+ oldKey: 'extras.classificationModel',
84+ newKey: 'extensions.models.classification',
85+ migrate: (value) => value,
86+ },
87+ {
88+ oldKey: 'extras.captioningModel',
89+ newKey: 'extensions.models.captioning',
90+ migrate: (value) => value,
91+ },
92+ {
93+ oldKey: 'extras.embeddingModel',
94+ newKey: 'extensions.models.embedding',
95+ migrate: (value) => value,
96+ },
97+ {
98+ oldKey: 'extras.speechToTextModel',
99+ newKey: 'extensions.models.speechToText',
100+ migrate: (value) => value,
101+ },
102+ {
103+ oldKey: 'extras.textToSpeechModel',
104+ newKey: 'extensions.models.textToSpeech',
105+ migrate: (value) => value,
106+ },
67107];
68108
69109/**
@@ -73,7 +113,7 @@ const keyMigrationMap = [
73113 * @returns {string[]} Array of all keys in the object
74114 */
75115function getAllKeys(obj, prefix = '') {
76116 if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
77117 return [];
78118 }
79119
@@ -173,20 +213,60 @@ function addMissingConfigValues() {
173213 * Creates the default config files if they don't exist yet.
174214 */
175215function createDefaultFiles() {
176- const files = {
216+ /**
177- config: './config.yaml',
217+ * @typedef DefaultItem
178- user: './public/css/user.css',
218+ * @type {object}
179- };
219+ * @property {'file' | 'directory'} type - Whether the item should be copied as a single file or merged into a directory structure.
220+ * @property {string} defaultPath - The path to the default item (typically in `default/`).
221+ * @property {string} productionPath - The path to the copied item for production use.
222+ */
180223
181- for (const file of Object.values(files)) {
224+ /** @type {DefaultItem[]} */
225+ const defaultItems = [
226+ {
227+ type: 'file',
228+ defaultPath: './default/config.yaml',
229+ productionPath: './config.yaml',
230+ },
231+ {
232+ type: 'directory',
233+ defaultPath: './default/public/',
234+ productionPath: './public/',
235+ },
236+ ];
237+
238+ for (const defaultItem of defaultItems) {
182239 try {
183240 if (!fsdefaultItem.existsSync(type === 'file)') {
184- const defaultFilePath = path.join('./default', path.parse(file).base);
241+ if (!fs.existsSync(defaultItem.productionPath)) {
185242 fs.copyFileSync(defaultFilePath, file);
186- console.log(color.green(`Created default file: ${file}`));
243+ defaultItem.defaultPath,
244+ defaultItem.productionPath,
245+ );
246+ console.log(
247+ color.green(`Created default file: ${defaultItem.productionPath}`),
248+ );
249+ }
250+ } else if (defaultItem.type === 'directory') {
251+ fs.cpSync(defaultItem.defaultPath, defaultItem.productionPath, {
252+ force: false, // Don't overwrite existing files!
253+ recursive: true,
254+ });
255+ console.log(
256+ color.green(`Synchronized missing files: ${defaultItem.productionPath}`),
257+ );
258+ } else {
259+ throw new Error(
260+ 'FATAL: Unexpected default file format in `post-install.js#createDefaultFiles()`.',
261+ );
187262 }
188263 } catch (error) {
189- console.error(color.red(`FATAL: Could not write default file: ${file}`), error);
264+ console.error(
265+ color.red(
266+ `FATAL: Could not write default ${defaultItem.type}: ${defaultItem.productionPath}`,
267+ ),
268+ error,
269+ );
190270 }
191271 }
192272}
public/index.html+62 -11
@@ -1977,12 +1977,12 @@
19771977 </span>
19781978 </div>
19791979 </div>
19801980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
19811981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19821982 <input id="openai_show_thoughts" type="checkbox" />
19831983 <span>
19841984 <span data-i18n="Show model thoughtsreasoning">Show model thoughtsreasoning</span>
19851985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
19861986 </span>
19871987 </label>
19881988 <div class="toggle-description justifyLeft marginBot5">
@@ -2692,7 +2692,7 @@
26922692 <option value="windowai">Window AI</option>
26932693 </optgroup>
26942694 </select>
26952695 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">
26962696 <div class="inline-drawer-toggle inline-drawer-header">
26972697 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
26982698 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2755,7 +2755,7 @@
27552755 </div>
27562756 </div>
27572757 </div>
27582758 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">
27592759 <div class="reverse_proxy_warning">
27602760 <b>
27612761 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -3062,7 +3062,9 @@
30623062 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
30633063 </optgroup>
30643064 <optgroup label="Subversions">
30653065 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental</option>
3066+ <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3067+ <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
30663068 <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash Experimental</option>
30673069 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
30683070 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
@@ -3209,6 +3211,7 @@
32093211 <select id="model_deepseek_select">
32103212 <option value="deepseek-chat">deepseek-chat</option>
32113213 <option value="deepseek-coder">deepseek-coder</option>
3214+ <option value="deepseek-reasoner">deepseek-reasoner</option>
32123215 </select>
32133216 </div>
32143217 </div>
@@ -3797,6 +3800,39 @@
37973800 </div>
37983801 </div>
37993802 <div>
3803+ <h4 class="standoutHeader">
3804+ <span data-i18n="Reasoning">Reasoning</span>
3805+ </h4>
3806+ <div>
3807+ <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3808+ <input id="reasoning_add_to_prompts" type="checkbox" />
3809+ <small data-i18n="Add Reasoning to Prompts">
3810+ Add Reasoning to Prompts
3811+ </small>
3812+ </label>
3813+ <div class="flex-container">
3814+ <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3815+ <small data-i18n="Prefix">Prefix</small>
3816+ <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3817+ </div>
3818+ <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3819+ <small data-i18n="Suffix">Suffix</small>
3820+ <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3821+ </div>
3822+ </div>
3823+ <div class="flex-container">
3824+ <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3825+ <small data-i18n="Separator">Separator</small>
3826+ <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3827+ </div>
3828+ <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3829+ <small data-i18n="Max Additions">Max Additions</small>
3830+ <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3831+ </div>
3832+ </div>
3833+ </div>
3834+ </div>
3835+ <div>
38003836 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
38013837 <div>
38023838 <small>
@@ -6218,14 +6254,26 @@
62186254 <div class="mes_edit_buttons">
62196255 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
62206256 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
62216257 <div class="mes_edit_deletemes_edit_add_reasoning menu_button fa-solid fa-trash-canlightbulb" title="DeleteAdd thisa messagereasoning block" data-i18n="[title]DeleteAdd thisa messagereasoning block"></div>
6222- </div>
6258+ <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
62236259 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
62246260 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down"></div>
6225- </div>
62266261 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
62276262 </div>
62286263 </div>
6264+ <details class="mes_reasoning_details">
6265+ <summary class="mes_reasoning_summary">
6266+ <span data-i18n="Reasoning">Reasoning</span>
6267+ <div class="mes_reasoning_actions">
6268+ <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6269+ <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6270+ <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6271+ <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6272+ <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6273+ </div>
6274+ </summary>
6275+ <div class="mes_reasoning"></div>
6276+ </details>
62296277 <div class="mes_text"></div>
62306278 <div class="mes_img_container">
62316279 <div class="mes_img_controls">
@@ -6325,7 +6373,10 @@
63256373 <img alt="Avatar" src="" />
63266374 </div>
63276375 <div class="group_member_name">
63286376 <div class="ch_namecharacter_name_block"></div>
6377+ <span class="ch_name"></span>
6378+ <small class="ch_additional_info character_version"></small>
6379+ </div>
63296380 <div class="tags tags_inline"></div>
63306381 </div>
63316382 <input class="ch_fav" value="" hidden />
public/locales/ar-sa.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)",
13771377 "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)",
13781378 "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)",
13791379 "char_import_5": "حرف AICharacterCardAICharacterCards.com (رابط مباشر أو معرف)",
13801380 "char_import_6": "رابط PNG المباشر (راجع",
13811381 "char_import_7": "للمضيفين المسموح بهم)",
13821382 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
public/locales/de-de.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (Direktlink oder ID)",
13771377 "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)",
13781378 "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com-Charakter (Direktlink oder ID)",
13801380 "char_import_6": "Direkter PNG-Link (siehe",
13811381 "char_import_7": "für erlaubte Hosts)",
13821382 "char_import_8": "RisuRealm-Charakter (Direktlink)",
public/locales/es-es.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (enlace directo o ID)",
13771377 "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)",
13781378 "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)",
13791379 "char_import_5": "Carácter AICharacterCardAICharacterCards.com (enlace directo o ID)",
13801380 "char_import_6": "Enlace PNG directo (consulte",
13811381 "char_import_7": "para hosts permitidos)",
13821382 "char_import_8": "Personaje RisuRealm (Enlace directo)",
public/locales/fr-fr.json+2 -2
@@ -1297,7 +1297,7 @@
12971297 "char_import_2": "Lorebook de Chub (lien direct ou ID)",
12981298 "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)",
12991299 "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)",
13001300 "char_import_5": "Personnage de AICharacterCardAICharacterCards.com (lien direct ou identifiant)",
13011301 "char_import_6": "Lien PNG direct (voir",
13021302 "char_import_7": "pour les hôtes autorisés)",
13031303 "char_import_8": "Personnage de RisuRealm (lien direct)",
@@ -1385,7 +1385,7 @@
13851385 "enable_functions_desc_1": "Autorise l'utilisation",
13861386 "enable_functions_desc_2": "outils de fonction",
13871387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
13881388 "Show model thoughtsreasoning": "Afficher les pensées du modèle",
13891389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
13901390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
13911391 "openai_logit_bias_no_items": "Aucun élément",
public/locales/is-is.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)",
13771377 "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)",
13781378 "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com Karakter (beinn hlekkur eða auðkenni)",
13801380 "char_import_6": "Beinn PNG hlekkur (sjá",
13811381 "char_import_7": "fyrir leyfilega gestgjafa)",
13821382 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
public/locales/it-it.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Lorebook di Chub (collegamento diretto o ID)",
13771377 "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)",
13781378 "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)",
13791379 "char_import_5": "Carattere AICharacterCardAICharacterCards.com (Link diretto o ID)",
13801380 "char_import_6": "Collegamento PNG diretto (fare riferimento a",
13811381 "char_import_7": "per gli host consentiti)",
13821382 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
public/locales/ja-jp.json+1 -1
@@ -1378,7 +1378,7 @@
13781378 "char_import_2": "Chub ロアブック (直接リンクまたは ID)",
13791379 "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)",
13801380 "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)",
13811381 "char_import_5": "AICharacterCardAICharacterCards.com キャラクター (直接リンクまたは ID)",
13821382 "char_import_6": "直接PNGリンク(参照",
13831383 "char_import_7": "許可されたホストの場合)",
13841384 "char_import_8": "RisuRealm キャラクター (直接リンク)",
public/locales/ko-kr.json+1 -1
@@ -1395,7 +1395,7 @@
13951395 "char_import_2": "Chub Lorebook(직접 링크 또는 ID)",
13961396 "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)",
13971397 "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)",
13981398 "char_import_5": "AICharacterCardAICharacterCards.com 캐릭터(직접 링크 또는 ID)",
13991399 "char_import_6": "직접 PNG 링크(참조",
14001400 "char_import_7": "허용된 호스트의 경우)",
14011401 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
public/locales/nl-nl.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (directe link of ID)",
13771377 "char_import_3": "JanitorAI-personage (directe link of UUID)",
13781378 "char_import_4": "Pygmalion.chat-teken (directe link of UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com-teken (directe link of ID)",
13801380 "char_import_6": "Directe PNG-link (zie",
13811381 "char_import_7": "voor toegestane hosts)",
13821382 "char_import_8": "RisuRealm-personage (directe link)",
public/locales/pt-pt.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (link direto ou ID)",
13771377 "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)",
13781378 "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)",
13791379 "char_import_5": "Personagem AICharacterCardAICharacterCards.com (link direto ou ID)",
13801380 "char_import_6": "Link PNG direto (consulte",
13811381 "char_import_7": "para hosts permitidos)",
13821382 "char_import_8": "Personagem RisuRealm (link direto)",
public/locales/ru-ru.json+1 -1
@@ -966,7 +966,7 @@
966966 "char_import_2": "Лорбук с Chub (прямая ссылка или ID)",
967967 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",
968968 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",
969969 "char_import_5": "Персонаж с AICharacterCardAICharacterCards.com (прямая ссылка или ID)",
970970 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
971971 "char_import_7": ")",
972972 "Grammar String": "Грамматика",
public/locales/uk-ua.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub Lorebook (пряме посилання або ID)",
13771377 "char_import_3": "Символ JanitorAI (пряме посилання або UUID)",
13781378 "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)",
13791379 "char_import_5": "Символ AICharacterCardAICharacterCards.com (пряме посилання або ідентифікатор)",
13801380 "char_import_6": "Пряме посилання на PNG (див",
13811381 "char_import_7": "для дозволених хостів)",
13821382 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
public/locales/vi-vn.json+1 -1
@@ -1376,7 +1376,7 @@
13761376 "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)",
13771377 "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)",
13781378 "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)",
13791379 "char_import_5": "AICharacterCardAICharacterCards.com (Nhập URL trực tiếp hoặc ID)",
13801380 "char_import_6": "Nhập PNG trực tiếp (tham khảo",
13811381 "char_import_7": "đối với các máy chủ được phép)",
13821382 "char_import_8": "RisuRealm (URL trực tiếp)",
public/locales/zh-cn.json+10 -10
@@ -266,7 +266,7 @@
266266 "Use system prompt": "使用系统提示词",
267267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268268 "Merges_all_system_messages_desc_2": "字段发送。",
269269 "Show model thoughtsreasoning": "展示思维链",
270270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
271271 "Assistant Prefill": "AI预填",
272272 "Expand the editor": "展开编辑器",
@@ -1191,9 +1191,9 @@
11911191 "welcome_message_part_8": "您可随时通过",
11921192 "welcome_message_part_9": "图标来更改此设置。",
11931193 "Persona Name:": "用户角色名称:",
11941194 "Temporarily disable automatic replies from this character": "暂时禁用此角色的自动回复临时禁言此角色",
11951195 "Enable automatic replies from this character": "启用此角色的自动回复解除禁言此角色",
11961196 "Trigger a message from this character": "从此角色触发消息强制触发该角色发言",
11971197 "Move up": "向上移动",
11981198 "Move down": "向下移动",
11991199 "View character card": "查看角色卡片",
@@ -1829,7 +1829,7 @@
18291829 "char_import_2": "Chub 知识书(直链或ID)",
18301830 "char_import_3": "JanitorAI 角色(直链或UUID)",
18311831 "char_import_4": "Pygmalion.chat 角色(直链或UUID)",
18321832 "char_import_5": "AICharacterCardAICharacterCards.com 角色(直链或ID)",
18331833 "char_import_6": "被允许的PNG直链(请参阅",
18341834 "char_import_7": ")",
18351835 "char_import_8": "RisuRealm 角色(直链)",
@@ -1838,7 +1838,7 @@
18381838 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
18391839 "Disclaimer:": "免责声明:",
18401840 "Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.": "使用外部的扩展程序可能存在意料外的副作用和安全隐患。在导入扩展程序前,请一定确认其来源可信。我们不为第三方扩展程序造成的任何损失负责。",
18411841 "Prompt Itemization": "将提示词分条提示词拆分",
18421842 "Show Raw Prompt": "显示原始提示词",
18431843 "Copy Prompt": "复制提示词",
18441844 "Show Prompt Differences": "显示提示词差异",
@@ -2045,8 +2045,8 @@
20452045 "Post a GitHub issue": "在 GitHub 发布问题",
20462046 "Contact the developers": "联系开发者",
20472047 "If you're connected to an API, try asking me something!": "若您已经配置好API,尝试发送些什么吧!",
20482048 "Title/Memo": "标题/备忘录(备忘)",
20492049 "Strategy": "Strategy触发策略",
20502050 "Position": "位置插入位置",
20512051 "Trigger %": "触发率 %触发概率%"
20522052}
public/locales/zh-tw.json+2 -2
@@ -1381,7 +1381,7 @@
13811381 "char_import_2": "Chub Lorebook(直接連結或 ID)",
13821382 "char_import_3": "JanitorAI 角色(直接連結或 ID)",
13831383 "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)",
13841384 "char_import_5": "AICharacterCardAICharacterCards.com 角色(直接連結或 ID)",
13851385 "char_import_6": "直接 PNG 連結(請參閱",
13861386 "char_import_7": "對於允許的主機)",
13871387 "char_import_8": "RisuRealm角色(直接連結)",
@@ -2357,7 +2357,7 @@
23572357 "Forbid": "禁止",
23582358 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
23592359 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
23602360 "Show model thoughtsreasoning": "顯示模型思維鏈",
23612361 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
23622362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
23632363 "Model ID (optional)": "模型 ID(可選)",
public/script.js+134 -26
@@ -267,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267267import { initBulkEdit } from './scripts/bulk-edit.js';
268268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269269import { getContext } from './scripts/st-context.js';
270+import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
270271
271272// API OBJECT FOR EXTERNAL WIRING
272273globalThis.SillyTavern = {
@@ -443,6 +444,7 @@ export const event_types = {
443444 MESSAGE_DELETED: 'message_deleted',
444445 MESSAGE_UPDATED: 'message_updated',
445446 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
447+ MORE_MESSAGES_LOADED: 'more_messages_loaded',
446448 IMPERSONATE_READY: 'impersonate_ready',
447449 CHAT_CHANGED: 'chat_id_changed',
448450 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
@@ -723,6 +725,7 @@ async function getSystemMessages() {
723725 is_user: false,
724726 is_system: true,
725727 mes: await renderTemplateAsync('assistantNote'),
728+ uses_system_ui: true,
726729 extra: {
727730 isSmallSys: true,
728731 },
@@ -980,6 +983,7 @@ async function firstLoadInit() {
980983 initServerHistory();
981984 initSettingsSearch();
982985 initBulkEdit();
986+ initReasoning();
983987 await initScrapers();
984988 doDailyExtensionUpdatesCheck();
985989 await hideLoader();
@@ -1829,7 +1833,7 @@ export async function replaceCurrentChat() {
18291833 }
18301834}
18311835
18321836export async function showMoreMessages(messagesToLoad = null) {
18331837 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
18341838 let messageId = Number(firstDisplayedMesId);
18351839 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
@@ -1859,6 +1863,8 @@ export function showMoreMessages(messagesToLoad = null) {
18591863 const newHeight = $('#chat').prop('scrollHeight');
18601864 $('#chat').scrollTop(newHeight - prevHeight);
18611865 }
1866+
1867+ await eventSource.emit(event_types.MORE_MESSAGES_LOADED);
18621868}
18631869
18641870export async function printMessages() {
@@ -2196,6 +2202,7 @@ function getMessageFromTemplate({
21962202 isUser,
21972203 avatarImg,
21982204 bias,
2205+ reasoning,
21992206 isSystem,
22002207 title,
22012208 timerValue,
@@ -2220,6 +2227,7 @@ function getMessageFromTemplate({
22202227 mes.find('.avatar img').attr('src', avatarImg);
22212228 mes.find('.ch_name .name_text').text(characterName);
22222229 mes.find('.mes_bias').html(bias);
2230+ mes.find('.mes_reasoning').html(reasoning);
22232231 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
22242232 mes.find('.mesIDDisplay').text(`#${mesId}`);
22252233 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2234,10 +2242,16 @@ function getMessageFromTemplate({
22342242 return mes;
22352243}
22362244
2245+/**
2246+ * Re-renders a message block with updated content.
2247+ * @param {number} messageId Message ID
2248+ * @param {object} message Message object
2249+ */
22372250export function updateMessageBlock(messageId, message) {
22382251 const messageElement = $(`#chat [mesid="${messageId}"]`);
22392252 const text = message?.extra?.display_text ?? message.mes;
22402253 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
2254+ messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));
22412255 addCopyToCodeBlocks(messageElement);
22422256 appendMediaToMessage(message, messageElement);
22432257}
@@ -2396,6 +2410,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23962410 sanitizerOverrides,
23972411 );
23982412 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
2413+ const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
23992414 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24002415
24012416 let params = {
@@ -2405,6 +2420,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24052420 isUser: mes.is_user,
24062421 avatarImg: avatarImg,
24072422 bias: bias,
2423+ reasoning: reasoning,
24082424 isSystem: isSystem,
24092425 title: title,
24102426 bookmarkLink: bookmarkLink,
@@ -2464,6 +2480,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24642480 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
24652481 swipeMessage.attr('swipeid', params.swipeId);
24662482 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2483+ swipeMessage.find('.mes_reasoning').html(reasoning);
24672484 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
24682485 appendMediaToMessage(mes, swipeMessage);
24692486 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3074,6 +3091,7 @@ class StreamingProcessor {
30743091 this.messageTextDom = null;
30753092 this.messageTimerDom = null;
30763093 this.messageTokenCounterDom = null;
3094+ this.messageReasoningDom = null;
30773095 /** @type {HTMLTextAreaElement} */
30783096 this.sendTextarea = document.querySelector('#send_textarea');
30793097 this.type = type;
@@ -3089,6 +3107,7 @@ class StreamingProcessor {
30893107 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
30903108 this.messageLogprobs = [];
30913109 this.toolCalls = [];
3110+ this.reasoning = '';
30923111 }
30933112
30943113 #checkDomElements(messageId) {
@@ -3097,6 +3116,7 @@ class StreamingProcessor {
30973116 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
30983117 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
30993118 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3119+ this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
31003120 }
31013121 }
31023122
@@ -3174,18 +3194,27 @@ class StreamingProcessor {
31743194 this.#checkDomElements(messageId);
31753195 this.#updateMessageBlockVisibility();
31763196 const currentTime = new Date();
3177- // Don't waste time calculating token count for streaming
3178- const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
3179- const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
31803197 chat[messageId]['mes'] = processedText;
31813198 chat[messageId]['gen_started'] = this.timeStarted;
31823199 chat[messageId]['gen_finished'] = currentTime;
31833200
31843201 if (currentTokenCount!chat[messageId]['extra']) {
31853202 if (!chat[messageId]['extra']) = {};
3186- chat[messageId]['extra'] = {};
3203+ }
3204+
3205+ if (this.reasoning) {
3206+ chat[messageId]['extra']['reasoning'] = this.reasoning;
3207+ if (this.messageReasoningDom instanceof HTMLElement) {
3208+ const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);
3209+ this.messageReasoningDom.innerHTML = formattedReasoning;
31873210 }
3211+ }
3212+
3213+ // Don't waste time calculating token count for streaming
3214+ const tokenCountText = (this.reasoning || '') + processedText;
3215+ const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
31883216
3217+ if (currentTokenCount) {
31893218 chat[messageId]['extra']['token_count'] = currentTokenCount;
31903219 if (this.messageTokenCounterDom instanceof HTMLElement) {
31913220 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3207,10 +3236,13 @@ class StreamingProcessor {
32073236 if (this.messageTextDom instanceof HTMLElement) {
32083237 this.messageTextDom.innerHTML = formattedText;
32093238 }
3239+
3240+ const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
32103241 if (this.messageTimerDom instanceof HTMLElement) {
32113242 this.messageTimerDom.textContent = timePassed.timerValue;
32123243 this.messageTimerDom.title = timePassed.timerTitle;
32133244 }
3245+
32143246 this.setFirstSwipe(messageId);
32153247 }
32163248
@@ -3317,7 +3349,7 @@ class StreamingProcessor {
33173349 }
33183350
33193351 /**
33203352 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
33213353 */
33223354 *nullStreamingGeneration() {
33233355 throw new Error('Generation function for streaming is not hooked up');
@@ -3339,7 +3371,7 @@ class StreamingProcessor {
33393371 try {
33403372 const sw = new Stopwatch(1000 / power_user.streaming_fps);
33413373 const timestamps = [];
33423374 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33433375 timestamps.push(Date.now());
33443376 if (this.isStopped) {
33453377 return;
@@ -3351,6 +3383,7 @@ class StreamingProcessor {
33513383 if (logprobs) {
33523384 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33533385 }
3386+ this.reasoning = state?.reasoning ?? '';
33543387 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33553388 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
33563389 }
@@ -3817,6 +3850,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38173850 coreChat.pop();
38183851 }
38193852
3853+ const reasoning = new PromptReasoning();
3854+ for (let i = coreChat.length - 1; i >= 0; i--) {
3855+ if (reasoning.isLimitReached()) {
3856+ break;
3857+ }
3858+ coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
3859+ }
3860+
38203861 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
38213862 let message = chatItem.mes;
38223863 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -4738,6 +4779,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47384779 //const getData = await response.json();
47394780 let getMessage = extractMessageFromData(data);
47404781 let title = extractTitleFromData(data);
4782+ let reasoning = extractReasoningFromData(data);
47414783 kobold_horde_model = title;
47424784
47434785 const swipes = extractMultiSwipes(data, type);
@@ -4764,10 +4806,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47644806 else {
47654807 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
47664808 if (originalType !== 'continue') {
47674809 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
47684810 }
47694811 else {
47704812 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
47714813 }
47724814
47734815 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -5672,6 +5714,26 @@ function extractMessageFromData(data) {
56725714}
56735715
56745716/**
5717+ * Extracts the reasoning from the response data.
5718+ * @param {object} data Response data
5719+ * @returns {string} Extracted reasoning
5720+ */
5721+function extractReasoningFromData(data) {
5722+ if (main_api === 'openai' && oai_settings.show_thoughts) {
5723+ switch (oai_settings.chat_completion_source) {
5724+ case chat_completion_sources.DEEPSEEK:
5725+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
5726+ case chat_completion_sources.OPENROUTER:
5727+ return data?.choices?.[0]?.message?.reasoning ?? '';
5728+ case chat_completion_sources.MAKERSUITE:
5729+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5730+ }
5731+ }
5732+
5733+ return '';
5734+}
5735+
5736+/**
56755737 * Extracts multiswipe swipes from the response data.
56765738 * @param {Object} data Response data
56775739 * @param {string} type Type of generation
@@ -5851,7 +5913,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
58515913 return getMessage;
58525914}
58535915
58545916export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
58555917 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
58565918 chat[chat.length - 1]['is_user'])) {
58575919 type = 'normal';
@@ -5876,8 +5938,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58765938 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58775939 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58785940 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5941+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
58795942 if (power_user.message_token_count_enabled) {
58805943 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5944+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
58815945 }
58825946 const chat_id = (chat.length - 1);
58835947 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5896,8 +5960,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58965960 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58975961 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58985962 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5963+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
58995964 if (power_user.message_token_count_enabled) {
59005965 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5966+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59015967 }
59025968 const chat_id = (chat.length - 1);
59035969 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5913,8 +5979,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59135979 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59145980 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59155981 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5982+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
59165983 if (power_user.message_token_count_enabled) {
59175984 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5985+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59185986 }
59195987 const chat_id = (chat.length - 1);
59205988 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5930,6 +5998,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59305998 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59315999 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59326000 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
6001+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
59336002 if (power_user.trim_spaces) {
59346003 getMessage = getMessage.trim();
59356004 }
@@ -5939,7 +6008,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59396008 chat[chat.length - 1]['gen_finished'] = generationFinished;
59406009
59416010 if (power_user.message_token_count_enabled) {
59426011 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
6012+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59436013 }
59446014
59456015 if (selected_group) {
@@ -6004,6 +6074,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
60046074 return { type, getMessage };
60056075}
60066076
6077+export function syncCurrentSwipeInfoExtras() {
6078+ if (!chat.length) {
6079+ return;
6080+ }
6081+ const currentMessage = chat[chat.length - 1];
6082+ if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6083+ const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6084+ if (swipeInfo && typeof swipeInfo === 'object') {
6085+ swipeInfo.extra = structuredClone(currentMessage.extra);
6086+ }
6087+ }
6088+}
6089+
60076090function saveImageToMessage(img, mes) {
60086091 if (mes && img.image) {
60096092 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -7982,9 +8065,23 @@ function updateEditArrowClasses() {
79828065 }
79838066}
79848067
7985-function closeMessageEditor() {
8068+/**
7986- if (this_edit_mes_id) {
8069+ * Closes the message editor.
7987- $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8070+ * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8071+ */
8072+export function closeMessageEditor(what = 'all') {
8073+ if (what === 'message' || what === 'all') {
8074+ if (this_edit_mes_id) {
8075+ $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8076+ }
8077+ }
8078+ if (what === 'reasoning' || what === 'all') {
8079+ document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8080+ const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8081+ if (cancelButton instanceof HTMLElement) {
8082+ cancelButton.click();
8083+ }
8084+ });
79888085 }
79898086}
79908087
@@ -8417,6 +8514,9 @@ function swipe_left() { // when we swipe left..but no generation.
84178514 streamingProcessor.onStopStreaming();
84188515 }
84198516
8517+ // Make sure ad-hoc changes to extras are saved before swiping away
8518+ syncCurrentSwipeInfoExtras();
8519+
84208520 const swipe_duration = 120;
84218521 const swipe_range = '700px';
84228522 chat[chat.length - 1]['swipe_id']--;
@@ -8468,7 +8568,8 @@ function swipe_left() { // when we swipe left..but no generation.
84688568 }
84698569
84708570 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
84718571 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8572+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
84728573 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
84738574 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
84748575 }
@@ -8551,6 +8652,9 @@ const swipe_right = () => {
85518652 return unblockGeneration();
85528653 }
85538654
8655+ // Make sure ad-hoc changes to extras are saved before swiping away
8656+ syncCurrentSwipeInfoExtras();
8657+
85548658 const swipe_duration = 200;
85558659 const swipe_range = 700;
85568660 //console.log(swipe_range);
@@ -8632,6 +8736,7 @@ const swipe_right = () => {
86328736 // resets the timer
86338737 swipeMessage.find('.mes_timer').html('');
86348738 swipeMessage.find('.tokenCounterDisplay').text('');
8739+ swipeMessage.find('.mes_reasoning').html('');
86358740 } else {
86368741 //console.log('showing previously generated swipe candidate, or "..."');
86378742 //console.log('onclick right swipe calling addOneMessage');
@@ -8642,7 +8747,8 @@ const swipe_right = () => {
86428747 chat[chat.length - 1].extra = {};
86438748 }
86448749
86458750 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8751+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
86468752 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
86478753 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
86488754 }
@@ -9444,7 +9550,8 @@ function addDebugFunctions() {
94449550 message.extra = {};
94459551 }
94469552
9447- message.extra.token_count = await getTokenCountAsync(message.mes, 0);
9553+ const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9554+ message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
94489555 }
94499556
94509557 await saveChatConditional();
@@ -11213,14 +11320,15 @@ jQuery(async function () {
1121311320
1121411321 $(document).keyup(function (e) {
1121511322 if (e.key === 'Escape') {
1121611323 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
1121711324 if (isEditVisible && power_user.auto_save_msg_edits === false) {
1121811325 closeMessageEditor('all');
1121911326 $('#send_textarea').focus();
1122011327 return;
1122111328 }
1122211329 if (isEditVisible && power_user.auto_save_msg_edits === true) {
1122311330 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11331+ closeMessageEditor('reasoning');
1122411332 $('#send_textarea').focus();
1122511333 return;
1122611334 }
@@ -11454,8 +11562,8 @@ jQuery(async function () {
1145411562 $('#avatar-and-name-block').slideToggle();
1145511563 });
1145611564
1145711565 $(document).on('mouseup touchend', '#show_more_messages', async function () => {
1145811566 await showMoreMessages();
1145911567 });
1146011568
1146111569 $(document).on('click', '.open_characters_library', async function () {
public/scripts/backgrounds.js+4 -4
@@ -482,10 +482,10 @@ function highlightNewBackground(bg) {
482482 */
483483function setFittingClass(fitting) {
484484 const backgrounds = $('#bg1, #bg_custom');
485- backgrounds.toggleClass('cover', fitting === 'cover');
485+ for (const option of ['cover', 'contain', 'stretch', 'center']) {
486486 backgrounds.toggleClass('contain'option, fittingoption === 'contain'fitting);
487- backgrounds.toggleClass('stretch', fitting === 'stretch');
487+ }
488- backgrounds.toggleClass('center', fitting === 'center');
488+ background_settings.fitting = fitting;
489489}
490490
491491function onBackgroundFilterInput() {
public/scripts/chat-templates.js+10 -0
@@ -59,6 +59,16 @@ const hash_derivations = {
5959 // Tulu-3-8B
6060 // Tulu-3-70B
6161 'Tulu'
62+ ,
63+
64+ // DeepSeek V2.5
65+ '54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca':
66+ 'DeepSeek-V2.5'
67+ ,
68+
69+ // DeepSeek R1
70+ 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
71+ 'DeepSeek-V2.5'
6272};
6373
6474const substr_derivations = {
public/scripts/chats.js+17 -0
@@ -11,6 +11,7 @@ import {
1111 getCurrentChatId,
1212 getRequestHeaders,
1313 hideSwipeButtons,
14+ name1,
1415 name2,
1516 reloadCurrentChat,
1617 saveChatDebounced,
@@ -21,6 +22,7 @@ import {
2122 chat_metadata,
2223 neutralCharacterName,
2324 updateChatMetadata,
25+ system_message_types,
2426} from '../script.js';
2527import { selected_group } from './group-chats.js';
2628import { power_user } from './power-user.js';
@@ -34,6 +36,7 @@ import {
3436 humanFileSize,
3537 saveBase64AsFile,
3638 extractTextFromOffice,
39+ download,
3740} from './utils.js';
3841import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
3942import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -41,6 +44,7 @@ import { ScraperManager } from './scrapers.js';
4144import { DragAndDropHandler } from './dragdrop.js';
4245import { renderTemplateAsync } from './templates.js';
4346import { t } from './i18n.js';
47+import { humanizedDateTime } from './RossAscends-mods.js';
4448
4549/**
4650 * @typedef {Object} FileAttachment
@@ -1437,6 +1441,19 @@ jQuery(function () {
14371441 await viewMessageFile(messageId);
14381442 });
14391443
1444+ $(document).on('click', '.assistant_note_export', async function () {
1445+ const chatToSave = [
1446+ {
1447+ user_name: name1,
1448+ character_name: name2,
1449+ chat_metadata: chat_metadata,
1450+ },
1451+ ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
1452+ ];
1453+
1454+ download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
1455+ });
1456+
14401457 // Do not change. #attachFile is added by extension.
14411458 $(document).on('click', '#attachFile', function () {
14421459 $('#file_form_input').trigger('click');
public/scripts/extensions/caption/settings.html+2 -0
@@ -54,6 +54,8 @@
5454 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
5555 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
5656 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
57+ <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
58+ <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
5759 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
5860 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
5961 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
public/scripts/extensions/connection-manager/index.js+4 -0
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
3030 'api-url',
3131 'model',
3232 'proxy',
33+ 'stop-strings',
3334];
3435
3536const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
4344 'context',
4445 'instruct-state',
4546 'tokenizer',
47+ 'stop-strings',
4648];
4749
4850const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
5759 'instruct': 'Instruct Template',
5860 'context': 'Context Template',
5961 'tokenizer': 'Tokenizer',
62+ 'stop-strings': 'Custom Stopping Strings',
6063};
6164
6265/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
138141 * @property {string} [context] Context Template
139142 * @property {string} [instruct-state] Instruct Mode
140143 * @property {string} [tokenizer] Tokenizer
144+ * @property {string} [stop-strings] Custom Stopping Strings
141145 * @property {string[]} [exclude] Commands to exclude
142146 */
143147
public/scripts/extensions/tts/index.js+49 -6
@@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js';
3030export { talkingAnimation };
3131
3232const UPDATE_INTERVAL = 1000;
33+const wrapper = new ModuleWorkerWrapper(moduleWorker);
3334
3435let voiceMapEntries = [];
3536let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -120,7 +121,7 @@ async function onNarrateOneMessage() {
120121 }
121122
122123 resetTtsPlayback();
123124 ttsJobQueue.pushprocessAndQueueTtsMessage(message);
124125 moduleWorker();
125126}
126127
@@ -147,7 +148,7 @@ async function onNarrateText(args, text) {
147148 }
148149
149150 resetTtsPlayback();
150151 ttsJobQueue.pushprocessAndQueueTtsMessage({ mes: text, name: name });
151152 await moduleWorker();
152153
153154 // Return back to the chat voices
@@ -220,6 +221,36 @@ function isTtsProcessing() {
220221 return processing;
221222}
222223
224+/**
225+ * Splits a message into lines and adds each non-empty line to the TTS job queue.
226+ * @param {Object} message - The message object to be processed.
227+ * @param {string} message.mes - The text of the message to be split into lines.
228+ * @param {string} message.name - The name associated with the message.
229+ * @returns {void}
230+ */
231+function processAndQueueTtsMessage(message) {
232+ if (!extension_settings.tts.narrate_by_paragraphs) {
233+ ttsJobQueue.push(message);
234+ return;
235+ }
236+
237+ const lines = message.mes.split('\n');
238+
239+ for (let i = 0; i < lines.length; i++) {
240+ const line = lines[i];
241+
242+ if (line.length === 0) {
243+ continue;
244+ }
245+
246+ ttsJobQueue.push(
247+ Object.assign({}, message, {
248+ mes: line,
249+ }),
250+ );
251+ }
252+}
253+
223254function debugTtsPlayback() {
224255 console.log(JSON.stringify(
225256 {
@@ -350,7 +381,7 @@ function onAudioControlClicked() {
350381 talkingAnimation(false);
351382 } else {
352383 // Default play behavior if not processing or playing is to play the last message.
353384 ttsJobQueue.pushprocessAndQueueTtsMessage(context.chat[context.chat.length - 1]);
354385 }
355386 updateUiAudioPlayState();
356387}
@@ -376,6 +407,7 @@ function completeCurrentAudioJob() {
376407 currentAudioJob = null;
377408 talkingAnimation(false); //stop lip animation
378409 // updateUiPlayState();
410+ wrapper.update();
379411}
380412
381413/**
@@ -466,7 +498,7 @@ async function processTtsQueue() {
466498 }
467499
468500 if (extension_settings.tts.skip_tags) {
469501 text = text.replace(/<.*?>.[\s\S]*?<\/.*?>/g, '').trim();
470502 }
471503
472504 if (!extension_settings.tts.pass_asterisks) {
@@ -569,6 +601,7 @@ function loadSettings() {
569601 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
570602 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
571603 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
604+ $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
572605 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
573606 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
574607 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() {
638671 saveSettingsDebounced();
639672}
640673
674+function onNarrateByParagraphsClick() {
675+ extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
676+ saveSettingsDebounced();
677+}
678+
641679
642680function onNarrateDialoguesClick() {
643681 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
816854 lastChatId = context.chatId;
817855
818856 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
819- ttsJobQueue.push(message);
857+
858+ if (extension_settings.tts.periodic_auto_generation) {
859+ ttsJobQueue.push(message);
860+ } else {
861+ processAndQueueTtsMessage(message);
862+ }
820863}
821864
822865async function onMessageDeleted() {
@@ -1156,6 +1199,7 @@ jQuery(async function () {
11561199 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
11571200 $('#tts_auto_generation').on('click', onAutoGenerationClick);
11581201 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1202+ $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
11591203 $('#tts_narrate_user').on('click', onNarrateUserClick);
11601204
11611205 $('#playback_rate').on('input', function () {
@@ -1177,7 +1221,6 @@ jQuery(async function () {
11771221 loadSettings(); // Depends on Extension Controls and loadTtsProvider
11781222 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
11791223 addAudioControl(); // Depends on Extension Controls
1180- const wrapper = new ModuleWorkerWrapper(moduleWorker);
11811224 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
11821225 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
11831226 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
public/scripts/extensions/tts/settings.html+4 -0
@@ -30,6 +30,10 @@
3030 <input type="checkbox" id="tts_periodic_auto_generation">
3131 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
3232 </label>
33+ <label class="checkbox_label" for="tts_narrate_by_paragraphs">
34+ <input type="checkbox" id="tts_narrate_by_paragraphs">
35+ <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
36+ </label>
3337 <label class="checkbox_label" for="tts_narrate_quoted">
3438 <input type="checkbox" id="tts_narrate_quoted">
3539 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
public/scripts/group-chats.js+10 -0
@@ -81,6 +81,7 @@ import { t } from './i18n.js';
8181
8282export {
8383 selected_group,
84+ openGroupId,
8485 is_group_automode_enabled,
8586 hideMutedSprites,
8687 is_group_generating,
@@ -1367,6 +1368,15 @@ function getGroupCharacterBlock(character) {
13671368 template.find('.ch_fav').val(isFav);
13681369 template.toggleClass('is_fav', isFav);
13691370
1371+ const auxFieldName = power_user.aux_field || 'character_version';
1372+ const auxFieldValue = (character.data && character.data[auxFieldName]) || '';
1373+ if (auxFieldValue) {
1374+ template.find('.character_version').text(auxFieldValue);
1375+ }
1376+ else {
1377+ template.find('.character_version').hide();
1378+ }
1379+
13701380 let queuePosition = groupChatQueueOrder.get(character.avatar);
13711381 if (queuePosition) {
13721382 template.find('.queue_position').text(queuePosition);
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188188 if (data?.token) {
189189 text += data.token;
190190 }
191191 yield { text, swipes: [], toolCalls: [], state: {} };
192192 }
193193 };
194194}
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746746 text += data.token;
747747 }
748748
749749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750750 }
751751 };
752752}
public/scripts/openai.js+44 -12
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
10961096 // Unordered prompts without marker
10971097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
10981098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099- { role: 'system', content: bias, identifier: 'bias' },
11001099 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1100+ { role: 'assistant', content: bias, identifier: 'bias' },
11011101 ];
11021102
11031103 // Tavern Extras - Summary
@@ -1922,7 +1922,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19221922 }
19231923
19241924 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
19251925 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
19261926 await validateReverseProxy();
19271927 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
19281928 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -2030,6 +2030,16 @@ async function sendOpenAIRequest(type, messages, signal) {
20302030 // https://api-docs.deepseek.com/api/create-chat-completion
20312031 if (isDeepSeek) {
20322032 generate_data.top_p = generate_data.top_p || Number.EPSILON;
2033+
2034+ if (generate_data.model.endsWith('-reasoner')) {
2035+ delete generate_data.top_p;
2036+ delete generate_data.temperature;
2037+ delete generate_data.frequency_penalty;
2038+ delete generate_data.presence_penalty;
2039+ delete generate_data.top_logprobs;
2040+ delete generate_data.logprobs;
2041+ delete generate_data.logit_bias;
2042+ }
20332043 }
20342044
20352045 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
@@ -2085,6 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20852095 let text = '';
20862096 const swipes = [];
20872097 const toolCalls = [];
2098+ const state = { reasoning: '' };
20882099 while (true) {
20892100 const { done, value } = await reader.read();
20902101 if (done) return;
@@ -2095,14 +2106,14 @@ async function sendOpenAIRequest(type, messages, signal) {
20952106
20962107 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
20972108 const swipeIndex = parsed.choices[0].index - 1;
20982109 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
20992110 } else {
21002111 text += getStreamingReply(parsed, state);
21012112 }
21022113
21032114 ToolManager.parseToolCalls(toolCalls, parsed);
21042115
21052116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
21062117 }
21072118 };
21082119 }
@@ -2129,14 +2140,33 @@ async function sendOpenAIRequest(type, messages, signal) {
21292140 }
21302141}
21312142
2132-function getStreamingReply(data) {
2143+/**
2144+ * Extracts the reply from the response data from a chat completions-like source
2145+ * @param {object} data Response data from the chat completions-like source
2146+ * @param {object} state Additional state to keep track of
2147+ * @returns {string} The reply extracted from the response data
2148+ */
2149+function getStreamingReply(data, state) {
21332150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
21342151 return data?.delta?.text || '';
21352152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2136- return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';
2153+ if (oai_settings.show_thoughts) {
2154+ state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2155+ }
2156+ return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
21372157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
21382158 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2139- } else {
2159+ } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2160+ if (oai_settings.show_thoughts) {
2161+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2162+ }
2163+ return data.choices?.[0]?.delta?.content || '';
2164+ } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2165+ if (oai_settings.show_thoughts) {
2166+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167+ }
2168+ return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2169+ } else {
21402170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21412171 }
21422172}
@@ -3346,7 +3376,7 @@ async function getStatusOpen() {
33463376 chat_completion_source: oai_settings.chat_completion_source,
33473377 };
33483378
33493379 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
33503380 await validateReverseProxy();
33513381 }
33523382
@@ -4204,7 +4234,7 @@ async function onModelChange() {
42044234 $('#openai_max_context').attr('max', max_32k);
42054235 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
42064236 $('#openai_max_context').attr('max', max_2mil);
42074237 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {
42084238 $('#openai_max_context').attr('max', max_1mil);
42094239 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
42104240 $('#openai_max_context').attr('max', max_32k);
@@ -4488,7 +4518,7 @@ async function onModelChange() {
44884518 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
44894519 if (oai_settings.max_context_unlocked) {
44904520 $('#openai_max_context').attr('max', unlocked_max);
44914521 } else if (oai_settings.deepseek_model ==['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
44924522 $('#openai_max_context').attr('max', max_64k);
44934523 } else if (oai_settings.deepseek_model == 'deepseek-coder') {
44944524 $('#openai_max_context').attr('max', max_16k);
@@ -4725,7 +4755,7 @@ async function onConnectButtonClick(e) {
47254755 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);
47264756 }
47274757
47284758 if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) {
47294759 console.log('No secret key saved for DeepSeek');
47304760 return;
47314761 }
@@ -4901,6 +4931,8 @@ export function isImageInliningSupported() {
49014931 const visionSupportedModels = [
49024932 'gpt-4-vision',
49034933 'gemini-2.0-flash-thinking-exp-1219',
4934+ 'gemini-2.0-flash-thinking-exp-01-21',
4935+ 'gemini-2.0-flash-thinking-exp',
49044936 'gemini-2.0-flash-exp',
49054937 'gemini-1.5-flash',
49064938 'gemini-1.5-flash-latest',
public/scripts/power-user.js+50 -1
@@ -253,6 +253,14 @@ let power_user = {
253253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254254 },
255255
256+ reasoning: {
257+ add_to_prompts: false,
258+ prefix: '<think>\n',
259+ suffix: '\n</think>',
260+ separator: '\n\n',
261+ max_additions: 1,
262+ },
263+
256264 personas: {},
257265 default_persona: null,
258266 persona_descriptions: {},
@@ -2534,7 +2542,7 @@ async function loadUntilMesId(mesId) {
25342542 let target;
25352543
25362544 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
25372545 await showMoreMessages();
25382546 await delay(1);
25392547 target = $('#chat').find(`.mes[mesid=${mesId}]`);
25402548
@@ -4064,4 +4072,45 @@ $(document).ready(() => {
40644072 ],
40654073 helpString: 'activates a movingUI preset by name',
40664074 }));
4075+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4076+ name: 'stop-strings',
4077+ aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4078+ helpString: `
4079+ <div>
4080+ Sets a list of custom stopping strings. Gets the list if no value is provided.
4081+ </div>
4082+ <div>
4083+ <strong>Examples:</strong>
4084+ </div>
4085+ <ul>
4086+ <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4087+ <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4088+ </ul>
4089+ `,
4090+ returns: ARGUMENT_TYPE.LIST,
4091+ unnamedArgumentList: [
4092+ SlashCommandArgument.fromProps({
4093+ description: 'list of strings',
4094+ typeList: [ARGUMENT_TYPE.LIST],
4095+ acceptsMultiple: false,
4096+ isRequired: false,
4097+ }),
4098+ ],
4099+ callback: (_, value) => {
4100+ if (String(value ?? '').trim()) {
4101+ const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4102+ if (!parsedValue || !Array.isArray(parsedValue)) {
4103+ throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4104+ }
4105+ parsedValue.forEach((item, index) => {
4106+ parsedValue[index] = String(item);
4107+ });
4108+ power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4109+ $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4110+ saveSettingsDebounced();
4111+ }
4112+
4113+ return power_user.custom_stopping_strings;
4114+ },
4115+ }));
40674116});
public/scripts/reasoning.js+297 -0
@@ -0,0 +1,297 @@
1+import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2+import { t } from './i18n.js';
3+import { MacrosParser } from './macros.js';
4+import { Popup } from './popup.js';
5+import { power_user } from './power-user.js';
6+import { SlashCommand } from './slash-commands/SlashCommand.js';
7+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
9+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10+import { copyText } from './utils.js';
11+
12+/**
13+ * Gets a message from a jQuery element.
14+ * @param {Element} element
15+ * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
16+ */
17+function getMessageFromJquery(element) {
18+ const messageBlock = $(element).closest('.mes');
19+ const messageId = Number(messageBlock.attr('mesid'));
20+ const message = chat[messageId];
21+ return { messageId: messageId, message, messageBlock };
22+}
23+
24+/**
25+ * Helper class for adding reasoning to messages.
26+ * Keeps track of the number of reasoning additions.
27+ */
28+export class PromptReasoning {
29+ static REASONING_PLACEHOLDER = '\u200B';
30+ static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
31+
32+ constructor() {
33+ this.counter = 0;
34+ }
35+
36+ /**
37+ * Checks if the limit of reasoning additions has been reached.
38+ * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
39+ */
40+ isLimitReached() {
41+ if (!power_user.reasoning.add_to_prompts) {
42+ return true;
43+ }
44+
45+ return this.counter >= power_user.reasoning.max_additions;
46+ }
47+
48+ /**
49+ * Add reasoning to a message according to the power user settings.
50+ * @param {string} content Message content
51+ * @param {string} reasoning Message reasoning
52+ * @returns {string} Message content with reasoning
53+ */
54+ addToMessage(content, reasoning) {
55+ // Disabled or reached limit of additions
56+ if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
57+ return content;
58+ }
59+
60+ // No reasoning provided or a placeholder
61+ if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
62+ return content;
63+ }
64+
65+ // Increment the counter
66+ this.counter++;
67+
68+ // Substitute macros in variable parts
69+ const prefix = substituteParams(power_user.reasoning.prefix || '');
70+ const separator = substituteParams(power_user.reasoning.separator || '');
71+ const suffix = substituteParams(power_user.reasoning.suffix || '');
72+
73+ // Combine parts with reasoning and content
74+ return `${prefix}${reasoning}${suffix}${separator}${content}`;
75+ }
76+}
77+
78+function loadReasoningSettings() {
79+ $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
80+ $('#reasoning_add_to_prompts').on('change', function () {
81+ power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
82+ saveSettingsDebounced();
83+ });
84+
85+ $('#reasoning_prefix').val(power_user.reasoning.prefix);
86+ $('#reasoning_prefix').on('input', function () {
87+ power_user.reasoning.prefix = String($(this).val());
88+ saveSettingsDebounced();
89+ });
90+
91+ $('#reasoning_suffix').val(power_user.reasoning.suffix);
92+ $('#reasoning_suffix').on('input', function () {
93+ power_user.reasoning.suffix = String($(this).val());
94+ saveSettingsDebounced();
95+ });
96+
97+ $('#reasoning_separator').val(power_user.reasoning.separator);
98+ $('#reasoning_separator').on('input', function () {
99+ power_user.reasoning.separator = String($(this).val());
100+ saveSettingsDebounced();
101+ });
102+
103+ $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
104+ $('#reasoning_max_additions').on('input', function () {
105+ power_user.reasoning.max_additions = Number($(this).val());
106+ saveSettingsDebounced();
107+ });
108+}
109+
110+function registerReasoningSlashCommands() {
111+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112+ name: 'reasoning-get',
113+ returns: ARGUMENT_TYPE.STRING,
114+ helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
115+ unnamedArgumentList: [
116+ SlashCommandArgument.fromProps({
117+ description: 'Message ID. If not provided, the message ID of the last message is used.',
118+ typeList: ARGUMENT_TYPE.NUMBER,
119+ enumProvider: commonEnumProviders.messages(),
120+ }),
121+ ],
122+ callback: (_args, value) => {
123+ const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
124+ const message = chat[messageId];
125+ const reasoning = String(message?.extra?.reasoning ?? '');
126+ return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
127+ },
128+ }));
129+
130+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
131+ name: 'reasoning-set',
132+ returns: ARGUMENT_TYPE.STRING,
133+ helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
134+ namedArgumentList: [
135+ SlashCommandNamedArgument.fromProps({
136+ name: 'at',
137+ description: 'Message ID. If not provided, the message ID of the last message is used.',
138+ typeList: ARGUMENT_TYPE.NUMBER,
139+ enumProvider: commonEnumProviders.messages(),
140+ }),
141+ ],
142+ unnamedArgumentList: [
143+ SlashCommandArgument.fromProps({
144+ description: 'Reasoning block content.',
145+ typeList: ARGUMENT_TYPE.STRING,
146+ }),
147+ ],
148+ callback: async (args, value) => {
149+ const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
150+ const message = chat[messageId];
151+ if (!message?.extra) {
152+ return '';
153+ }
154+
155+ message.extra.reasoning = String(value ?? '');
156+ await saveChatConditional();
157+
158+ closeMessageEditor('reasoning');
159+ updateMessageBlock(messageId, message);
160+ return message.extra.reasoning;
161+ },
162+ }));
163+}
164+
165+function registerReasoningMacros() {
166+ MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
167+ MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
168+ MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
169+}
170+
171+function setReasoningEventHandlers(){
172+ $(document).on('click', '.mes_reasoning_copy', (e) => {
173+ e.stopPropagation();
174+ e.preventDefault();
175+ });
176+
177+ $(document).on('click', '.mes_reasoning_edit', function (e) {
178+ e.stopPropagation();
179+ e.preventDefault();
180+ const { message, messageBlock } = getMessageFromJquery(this);
181+ if (!message?.extra) {
182+ return;
183+ }
184+
185+ const reasoning = String(message?.extra?.reasoning ?? '');
186+ const chatElement = document.getElementById('chat');
187+ const textarea = document.createElement('textarea');
188+ const reasoningBlock = messageBlock.find('.mes_reasoning');
189+ textarea.classList.add('reasoning_edit_textarea');
190+ textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
191+ $(textarea).insertBefore(reasoningBlock);
192+
193+ if (!CSS.supports('field-sizing', 'content')) {
194+ const resetHeight = function () {
195+ const scrollTop = chatElement.scrollTop;
196+ textarea.style.height = '0px';
197+ textarea.style.height = `${textarea.scrollHeight}px`;
198+ chatElement.scrollTop = scrollTop;
199+ };
200+
201+ textarea.addEventListener('input', resetHeight);
202+ resetHeight();
203+ }
204+
205+ textarea.focus();
206+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
207+
208+ const textareaRect = textarea.getBoundingClientRect();
209+ const chatRect = chatElement.getBoundingClientRect();
210+
211+ // Scroll if textarea bottom is below visible area
212+ if (textareaRect.bottom > chatRect.bottom) {
213+ const scrollOffset = textareaRect.bottom - chatRect.bottom;
214+ chatElement.scrollTop += scrollOffset;
215+ }
216+ });
217+
218+ $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
219+ e.stopPropagation();
220+ e.preventDefault();
221+ const { message, messageId, messageBlock } = getMessageFromJquery(this);
222+ if (!message?.extra) {
223+ return;
224+ }
225+
226+ const textarea = messageBlock.find('.reasoning_edit_textarea');
227+ const reasoning = String(textarea.val());
228+ message.extra.reasoning = reasoning;
229+ await saveChatConditional();
230+ updateMessageBlock(messageId, message);
231+ textarea.remove();
232+ });
233+
234+ $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
235+ e.stopPropagation();
236+ e.preventDefault();
237+
238+ const { messageBlock } = getMessageFromJquery(this);
239+ const textarea = messageBlock.find('.reasoning_edit_textarea');
240+ textarea.remove();
241+ });
242+
243+ $(document).on('click', '.mes_edit_add_reasoning', async function () {
244+ const { message, messageId } = getMessageFromJquery(this);
245+ if (!message?.extra) {
246+ return;
247+ }
248+
249+ if (message.extra.reasoning) {
250+ toastr.info(t`Reasoning already exists.`, t`Edit Message`);
251+ return;
252+ }
253+
254+ message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
255+ await saveChatConditional();
256+ closeMessageEditor();
257+ updateMessageBlock(messageId, message);
258+ });
259+
260+ $(document).on('click', '.mes_reasoning_delete', async function (e) {
261+ e.stopPropagation();
262+ e.preventDefault();
263+
264+ const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
265+
266+ if (!confirm) {
267+ return;
268+ }
269+
270+ const { message, messageId } = getMessageFromJquery(this);
271+ if (!message?.extra) {
272+ return;
273+ }
274+ message.extra.reasoning = '';
275+ await saveChatConditional();
276+ updateMessageBlock(messageId, message);
277+ });
278+
279+ $(document).on('pointerup', '.mes_reasoning_copy', async function () {
280+ const { message } = getMessageFromJquery(this);
281+ const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
282+
283+ if (!reasoning) {
284+ return;
285+ }
286+
287+ await copyText(reasoning);
288+ toastr.info(t`Copied!`, '', { timeOut: 2000 });
289+ });
290+}
291+
292+export function initReasoning() {
293+ loadReasoningSettings();
294+ setReasoningEventHandlers();
295+ registerReasoningSlashCommands();
296+ registerReasoningMacros();
297+}
public/scripts/slash-commands.js+6 -2
@@ -42,6 +42,7 @@ import {
4242 showMoreMessages,
4343 stopGeneration,
4444 substituteParams,
45+ syncCurrentSwipeInfoExtras,
4546 system_avatar,
4647 system_message_types,
4748 this_chid,
@@ -1968,8 +1969,8 @@ export function initDefaultSlashCommands() {
19681969 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
19691970 name: 'chat-render',
19701971 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
19711972 callback: async (args, number) => {
19721973 await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
19731974 if (isTrueBoolean(String(args?.scroll ?? ''))) {
19741975 $('#chat').scrollTop(0);
19751976 }
@@ -2814,8 +2815,11 @@ async function addSwipeCallback(args, value) {
28142815 const newSwipeId = lastMessage.swipes.length - 1;
28152816
28162817 if (isTrueBoolean(args.switch)) {
2818+ // Make sure ad-hoc changes to extras are saved before swiping away
2819+ syncCurrentSwipeInfoExtras();
28172820 lastMessage.swipe_id = newSwipeId;
28182821 lastMessage.mes = lastMessage.swipes[newSwipeId];
2822+ lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
28192823 }
28202824
28212825 await saveChatConditional();
public/scripts/sse-stream.js+30 -0
@@ -220,6 +220,36 @@ async function* parseStreamData(json) {
220220 }
221221 return;
222222 }
223+ else if (typeof json.choices[0].delta.reasoning_content === 'string' && json.choices[0].delta.reasoning_content.length > 0) {
224+ for (let j = 0; j < json.choices[0].delta.reasoning_content.length; j++) {
225+ const str = json.choices[0].delta.reasoning_content[j];
226+ const isLastSymbol = j === json.choices[0].delta.reasoning_content.length - 1;
227+ const choiceClone = structuredClone(json.choices[0]);
228+ choiceClone.delta.reasoning_content = str;
229+ choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
230+ const choices = [choiceClone];
231+ yield {
232+ data: { ...json, choices },
233+ chunk: str,
234+ };
235+ }
236+ return;
237+ }
238+ else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
239+ for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
240+ const str = json.choices[0].delta.reasoning[j];
241+ const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
242+ const choiceClone = structuredClone(json.choices[0]);
243+ choiceClone.delta.reasoning = str;
244+ choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
245+ const choices = [choiceClone];
246+ yield {
247+ data: { ...json, choices },
248+ chunk: str,
249+ };
250+ }
251+ return;
252+ }
223253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
224254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
225255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+10 -2
@@ -1,6 +1,7 @@
11import {
22 activateSendButtons,
33 addOneMessage,
4+ appendMediaToMessage,
45 callPopup,
56 characters,
67 chat,
@@ -12,6 +13,7 @@ import {
1213 extension_prompts,
1314 Generate,
1415 generateQuietPrompt,
16+ getCharacters,
1517 getCurrentChatId,
1618 getRequestHeaders,
1719 getThumbnailUrl,
@@ -40,6 +42,7 @@ import {
4042 substituteParamsExtended,
4143 this_chid,
4244 updateChatMetadata,
45+ updateMessageBlock,
4346} from '../script.js';
4447import {
4548 extension_settings,
@@ -55,7 +58,7 @@ import { MacrosParser } from './macros.js';
5558import { oai_settings } from './openai.js';
5659import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
5760import { power_user, registerDebugFunction } from './power-user.js';
5861import { humanizedDateTime, isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
5962import { ScraperManager } from './scrapers.js';
6063import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
6164import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -65,7 +68,7 @@ import { tag_map, tags } from './tags.js';
6568import { textgenerationwebui_settings } from './textgen-settings.js';
6669import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
6770import { ToolManager } from './tool-calling.js';
6871import { timestampToMoment, uuidv4 } from './utils.js';
6972
7073export function getContext() {
7174 return {
@@ -167,6 +170,11 @@ export function getContext() {
167170 chatCompletionSettings: oai_settings,
168171 textCompletionSettings: textgenerationwebui_settings,
169172 powerUserSettings: power_user,
173+ getCharacters,
174+ uuidv4,
175+ humanizedDateTime,
176+ updateMessageBlock,
177+ appendMediaToMessage,
170178 };
171179}
172180
public/scripts/templates/assistantNote.html+8 -2
@@ -1,3 +1,9 @@
1-<div>
1+<div data-type="assistant_note">
2- <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
2+ <div>
3+ <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
4+ <span>Click the button to save it as a file.</span>
5+ </div>
6+ <div class="assistant_note_export menu_button menu_button_icon" title="Export as JSONL">
7+ <i class="fa-solid fa-file-export"></i>
8+ </div>
39</div>
public/scripts/templates/importCharacters.html+1 -1
@@ -7,7 +7,7 @@
77 <li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li>
88 <li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
99 <li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
1010 <li><span data-i18n="char_import_5">AICharacterCardAICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
1111 <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>
1212 <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>
1313 </ul>
public/scripts/textgen-settings.js+4 -3
@@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986986 let logprobs = null;
987987 const swipes = [];
988988 const toolCalls = [];
989+ const state = {};
989990 while (true) {
990991 const { done, value } = await reader.read();
991992 if (done) return;
@@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
10041005 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
10051006 }
10061007
10071008 yield { text, swipes, logprobs, toolCalls, state };
10081009 }
10091010 };
10101011}
@@ -1231,7 +1232,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12311232 'top_p': settings.top_p,
12321233 'typical_p': settings.typical_p,
12331234 'typical': settings.typical_p,
12341235 'sampler_seed': settings.seed >= 0 ? settings.seed : undefined,
12351236 'min_p': settings.min_p,
12361237 'repetition_penalty': settings.rep_pen,
12371238 'frequency_penalty': settings.freq_pen,
@@ -1294,7 +1295,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12941295 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
12951296 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
12961297 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
12971298 'seed': settings.seed >= 0 ? settings.seed : undefined,
12981299 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
12991300 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
13001301 'grammar_string': settings.grammar_string,
public/style.css+131 -24
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292292 filter: grayscale(25%);
293293}
294294
295295.mes_text table {,
296+.mes_reasoning table {
296297 border-spacing: 0;
297298 border-collapse: collapse;
298299 margin-bottom: 10px;
299300}
300301
301302.mes_text td,
302303.mes_text th {,
304+.mes_reasoning td,
305+.mes_reasoning th {
303306 border: 1px solid;
304307 border-collapse: collapse;
305308 padding: 0.25em;
306309}
307310
308311.mes_text p {,
312+.mes_reasoning p {
309313 margin-top: 0;
310314 margin-bottom: 10px;
311315}
312316
313317.mes_text li tt {,
318+.mes_reasoning li tt {
314319 display: inline-block;
315320}
316321
317322.mes_text ol,
318323.mes_text ul {,
324+.mes_reasoning ol,
325+.mes_reasoning ul {
319326 margin-top: 5px;
320327 margin-bottom: 5px;
321328}
322329
323330.mes_text br,
324331.mes_bias br {,
332+.mes_reasoning br {
325333 content: ' ';
326334}
327335
@@ -332,8 +340,62 @@ input[type='checkbox']:focus-visible {
332340 color: var(--SmartThemeQuoteColor);
333341}
334342
343+.mes_reasoning {
344+ display: block;
345+ border: 1px solid var(--SmartThemeBorderColor);
346+ background-color: var(--black30a);
347+ border-radius: 5px;
348+ padding: 5px;
349+ margin: 5px 0;
350+ overflow-y: auto;
351+}
352+
353+.mes_reasoning_summary {
354+ cursor: pointer;
355+ position: relative;
356+ margin: 2px;
357+}
358+
359+@supports not selector(:has(*)) {
360+ .mes_reasoning_details {
361+ display: none !important;
362+ }
363+}
364+
365+.mes_bias:empty,
366+.mes_reasoning:empty,
367+.mes_reasoning_details:has(.mes_reasoning:empty),
368+.mes_block:has(.edit_textarea) .mes_reasoning_details,
369+.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
371+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
372+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
373+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
374+ display: none;
375+}
376+
377+.mes_reasoning_actions {
378+ position: absolute;
379+ right: 0;
380+ top: 0;
381+
382+ display: flex;
383+ gap: 4px;
384+ flex-wrap: nowrap;
385+ justify-content: flex-end;
386+ transition: all 200ms;
387+ overflow-x: hidden;
388+ padding: 1px;
389+}
390+
391+.mes_reasoning_summary>span {
392+ margin-left: 0.5em;
393+}
394+
335395.mes_text i,
336396.mes_text em {,
397+.mes_reasoning i,
398+.mes_reasoning em {
337399 color: var(--SmartThemeEmColor);
338400}
339401
@@ -342,20 +404,24 @@ input[type='checkbox']:focus-visible {
342404 color: inherit;
343405}
344406
345407.mes_text u {,
408+.mes_reasoning u {
346409 color: var(--SmartThemeUnderlineColor);
347410}
348411
349412.mes_text q {,
413+.mes_reasoning q {
350414 color: var(--SmartThemeQuoteColor);
351415}
352416
353417.mes_text font[color] em,
354418.mes_text font[color] i {,
355- color: inherit;
419+.mes_text font[color] u,
356-}
420+.mes_text font[color] q,
357-
421+.mes_reasoning font[color] em,
358422.mes_textmes_reasoning font[color] q {i,
423+.mes_reasoning font[color] u,
424+.mes_reasoning font[color] q {
359425 color: inherit;
360426}
361427
@@ -363,7 +429,8 @@ input[type='checkbox']:focus-visible {
363429 display: block;
364430}
365431
366432.mes_text blockquote {,
433+.mes_reasoning blockquote {
367434 border-left: 3px solid var(--SmartThemeQuoteColor);
368435 padding-left: 10px;
369436 background-color: var(--black30a);
@@ -373,18 +440,24 @@ input[type='checkbox']:focus-visible {
373440.mes_text strong em,
374441.mes_text strong,
375442.mes_text h2,
376443.mes_text h1 {,
444+.mes_reasoning strong em,
445+.mes_reasoning strong,
446+.mes_reasoning h2,
447+.mes_reasoning h1 {
377448 font-weight: bold;
378449}
379450
380451.mes_text pre code {,
452+.mes_reasoning pre code {
381453 position: relative;
382454 display: block;
383455 overflow-x: auto;
384456 padding: 1em;
385457}
386458
387459.mes_text img:not(.mes_img) {,
460+.mes_reasoning img:not(.mes_img) {
388461 max-width: 100%;
389462 max-height: var(--doc-height);
390463}
@@ -1027,6 +1100,11 @@ body .panelControlBar {
10271100 /*only affects bubblechat to make it sit nicely at the bottom*/
10281101}
10291102
1103+.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1104+ margin-bottom: 30px;
1105+}
1106+
1107+.last_mes .mes_reasoning,
10301108.last_mes .mes_text {
10311109 padding-right: 30px;
10321110}
@@ -1240,14 +1318,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12401318 overflow-y: clip;
12411319}
12421320
12431321.mes_text {,
1322+.mes_reasoning {
12441323 font-weight: 500;
12451324 line-height: calc(var(--mainFontSize) + .5rem);
1325+ max-width: 100%;
1326+ overflow-wrap: anywhere;
1327+}
1328+
1329+.mes_text {
12461330 padding-left: 0;
12471331 padding-top: 5px;
12481332 padding-bottom: 5px;
1249- max-width: 100%;
1250- overflow-wrap: anywhere;
12511333}
12521334
12531335br {
@@ -2926,7 +3008,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
29263008 position: relative;
29273009}
29283010
29293011#rm_print_characters_block.character_name_block .ch_name,
29303012.avatar-container .ch_name {
29313013 flex: 1 1 auto;
29323014 white-space: nowrap;
@@ -2936,6 +3018,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
29363018 display: block;
29373019}
29383020
3021+.character_name_block .character_version {
3022+ text-overflow: ellipsis;
3023+ overflow: hidden;
3024+ text-wrap: nowrap;
3025+ max-width: 50%;
3026+}
3027+
29393028#rm_print_characters_block .character_name_block> :last-child {
29403029 flex: 0 100000 auto;
29413030 /* Force shrinking first */
@@ -4148,10 +4237,12 @@ input[type="range"]::-webkit-slider-thumb {
41484237 align-items: center;
41494238}
41504239
4240+.mes_reasoning_edit_cancel,
41514241.mes_edit_cancel.menu_button {
41524242 background-color: var(--crimson70a);
41534243}
41544244
4245+.mes_reasoning_edit_done,
41554246.mes_edit_done.menu_button {
41564247 background-color: var(--okGreen70a);
41574248}
@@ -4160,6 +4251,7 @@ input[type="range"]::-webkit-slider-thumb {
41604251 opacity: 1;
41614252}
41624253
4254+.reasoning_edit_textarea,
41634255.edit_textarea {
41644256 padding: 5px;
41654257 margin: 0;
@@ -5648,6 +5740,7 @@ body:not(.movingUI) .drawer-content.maximized {
56485740
56495741.model-card .details-container {
56505742 text-align: right;
5743+ line-height: 0.9;
56515744}
56525745
56535746.model-card:hover {
@@ -5670,7 +5763,7 @@ body:not(.movingUI) .drawer-content.maximized {
56705763}
56715764
56725765.model-title {
5673- font-size: 13px;
5766+ font-size: calc(var(--mainFontSize) * 0.95);
56745767 font-weight: bold;
56755768 overflow: hidden;
56765769}
@@ -5686,7 +5779,7 @@ body:not(.movingUI) .drawer-content.maximized {
56865779.model-class,
56875780.model-context-length,
56885781.model-date-added {
5689- font-size: 10px;
5782+ font-size: calc(var(--mainFontSize) * 0.75);
56905783}
56915784
56925785.model-class,
@@ -5768,3 +5861,17 @@ body:not(.movingUI) .drawer-content.maximized {
57685861.alternate_greetings_list {
57695862 overflow-y: scroll;
57705863}
5864+
5865+.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) {
5866+ display: flex;
5867+ flex-direction: row;
5868+ flex-wrap: nowrap;
5869+ justify-content: space-between;
5870+ align-items: center;
5871+ gap: 10px;
5872+ padding: 0 2px;
5873+}
5874+
5875+.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5876+ flex: 1;
5877+}
server.js+39 -18
@@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers';
1818
1919// express/server related library imports
2020import cors from 'cors';
2121import { doubleCsrfcsrfSync } from 'csrf-csrfsync';
2222import express from 'express';
2323import compression from 'compression';
24-import cookieParser from 'cookie-parser';
2524import cookieSession from 'cookie-session';
2625import multer from 'multer';
2726import responseTime from 'response-time';
@@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4;
4039import { loadPlugins } from './src/plugin-loader.js';
4140import {
4241 initUserStorage,
43- getCsrfSecret,
4442 getCookieSecret,
4543 getCookieSessionName,
4644 getAllEnabledUsers,
@@ -67,6 +65,7 @@ import {
6765 forwardFetchResponse,
6866 removeColorFormatting,
6967 getSeparator,
68+ safeReadFileSync,
7069} from './src/util.js';
7170import { UPLOADS_DIRECTORY } from './src/constants.js';
7271import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -347,8 +346,8 @@ if (enableCorsProxy) {
347346}
348347
349348function getSessionCookieAge() {
350349 // Defaults to 24 hours in"no secondsexpiration" if not set
351350 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60-1);
352351
353352 // Convert to milliseconds
354353 if (configValue > 0) {
@@ -377,27 +376,38 @@ app.use(setUserDataMiddleware);
377376
378377// CSRF Protection //
379378if (!disableCsrf) {
380379 const COOKIES_SECRETcsrfSyncProtection = getCookieSecretcsrfSync();{
381-
380+ getTokenFromState: (req) => {
382- const { generateToken, doubleCsrfProtection } = doubleCsrf({
381+ if (!req.session) {
383- getSecret: getCsrfSecret,
382+ console.error('(CSRF error) getTokenFromState: Session object not initialized');
384- cookieName: 'X-CSRF-Token',
383+ return;
385- cookieOptions: {
384+ }
386- sameSite: 'strict',
385+ return req.session.csrfToken;
387- secure: false,
386+ },
387+ getTokenFromRequest: (req) => {
388+ return req.headers['x-csrf-token']?.toString();
389+ },
390+ storeTokenInState: (req, token) => {
391+ if (!req.session) {
392+ console.error('(CSRF error) storeTokenInState: Session object not initialized');
393+ return;
394+ }
395+ req.session.csrfToken = token;
388396 },
389397 size: 6432,
390- getTokenFromRequest: (req) => req.headers['x-csrf-token'],
391398 });
392399
393400 app.get('/csrf-token', (req, res) => {
394401 res.json({
395402 'token': csrfSyncProtection.generateToken(res, req),
396403 });
397404 });
398405
399- app.use(cookieParser(COOKIES_SECRET));
406+ // Customize the error message
400- app.use(doubleCsrfProtection);
407+ csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
408+ csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
409+
410+ app.use(csrfSyncProtection.csrfSynchronisedProtection);
401411} else {
402412 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
403413 app.get('/csrf-token', (req, res) => {
@@ -921,6 +931,16 @@ async function verifySecuritySettings() {
921931 }
922932}
923933
934+/**
935+ * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
936+ */
937+function apply404Middleware() {
938+ const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
939+ app.use((req, res) => {
940+ res.status(404).send(notFoundWebpage);
941+ });
942+}
943+
924944// User storage module needs to be initialized before starting the server
925945initUserStorage(dataRoot)
926946 .then(ensurePublicDirectoriesExist)
@@ -928,4 +948,5 @@ initUserStorage(dataRoot)
928948 .then(migrateSystemPrompts)
929949 .then(verifySecuritySettings)
930950 .then(preSetupTasks)
951+ .then(apply404Middleware)
931952 .finally(startServer);
src/endpoints/avatars.js+2 -1
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import { jsonParser, urlencodedParser } from '../express-common.js';
1010import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1111import { getImages, tryParse } from '../util.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314export const router = express.Router();
1415
@@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) {
1718 response.send(JSON.stringify(images));
1819});
1920
2021router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) {
2122 if (!request.body) return response.sendStatus(400);
2223
2324 if (request.body.avatar !== sanitize(request.body.avatar)) {
src/endpoints/backends/chat-completions.js+113 -23
@@ -37,6 +37,8 @@ import {
3737 getTiktokenTokenizer,
3838 sentencepieceTokenizers,
3939 TEXT_COMPLETION_MODELS,
40+ webTokenizers,
41+ getWebTokenizer,
4042} from '../tokenizers.js';
4143
4244const API_OPENAI = 'https://api.openai.com/v1';
@@ -61,6 +63,7 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta';
6163 * @returns
6264 */
6365function postProcessPrompt(messages, type, names) {
66+ const addAssistantPrefix = x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x;
6467 switch (type) {
6568 case 'merge':
6669 case 'claude':
@@ -70,7 +73,9 @@ function postProcessPrompt(messages, type, names) {
7073 case 'strict':
7174 return mergeMessages(messages, names, true, true);
7275 case 'deepseek':
73- return (x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x)(mergeMessages(messages, names, true, false));
76+ return addAssistantPrefix(mergeMessages(messages, names, true, false));
77+ case 'deepseek-reasoner':
78+ return addAssistantPrefix(mergeMessages(messages, names, true, true));
7479 default:
7580 return messages;
7681 }
@@ -284,6 +289,7 @@ async function sendMakerSuiteRequest(request, response) {
284289 const model = String(request.body.model);
285290 const stream = Boolean(request.body.stream);
286291 const showThoughts = Boolean(request.body.show_thoughts);
292+ const isThinking = model.includes('thinking');
287293
288294 const generationConfig = {
289295 stopSequences: request.body.stop,
@@ -324,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
324330 body.systemInstruction = prompt.system_instruction;
325331 }
326332
333+ if (isThinking && showThoughts) {
334+ generationConfig.thinkingConfig = {
335+ includeThoughts: true,
336+ };
337+ }
338+
327339 return body;
328340 }
329341
@@ -337,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
337349 controller.abort();
338350 });
339351
340- const isThinking = model.includes('thinking');
341352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
342353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
343354
@@ -382,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
382393 const responseContent = candidates[0].content ?? candidates[0].output;
383394 console.log('Google AI Studio response:', responseContent);
384395
385- if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {
396+ const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
386- responseContent.parts = responseContent.parts.filter(part => !part.thought);
387- }
388-
389- const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
390397 if (!responseText) {
391398 let message = 'Google AI Studio Candidate text empty';
392399 console.log(message, generateResponseJson);
@@ -394,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
394401 }
395402
396403 // Wrap it back to OAI format
397404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
398405 return response.send(reply);
399406 }
400407 } catch (error) {
@@ -636,6 +643,89 @@ async function sendCohereRequest(request, response) {
636643 }
637644}
638645
646+/**
647+ * Sends a request to DeepSeek API.
648+ * @param {express.Request} request Express request
649+ * @param {express.Response} response Express response
650+ */
651+async function sendDeepSeekRequest(request, response) {
652+ const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
653+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
654+
655+ if (!apiKey && !request.body.reverse_proxy) {
656+ console.log('DeepSeek API key is missing.');
657+ return response.status(400).send({ error: true });
658+ }
659+
660+ const controller = new AbortController();
661+ request.socket.removeAllListeners('close');
662+ request.socket.on('close', function () {
663+ controller.abort();
664+ });
665+
666+ try {
667+ let bodyParams = {};
668+
669+ if (request.body.logprobs > 0) {
670+ bodyParams['top_logprobs'] = request.body.logprobs;
671+ bodyParams['logprobs'] = true;
672+ }
673+
674+ const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
675+ const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
676+
677+ const requestBody = {
678+ 'messages': processedMessages,
679+ 'model': request.body.model,
680+ 'temperature': request.body.temperature,
681+ 'max_tokens': request.body.max_tokens,
682+ 'stream': request.body.stream,
683+ 'presence_penalty': request.body.presence_penalty,
684+ 'frequency_penalty': request.body.frequency_penalty,
685+ 'top_p': request.body.top_p,
686+ 'stop': request.body.stop,
687+ 'seed': request.body.seed,
688+ ...bodyParams,
689+ };
690+
691+ const config = {
692+ method: 'POST',
693+ headers: {
694+ 'Content-Type': 'application/json',
695+ 'Authorization': 'Bearer ' + apiKey,
696+ },
697+ body: JSON.stringify(requestBody),
698+ signal: controller.signal,
699+ };
700+
701+ console.log('DeepSeek request:', requestBody);
702+
703+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
704+
705+ if (request.body.stream) {
706+ forwardFetchResponse(generateResponse, response);
707+ } else {
708+ if (!generateResponse.ok) {
709+ const errorText = await generateResponse.text();
710+ console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
711+ const errorJson = tryParse(errorText) ?? { error: true };
712+ return response.status(500).send(errorJson);
713+ }
714+ const generateResponseJson = await generateResponse.json();
715+ console.log('DeepSeek response:', generateResponseJson);
716+ return response.send(generateResponseJson);
717+ }
718+ } catch (error) {
719+ console.log('Error communicating with DeepSeek API: ', error);
720+ if (!response.headersSent) {
721+ response.send({ error: true });
722+ } else {
723+ response.end();
724+ }
725+ }
726+}
727+
728+
639729export const router = express.Router();
640730
641731router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@@ -680,8 +770,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
680770 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
681771 headers = {};
682772 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
683773 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
684774 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
685775 headers = {};
686776 } else {
687777 console.log('This chat completion source is not supported yet.');
@@ -777,6 +867,14 @@ router.post('/bias', jsonParser, async function (request, response) {
777867 return response.send({});
778868 }
779869 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
870+ } else if (webTokenizers.includes(model)) {
871+ const tokenizer = getWebTokenizer(model);
872+ const instance = await tokenizer?.get();
873+ if (!instance) {
874+ console.warn('Tokenizer not initialized:', model);
875+ return response.send({});
876+ }
877+ encodeFunction = (text) => new Uint32Array(instance.encode(text));
780878 } else {
781879 const tokenizer = getTiktokenTokenizer(model);
782880 encodeFunction = (tokenizer.encode.bind(tokenizer));
@@ -841,6 +939,7 @@ router.post('/generate', jsonParser, function (request, response) {
841939 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
842940 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
843941 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
942+ case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
844943 }
845944
846945 let apiUrl;
@@ -899,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
899998 bodyParams['route'] = 'fallback';
900999 }
9011000
1001+ if (request.body.show_thoughts) {
1002+ bodyParams['include_reasoning'] = true;
1003+ }
1004+
9021005 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
9031006 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
9041007 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -954,18 +1057,6 @@ router.post('/generate', jsonParser, function (request, response) {
9541057 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
9551058 headers = {};
9561059 bodyParams = {};
957- } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
958- apiUrl = API_DEEPSEEK;
959- apiKey = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
960- headers = {};
961- bodyParams = {};
962-
963- if (request.body.logprobs > 0) {
964- bodyParams['top_logprobs'] = request.body.logprobs;
965- bodyParams['logprobs'] = true;
966- }
967-
968- request.body.messages = postProcessPrompt(request.body.messages, 'deepseek', getPromptNames(request));
9691060 } else {
9701061 console.log('This chat completion source is not supported yet.');
9711062 return response.status(400).send({ error: true });
@@ -1103,4 +1194,3 @@ router.post('/generate', jsonParser, function (request, response) {
11031194 }
11041195 }
11051196});
1106-
src/endpoints/backgrounds.js+2 -1
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename';
77import { jsonParser, urlencodedParser } from '../express-common.js';
88import { invalidateThumbnail } from './thumbnails.js';
99import { getImages } from '../util.js';
10+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1011
1112export const router = express.Router();
1213
@@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) {
1516 response.send(JSON.stringify(images));
1617});
1718
1819router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) {
1920 if (!request.body) return response.sendStatus(400);
2021
2122 if (request.body.bg !== sanitize(request.body.bg)) {
src/endpoints/characters.js+21 -14
@@ -14,6 +14,7 @@ import jimp from 'jimp';
1414
1515import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1616import { jsonParser, urlencodedParser } from '../express-common.js';
17+import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
1718import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
1819import { TavernCardValidator } from '../validator/TavernCardValidator.js';
1920import { parse, write } from '../character-card-parser.js';
@@ -73,12 +74,18 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
7374 * Read the image, resize, and save it as a PNG into the buffer.
7475 * @returns {Promise<Buffer>} Image buffer
7576 */
7677 async function getInputImage() {
77- if (Buffer.isBuffer(inputFile)) {
78+ try {
78- return parseImageBuffer(inputFile, crop);
79+ if (Buffer.isBuffer(inputFile)) {
79- }
80+ return await parseImageBuffer(inputFile, crop);
81+ }
8082
8183 return await tryReadImage(inputFile, crop);
84+ } catch (error) {
85+ const message = Buffer.isBuffer(inputFile) ? 'Failed to read image buffer.' : `Failed to read image: ${inputFile}.`;
86+ console.warn(message, 'Using a fallback image.', error);
87+ return await fs.promises.readFile(defaultAvatarPath);
88+ }
8289 }
8390
8491 const inputImage = await getInputImage();
@@ -756,7 +763,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
756763 }
757764});
758765
759766router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
760767 if (!request.body.avatar_url || !request.body.new_name) {
761768 return response.sendStatus(400);
762769 }
@@ -803,7 +810,7 @@ router.post('/rename', jsonParser, async function (request, response) {
803810 }
804811});
805812
806813router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
807814 if (!request.body) {
808815 console.error('Error: no response body detected');
809816 response.status(400).send('Error: no response body detected');
@@ -852,7 +859,7 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852859 * @param {Object} response - The HTTP response object.
853860 * @returns {void}
854861 */
855862router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856863 console.log(request.body);
857864 if (!request.body) {
858865 console.error('Error: no response body detected');
@@ -898,7 +905,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898905 *
899906 * @returns {void}
900907 * */
901908router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902909 try {
903910 const update = request.body;
904911 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -929,7 +936,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929936 }
930937});
931938
932939router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933940 if (!request.body || !request.body.avatar_url) {
934941 return response.sendStatus(400);
935942 }
@@ -992,7 +999,7 @@ router.post('/all', jsonParser, async function (request, response) {
992999 }
9931000});
9941001
9951002router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
9961003 try {
9971004 if (!request.body) return response.sendStatus(400);
9981005 const item = request.body.avatar_url;
@@ -1011,7 +1018,7 @@ router.post('/get', jsonParser, async function (request, response) {
10111018 }
10121019});
10131020
10141021router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
10151022 if (!request.body) return response.sendStatus(400);
10161023
10171024 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1160,7 +1167,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11601167 }
11611168});
11621169
11631170router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
11641171 try {
11651172 if (!request.body.avatar_url) {
11661173 console.log('avatar URL not found in request body');
@@ -1207,7 +1214,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
12071214 }
12081215});
12091216
12101217router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
12111218 try {
12121219 if (!request.body.format || !request.body.avatar_url) {
12131220 return response.sendStatus(400);
src/endpoints/chats.js+8 -7
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import _ from 'lodash';
1010
1111import { jsonParser, urlencodedParser } from '../express-common.js';
12+import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
1213import {
1314 getConfigValue,
1415 humanizedISO8601DateTime,
@@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) {
294295
295296export const router = express.Router();
296297
297298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
298299 try {
299300 const directoryName = String(request.body.avatar_url).replace('.png', '');
300301 const chatData = request.body.chat;
@@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) {
310311 }
311312});
312313
313314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
314315 try {
315316 const dirName = String(request.body.avatar_url).replace('.png', '');
316317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
347348});
348349
349350
350351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
351352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
352353 return response.sendStatus(400);
353354 }
@@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) {
372373 return response.send({ ok: true, sanitizedFileName });
373374});
374375
375376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
376377 const dirName = String(request.body.avatar_url).replace('.png', '');
377378 const fileName = String(request.body.chatfile);
378379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
@@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) {
388389 return response.send('ok');
389390});
390391
391392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
392393 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
393394 return response.sendStatus(400);
394395 }
@@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
478479 }
479480});
480481
481482router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482483 if (!request.body) return response.sendStatus(400);
483484
484485 const format = request.body.file_type;
@@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => {
626627 return response.send({ ok: true });
627628});
628629
629630router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
630631 try {
631632 const { query, avatar_url, group_id } = request.body;
632633 let chatFiles = [];
src/endpoints/settings.js+5 -4
@@ -9,9 +9,10 @@ import { SETTINGS_FILE } from '../constants.js';
99import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
1010import { jsonParser } from '../express-common.js';
1111import { getAllUserHandles, getUserDirectories } from '../users.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314const ENABLE_EXTENSIONS = !!getConfigValue('enableExtensionsextensions.enabled', true);
1415const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('enableExtensionsAutoUpdateextensions.autoUpdate', true);
1516const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1617
1718// 10 minutes
@@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
296297 }
297298});
298299
299300router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
300301 try {
301302 const userFilesPattern = getFilePrefix(request.user.profile.handle);
302303
@@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
330331 }
331332});
332333
333334router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
334335 try {
335336 const userFilesPattern = getFilePrefix(request.user.profile.handle);
336337
src/endpoints/tokenizers.js+42 -0
@@ -238,6 +238,15 @@ export const sentencepieceTokenizers = [
238238 'jamba',
239239];
240240
241+export const webTokenizers = [
242+ 'claude',
243+ 'llama3',
244+ 'command-r',
245+ 'qwen2',
246+ 'nemo',
247+ 'deepseek',
248+];
249+
241250/**
242251 * Gets the Sentencepiece tokenizer by the model name.
243252 * @param {string} model Sentencepiece model name
@@ -276,6 +285,39 @@ export function getSentencepiceTokenizer(model) {
276285}
277286
278287/**
288+ * Gets the Web tokenizer by the model name.
289+ * @param {string} model Web tokenizer model name
290+ * @returns {WebTokenizer|null} Web tokenizer
291+ */
292+export function getWebTokenizer(model) {
293+ if (model.includes('llama3')) {
294+ return llama3_tokenizer;
295+ }
296+
297+ if (model.includes('claude')) {
298+ return claude_tokenizer;
299+ }
300+
301+ if (model.includes('command-r')) {
302+ return commandTokenizer;
303+ }
304+
305+ if (model.includes('qwen2')) {
306+ return qwen2Tokenizer;
307+ }
308+
309+ if (model.includes('nemo')) {
310+ return nemoTokenizer;
311+ }
312+
313+ if (model.includes('deepseek')) {
314+ return deepseekTokenizer;
315+ }
316+
317+ return null;
318+}
319+
320+/**
279321 * Counts the token ids for the given text using the Sentencepiece tokenizer.
280322 * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
281323 * @param {string} text Text to tokenize
src/endpoints/users-private.js+1 -0
@@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => {
2323 }
2424
2525 request.session.handle = null;
26+ request.session.csrfToken = null;
2627 request.session = null;
2728 return response.sendStatus(204);
2829 } catch (error) {
src/endpoints/vectors.js+1 -1
@@ -164,7 +164,7 @@ function getSourceSettings(source, request) {
164164 };
165165 case 'transformers':
166166 return {
167167 model: getConfigValue('extrasextensions.embeddingModelmodels.embedding', ''),
168168 };
169169 case 'palm':
170170 return {
src/middleware/basicAuth.js+7 -6
@@ -5,17 +5,18 @@
55import { Buffer } from 'node:buffer';
66import storage from 'node-persist';
77import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
88import { getConfig, getConfigValue, safeReadFileSync } from '../util.js';
99
1010const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
1111const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1212
13-const unauthorizedResponse = (res) => {
14- res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
15- return res.status(401).send('Authentication required');
16-};
17-
1813const basicAuthMiddleware = async function (request, response, callback) {
14+ const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? '';
15+ const unauthorizedResponse = (res) => {
16+ res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
17+ return res.status(401).send(unauthorizedWebpage);
18+ };
19+
1920 const config = getConfig();
2021 const authHeader = request.headers.authorization;
2122
src/middleware/validateFileName.js+34 -0
@@ -0,0 +1,34 @@
1+import path from 'node:path';
2+
3+/**
4+ * Gets a middleware function that validates the field in the request body.
5+ * @param {string} fieldName Field name
6+ * @returns {import('express').RequestHandler} Middleware function
7+ */
8+export function getFileNameValidationFunction(fieldName) {
9+ /**
10+ * Validates the field in the request body.
11+ * @param {import('express').Request} req Request object
12+ * @param {import('express').Response} res Response object
13+ * @param {import('express').NextFunction} next Next middleware
14+ */
15+ return function validateAvatarUrlMiddleware(req, res, next) {
16+ if (req.body && fieldName in req.body && typeof req.body[fieldName] === 'string') {
17+ const forbiddenRegExp = path.sep === '/' ? /[/\x00]/ : /[/\x00\\]/;
18+ if (forbiddenRegExp.test(req.body[fieldName])) {
19+ console.error('An error occurred while validating the request body', {
20+ handle: req.user.profile.handle,
21+ path: req.originalUrl,
22+ field: fieldName,
23+ value: req.body[fieldName],
24+ });
25+ return res.sendStatus(400);
26+ }
27+ }
28+
29+ next();
30+ };
31+}
32+
33+const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url');
34+export default avatarUrlValidationFunction;
src/middleware/whitelist.js+16 -5
@@ -1,10 +1,11 @@
11import path from 'node:path';
22import fs from 'node:fs';
33import process from 'node:process';
4+import Handlebars from 'handlebars';
45import ipMatching from 'ip-matching';
56
67import { getIpFromRequest } from '../express-common.js';
78import { color, getConfigValue, safeReadFileSync } from '../util.js';
89
910const whitelistPath = path.join(process.cwd(), './whitelist.txt');
1011const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
@@ -52,12 +53,16 @@ function getForwardedIp(req) {
5253 * @returns {import('express').RequestHandler} The middleware function
5354 */
5455export default function whitelistMiddleware(whitelistMode, listen) {
56+ const forbiddenWebpage = Handlebars.compile(
57+ safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
58+ );
59+
5560 return function (req, res, next) {
5661 const clientIp = getIpFromRequest(req);
5762 const forwardedIp = getForwardedIp(req);
63+ const userAgent = req.headers['user-agent'];
5864
5965 if (listen && !knownIPs.has(clientIp)) {
60- const userAgent = req.headers['user-agent'];
6166 console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
6267 knownIPs.add(clientIp);
6368
@@ -76,9 +81,15 @@ export default function whitelistMiddleware(whitelistMode, listen) {
7681 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
7782 ) {
7883 // Log the connection attempt with real IP address
79- const ipDetails = forwardedIp ? `${clientIp} (forwarded from ${forwardedIp})` : clientIp;
84+ const ipDetails = forwardedIp
80- console.log(color.red('Forbidden: Connection attempt from ' + ipDetails + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n'));
85+ ? `${clientIp} (forwarded from ${forwardedIp})`
81- return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + ipDetails + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.');
86+ : clientIp;
87+ console.log(
88+ color.red(
89+ `Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
90+ ),
91+ );
92+ return res.status(403).send(forbiddenWebpage({ ipDetails }));
8293 }
8394 next();
8495 };
src/prompt-converters.js+2 -0
@@ -360,6 +360,8 @@ export function convertCohereMessages(messages, names) {
360360 */
361361export function convertGooglePrompt(messages, model, useSysPrompt, names) {
362362 const visionSupportedModels = [
363+ 'gemini-2.0-flash-thinking-exp',
364+ 'gemini-2.0-flash-thinking-exp-01-21',
363365 'gemini-2.0-flash-thinking-exp-1219',
364366 'gemini-2.0-flash-exp',
365367 'gemini-1.5-flash',
src/transformers.js+6 -6
@@ -19,31 +19,31 @@ const tasks = {
1919 'text-classification': {
2020 defaultModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx',
2121 pipeline: null,
2222 configField: 'extrasextensions.classificationModelmodels.classification',
2323 quantized: true,
2424 },
2525 'image-to-text': {
2626 defaultModel: 'Xenova/vit-gpt2-image-captioning',
2727 pipeline: null,
2828 configField: 'extrasextensions.captioningModelmodels.captioning',
2929 quantized: true,
3030 },
3131 'feature-extraction': {
3232 defaultModel: 'Xenova/all-mpnet-base-v2',
3333 pipeline: null,
3434 configField: 'extrasextensions.embeddingModelmodels.embedding',
3535 quantized: true,
3636 },
3737 'automatic-speech-recognition': {
3838 defaultModel: 'Xenova/whisper-small',
3939 pipeline: null,
4040 configField: 'extrasextensions.speechToTextModelmodels.speechToText',
4141 quantized: true,
4242 },
4343 'text-to-speech': {
4444 defaultModel: 'Xenova/speecht5_tts',
4545 pipeline: null,
4646 configField: 'extrasextensions.textToSpeechModelmodels.textToSpeech',
4747 quantized: false,
4848 },
4949};
@@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') {
132132
133133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');
134134 const model = forceModel || getModelForTask(task);
135135 const localOnly = !getConfigValue('extrasextensions.disableAutoDownloadmodels.autoDownload', falsetrue);
136136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
137137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
138138 tasks[task].pipeline = instance;
src/users.js+2 -1
@@ -458,7 +458,8 @@ export function getPasswordSalt() {
458458 */
459459export function getCookieSessionName() {
460460 // Get server hostname and hash it to generate a session suffix
461- const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8);
461+ const hostname = os.hostname() || 'localhost';
462+ const suffix = crypto.createHash('sha256').update(hostname).digest('hex').slice(0, 8);
462463 return `session-${suffix}`;
463464}
464465
src/util.js+11 -0
@@ -871,3 +871,14 @@ export class MemoryLimitedMap {
871871 return this.map[Symbol.iterator]();
872872 }
873873}
874+
875+/**
876+ * A 'safe' version of `fs.readFileSync()`. Returns the contents of a file if it exists, falling back to a default value if not.
877+ * @param {string} filePath Path of the file to be read.
878+ * @param {Parameters<typeof fs.readFileSync>[1]} options Options object to pass through to `fs.readFileSync()` (default: `{ encoding: 'utf-8' }`).
879+ * @returns The contents at `filePath` if it exists, or `null` if not.
880+ */
881+export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) {
882+ if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options);
883+ return null;
884+}