Merge pull request #3493 from SillyTavern/staging Staging

75aec772719006bdefcdfa2f5a0fb0ac0d257158

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

Signed
148 files changed, +3871 -1204Ignore 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+32 -16
@@ -6,7 +6,13 @@ cardsCacheCapacity: 100
66# -- SERVER CONFIGURATION --
77# Listen for incoming connections
88listen: false
9+# Listen on a specific address, supports IPv4 and IPv6
10+listenAddress:
11+ ipv4: 0.0.0.0
12+ ipv6: '[::]'
913# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
14+# - Use option "auto" to automatically detect support
15+# - Use true or false (no qoutes) to enable or disable each protocol
1016protocol:
1117 ipv4: true
1218 ipv6: false
@@ -65,12 +71,14 @@ autheliaAuth: false
6571# the username and passwords for basic auth are the same as those
6672# for the individual accounts
6773perUserBasicAuth: false
74+# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
75+minLogLevel: 0
6876
6977# User session timeout *in seconds* (defaults to 24 hours).
7078## Set to a positive number to expire session after a certain time of inactivity
7179## Set to 0 to expire session when the browser is closed
7280## Set to a negative number to disable session expiration
7381sessionTimeout: 86400-1
7482# Used to sign session cookies. Will be auto-generated if not set
7583cookieSecret: ''
7684# Disable CSRF protection - NOT RECOMMENDED
@@ -133,24 +141,26 @@ whitelistImportDomains:
133141## headers:
134142## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
135143requestOverrides: []
136-# -- EXTENSIONS CONFIGURATION --
144+
137145# Enable UIEXTENSIONS extensionsCONFIGURATION
138-enableExtensions: true
146+extensions:
139-# Automatically update extensions when a release version changes
147+ # Enable UI extensions
140148enableExtensionsAutoUpdate enabled: true
149+ # Automatically update extensions when a release version changes
150+ autoUpdate: true
151+ models:
152+ # Enables automatic model download from HuggingFace
153+ autoDownload: true
154+ # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
155+ classification: Cohee/distilbert-base-uncased-go-emotions-onnx
156+ captioning: Xenova/vit-gpt2-image-captioning
157+ embedding: Cohee/jina-embeddings-v2-base-en
158+ speechToText: Xenova/whisper-small
159+ textToSpeech: Xenova/speecht5_tts
160+
141161# Additional model tokenizers can be downloaded on demand.
142162# Disabling will fallback to another locally available tokenizer.
143163enableDownloadableTokenizers: 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
154164# -- OPENAI CONFIGURATION --
155165# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
156166promptPlaceholder: "[Start a new chat]"
@@ -177,6 +187,10 @@ ollama:
177187 # * 0: Unload the model immediately after the request
178188 # * N (any positive number): Keep the model loaded for N seconds after the request.
179189 keepAlive: -1
190+ # Controls the "num_batch" (batch size) parameter of the generation request
191+ # * -1: Use the default value of the model
192+ # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
193+ batchSize: -1
180194# -- ANTHROPIC CLAUDE API CONFIGURATION --
181195claude:
182196 # Enables caching of the system prompt (if supported).
@@ -196,3 +210,5 @@ claude:
196210 cachingAtDepth: -1
197211# -- SERVER PLUGIN CONFIGURATION --
198212enableServerPlugins: false
213+# Attempt to automatically update server plugins on startup
214+enableServerPluginsAutoUpdate: true
default/content/index.json+8 -4
@@ -672,10 +672,6 @@
672672 "type": "moving_ui"
673673 },
674674 {
675- "filename": "presets/moving-ui/Black Magic Time.json",
676- "type": "moving_ui"
677- },
678- {
679675 "filename": "presets/quick-replies/Default.json",
680676 "type": "quick_replies"
681677 },
@@ -782,5 +778,13 @@
782778 {
783779 "filename": "presets/context/Mistral V7.json",
784780 "type": "context"
781+ },
782+ {
783+ "filename": "presets/instruct/DeepSeek-V2.5.json",
784+ "type": "instruct"
785+ },
786+ {
787+ "filename": "presets/context/DeepSeek-V2.5.json",
788+ "type": "context"
785789 }
786790]
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/content/presets/moving-ui/Black Magic Time.json+0 -45
@@ -1,45 +0,0 @@
1-{
2- "name": "Black Magic Time",
3- "movingUIState": {
4- "sheld": {
5- "top": 488,
6- "left": 1407,
7- "right": 1,
8- "bottom": 4,
9- "margin": "unset",
10- "width": 471,
11- "height": 439
12- },
13- "floatingPrompt": {
14- "width": 369,
15- "height": 441
16- },
17- "right-nav-panel": {
18- "top": 0,
19- "left": 1400,
20- "right": 111,
21- "bottom": 446,
22- "margin": "unset",
23- "width": 479,
24- "height": 487
25- },
26- "WorldInfo": {
27- "top": 41,
28- "left": 369,
29- "right": 642,
30- "bottom": 51,
31- "margin": "unset",
32- "width": 1034,
33- "height": 858
34- },
35- "left-nav-panel": {
36- "top": 442,
37- "left": 0,
38- "right": 1546,
39- "bottom": 25,
40- "margin": "unset",
41- "width": 368,
42- "height": 483
43- }
44- }
45-}
45 \ No newline at end of file
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+38 -7
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.12.1112",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.12.1112",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
@@ -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",
@@ -41,6 +41,7 @@
4141 "html-entities": "^2.5.2",
4242 "iconv-lite": "^0.6.3",
4343 "ip-matching": "^2.1.2",
44+ "ip-regex": "^5.0.0",
4445 "ipaddr.js": "^2.0.1",
4546 "jimp": "^0.22.10",
4647 "localforage": "^1.10.0",
@@ -86,6 +87,7 @@
8687 "@types/cookie-session": "^2.0.49",
8788 "@types/cors": "^2.8.17",
8889 "@types/deno": "^2.0.0",
90+ "@types/dompurify": "^3.0.5",
8991 "@types/express": "^4.17.21",
9092 "@types/jquery": "^3.5.29",
9193 "@types/jquery-cropper": "^1.0.4",
@@ -1179,6 +1181,16 @@
11791181 "dev": true,
11801182 "license": "MIT"
11811183 },
1184+ "node_modules/@types/dompurify": {
1185+ "version": "3.0.5",
1186+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
1187+ "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
1188+ "dev": true,
1189+ "license": "MIT",
1190+ "dependencies": {
1191+ "@types/trusted-types": "*"
1192+ }
1193+ },
11821194 "node_modules/@types/estree": {
11831195 "version": "1.0.6",
11841196 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1462,6 +1474,13 @@
14621474 "@types/jquery": "*"
14631475 }
14641476 },
1477+ "node_modules/@types/trusted-types": {
1478+ "version": "2.0.7",
1479+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1480+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1481+ "dev": true,
1482+ "license": "MIT"
1483+ },
14651484 "node_modules/@types/write-file-atomic": {
14661485 "version": "4.0.3",
14671486 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
@@ -2987,10 +3006,10 @@
29873006 "node": "*"
29883007 }
29893008 },
29903009 "node_modules/csrf-csrfsync": {
29913010 "version": "24.20.43",
29923011 "resolved": "https://registry.npmjs.org/csrf-csrfsync/-/csrf-csrfsync-24.20.43.tgz",
29933012 "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRTwXzltBBzt/YMl1Njo7imzDt6ZT7G/yKh2L9AYsIwSlTPnx2AaxQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==",
29943013 "license": "ISC",
29953014 "dependencies": {
29963015 "http-errors": "^2.0.0"
@@ -4610,6 +4629,18 @@
46104629 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
46114630 "license": "LGPL-3.0-only"
46124631 },
4632+ "node_modules/ip-regex": {
4633+ "version": "5.0.0",
4634+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
4635+ "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
4636+ "license": "MIT",
4637+ "engines": {
4638+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4639+ },
4640+ "funding": {
4641+ "url": "https://github.com/sponsors/sindresorhus"
4642+ }
4643+ },
46134644 "node_modules/ipaddr.js": {
46144645 "version": "2.1.0",
46154646 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
package.json+5 -2
@@ -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",
@@ -31,6 +31,7 @@
3131 "html-entities": "^2.5.2",
3232 "iconv-lite": "^0.6.3",
3333 "ip-matching": "^2.1.2",
34+ "ip-regex": "^5.0.0",
3435 "ipaddr.js": "^2.0.1",
3536 "jimp": "^0.22.10",
3637 "localforage": "^1.10.0",
@@ -86,9 +87,10 @@
8687 "type": "git",
8788 "url": "https://github.com/SillyTavern/SillyTavern.git"
8889 },
8990 "version": "1.12.1112",
9091 "scripts": {
9192 "start": "node server.js",
93+ "debug": "node server.js --inspect",
9294 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
9395 "start:bun": "bun server.js",
9496 "start:no-csrf": "node server.js --disableCsrf",
@@ -114,6 +116,7 @@
114116 "@types/cookie-session": "^2.0.49",
115117 "@types/cors": "^2.8.17",
116118 "@types/deno": "^2.0.0",
119+ "@types/dompurify": "^3.0.5",
117120 "@types/express": "^4.17.21",
118121 "@types/jquery": "^3.5.29",
119122 "@types/jquery-cropper": "^1.0.4",
plugins.js+7 -0
@@ -48,6 +48,13 @@ async function updatePlugins() {
4848 console.log(`Updating plugin ${color.green(directory)}...`);
4949 const pluginPath = path.join(pluginsPath, directory);
5050 const pluginRepo = git(pluginPath);
51+
52+ const isRepo = await pluginRepo.checkIsRepo();
53+ if (!isRepo) {
54+ console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
55+ continue;
56+ }
57+
5158 await pluginRepo.fetch();
5259 const commitHash = await pluginRepo.revparse(['HEAD']);
5360 const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);
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/css/mobile-styles.css+0 -2
@@ -216,8 +216,6 @@
216216
217217 }
218218
219- #showRawPrompt,
220- #copyPromptToClipboard,
221219 #groupCurrentMemberPopoutButton,
222220 #summaryExtensionPopoutButton {
223221 display: none;
public/css/popup.css+4 -0
@@ -72,6 +72,10 @@ dialog {
7272 overflow-x: auto;
7373}
7474
75+.popup.left_aligned_dialogue_popup .popup-content {
76+ text-align: start;
77+}
78+
7579/* Opening animation */
7680.popup[opening] {
7781 animation: pop-in var(--popup-animation-speed) ease-in-out;
public/css/select2-overrides.css+7 -0
@@ -100,6 +100,13 @@
100100 border: 1px solid var(--SmartThemeBorderColor);
101101}
102102
103+.select2-container .select2-results .select2-results__option--disabled {
104+ color: inherit;
105+ background-color: inherit;
106+ cursor: not-allowed;
107+ filter: brightness(0.5);
108+}
109+
103110.select2-container .select2-selection--multiple .select2-selection__choice,
104111.select2-container .select2-selection--single .select2-selection__choice {
105112 border-radius: 5px;
public/css/toggle-dependent.css+9 -0
@@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
472472 display: none;
473473}
474474
475+label[for="trim_spaces"]:not(:has(input:checked)) small {
476+ color: var(--warning);
477+ opacity: 1;
478+}
479+
475480#claude_function_prefill_warning {
476481 display: none;
477482 color: red;
@@ -488,3 +493,7 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
488493#mistralai_other_models:empty {
489494 display: none;
490495}
496+
497+#banned_tokens_block_ooba:not(:has(#send_banned_tokens_textgenerationwebui:checked)) #banned_tokens_controls_ooba {
498+ filter: brightness(0.5);
499+}
public/index.html+204 -113
@@ -730,7 +730,7 @@
730730 <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">
731731 </div>
732732 <div class="range-block-counter">
733733 <input type="number" min="0" max="200500" step="1" data-for="top_k_openai" id="top_k_counter_openai">
734734 </div>
735735 </div>
736736 </div>
@@ -1587,6 +1587,10 @@
15871587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
15881588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
15891589 </label>
1590+ <label data-tg-type="openrouter" class="checkbox_label flexGrow flexShrink" for="include_reasoning_textgenerationwebui">
1591+ <input type="checkbox" id="include_reasoning_textgenerationwebui" />
1592+ <small data-i18n="Request Model Reasoning">Request Model Reasoning</small>
1593+ </label>
15901594 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
15911595 <input type="checkbox" id="temperature_last_textgenerationwebui" />
15921596 <label>
@@ -1617,17 +1621,34 @@
16171621 </div>
16181622 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">
16191623 <hr class="width100p">
16201624 <h4div class="range-block-title justifyCentertitle_restorable">
1621- <span data-i18n="Banned Tokens">Banned Tokens/Strings</span>
1625+ <div>
1622- <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>
1626+ <strong data-i18n="Banned Tokens">Banned Tokens/Strings</strong>
1623- </h4>
1627+ <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]LLaMA / Mistral / Yi models only" title="Enter sequences you don't want to appear in the output.&#13;Unquoted text will be tokenized in the back end and banned as tokens.&#13;[token ids] will be banned as-is.&#13;Most tokens have a leading space. Use token counter (with the correct tokenizer selected first!) if you are unsure.&#13;Enclose text in double quotes to ban the entire string as a set.&#13;Quoted Strings and [Token ids] must be on their own line."></div>
1624- <div class="wide100p">
1628+ </div>
1625- <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1629+ <label id="send_banned_tokens_label" for="send_banned_tokens_textgenerationwebui" class="checkbox_label">
1630+ <input id="send_banned_tokens_textgenerationwebui" type="checkbox" style="display:none;" />
1631+ <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
1632+ </label>
1633+ </div>
1634+ <div id="banned_tokens_controls_ooba">
1635+ <div class="textAlignCenter">
1636+ <small data-i18n="Global list">Global list</small>
1637+ </div>
1638+ <div class="wide100p marginBot10">
1639+ <textarea id="global_banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="global_banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1640+ </div>
1641+ <div class="textAlignCenter">
1642+ <small data-i18n="Preset-specific list">Preset-specific list</small>
1643+ </div>
1644+ <div class="wide100p">
1645+ <textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" data-i18n="[placeholder]Example: some text [42, 69, 1337]" placeholder='some text as tokens&#10;[420, 69, 1337]&#10;"Some verbatim string"'></textarea>
1646+ </div>
16261647 </div>
16271648 </div>
16281649 <div class="range-block wide100p">
16291650 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
16301651 <spanstrong data-i18n="Logit Bias">Logit Bias</spanstrong>
16311652 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
16321653 <i class="fa-xs fa-solid fa-plus"></i>
16331654 <small data-i18n="Add">Add</small>
@@ -1930,7 +1951,7 @@
19301951 </span>
19311952 </div>
19321953 </div>
19331954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
19341955 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19351956 <input id="openai_function_calling" type="checkbox" />
19361957 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1953,14 +1974,16 @@
19531974 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
19541975 </div>
19551976 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
1956- <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1977+ <div class="flex-container oneline-dropdown">
1957- Inline Image Quality
1978+ <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1958- </label>
1979+ Inline Image Quality
1959- <select id="openai_inline_image_quality">
1980+ </label>
1960- <option data-i18n="openai_inline_image_quality_auto" value="auto">Auto</option>
1981+ <select id="openai_inline_image_quality">
19611982 <option data-i18n="openai_inline_image_quality_lowopenai_inline_image_quality_auto" value="lowauto">LowAuto</option>
19621983 <option data-i18n="openai_inline_image_quality_highopenai_inline_image_quality_low" value="highlow">HighLow</option>
1963- </select>
1984+ <option data-i18n="openai_inline_image_quality_high" value="high">High</option>
1985+ </select>
1986+ </div>
19641987 </div>
19651988 </div>
19661989 <div class="range-block" data-source="makersuite">
@@ -1977,20 +2000,32 @@
19772000 </span>
19782001 </div>
19792002 </div>
19802003 <div class="range-block" data-source="makersuitedeepseek,openrouter">
19812004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19822005 <input id="openai_show_thoughts" type="checkbox" />
19832006 <span>
19842007 <span data-i18n="ShowRequest model thoughtsreasoning">ShowRequest model thoughtsreasoning</span>
19852008 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0DeepSeek ThinkingReasoner"></i>
19862009 </span>
19872010 </label>
19882011 <div class="toggle-description justifyLeft marginBot5">
19892012 <span data-i18n="DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.">
19902013 DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.
19912014 </span>
19922015 </div>
19932016 </div>
2017+ <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
2018+ <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.">
2019+ <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
2020+ Reasoning Effort
2021+ </label>
2022+ <select id="openai_reasoning_effort">
2023+ <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
2024+ <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
2025+ <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2026+ </select>
2027+ </div>
2028+ </div>
19942029 <div class="range-block" data-source="claude">
19952030 <div class="wide100p">
19962031 <div class="flex-container alignItemsCenter">
@@ -2692,7 +2727,7 @@
26922727 <option value="windowai">Window AI</option>
26932728 </optgroup>
26942729 </select>
26952730 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">
26962731 <div class="inline-drawer-toggle inline-drawer-header">
26972732 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
26982733 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2755,7 +2790,7 @@
27552790 </div>
27562791 </div>
27572792 </div>
27582793 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">
27592794 <div class="reverse_proxy_warning">
27602795 <b>
27612796 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -2805,27 +2840,6 @@
28052840 <div>
28062841 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
28072842 <select id="model_openai_select">
2808- <optgroup label="GPT-3.5 Turbo">
2809- <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2810- <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2811- <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2812- <option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option>
2813- <option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option>
2814- <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
2815- <option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option>
2816- </optgroup>
2817- <optgroup label="GPT-3.5 Turbo Instruct">
2818- <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2819- <option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option>
2820- </optgroup>
2821- <optgroup label="GPT-4">
2822- <option value="gpt-4">gpt-4</option>
2823- <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2824- <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2825- <option value="gpt-4-32k">gpt-4-32k</option>
2826- <option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
2827- <option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
2828- </optgroup>
28292843 <optgroup label="GPT-4o">
28302844 <option value="gpt-4o">gpt-4o</option>
28312845 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@@ -2833,29 +2847,44 @@
28332847 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
28342848 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
28352849 </optgroup>
28362850 <optgroup label="gptGPT-4o- mini">
28372851 <option value="gpt-4o-mini">gpt-4o-mini</option>
28382852 <option value="gpt-4o-mini-2024-0711-1820">gpt-4o-mini-2024-0711-1820</option>
2853+ <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2854+ <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2855+ <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2856+ </optgroup>
2857+ <optgroup label="o1 and o1-mini">
2858+ <option value="o1">o1</option>
2859+ <option value="o1-2024-12-17">o1-2024-12-17</option>
2860+ <option value="o1-mini">o1-mini</option>
2861+ <option value="o1-mini-2024-09-12">o1-mini-2024-09-12</option>
2862+ <option value="o1-preview">o1-preview</option>
2863+ <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
2864+ </optgroup>
2865+ <optgroup label="o3">
2866+ <option value="o3-mini">o3-mini</option>
2867+ <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
28392868 </optgroup>
28402869 <optgroup label="GPT-4 Turbo and GPT-4">
28412870 <option value="gpt-4-turbo">gpt-4-turbo</option>
28422871 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
28432872 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>
2844- <option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
28452873 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>
28462874 <option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>
2875+ <option value="gpt-4">gpt-4</option>
2876+ <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2877+ <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
28472878 </optgroup>
28482879 <optgroup label="o1GPT-3.5 Turbo">
28492880 <option value="o1gpt-preview3.5-turbo">o1gpt-preview3.5-turbo</option>
28502881 <option value="o1gpt-mini3.5-turbo-0125">o1gpt-mini3.5-turbo-0125 (2024)</option>
2882+ <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2883+ <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
28512884 </optgroup>
28522885 <optgroup label="Other">
28532886 <option value="text-davincibabbage-003002">text-davincibabbage-003002</option>
28542887 <option value="text-davinci-002">text-davinci-002</option>
2855- <option value="text-curie-001">text-curie-001</option>
2856- <option value="text-babbage-001">text-babbage-001</option>
2857- <option value="text-ada-001">text-ada-001</option>
2858- <option value="code-davinci-002">code-davinci-002</option>
28592888 </optgroup>
28602889 <optgroup id="openai_external_category" label="External">
28612890 </optgroup>
@@ -3054,6 +3083,7 @@
30543083 <h4 data-i18n="Google Model">Google Model</h4>
30553084 <select id="model_google_select">
30563085 <optgroup label="Primary">
3086+ <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
30573087 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
30583088 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
30593089 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
@@ -3062,7 +3092,14 @@
30623092 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
30633093 </optgroup>
30643094 <optgroup label="Subversions">
30653095 <option value="gemini-2.0-flash-thinkingpro-exp-1219">Gemini 2.0 Flash ThinkingPro Experimental</option>
3096+ <option value="gemini-2.0-pro-exp-02-05">Gemini 2.0 Pro Experimental 2025-02-05</option>
3097+ <option value="gemini-2.0-flash-lite-preview">Gemini 2.0 Flash-Lite Preview</option>
3098+ <option value="gemini-2.0-flash-lite-preview-02-05">Gemini 2.0 Flash-Lite Preview 2025-02-05</option>
3099+ <option value="gemini-2.0-flash-001">Gemini 2.0 Flash [001]</option>
3100+ <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
3101+ <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3102+ <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
30663103 <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash Experimental</option>
30673104 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
30683105 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
@@ -3149,34 +3186,22 @@
31493186 </div>
31503187 <h4 data-i18n="Groq Model">Groq Model</h4>
31513188 <select id="model_groq_select">
31523189 <optgroup label="LlamaProduction 3.3Models">
3190+ <option value="gemma2-9b-it">gemma2-9b-it</option>
31533191 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
3192+ <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3193+ <option value="llama3-70b-8192">llama3-70b-8192</option>
3194+ <option value="llama3-8b-8192">llama3-8b-8192</option>
3195+ <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
31543196 </optgroup>
31553197 <optgroup label="LlamaPreview 3.2Models">
3198+ <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3199+ <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
31563200 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
31573201 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
31583202 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
31593203 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
31603204 </optgroup>
3161- <optgroup label="Llama 3.1">
3162- <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3163- <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
3164- <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
3165- </optgroup>
3166- <optgroup label="Llama 3">
3167- <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
3168- <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
3169- <option value="llama3-8b-8192">llama3-8b-8192</option>
3170- <option value="llama3-70b-8192">llama3-70b-8192</option>
3171- </optgroup>
3172- <optgroup label="Gemma">
3173- <option value="gemma-7b-it">gemma-7b-it</option>
3174- <option value="gemma2-9b-it">gemma2-9b-it</option>
3175- </optgroup>
3176- <optgroup label="Other">
3177- <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3178- <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
3179- </optgroup>
31803205 </select>
31813206 </div>
31823207 <div id="nanogpt_form" data-source="nanogpt">
@@ -3209,6 +3234,7 @@
32093234 <select id="model_deepseek_select">
32103235 <option value="deepseek-chat">deepseek-chat</option>
32113236 <option value="deepseek-coder">deepseek-coder</option>
3237+ <option value="deepseek-reasoner">deepseek-reasoner</option>
32123238 </select>
32133239 </div>
32143240 </div>
@@ -3224,32 +3250,19 @@
32243250 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
32253251 <select id="model_perplexity_select">
32263252 <optgroup label="Perplexity Sonar Models">
3253+ <option value="sonar">sonar</option>
3254+ <option value="sonar-pro">sonar-pro</option>
3255+ <option value="sonar-reasoning">sonar-reasoning</option>
3256+ </optgroup>
3257+ <optgroup label="Deprecated Models">
3258+ <!-- These are scheduled for deprecation after 2/22/2025 -->
32273259 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
32283260 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
32293261 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
3230- </optgroup>
3262+ <!-- These are not listed on the site anymore -->
3231- <optgroup label="Perplexity Chat Models">
32323263 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
32333264 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
32343265 </optgroup>
3235- <optgroup label="Open-Source Models">
3236- <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
3237- <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
3238- </optgroup>
3239- <optgroup label="Deprecated Models">
3240- <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
3241- <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
3242- <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
3243- <option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
3244- <option value="sonar-small-chat">sonar-small-chat</option>
3245- <option value="sonar-small-online">sonar-small-online</option>
3246- <option value="sonar-medium-chat">sonar-medium-chat</option>
3247- <option value="sonar-medium-online">sonar-medium-online</option>
3248- <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
3249- <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
3250- <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
3251- <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
3252- </optgroup>
32533266 </select>
32543267 </div>
32553268 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@@ -3521,7 +3534,7 @@
35213534 </label>
35223535 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
35233536 <input id="instruct_enabled" type="checkbox" style="display:none;" />
35243537 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
35253538 </label>
35263539 </div>
35273540 </h4>
@@ -3699,7 +3712,7 @@
36993712 <div class="flex-container">
37003713 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
37013714 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
37023715 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
37033716 </label>
37043717 </div>
37053718 </h4>
@@ -3753,8 +3766,8 @@
37533766 </div>
37543767 <label class="checkbox_label" for="custom_stopping_strings_macro">
37553768 <input id="custom_stopping_strings_macro" type="checkbox" checked>
37563769 <small data-i18n="Replace Macro in Custom StoppingStop Strings">
37573770 Replace Macro in Custom StoppingStop Strings
37583771 </small>
37593772 </label>
37603773 </div>
@@ -3797,6 +3810,66 @@
37973810 </div>
37983811 </div>
37993812 <div>
3813+ <h4 class="standoutHeader">
3814+ <span data-i18n="Reasoning">Reasoning</span>
3815+ </h4>
3816+ <div>
3817+ <div class="flex-container alignItemsBaseline">
3818+ <label class="checkbox_label flex1" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
3819+ <input id="reasoning_auto_parse" type="checkbox" />
3820+ <small data-i18n="Auto-Parse">
3821+ Auto-Parse
3822+ </small>
3823+ </label>
3824+ <label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand">
3825+ <input id="reasoning_auto_expand" type="checkbox" />
3826+ <small data-i18n="Auto-Expand">
3827+ Auto-Expand
3828+ </small>
3829+ </label>
3830+ <label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
3831+ <input id="reasoning_show_hidden" type="checkbox" />
3832+ <small data-i18n="Show Hidden">
3833+ Show Hidden
3834+ </small>
3835+ </label>
3836+ </div>
3837+ <div class="flex-container alignItemsBaseline">
3838+ <label class="checkbox_label flex1" 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">
3839+ <input id="reasoning_add_to_prompts" type="checkbox" />
3840+ <small data-i18n="Add to Prompts">
3841+ Add to Prompts
3842+ </small>
3843+ </label>
3844+ <div class="flex1 flex-container alignItemsBaseline" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3845+ <input id="reasoning_max_additions" class="text_pole textarea_compact widthUnset" type="number" min="0" max="999"></textarea>
3846+ <small data-i18n="Max">Max</small>
3847+ </div>
3848+ </div>
3849+ <details>
3850+ <summary data-i18n="Reasoning Formatting">
3851+ Reasoning Formatting
3852+ </summary>
3853+ <div class="flex-container">
3854+ <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3855+ <small data-i18n="Prefix">Prefix</small>
3856+ <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3857+ </div>
3858+ <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3859+ <small data-i18n="Suffix">Suffix</small>
3860+ <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3861+ </div>
3862+ </div>
3863+ <div class="flex-container">
3864+ <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3865+ <small data-i18n="Separator">Separator</small>
3866+ <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3867+ </div>
3868+ </div>
3869+ </details>
3870+ </div>
3871+ </div>
3872+ <div>
38003873 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
38013874 <div>
38023875 <small>
@@ -4809,6 +4882,7 @@
48094882 </div>
48104883 <div id="extensions_settings" class="flex1 wide50p">
48114884 <div id="assets_container" class="extension_container"></div>
4885+ <div id="typing_indicator_container" class="extension_container"></div>
48124886 <div id="expressions_container" class="extension_container"></div>
48134887 <div id="sd_container" class="extension_container"></div>
48144888 <div id="tts_container" class="extension_container"></div>
@@ -5804,7 +5878,7 @@
58045878 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">
58055879 <div class="flex-container wide100p alignitemscenter">
58065880 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">
58075881 <div class="world_entry_form_control keyprimary flex1">
58085882 <small class="displayNone">
58095883 <span data-i18n="Comma separated (required)">
58105884 Comma separated (required)
@@ -6218,14 +6292,31 @@
62186292 <div class="mes_edit_buttons">
62196293 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
62206294 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
62216295 <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>
6296+ <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
62236297 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
62246298 <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>
62266299 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
62276300 </div>
62286301 </div>
6302+ <details class="mes_reasoning_details">
6303+ <summary class="mes_reasoning_summary flex-container">
6304+ <div class="mes_reasoning_header_block flex-container">
6305+ <div class="mes_reasoning_header flex-container">
6306+ <span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span>
6307+ <div class="mes_reasoning_arrow fa-solid fa-chevron-up"></div>
6308+ </div>
6309+ </div>
6310+ <div class="mes_reasoning_actions flex-container">
6311+ <div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6312+ <div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6313+ <div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6314+ <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6315+ <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6316+ </div>
6317+ </summary>
6318+ <div class="mes_reasoning"></div>
6319+ </details>
62296320 <div class="mes_text"></div>
62306321 <div class="mes_img_container">
62316322 <div class="mes_img_controls">
@@ -6325,7 +6416,10 @@
63256416 <img alt="Avatar" src="" />
63266417 </div>
63276418 <div class="group_member_name">
63286419 <div class="ch_namecharacter_name_block"></div>
6420+ <span class="ch_name"></span>
6421+ <small class="ch_additional_info character_version"></small>
6422+ </div>
63296423 <div class="tags tags_inline"></div>
63306424 </div>
63316425 <input class="ch_fav" value="" hidden />
@@ -6437,9 +6531,6 @@
64376531 </div>
64386532
64396533 <!-- chat and input bar -->
6440- <div id="typing_indicator_template" class="template_element">
6441- <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
6442- </div>
64436534 <div id="message_file_template" class="template_element">
64446535 <div class="mes_file_container">
64456536 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
public/locales/ar-sa.json+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",
483483 "Custom Stopping Strings": "سلاسل توقف مخصصة",
484484 "JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",
485485 "Replace Macro in Custom StoppingStop Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",
486486 "Auto-Continue": "المتابعة التلقائية",
487487 "Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",
488488 "Target length (tokens)": "الطول المستهدف (رموز)",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",
483483 "Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",
484484 "JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",
485485 "Replace Macro in Custom StoppingStop Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",
486486 "Auto-Continue": "Automatisch fortsetzen",
487487 "Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",
488488 "Target length (tokens)": "Ziel-Länge (Tokens)",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "separe con comas sin espacio entre ellas",
483483 "Custom Stopping Strings": "Cadenas de Detención Personalizadas",
484484 "JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",
485485 "Replace Macro in Custom StoppingStop Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",
486486 "Auto-Continue": "Autocontinuar",
487487 "Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",
488488 "Target length (tokens)": "Longitud objetivo (tokens)",
@@ -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+4 -4
@@ -434,7 +434,7 @@
434434 "Non-markdown strings": "Chaînes non Markdown",
435435 "Custom Stopping Strings": "Chaînes d'arrêt personnalisées",
436436 "JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",
437437 "Replace Macro in Custom StoppingStop Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",
438438 "Auto-Continue": "Auto-Continue",
439439 "Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",
440440 "Target length (tokens)": "Longueur cible (tokens)",
@@ -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,8 +1385,8 @@
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 "ShowRequest model thoughtsreasoning": "AfficherDemander les pensées du modèle",
13891389 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "AfficherPermet lesau penséesmodèle internesde duretourner modèleson dansprocessus lade réponseréflexion.",
13901390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
13911391 "openai_logit_bias_no_items": "Aucun élément",
13921392 "api_no_connection": "Pas de connection...",
public/locales/is-is.json+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "aðskilið með kommum án bila milli",
483483 "Custom Stopping Strings": "Eigin stopp-strengir",
484484 "JSON serialized array of strings": "JSON raðað fylki af strengjum",
485485 "Replace Macro in Custom StoppingStop Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",
486486 "Auto-Continue": "Sjálfvirk Forná",
487487 "Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",
488488 "Target length (tokens)": "Markaðarlengd (texti)",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "separati con virgole senza spazio tra loro",
483483 "Custom Stopping Strings": "Stringhe di Stop Personalizzate",
484484 "JSON serialized array of strings": "Matrice serializzata JSON di stringhe",
485485 "Replace Macro in Custom StoppingStop Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",
486486 "Auto-Continue": "Auto-continua",
487487 "Allow for Chat Completion APIs": "Consenti per API di completamento chat",
488488 "Target length (tokens)": "Lunghezza obiettivo (token)",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "間にスペースのないカンマで区切ります",
483483 "Custom Stopping Strings": "カスタム停止文字列",
484484 "JSON serialized array of strings": "文字列のJSONシリアル化配列",
485485 "Replace Macro in Custom StoppingStop Strings": "カスタム停止文字列内のマクロを置換する",
486486 "Auto-Continue": "自動継続",
487487 "Allow for Chat Completion APIs": "チャット補完APIを許可",
488488 "Target length (tokens)": "ターゲット長さ(トークン)",
@@ -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+11 -11
@@ -211,7 +211,7 @@
211211 "Sampler Priority": "샘플러 우선 순위",
212212 "Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",
213213 "Character Names Behavior": "캐릭터 이름 동작",
214214 "[title]character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",
215215 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
216216 "None": "없음",
217217 "None (not injected)": "없음 (삽입되지 않음)",
@@ -404,7 +404,7 @@
404404 "Custom API Key": "커스텀 API 키",
405405 "Available Models": "사용 가능한 모델",
406406 "Prompt Post-Processing": "신속한 후처리",
407407 "[title]API Connections;[no_connection_text]api_no_connection": "연결이 되지 않았습니다...",
408408 "Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",
409409 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",
410410 "Test Message": "테스트 메시지",
@@ -492,7 +492,7 @@
492492 "separate with commas w/o space between": "쉼표로 구분 (공백 없이)",
493493 "Custom Stopping Strings": "사용자 정의 중지 문자열",
494494 "JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",
495495 "Replace Macro in Custom StoppingStop Strings": "사용자 정의 중단 문자열에서 매크로 교체",
496496 "Auto-Continue": "자동 계속하기",
497497 "Allow for Chat Completion APIs": "채팅 완성 API 허용",
498498 "Target length (tokens)": "대상 길이 (토큰)",
@@ -625,7 +625,7 @@
625625 "Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",
626626 "Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",
627627 "Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",
628628 "[title]Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",
629629 "In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",
630630 "Characters Hotswap": "캐릭터 핫스왑",
631631 "Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",
@@ -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 캐릭터 (직접링크)",
@@ -1538,7 +1538,7 @@
15381538 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
15391539 "qr--colorClear": "색상 지우기",
15401540 "Color": "색상",
15411541 "[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
15421542 "Select TTS Provider": "TTS 공급자 선택",
15431543 "tts_enabled": "활성화",
15441544 "Narrate user messages": "사용자 메시지 나레이션",
@@ -1583,15 +1583,15 @@
15831583 "Prompt Content": "프롬프트 내용",
15841584 "Instruct Sequences": "지시 시퀀스",
15851585 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",
15861586 "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
15871587 "Auto-select Input Text": "입력 텍스트 자동 선택",
15881588 "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
15891589 "Markdown Hotkeys": "마크다운 입력 단축키",
15901590 "[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
15911591 "Show group chat queue": "그룹 채팅 대기열 표시",
15921592 "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
15931593 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",
15941594 "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
15951595 "Injection Template": "삽입 템플릿",
15961596 "Query messages": "쿼리 메시지 수",
15971597 "Score threshold": "점수 임계값",
public/locales/nl-nl.json+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",
483483 "Custom Stopping Strings": "Aangepaste Stopreeksen",
484484 "JSON serialized array of strings": "JSON geserialiseerde reeks van strings",
485485 "Replace Macro in Custom StoppingStop Strings": "Macro vervangen in aangepaste stopreeksen",
486486 "Auto-Continue": "Automatisch doorgaan",
487487 "Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",
488488 "Target length (tokens)": "Doellengte (tokens)",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "separe com vírgulas sem espaço entre",
483483 "Custom Stopping Strings": "Cadeias de parada personalizadas",
484484 "JSON serialized array of strings": "Matriz de strings serializada em JSON",
485485 "Replace Macro in Custom StoppingStop Strings": "Substituir Macro em Strings de Parada Personalizadas",
486486 "Auto-Continue": "Auto-Continuar",
487487 "Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",
488488 "Target length (tokens)": "Comprimento alvo (tokens)",
@@ -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+2 -2
@@ -161,7 +161,7 @@
161161 "View hidden API keys": "Посмотреть скрытые API-ключи",
162162 "Advanced Formatting": "Расширенное форматирование",
163163 "Context Template": "Шаблон контекста",
164164 "Replace Macro in Custom StoppingStop Strings": "Заменять макросы в пользовательских стоп-строках",
165165 "Story String": "Строка истории",
166166 "Example Separator": "Разделитель примеров сообщений",
167167 "Chat Start": "Начало чата",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "розділяйте комами без пропусків між ними",
483483 "Custom Stopping Strings": "Власні рядки зупинки",
484484 "JSON serialized array of strings": "JSON-серіалізований масив рядків",
485485 "Replace Macro in Custom StoppingStop Strings": "Замінювати макроси у власних рядках зупинки",
486486 "Auto-Continue": "Автоматичне продовження",
487487 "Allow for Chat Completion APIs": "Дозволити для Chat Completion API",
488488 "Target length (tokens)": "Цільова довжина (токени)",
@@ -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+2 -2
@@ -482,7 +482,7 @@
482482 "separate with commas w/o space between": "phân tách bằng dấu phẩy không có khoảng trắng giữa",
483483 "Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",
484484 "JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",
485485 "Replace Macro in Custom StoppingStop Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",
486486 "Auto-Continue": "Tự động Tiếp tục",
487487 "Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",
488488 "Target length (tokens)": "Độ dài mục tiêu (token)",
@@ -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+22 -22
@@ -215,7 +215,7 @@
215215 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",
216216 "Scale": "缩放比例",
217217 "Negative Prompt": "负面提示词",
218218 "Used if CFG Scale is unset globally, per chat or character": "如果无分类器指导(CFG)缩放比例未在全局设置如果CFG缩放比例未被全局设置它将作用于每个聊天或每个角色它将作用于所有聊天或角色",
219219 "Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",
220220 "Grammar String": "语法字符串",
221221 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
@@ -266,8 +266,8 @@
266266 "Use system prompt": "使用系统提示词",
267267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268268 "Merges_all_system_messages_desc_2": "字段发送。",
269269 "ShowRequest model thoughtsreasoning": "展示思维链请求思维链",
270270 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "展示模型在回复时的内部思维链允许模型返回其思维过程。",
271271 "Assistant Prefill": "AI预填",
272272 "Expand the editor": "展开编辑器",
273273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",
@@ -559,7 +559,7 @@
559559 "Prompt Content": "提示词内容",
560560 "Custom Stopping Strings": "自定义停止字符串",
561561 "JSON serialized array of strings": "JSON序列化的字符串数组",
562562 "Replace Macro in Custom StoppingStop Strings": "替换自定义停止字符串中的宏",
563563 "Token Padding": "词符填充",
564564 "Miscellaneous": "杂项",
565565 "Non-markdown strings": "非 Markdown 字符串",
@@ -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": "查看角色卡片",
@@ -1208,7 +1208,7 @@
12081208 "View contents": "查看内容",
12091209 "Remove the file": "删除文件",
12101210 "Author's Note": "作者注释",
12111211 "Unique to this chat": "此聊天独有仅对此聊天生效",
12121212 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",
12131213 "Include in World Info Scanning": "纳入世界信息扫描",
12141214 "Before Main Prompt / Story String": "主提示词/故事线之前",
@@ -1224,13 +1224,13 @@
12241224 "Replace Author's Note": "替换作者注",
12251225 "Default Author's Note": "默认作者注",
12261226 "Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",
12271227 "Chat CFG": "聊天CFG本聊天的CFG缩放",
12281228 "1 = disabled": "“1”为已禁用为禁用",
12291229 "write short replies, write replies using past tense": "写简短的回复,用过去时写回复",
12301230 "Positive Prompt": "正面提示词",
12311231 "Use character CFG scales": "单独为各个角色设置CFG缩放",
12321232 "Character CFG": "角色CFG配置",
12331233 "Will be automatically added as the CFG for this character.": "将自动添加为该角色的 CFG将自动添加到该角色的CFG设置中。",
12341234 "Global CFG": "全局CFG",
12351235 "Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",
12361236 "CFG Prompt Cascading": "CFG 提示词级联",
@@ -1486,7 +1486,7 @@
14861486 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",
14871487 "Trim Out": "修剪掉",
14881488 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",
14891489 "ext_regex_affects": "影响作用范围",
14901490 "ext_regex_user_input_desc": "用户发送的消息",
14911491 "ext_regex_user_input": "用户输入",
14921492 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",
@@ -1720,9 +1720,9 @@
17201720 "Chat Lorebook for": "聊天知识书",
17211721 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
17221722 "chat_rename_1": "输入聊天的新名称:",
17231723 "chat_rename_2": "注意!!使用已有文件名会导致错误与其他文件重名会导致错误!!",
17241724 "chat_rename_3": "此举会将次聊天与标记为此举会将此聊天与标记为“检查点”的聊天解绑。",
17251725 "chat_rename_4": "不需要在结尾添加 '.JSONL' 后缀)",
17261726 "Enter Checkpoint Name:": "输入检查点名称:",
17271727 "(Leave empty to auto-generate)": "(留空以自动生成)",
17281728 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",
@@ -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": "显示提示词差异",
@@ -1975,7 +1975,7 @@
19751975 "Enter your password below to confirm:": "输入您的密码以确认:",
19761976 "Chat Scenario Override": "聊天场景覆盖",
19771977 "Remove": "移除",
19781978 "Unique to this chat.": "Unique to this chat.仅对此聊天生效。",
19791979 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",
19801980 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",
19811981 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",
@@ -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+4 -4
@@ -483,7 +483,7 @@
483483 "separate with commas w/o space between": "用逗號分隔,之間無空格",
484484 "Custom Stopping Strings": "自訂停止字串",
485485 "JSON serialized array of strings": "JSON 序列化字串數組",
486486 "Replace Macro in Custom StoppingStop Strings": "取代自訂停止字串中的巨集",
487487 "Auto-Continue": "自動繼續",
488488 "Allow for Chat Completion APIs": "允許聊天補全 API",
489489 "Target length (tokens)": "目標長度(符元)",
@@ -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,8 +2357,8 @@
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 "ShowRequest model thoughtsreasoning": "顯示模型思維鏈請求模型思維鏈",
23612361 "DisplayAllows the model's internalto thoughtsreturn inits thethinking responseprocess.": "在回應中顯示模型的思維鏈(內部思考過程)讓模型回傳其思考過程。",
23622362 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
23632363 "Model ID (optional)": "模型 ID(可選)",
23642364 "DeepSeek API Key": "DeepSeek API 金鑰",
public/script.js+311 -183
@@ -95,6 +95,7 @@ import {
9595 resetMovableStyles,
9696 forceCharacterEditorTokenize,
9797 applyPowerUserSettings,
98+ generatedTextFiltered,
9899} from './scripts/power-user.js';
99100
100101import {
@@ -169,6 +170,7 @@ import {
169170 toggleDrawer,
170171 isElementInViewport,
171172 copyText,
173+ escapeHtml,
172174} from './scripts/utils.js';
173175import { debounce_timeout } from './scripts/constants.js';
174176
@@ -267,6 +269,8 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267269import { initBulkEdit } from './scripts/bulk-edit.js';
268270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269271import { getContext } from './scripts/st-context.js';
272+import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
273+import { accountStorage } from './scripts/util/AccountStorage.js';
270274
271275// API OBJECT FOR EXTERNAL WIRING
272276globalThis.SillyTavern = {
@@ -416,7 +420,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
416420 const entityId = getCurrentEntityId();
417421 const warningShownKey = `mediaWarningShown:${entityId}`;
418422
419423 if (localStorageaccountStorage.getItem(warningShownKey) === null) {
420424 const warningToast = toastr.warning(
421425 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
422426 t`External media has been blocked`,
@@ -427,7 +431,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
427431 },
428432 );
429433
430434 localStorageaccountStorage.setItem(warningShownKey, 'true');
431435 }
432436 }
433437});
@@ -443,6 +447,7 @@ export const event_types = {
443447 MESSAGE_DELETED: 'message_deleted',
444448 MESSAGE_UPDATED: 'message_updated',
445449 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
450+ MORE_MESSAGES_LOADED: 'more_messages_loaded',
446451 IMPERSONATE_READY: 'impersonate_ready',
447452 CHAT_CHANGED: 'chat_id_changed',
448453 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
@@ -491,6 +496,7 @@ export const event_types = {
491496 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
492497 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
493498 STREAM_TOKEN_RECEIVED: 'stream_token_received',
499+ STREAM_REASONING_DONE: 'stream_reasoning_done',
494500 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
495501 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
496502 OPEN_CHARACTER_LIBRARY: 'open_character_library',
@@ -723,6 +729,7 @@ async function getSystemMessages() {
723729 is_user: false,
724730 is_system: true,
725731 mes: await renderTemplateAsync('assistantNote'),
732+ uses_system_ui: true,
726733 extra: {
727734 isSmallSys: true,
728735 },
@@ -980,6 +987,7 @@ async function firstLoadInit() {
980987 initServerHistory();
981988 initSettingsSearch();
982989 initBulkEdit();
990+ initReasoning();
983991 await initScrapers();
984992 doDailyExtensionUpdatesCheck();
985993 await hideLoader();
@@ -1483,7 +1491,7 @@ export async function printCharacters(fullRefresh = false) {
14831491
14841492 $('#rm_print_characters_pagination').pagination({
14851493 dataSource: entities,
14861494 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || per_page_default,
14871495 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
14881496 pageRange: 1,
14891497 pageNumber: saveCharactersPage || 1,
@@ -1527,7 +1535,7 @@ export async function printCharacters(fullRefresh = false) {
15271535 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
15281536 },
15291537 afterSizeSelectorChange: function (e) {
15301538 localStorageaccountStorage.setItem(storageKey, e.target.value);
15311539 },
15321540 afterPaging: function (e) {
15331541 saveCharactersPage = e;
@@ -1829,7 +1837,7 @@ export async function replaceCurrentChat() {
18291837 }
18301838}
18311839
18321840export async function showMoreMessages(messagesToLoad = null) {
18331841 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
18341842 let messageId = Number(firstDisplayedMesId);
18351843 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
@@ -1859,6 +1867,8 @@ export function showMoreMessages(messagesToLoad = null) {
18591867 const newHeight = $('#chat').prop('scrollHeight');
18601868 $('#chat').scrollTop(newHeight - prevHeight);
18611869 }
1870+
1871+ await eventSource.emit(event_types.MORE_MESSAGES_LOADED);
18621872}
18631873
18641874export async function printMessages() {
@@ -1987,14 +1997,15 @@ export async function sendTextareaMessage() {
19871997 * @param {boolean} isUser If the message was sent by the user
19881998 * @param {number} messageId Message index in chat array
19891999 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
2000+ * @param {boolean} [isReasoning] If the message is reasoning output
19902001 * @returns {string} HTML string
19912002 */
19922003export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
19932004 if (!mes) {
19942005 return '';
19952006 }
19962007
19972008 if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
19982009 const mesBeforeReplace = mes;
19992010 const chatMessage = chat[messageId];
20002011 mes = substituteParams(mes, undefined, ch_name);
@@ -2023,6 +2034,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20232034 if (!isSystem) {
20242035 function getRegexPlacement() {
20252036 try {
2037+ if (isReasoning) {
2038+ return regex_placement.REASONING;
2039+ }
20262040 if (isUser) {
20272041 return regex_placement.USER_INPUT;
20282042 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2056,6 +2070,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20562070 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
20572071 }
20582072
2073+ // Make sure reasoning strings are always shown, even if they include "<" or ">"
2074+ [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2075+ if (!reasoningString || !reasoningString.trim().length) {
2076+ return;
2077+ }
2078+ // Only replace the first occurrence of the reasoning string
2079+ if (mes.includes(reasoningString)) {
2080+ mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2081+ }
2082+ });
2083+
20592084 if (!isSystem) {
20602085 // Save double quotes in tags as a special character to prevent them from being encoded
20612086 if (!power_user.encode_tags) {
@@ -2166,26 +2191,29 @@ function insertSVGIcon(mes, extra) {
21662191 modelName = extra.api;
21672192 }
21682193
2169- const image = new Image();
2194+ const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
2170- // Add classes for styling and identification
2195+ image.onload = async function () {
2171- image.classList.add('icon-svg', 'timestamp-icon');
2196+ let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`);
2172- image.src = `/img/${modelName}.svg`;
2197+ if (existingSVG.length) {
2173- image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2198+ existingSVG.replaceWith(image);
2174-
2199+ } else {
2175- image.onload = async function () {
2200+ if (insertBefore) mes.find(targetSelector).before(image);
2176- // Check if an SVG already exists adjacent to the timestamp
2201+ else mes.find(targetSelector).after(image);
2177- let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
2202+ }
2178-
2203+ await SVGInject(image);
2179- if (existingSVG.length) {
2204+ };
2180- // Replace existing SVG
2205+ };
2181- existingSVG.replaceWith(image);
2182- } else {
2183- // Append the new SVG if none exists
2184- mes.find('.timestamp').after(image);
2185- }
21862206
2187- await SVGInject(image);
2207+ const createModelImage = (className, targetSelector, insertBefore) => {
2208+ const image = new Image();
2209+ image.classList.add('icon-svg', className);
2210+ image.src = `/img/${modelName}.svg`;
2211+ image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2212+ insertOrReplaceSVG(image, className, targetSelector, insertBefore);
21882213 };
2214+
2215+ createModelImage('timestamp-icon', '.timestamp');
2216+ createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
21892217}
21902218
21912219
@@ -2227,6 +2255,8 @@ function getMessageFromTemplate({
22272255 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
22282256 bookmarkLink && updateBookmarkDisplay(mes);
22292257
2258+ updateReasoningUI(mes);
2259+
22302260 if (power_user.timestamp_model_icon && extra?.api) {
22312261 insertSVGIcon(mes, extra);
22322262 }
@@ -2234,10 +2264,22 @@ function getMessageFromTemplate({
22342264 return mes;
22352265}
22362266
2237-export function updateMessageBlock(messageId, message) {
2267+/**
2268+ * Re-renders a message block with updated content.
2269+ * @param {number} messageId Message ID
2270+ * @param {object} message Message object
2271+ * @param {object} [options={}] Optional arguments
2272+ * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
2273+ */
2274+export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
22382275 const messageElement = $(`#chat [mesid="${messageId}"]`);
2239- const text = message?.extra?.display_text ?? message.mes;
2276+ if (rerenderMessage) {
2240- messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
2277+ const text = message?.extra?.display_text ?? message.mes;
2278+ messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2279+ }
2280+
2281+ updateReasoningUI(messageElement);
2282+
22412283 addCopyToCodeBlocks(messageElement);
22422284 appendMediaToMessage(message, messageElement);
22432285}
@@ -2394,8 +2436,9 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23942436 mes.is_user,
23952437 chat.indexOf(mes),
23962438 sanitizerOverrides,
2439+ false,
23972440 );
23982441 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
23992442 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24002443
24012444 let params = {
@@ -2412,7 +2455,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24122455 timestamp: timestamp,
24132456 extra: mes.extra,
24142457 tokenCount: mes.extra?.token_count ?? 0,
24152458 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
24162459 };
24172460
24182461 const renderedMessage = getMessageFromTemplate(params);
@@ -2465,6 +2508,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24652508 swipeMessage.attr('swipeid', params.swipeId);
24662509 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
24672510 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2511+ updateReasoningUI(swipeMessage);
24682512 appendMediaToMessage(mes, swipeMessage);
24692513 if (power_user.timestamp_model_icon && params.extra?.api) {
24702514 insertSVGIcon(swipeMessage, params.extra);
@@ -2531,13 +2575,14 @@ export function formatCharacterAvatar(characterAvatar) {
25312575 * @param {Date} gen_started Date when generation was started
25322576 * @param {Date} gen_finished Date when generation was finished
25332577 * @param {number} tokenCount Number of tokens generated (0 if not available)
2578+ * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
25342579 * @returns {Object} Object containing the formatted timer value and title
25352580 * @example
25362581 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
25372582 * console.log(timerValue); // 1.2s
25382583 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s
25392584 */
25402585function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
25412586 if (!gen_started || !gen_finished) {
25422587 return {};
25432588 }
@@ -2551,8 +2596,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
25512596 `Generation queued: ${start.format(dateFormat)}`,
25522597 `Reply received: ${finish.format(dateFormat)}`,
25532598 `Time to generate: ${seconds} seconds`,
2599+ reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
25542600 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
25552601 ].filter(x => x).join('\n').trim();
25562602
25572603 if (isNaN(seconds) || seconds < 0) {
25582604 return { timerValue: '', timerTitle };
@@ -2740,7 +2786,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
27402786 TempResponseLength.save(main_api, responseLength);
27412787 eventHook = TempResponseLength.setupEventHook(main_api);
27422788 }
27432789 returnconst result = await Generate('quiet', options);
2790+ return removeReasoningFromString(result);
27442791 } finally {
27452792 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
27462793 TempResponseLength.restore(main_api);
@@ -3040,8 +3087,8 @@ export function isStreamingEnabled() {
30403087 (main_api == 'openai' &&
30413088 oai_settings.stream_openai &&
30423089 !noStreamSources.includes(oai_settings.chat_completion_source) &&
30433090 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith(['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model)) &&
3044- !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
3091+ )
30453092 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
30463093 || (main_api == 'novel' && nai_settings.streaming_novel)
30473094 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));
@@ -3070,9 +3117,13 @@ class StreamingProcessor {
30703117 constructor(type, forceName2, timeStarted, continueMessage) {
30713118 this.result = '';
30723119 this.messageId = -1;
3120+ /** @type {HTMLElement} */
30733121 this.messageDom = null;
3122+ /** @type {HTMLElement} */
30743123 this.messageTextDom = null;
3124+ /** @type {HTMLElement} */
30753125 this.messageTimerDom = null;
3126+ /** @type {HTMLElement} */
30763127 this.messageTokenCounterDom = null;
30773128 /** @type {HTMLTextAreaElement} */
30783129 this.sendTextarea = document.querySelector('#send_textarea');
@@ -3089,6 +3140,8 @@ class StreamingProcessor {
30893140 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
30903141 this.messageLogprobs = [];
30913142 this.toolCalls = [];
3143+ // Initialize reasoning in its own handler
3144+ this.reasoningHandler = new ReasoningHandler(timeStarted);
30923145 }
30933146
30943147 #checkDomElements(messageId) {
@@ -3098,6 +3151,7 @@ class StreamingProcessor {
30983151 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
30993152 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
31003153 }
3154+ this.reasoningHandler.updateDom(messageId);
31013155 }
31023156
31033157 #updateMessageBlockVisibility() {
@@ -3107,22 +3161,12 @@ class StreamingProcessor {
31073161 }
31083162 }
31093163
31103164 showMessageButtonsmarkUIGenStarted(messageId) {
3111- if (messageId == -1) {
3165+ deactivateSendButtons();
3112- return;
3113- }
3114-
3115- showStopButton();
3116- $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
31173166 }
31183167
31193168 hideMessageButtonsmarkUIGenStopped(messageId) {
3120- if (messageId == -1) {
3169+ activateSendButtons();
3121- return;
3122- }
3123-
3124- hideStopButton();
3125- $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });
31263170 }
31273171
31283172 async onStartStreaming(text) {
@@ -3131,20 +3175,18 @@ class StreamingProcessor {
31313175 if (this.type == 'impersonate') {
31323176 this.sendTextarea.value = '';
31333177 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
31343178 } else {
3135- else {
3179+ await saveReply(this.type, text, true, '', [], '');
3136- await saveReply(this.type, text, true);
31373180 messageId = chat.length - 1;
31383181 this.#checkDomElements(messageId);
31393182 this.showMessageButtonsmarkUIGenStarted(messageId);
31403183 }
3141-
31423184 hideSwipeButtons();
31433185 scrollChatToBottom();
31443186 return messageId;
31453187 }
31463188
31473189 async onProgressStreaming(messageId, text, isFinal) {
31483190 const isImpersonate = this.type == 'impersonate';
31493191 const isContinue = this.type == 'continue';
31503192
@@ -3156,11 +3198,9 @@ class StreamingProcessor {
31563198
31573199 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
31583200
3159- // Predict unbalanced asterisks / quotes during streaming
31603201 const charsToBalance = ['*', '"', '```'];
31613202 for (const char of charsToBalance) {
31623203 if (!isFinal && isOdd(countOccurrences(processedText, char))) {
3163- // Add character at the end to balance it
31643204 const separator = char.length > 1 ? '\n' : '';
31653205 processedText = processedText.trimEnd() + separator + char;
31663206 }
@@ -3169,23 +3209,25 @@ class StreamingProcessor {
31693209 if (isImpersonate) {
31703210 this.sendTextarea.value = processedText;
31713211 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
31723212 } else {
3173- else {
3213+ const mesChanged = chat[messageId]['mes'] !== processedText;
31743214 this.#checkDomElements(messageId);
31753215 this.#updateMessageBlockVisibility();
31763216 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);
31803217 chat[messageId]['mes'] = processedText;
31813218 chat[messageId]['gen_started'] = this.timeStarted;
31823219 chat[messageId]['gen_finished'] = currentTime;
3220+ if (!chat[messageId]['extra']) {
3221+ chat[messageId]['extra'] = {};
3222+ }
31833223
3184- if (currentTokenCount) {
3224+ // Update reasoning
3185- if (!chat[messageId]['extra']) {
3225+ await this.reasoningHandler.process(messageId, mesChanged);
3186- chat[messageId]['extra'] = {};
3187- }
31883226
3227+ // Token count update.
3228+ const tokenCountText = this.reasoningHandler.reasoning + processedText;
3229+ const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3230+ if (currentTokenCount) {
31893231 chat[messageId]['extra']['token_count'] = currentTokenCount;
31903232 if (this.messageTokenCounterDom instanceof HTMLElement) {
31913233 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3203,14 +3245,19 @@ class StreamingProcessor {
32033245 chat[messageId].is_system,
32043246 chat[messageId].is_user,
32053247 messageId,
3248+ {},
3249+ false,
32063250 );
32073251 if (this.messageTextDom instanceof HTMLElement) {
32083252 this.messageTextDom.innerHTML = formattedText;
32093253 }
3254+
3255+ const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
32103256 if (this.messageTimerDom instanceof HTMLElement) {
32113257 this.messageTimerDom.textContent = timePassed.timerValue;
32123258 this.messageTimerDom.title = timePassed.timerTitle;
32133259 }
3260+
32143261 this.setFirstSwipe(messageId);
32153262 }
32163263
@@ -3220,10 +3267,12 @@ class StreamingProcessor {
32203267 }
32213268
32223269 async onFinishStreaming(messageId, text) {
32233270 this.hideMessageButtonsmarkUIGenStopped(this.messageId);
32243271 await this.onProgressStreaming(messageId, text, true);
32253272 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
32263273
3274+ await this.reasoningHandler.finish(messageId);
3275+
32273276 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
32283277 const message = chat[messageId];
32293278 const swipeInfo = {
@@ -3251,39 +3300,11 @@ class StreamingProcessor {
32513300 unblockGeneration();
32523301 generatedPromptCache = '';
32533302
3254- //console.log("Generated text size:", text.length, text)
3255-
32563303 const isAborted = this.abortController.signal.aborted;
32573304 if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(text)) {
3258- function containsBlacklistedWords(str, blacklist, threshold) {
3305+ return swipe_right();
3259- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3260- const matches = str.match(regex) || [];
3261- return matches.length >= threshold;
3262- }
3263-
3264- const generatedTextFiltered = (text) => {
3265- if (text) {
3266- if (power_user.auto_swipe_minimum_length) {
3267- if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
3268- console.log('Generated text size too small');
3269- return true;
3270- }
3271- }
3272- if (power_user.auto_swipe_blacklist_threshold) {
3273- if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3274- console.log('Generated text has blacklisted words');
3275- return true;
3276- }
3277- }
3278- }
3279- return false;
3280- };
3281-
3282- if (generatedTextFiltered(text)) {
3283- swipe_right();
3284- return;
3285- }
32863306 }
3307+
32873308 playMessageSound();
32883309 }
32893310
@@ -3291,7 +3312,7 @@ class StreamingProcessor {
32913312 this.abortController.abort();
32923313 this.isStopped = true;
32933314
32943315 this.hideMessageButtonsmarkUIGenStopped(this.messageId);
32953316 generatedPromptCache = '';
32963317 unblockGeneration();
32973318
@@ -3317,7 +3338,7 @@ class StreamingProcessor {
33173338 }
33183339
33193340 /**
33203341 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
33213342 */
33223343 *nullStreamingGeneration() {
33233344 throw new Error('Generation function for streaming is not hooked up');
@@ -3339,10 +3360,10 @@ class StreamingProcessor {
33393360 try {
33403361 const sw = new Stopwatch(1000 / power_user.streaming_fps);
33413362 const timestamps = [];
33423363 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33433364 timestamps.push(Date.now());
33443365 if (this.isStopped || this.abortController.signal.aborted) {
33453366 return this.result;
33463367 }
33473368
33483369 this.toolCalls = toolCalls;
@@ -3351,8 +3372,10 @@ class StreamingProcessor {
33513372 if (logprobs) {
33523373 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33533374 }
3375+ // Get the updated reasoning string into the handler
3376+ this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
33543377 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33553378 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
33563379 }
33573380 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
33583381 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3428,7 +3451,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
34283451 break;
34293452 }
34303453 case 'textgenerationwebui':
34313454 generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
34323455 TempResponseLength.restore(api);
34333456 break;
34343457 case 'openai': {
@@ -3836,6 +3859,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38363859 };
38373860 }));
38383861
3862+ const reasoning = new PromptReasoning();
3863+ for (let i = coreChat.length - 1; i >= 0; i--) {
3864+ const depth = coreChat.length - i - 1;
3865+ const isPrefix = isContinue && i === coreChat.length - 1;
3866+ coreChat[i] = {
3867+ ...coreChat[i],
3868+ mes: reasoning.addToMessage(
3869+ coreChat[i].mes,
3870+ getRegexedString(
3871+ String(coreChat[i].extra?.reasoning ?? ''),
3872+ regex_placement.REASONING,
3873+ { isPrompt: true, depth: depth },
3874+ ),
3875+ isPrefix,
3876+ ),
3877+ };
3878+ if (reasoning.isLimitReached()) {
3879+ break;
3880+ }
3881+ }
3882+
38393883 // Determine token limit
38403884 let this_max_context = getMaxContextSize();
38413885
@@ -4394,7 +4438,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
43944438 // For prompt bit itemization
43954439 let mesSendString = '';
43964440
43974441 async function getCombinedPrompt(isNegative) {
43984442 // Only return if the guidance scale doesn't exist or the value is 1
43994443 // Also don't return if constructing the neutral prompt
44004444 if (isNegative && !useCfgPrompt) {
@@ -4421,7 +4465,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44214465 // TODO: Make all extension prompts use an array/splice method
44224466 const lengthDiff = mesSend.length - cfgPrompt.depth;
44234467 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
4424- finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4468+ const cfgMessage = finalMesSend[cfgDepth];
4469+ if (cfgMessage) {
4470+ if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
4471+ finalMesSend[cfgDepth].extensionPrompts = [];
4472+ }
4473+ finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4474+ }
44254475 }
44264476 }
44274477 }
@@ -4497,13 +4547,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44974547 };
44984548
44994549 // Before returning the combined prompt, give available context related information to all subscribers.
45004550 await eventSource.emitAndWaitemit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
45014551
45024552 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
45034553 return !data.combinedPrompt ? combine() : data.combinedPrompt;
45044554 }
45054555
45064556 let finalPrompt = await getCombinedPrompt(false);
45074557
45084558 const eventData = { prompt: finalPrompt, dryRun: dryRun };
45094559 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);
@@ -4537,8 +4587,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45374587 }
45384588 break;
45394589 case 'textgenerationwebui': {
45404590 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null;
45414591 generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
45424592 break;
45434593 }
45444594 case 'novel': {
@@ -4738,11 +4788,17 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47384788 //const getData = await response.json();
47394789 let getMessage = extractMessageFromData(data);
47404790 let title = extractTitleFromData(data);
4791+ let reasoning = extractReasoningFromData(data);
47414792 kobold_horde_model = title;
47424793
47434794 const swipes = extractMultiSwipes(data, type);
47444795
47454796 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4797+ reasoning = getRegexedString(reasoning, regex_placement.REASONING);
4798+
4799+ if (power_user.trim_spaces) {
4800+ reasoning = reasoning.trim();
4801+ }
47464802
47474803 if (isContinue) {
47484804 getMessage = continue_mag + getMessage;
@@ -4764,10 +4820,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47644820 else {
47654821 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
47664822 if (originalType !== 'continue') {
47674823 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
47684824 }
47694825 else {
47704826 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
47714827 }
47724828
47734829 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -4801,32 +4857,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
48014857 }
48024858
48034859 const isAborted = abortController && abortController.signal.aborted;
48044860 if (!isAborted && power_user.auto_swipe && !isAbortedgeneratedTextFiltered(getMessage)) {
4805- console.debug('checking for autoswipeblacklist on non-streaming message');
4861+ is_send_press = false;
4806- function containsBlacklistedWords(getMessage, blacklist, threshold) {
4862+ return swipe_right();
4807- console.debug('checking blacklisted words');
4808- const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
4809- const matches = getMessage.match(regex) || [];
4810- return matches.length >= threshold;
4811- }
4812-
4813- const generatedTextFiltered = (getMessage) => {
4814- if (power_user.auto_swipe_blacklist_threshold) {
4815- if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
4816- console.debug('Generated text has blacklisted words');
4817- return true;
4818- }
4819- }
4820-
4821- return false;
4822- };
4823- if (generatedTextFiltered(getMessage)) {
4824- console.debug('swiping right automatically');
4825- is_send_press = false;
4826- swipe_right();
4827- // TODO: do we want to resolve after an auto-swipe?
4828- return;
4829- }
48304863 }
48314864
48324865 console.debug('/api/chats/save called by /Generate');
@@ -5481,7 +5514,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
54815514 toastr.info(t`Copied!`);
54825515 });
54835516
54845517 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
54855518 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
54865519 console.log(PromptArrayItemForRawPromptDisplay);
54875520 console.log(itemizedPrompts);
@@ -5489,6 +5522,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
54895522
54905523 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
54915524
5525+ // Mobile needs special handholding. The side-view on the popup wouldn't work,
5526+ // so we just show an additional popup for this.
5527+ if (isMobile()) {
5528+ const content = document.createElement('div');
5529+ content.classList.add('tokenItemizingMaintext');
5530+ content.innerText = rawPrompt;
5531+ const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5532+ await popup.show();
5533+ return;
5534+ }
5535+
54925536 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
54935537 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
54945538 rawPromptWrapper.innerText = rawPrompt;
@@ -5851,7 +5895,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
58515895 return getMessage;
58525896}
58535897
58545898export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
58555899 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
58565900 chat[chat.length - 1]['is_user'])) {
58575901 type = 'normal';
@@ -5861,6 +5905,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58615905 chat[chat.length - 1]['extra'] = {};
58625906 }
58635907
5908+ // Coerce null/undefined to empty string
5909+ if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {
5910+ chat[chat.length - 1]['extra']['reasoning'] = '';
5911+ }
5912+
5913+ if (!reasoning) {
5914+ reasoning = '';
5915+ }
5916+
58645917 let oldMessage = '';
58655918 const generationFinished = new Date();
58665919 const img = extractImageFromMessage(getMessage);
@@ -5876,8 +5929,11 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58765929 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58775930 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58785931 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5932+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5933+ chat[chat.length - 1]['extra']['reasoning_duration'] = null;
58795934 if (power_user.message_token_count_enabled) {
58805935 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5936+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
58815937 }
58825938 const chat_id = (chat.length - 1);
58835939 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5896,8 +5952,11 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
58965952 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
58975953 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
58985954 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5955+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5956+ chat[chat.length - 1]['extra']['reasoning_duration'] = null;
58995957 if (power_user.message_token_count_enabled) {
59005958 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5959+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59015960 }
59025961 const chat_id = (chat.length - 1);
59035962 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5913,8 +5972,11 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59135972 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59145973 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59155974 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5975+ chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5976+ // We don't know if the reasoning duration extended, so we don't update it here on purpose.
59165977 if (power_user.message_token_count_enabled) {
59175978 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
5979+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59185980 }
59195981 const chat_id = (chat.length - 1);
59205982 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5930,6 +5992,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59305992 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59315993 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59325994 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5995+ chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5996+ chat[chat.length - 1]['extra']['reasoning_duration'] = null;
59335997 if (power_user.trim_spaces) {
59345998 getMessage = getMessage.trim();
59355999 }
@@ -5939,7 +6003,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
59396003 chat[chat.length - 1]['gen_finished'] = generationFinished;
59406004
59416005 if (power_user.message_token_count_enabled) {
59426006 chat[chat.lengthconst -tokenCountText 1]['extra'][= (reasoning || 'token_count'] =) await+ getTokenCountAsync(chat[chat.length - 1]['mes'], 0);
6007+ chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
59436008 }
59446009
59456010 if (selected_group) {
@@ -6004,6 +6069,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
60046069 return { type, getMessage };
60056070}
60066071
6072+export function syncCurrentSwipeInfoExtras() {
6073+ if (!chat.length) {
6074+ return;
6075+ }
6076+ const currentMessage = chat[chat.length - 1];
6077+ if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6078+ const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6079+ if (swipeInfo && typeof swipeInfo === 'object') {
6080+ swipeInfo.extra = structuredClone(currentMessage.extra);
6081+ }
6082+ }
6083+}
6084+
60076085function saveImageToMessage(img, mes) {
60086086 if (mes && img.image) {
60096087 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -6056,20 +6134,21 @@ function extractImageFromMessage(getMessage) {
60566134 return { getMessage, image, title };
60576135}
60586136
6137+/**
6138+ * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
6139+ */
60596140export function activateSendButtons() {
60606141 is_send_press = false;
6061- $('#send_but').removeClass('displayNone');
6062- $('#mes_continue').removeClass('displayNone');
6063- $('#mes_impersonate').removeClass('displayNone');
6064- $('.mes_buttons:last').show();
60656142 hideStopButton();
6143+ delete document.body.dataset.generating;
60666144}
60676145
6146+/**
6147+ * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
6148+ */
60686149export function deactivateSendButtons() {
6069- $('#send_but').addClass('displayNone');
6070- $('#mes_continue').addClass('displayNone');
6071- $('#mes_impersonate').addClass('displayNone');
60726150 showStopButton();
6151+ document.body.dataset.generating = 'true';
60736152}
60746153
60756154export function resetChatState() {
@@ -6765,10 +6844,11 @@ export async function getSettings() {
67656844 $('#your_name').val(name1);
67666845 }
67676846
6847+ accountStorage.init(settings?.accountStorage);
67686848 await setUserControls(data.enable_accounts);
67696849
67706850 // Allow subscribers to mutate settings
67716851 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
67726852
67736853 //Load KoboldAI settings
67746854 koboldai_setting_names = data.koboldai_setting_names;
@@ -6865,7 +6945,7 @@ export async function getSettings() {
68656945 loadProxyPresets(settings);
68666946
68676947 // Allow subscribers to mutate settings
68686948 await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
68696949
68706950 // Set context size after loading power user (may override the max value)
68716951 $('#max_context').val(max_context);
@@ -6925,7 +7005,7 @@ export async function getSettings() {
69257005 }
69267006 await validateDisabledSamplers();
69277007 settingsReady = true;
69287008 await eventSource.emit(event_types.SETTINGS_LOADED);
69297009}
69307010
69317011function selectKoboldGuiPreset() {
@@ -6936,7 +7016,8 @@ function selectKoboldGuiPreset() {
69367016
69377017export async function saveSettings(loopCounter = 0) {
69387018 if (!settingsReady) {
69397019 console.warn('Settings not ready, abortingscheduling another save');
7020+ saveSettingsDebounced();
69407021 return;
69417022 }
69427023
@@ -6957,6 +7038,7 @@ export async function saveSettings(loopCounter = 0) {
69577038 url: '/api/settings/save',
69587039 data: JSON.stringify({
69597040 firstRun: firstRun,
7041+ accountStorage: accountStorage.getState(),
69607042 currentVersion: currentVersion,
69617043 username: name1,
69627044 active_character: active_character,
@@ -7022,8 +7104,10 @@ export function setGenerationParamsFromPreset(preset) {
70227104// Common code for message editor done and auto-save
70237105function updateMessage(div) {
70247106 const mesBlock = div.closest('.mes_block');
70257107 let text = mesBlock.find('.edit_textarea').val();
7026- const mes = chat[this_edit_mes_id];
7108+ ?? mesBlock.find('.mes_text').text();
7109+ const mesElement = div.closest('.mes');
7110+ const mes = chat[mesElement.attr('mesid')];
70277111
70287112 let regexPlacement;
70297113 if (mes.is_user) {
@@ -7107,9 +7191,11 @@ function messageEditAuto(div) {
71077191 mes.is_system,
71087192 mes.is_user,
71097193 this_edit_mes_id,
7194+ {},
7195+ false,
71107196 ));
71117197 mesBlock.find('.mes_bias').empty();
71127198 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
71137199 saveChatDebounced();
71147200}
71157201
@@ -7131,13 +7217,20 @@ async function messageEditDone(div) {
71317217 mes.is_system,
71327218 mes.is_user,
71337219 this_edit_mes_id,
7220+ {},
7221+ false,
71347222 ),
71357223 );
71367224 mesBlock.find('.mes_bias').empty();
71377225 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
71387226 appendMediaToMessage(mes, div.closest('.mes'));
71397227 addCopyToCodeBlocks(div.closest('.mes'));
71407228
7229+ const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible');
7230+ if (reasoningEditDone.length > 0) {
7231+ reasoningEditDone.trigger('click');
7232+ }
7233+
71417234 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
71427235 this_edit_mes_id = undefined;
71437236 await saveChatConditional();
@@ -7401,7 +7494,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
74017494 }
74027495
74037496 try {
74047497 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
74057498 const page = Math.floor(charIndex / perPage) + 1;
74067499 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
74077500 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7433,7 +7526,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
74337526 return;
74347527 }
74357528
74367529 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
74377530 const page = Math.floor(charIndex / perPage) + 1;
74387531 $('#rm_print_characters_pagination').pagination('go', page);
74397532 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -7982,9 +8075,23 @@ function updateEditArrowClasses() {
79828075 }
79838076}
79848077
7985-function closeMessageEditor() {
8078+/**
7986- if (this_edit_mes_id) {
8079+ * Closes the message editor.
7987- $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8080+ * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8081+ */
8082+export function closeMessageEditor(what = 'all') {
8083+ if (what === 'message' || what === 'all') {
8084+ if (this_edit_mes_id) {
8085+ $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8086+ }
8087+ }
8088+ if (what === 'reasoning' || what === 'all') {
8089+ document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8090+ const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8091+ if (cancelButton instanceof HTMLElement) {
8092+ cancelButton.click();
8093+ }
8094+ });
79888095 }
79898096}
79908097
@@ -8417,6 +8524,9 @@ function swipe_left() { // when we swipe left..but no generation.
84178524 streamingProcessor.onStopStreaming();
84188525 }
84198526
8527+ // Make sure ad-hoc changes to extras are saved before swiping away
8528+ syncCurrentSwipeInfoExtras();
8529+
84208530 const swipe_duration = 120;
84218531 const swipe_range = '700px';
84228532 chat[chat.length - 1]['swipe_id']--;
@@ -8468,7 +8578,8 @@ function swipe_left() { // when we swipe left..but no generation.
84688578 }
84698579
84708580 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
84718581 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8582+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
84728583 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
84738584 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
84748585 }
@@ -8551,6 +8662,9 @@ const swipe_right = () => {
85518662 return unblockGeneration();
85528663 }
85538664
8665+ // Make sure ad-hoc changes to extras are saved before swiping away
8666+ syncCurrentSwipeInfoExtras();
8667+
85548668 const swipe_duration = 200;
85558669 const swipe_range = 700;
85568670 //console.log(swipe_range);
@@ -8617,11 +8731,6 @@ const swipe_right = () => {
86178731 easing: animation_easing,
86188732 queue: false,
86198733 complete: async function () {
8620- /*if (!selected_group) {
8621- var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
8622- typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
8623- } */
8624- /* $("#chat").append(typingIndicator); */
86258734 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
86268735 //console.log(parseInt(chat[chat.length-1]['swipe_id']));
86278736 //console.log(chat[chat.length-1]['swipes'].length);
@@ -8632,6 +8741,7 @@ const swipe_right = () => {
86328741 // resets the timer
86338742 swipeMessage.find('.mes_timer').html('');
86348743 swipeMessage.find('.tokenCounterDisplay').text('');
8744+ updateReasoningUI(swipeMessage, { reset: true });
86358745 } else {
86368746 //console.log('showing previously generated swipe candidate, or "..."');
86378747 //console.log('onclick right swipe calling addOneMessage');
@@ -8642,7 +8752,8 @@ const swipe_right = () => {
86428752 chat[chat.length - 1].extra = {};
86438753 }
86448754
86458755 const tokenCounttokenCountText = await getTokenCountAsync(chat[chat.length - 1]?.mes,extra?.reasoning 0|| '') + chat[chat.length - 1].mes;
8756+ const tokenCount = await getTokenCountAsync(tokenCountText, 0);
86468757 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
86478758 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
86488759 }
@@ -8680,7 +8791,6 @@ const swipe_right = () => {
86808791 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
86818792 console.debug('caught here 2');
86828793 is_send_press = true;
8683- $('.mes_buttons:last').hide();
86848794 await Generate('swipe');
86858795 } else {
86868796 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
@@ -9282,6 +9392,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
92829392 continue;
92839393 }
92849394
9395+ accountStorage.removeItem(`AlertWI_${character.avatar}`);
9396+ accountStorage.removeItem(`AlertRegex_${character.avatar}`);
9397+ accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
92859398 delete tag_map[character.avatar];
92869399 select_rm_info('char_delete', character.name);
92879400
@@ -9444,7 +9557,8 @@ function addDebugFunctions() {
94449557 message.extra = {};
94459558 }
94469559
9447- message.extra.token_count = await getTokenCountAsync(message.mes, 0);
9560+ const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9561+ message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
94489562 }
94499563
94509564 await saveChatConditional();
@@ -9483,8 +9597,8 @@ function addDebugFunctions() {
94839597 });
94849598
94859599 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
94869600 localStorageaccountStorage.setItem('RegenerateWithCtrlEnter', localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
94879601 toastr.info('Regenerate warning is now ' + (localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
94889602 });
94899603
94909604 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
@@ -10626,6 +10740,12 @@ jQuery(async function () {
1062610740 var edit_mes_id = $(this).closest('.mes').attr('mesid');
1062710741 this_edit_mes_id = edit_mes_id;
1062810742
10743+ // Also edit reasoning, if it exists
10744+ const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible');
10745+ if (reasoningEdit.length > 0) {
10746+ reasoningEdit.trigger('click');
10747+ }
10748+
1062910749 var text = chat[edit_mes_id]['mes'];
1063010750 if (chat[edit_mes_id]['is_user']) {
1063110751 this_edit_mes_chname = name1;
@@ -10753,10 +10873,17 @@ jQuery(async function () {
1075310873 chat[this_edit_mes_id].is_system,
1075410874 chat[this_edit_mes_id].is_user,
1075510875 this_edit_mes_id,
10876+ {},
10877+ false,
1075610878 ));
1075710879 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
1075810880 addCopyToCodeBlocks($(this).closest('.mes'));
1075910881
10882+ const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible');
10883+ if (reasoningEditDone.length > 0) {
10884+ reasoningEditDone.trigger('click');
10885+ }
10886+
1076010887 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
1076110888 this_edit_mes_id = undefined;
1076210889 });
@@ -11213,14 +11340,15 @@ jQuery(async function () {
1121311340
1121411341 $(document).keyup(function (e) {
1121511342 if (e.key === 'Escape') {
1121611343 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
1121711344 if (isEditVisible && power_user.auto_save_msg_edits === false) {
1121811345 closeMessageEditor('all');
1121911346 $('#send_textarea').focus();
1122011347 return;
1122111348 }
1122211349 if (isEditVisible && power_user.auto_save_msg_edits === true) {
1122311350 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11351+ closeMessageEditor('reasoning');
1122411352 $('#send_textarea').focus();
1122511353 return;
1122611354 }
@@ -11298,7 +11426,7 @@ jQuery(async function () {
1129811426 );
1129911427 break;*/
1130011428 default:
1130111429 await eventSource.emit('charManagementDropdown', target);
1130211430 }
1130311431 $('#char-management-dropdown').prop('selectedIndex', 0);
1130411432 });
@@ -11454,13 +11582,13 @@ jQuery(async function () {
1145411582 $('#avatar-and-name-block').slideToggle();
1145511583 });
1145611584
1145711585 $(document).on('mouseup touchend', '#show_more_messages', async function () => {
1145811586 await showMoreMessages();
1145911587 });
1146011588
1146111589 $(document).on('click', '.open_characters_library', async function () {
1146211590 await getCharacters();
1146311591 await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
1146411592 });
1146511593
1146611594 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
public/scripts/RossAscends-mods.js+41 -28
@@ -27,7 +27,6 @@ import {
2727 send_on_enter_options,
2828} from './power-user.js';
2929
30-import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
3130import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
3231import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
3332import {
@@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
4140import { debounce_timeout } from './constants.js';
4241
4342import { Popup } from './popup.js';
43+import { accountStorage } from './util/AccountStorage.js';
44+import { getCurrentUserHandle } from './user.js';
4445
4546var RPanelPin = document.getElementById('rm_button_panel_pin');
4647var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -409,32 +410,34 @@ function RA_autoconnect(PrevApi) {
409410function OpenNavPanels() {
410411 if (!isMobile()) {
411412 //auto-open R nav if locked and previously open
412413 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('NavOpened') == 'true') {
413414 //console.log("RA -- clicking right nav to open");
414415 $('#rightNavDrawerIcon').click();
415416 }
416417
417418 //auto-open L nav if locked and previously open
418419 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('LNavOpened') == 'true') {
419420 console.debug('RA -- clicking left nav to open');
420421 $('#leftNavDrawerIcon').click();
421422 }
422423
423424 //auto-open WI if locked and previously open
424425 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('WINavOpened') == 'true') {
425426 console.debug('RA -- clicking WI to open');
426427 $('#WIDrawerIcon').click();
427428 }
428429 }
429430}
430431
432+const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
433+
431434function restoreUserInput() {
432435 if (!power_user.restore_user_input) {
433436 console.debug('restoreUserInput disabled');
434437 return;
435438 }
436439
437440 const userInput = LoadLocallocalStorage.getItem('userInput'getUserInputKey());
438441 if (userInput) {
439442 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440443 }
@@ -442,7 +445,8 @@ function restoreUserInput() {
442445
443446function saveUserInput() {
444447 const userInput = String($('#send_textarea').val());
445448 SaveLocallocalStorage.setItem('userInput'getUserInputKey(), userInput);
449+ console.debug('User Input -- ', userInput);
446450}
447451const saveUserInputDebounced = debounce(saveUserInput);
448452
@@ -739,7 +743,7 @@ export function initRossMods() {
739743
740744 //toggle pin class when lock toggle clicked
741745 $(RPanelPin).on('click', function () {
742746 SaveLocalaccountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743747 if ($(RPanelPin).prop('checked') == true) {
744748 //console.log('adding pin class to right nav');
745749 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +761,7 @@ export function initRossMods() {
757761 }
758762 });
759763 $(LPanelPin).on('click', function () {
760764 SaveLocalaccountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761765 if ($(LPanelPin).prop('checked') == true) {
762766 //console.log('adding pin class to Left nav');
763767 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +780,7 @@ export function initRossMods() {
776780 });
777781
778782 $(WIPanelPin).on('click', function () {
779783 SaveLocalaccountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780784 if ($(WIPanelPin).prop('checked') == true) {
781785 console.debug('adding pin class to WI');
782786 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +800,8 @@ export function initRossMods() {
796800 });
797801
798802 // read the state of right Nav Lock and apply to rightnav classlist
799803 $(RPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true');
800804 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true') {
801805 //console.log('setting pin class via local var');
802806 $(RightNavPanel).addClass('pinnedOpen');
803807 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +812,8 @@ export function initRossMods() {
808812 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809813 }
810814 // read the state of left Nav Lock and apply to leftnav classlist
811815 $(LPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('LNavLockOn') === 'true');
812816 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true') {
813817 //console.log('setting pin class via local var');
814818 $(LeftNavPanel).addClass('pinnedOpen');
815819 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +825,8 @@ export function initRossMods() {
821825 }
822826
823827 // read the state of left Nav Lock and apply to leftnav classlist
824828 $(WIPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('WINavLockOn') === 'true');
825829 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true') {
826830 //console.log('setting pin class via local var');
827831 $(WorldInfo).addClass('pinnedOpen');
828832 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +841,22 @@ export function initRossMods() {
837841 //save state of Right nav being open or closed
838842 $('#rightNavDrawerIcon').on('click', function () {
839843 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840844 SaveLocalaccountStorage.setItem('NavOpened', 'true');
841845 } else { SaveLocalaccountStorage.setItem('NavOpened', 'false'); }
842846 });
843847
844848 //save state of Left nav being open or closed
845849 $('#leftNavDrawerIcon').on('click', function () {
846850 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847851 SaveLocalaccountStorage.setItem('LNavOpened', 'true');
848852 } else { SaveLocalaccountStorage.setItem('LNavOpened', 'false'); }
849853 });
850854
851855 //save state of Left nav being open or closed
852856 $('#WorldInfo').on('click', function () {
853857 if (!$('#WorldInfo').hasClass('openIcon')) {
854858 SaveLocalaccountStorage.setItem('WINavOpened', 'true');
855859 } else { SaveLocalaccountStorage.setItem('WINavOpened', 'false'); }
856860 });
857861
858862 var chatbarInFocus = false;
@@ -868,8 +872,8 @@ export function initRossMods() {
868872 OpenNavPanels();
869873 }, 300);
870874
871875 $(SelectedCharacterTab).click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872876 $('#rm_button_characters').click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873877
874878 // when a char is selected from the list, save them as the auto-load character for next page load
875879
@@ -1063,14 +1067,21 @@ export function initRossMods() {
10631067 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
10641068 if (event.ctrlKey && event.key == 'Enter') {
10651069 const editMesDone = $('.mes_edit_done:visible');
1070+ const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
10661071 if (editMesDone.length > 0) {
10671072 console.debug('Accepting edits with Ctrl+Enter');
10681073 $('#send_textarea').focustrigger('focus');
10691074 editMesDone.trigger('click');
10701075 return;
10711076 } else if (is_send_pressreasoningMesDone.length ==> false0) {
1077+ console.debug('Accepting edits with Ctrl+Enter');
1078+ $('#send_textarea').trigger('focus');
1079+ reasoningMesDone.trigger('click');
1080+ return;
1081+ }
1082+ else if (is_send_press == false) {
10721083 const skipConfirmKey = 'RegenerateWithCtrlEnter';
10731084 const skipConfirm = LoadLocalBoolaccountStorage.getItem(skipConfirmKey) === 'true';
10741085 function doRegenerate() {
10751086 console.debug('Regenerating with Ctrl+Enter');
10761087 $('#option_regenerate').trigger('click');
@@ -1082,13 +1093,15 @@ export function initRossMods() {
10821093 let regenerateWithCtrlEnter = false;
10831094 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
10841095 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
10851096 onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,{
1097+ regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
1098+ },
10861099 });
10871100 if (!result) {
10881101 return;
10891102 }
10901103
10911104 SaveLocalaccountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
10921105 doRegenerate();
10931106 }
10941107 return;
public/scripts/authors-note.js+1 -1
@@ -566,7 +566,7 @@ export function initAuthorsNote() {
566566 namedArgumentList: [],
567567 unnamedArgumentList: [
568568 new SlashCommandArgument(
569569 'positionrole', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
570570 ),
571571 ],
572572 helpString: `
public/scripts/backgrounds.js+19 -9
@@ -96,8 +96,13 @@ function highlightLockedBackground() {
9696 });
9797}
9898
99+/**
100+ * Locks the background for the current chat
101+ * @param {Event} e Click event
102+ * @returns {string} Empty string
103+ */
99104function onLockBackgroundClick(e) {
100105 e?.stopPropagation();
101106
102107 const chatName = getCurrentChatId();
103108
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
106111 return '';
107112 }
108113
109114 const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
110115
111116 saveBackgroundMetadata(relativeBgImage);
112117 setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
114119 return '';
115120}
116121
122+/**
123+ * Locks the background for the current chat
124+ * @param {Event} e Click event
125+ * @returns {string} Empty string
126+ */
117127function onUnlockBackgroundClick(e) {
118128 e?.stopPropagation();
119129 removeBackgroundMetadata();
120130 unsetCustomBackground();
121131 highlightLockedBackground();
@@ -482,10 +492,10 @@ function highlightNewBackground(bg) {
482492 */
483493function setFittingClass(fitting) {
484494 const backgrounds = $('#bg1, #bg_custom');
485- backgrounds.toggleClass('cover', fitting === 'cover');
495+ for (const option of ['cover', 'contain', 'stretch', 'center']) {
486496 backgrounds.toggleClass('contain'option, fittingoption === 'contain'fitting);
487- backgrounds.toggleClass('stretch', fitting === 'stretch');
497+ }
488- backgrounds.toggleClass('center', fitting === 'center');
498+ background_settings.fitting = fitting;
489499}
490500
491501function onBackgroundFilterInput() {
@@ -513,12 +523,12 @@ export function initBackgrounds() {
513523 $('#add_bg_button').on('change', onBackgroundUploadSelected);
514524 $('#bg-filter').on('input', onBackgroundFilterInput);
515525 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
516- callback: onLockBackgroundClick,
526+ callback: () => onLockBackgroundClick(new CustomEvent('click')),
517527 aliases: ['bglock'],
518528 helpString: 'Locks a background for the currently selected chat',
519529 }));
520530 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
521- callback: onUnlockBackgroundClick,
531+ callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
522532 aliases: ['bgunlock'],
523533 helpString: 'Unlocks a background for the currently selected chat',
524534 }));
public/scripts/chat-templates.js+12 -1
@@ -59,6 +59,17 @@ 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'
72+ ,
6273};
6374
6475const substr_derivations = {
@@ -87,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
8798 }
8899 }
89100
90101 console.logwarn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
91102 return null;
92103}
public/scripts/chats.js+65 -12
@@ -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,8 @@ 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';
48+import { accountStorage } from './util/AccountStorage.js';
4449
4550/**
4651 * @typedef {Object} FileAttachment
@@ -617,21 +622,56 @@ async function enlargeMessageImage() {
617622}
618623
619624async function deleteMessageImage() {
620625 const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);TEXT, '', {
626+ okButton: t`Delete one`,
627+ customButtons: [
628+ {
629+ text: t`Delete all`,
630+ appendAtEnd: true,
631+ result: POPUP_RESULT.CUSTOM1,
632+ },
633+ {
634+ text: t`Cancel`,
635+ appendAtEnd: true,
636+ result: POPUP_RESULT.CANCELLED,
637+ },
638+ ],
639+ });
621640
622641 if (value !== POPUP_RESULT.AFFIRMATIVEvalue) {
623642 return;
624643 }
625644
626645 const mesBlock = $(this).closest('.mes');
627646 const mesId = mesBlock.attr('mesid');
628647 const message = chat[mesId];
629- delete message.extra.image;
648+
630- delete message.extra.inline_image;
649+ let isLastImage = true;
631- delete message.extra.title;
650+
632- delete message.extra.append_title;
651+ if (Array.isArray(message.extra.image_swipes)) {
633- mesBlock.find('.mes_img_container').removeClass('img_extra');
652+ const indexOf = message.extra.image_swipes.indexOf(message.extra.image);
634- mesBlock.find('.mes_img').attr('src', '');
653+ if (indexOf > -1) {
654+ message.extra.image_swipes.splice(indexOf, 1);
655+ isLastImage = message.extra.image_swipes.length === 0;
656+ if (!isLastImage) {
657+ const newIndex = Math.min(indexOf, message.extra.image_swipes.length - 1);
658+ message.extra.image = message.extra.image_swipes[newIndex];
659+ }
660+ }
661+ }
662+
663+ if (isLastImage || value === POPUP_RESULT.CUSTOM1) {
664+ delete message.extra.image;
665+ delete message.extra.inline_image;
666+ delete message.extra.title;
667+ delete message.extra.append_title;
668+ delete message.extra.image_swipes;
669+ mesBlock.find('.mes_img_container').removeClass('img_extra');
670+ mesBlock.find('.mes_img').attr('src', '');
671+ } else {
672+ appendMediaToMessage(message, mesBlock);
673+ }
674+
635675 await saveChatConditional();
636676}
637677
@@ -1039,8 +1079,8 @@ async function openAttachmentManager() {
10391079 renderAttachments();
10401080 });
10411081
10421082 let sortField = localStorageaccountStorage.getItem('DataBank_sortField') || 'created';
10431083 let sortOrder = localStorageaccountStorage.getItem('DataBank_sortOrder') || 'desc';
10441084 let filterString = '';
10451085
10461086 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@@ -1056,8 +1096,8 @@ async function openAttachmentManager() {
10561096
10571097 sortField = this.selectedOptions[0].dataset.sortField;
10581098 sortOrder = this.selectedOptions[0].dataset.sortOrder;
10591099 localStorageaccountStorage.setItem('DataBank_sortField', sortField);
10601100 localStorageaccountStorage.setItem('DataBank_sortOrder', sortOrder);
10611101 renderAttachments();
10621102 });
10631103 function handleBulkAction(action) {
@@ -1437,6 +1477,19 @@ jQuery(function () {
14371477 await viewMessageFile(messageId);
14381478 });
14391479
1480+ $(document).on('click', '.assistant_note_export', async function () {
1481+ const chatToSave = [
1482+ {
1483+ user_name: name1,
1484+ character_name: name2,
1485+ chat_metadata: chat_metadata,
1486+ },
1487+ ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
1488+ ];
1489+
1490+ download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
1491+ });
1492+
14401493 // Do not change. #attachFile is added by extension.
14411494 $(document).on('click', '#attachFile', function () {
14421495 $('#file_form_input').trigger('click');
public/scripts/extensions.js+5 -4
@@ -9,6 +9,7 @@ import { getContext } from './st-context.js';
99import { isAdmin } from './user.js';
1010import { t } from './i18n.js';
1111import { debounce_timeout } from './constants.js';
12+import { accountStorage } from './util/AccountStorage.js';
1213
1314export {
1415 getContext,
@@ -714,7 +715,7 @@ async function showExtensionsDetails() {
714715 htmlExternal.append(htmlLoading);
715716
716717 const sortOrderKey = 'extensions_sortByName';
717718 const sortByName = localStorageaccountStorage.getItem(sortOrderKey) === 'true';
718719 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
719720 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
720721
@@ -745,7 +746,7 @@ async function showExtensionsDetails() {
745746 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
746747 action: async () => {
747748 abortController.abort();
748749 localStorageaccountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
749750 await showExtensionsDetails();
750751 },
751752 };
@@ -1153,11 +1154,11 @@ async function checkForExtensionUpdates(force) {
11531154 const currentDate = new Date().toDateString();
11541155
11551156 // Don't nag more than once a day
11561157 if (localStorageaccountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
11571158 return;
11581159 }
11591160
11601161 localStorageaccountStorage.setItem(STORAGE_NAG_KEY, currentDate);
11611162 }
11621163
11631164 const isCurrentUserAdmin = isAdmin();
public/scripts/extensions/assets/index.js+3 -2
@@ -8,6 +8,7 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from
88import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
99import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
1010import { executeSlashCommands } from '../../slash-commands.js';
11+import { accountStorage } from '../../util/AccountStorage.js';
1112import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
1213export { MODULE_NAME };
1314
@@ -432,14 +433,14 @@ jQuery(async () => {
432433 connectButton.on('click', async function () {
433434 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
434435 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
435436 const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true';
436437
437438 const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {
438439 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
439440 onClose: popup => {
440441 if (popup.result) {
441442 const rememberValue = popup.inputResults.get('assets-remember');
442443 localStorageaccountStorage.setItem(rememberKey, String(rememberValue));
443444 }
444445 },
445446 });
public/scripts/extensions/caption/settings.html+9 -1
@@ -10,7 +10,7 @@
1010 <select id="caption_source" class="text_pole">
1111 <option value="local" data-i18n="Local">Local</option>
1212 <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>
1313 <option value="extras" data-i18n="Extras">Extras (deprecated)</option>
1414 <option value="horde" data-i18n="Horde">Horde</option>
1515 </select>
1616 <div id="caption_multimodal_block" class="flex-container wide100p">
@@ -53,7 +53,15 @@
5353 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
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>
56+ <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>
57+ <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>
58+ <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
59+ <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
60+ <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
61+ <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
5662 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
63+ <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
64+ <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
5765 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
5866 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
5967 <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/expressions/index.js+0 -1
@@ -2178,7 +2178,6 @@ function migrateSettings() {
21782178 typeList: [ARGUMENT_TYPE.STRING],
21792179 isRequired: true,
21802180 enumProvider: commonEnumProviders.characters('character'),
2181- forceEnum: true,
21822181 }),
21832182 ],
21842183 helpString: 'Returns the last set sprite / expression for the named character.',
public/scripts/extensions/expressions/settings.html+1 -1
@@ -23,7 +23,7 @@
2323 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
2424 <select id="expression_api" class="flex1 margin0">
2525 <option value="0" data-i18n="Local">Local</option>
2626 <option value="1" data-i18n="Extras">Extras (deprecated)</option>
2727 <option value="2" data-i18n="Main API">Main API</option>
2828 <option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>
2929 </select>
public/scripts/extensions/gallery/index.js+0 -1
@@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441441 description: 'character name',
442442 typeList: [ARGUMENT_TYPE.STRING],
443443 enumProvider: commonEnumProviders.characters('character'),
444- forceEnum: true,
445444 }),
446445 SlashCommandNamedArgument.fromProps({
447446 name: 'group',
public/scripts/extensions/memory/settings.html+1 -1
@@ -12,7 +12,7 @@
1212 <label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
1313 <select id="summary_source">
1414 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
1515 <option value="extras">Extras API (deprecated)</option>
1616 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
1717 </select><br>
1818
public/scripts/extensions/quick-reply/src/QuickReply.js+9 -8
@@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
1010import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
1111import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
1212import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
13+import { accountStorage } from '../../../util/AccountStorage.js';
1314import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
1415import { log, quickReplyApi, warn } from '../index.js';
1516import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@@ -544,9 +545,9 @@ export class QuickReply {
544545 this.editorSyntax = messageSyntaxInner;
545546 /**@type {HTMLInputElement}*/
546547 const wrap = dom.querySelector('#qr--modal-wrap');
547548 wrap.checked = JSON.parse(localStorageaccountStorage.getItem('qr--wrap') ?? 'false');
548549 wrap.addEventListener('click', () => {
549550 localStorageaccountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
550551 updateWrap();
551552 });
552553 const updateWrap = () => {
@@ -594,27 +595,27 @@ export class QuickReply {
594595 };
595596 /**@type {HTMLInputElement}*/
596597 const tabSize = dom.querySelector('#qr--modal-tabSize');
597598 tabSize.value = JSON.parse(localStorageaccountStorage.getItem('qr--tabSize') ?? '4');
598599 const updateTabSize = () => {
599600 message.style.tabSize = tabSize.value;
600601 messageSyntaxInner.style.tabSize = tabSize.value;
601602 updateScrollDebounced();
602603 };
603604 tabSize.addEventListener('change', () => {
604605 localStorageaccountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
605606 updateTabSize();
606607 });
607608 /**@type {HTMLInputElement}*/
608609 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
609610 executeShortcut.checked = JSON.parse(localStorageaccountStorage.getItem('qr--executeShortcut') ?? 'true');
610611 executeShortcut.addEventListener('click', () => {
611612 localStorageaccountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
612613 });
613614 /**@type {HTMLInputElement}*/
614615 const syntax = dom.querySelector('#qr--modal-syntax');
615616 syntax.checked = JSON.parse(localStorageaccountStorage.getItem('qr--syntax') ?? 'true');
616617 syntax.addEventListener('click', () => {
617618 localStorageaccountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
618619 updateSyntaxEnabled();
619620 });
620621 if (navigator.keyboard) {
public/scripts/extensions/quick-reply/src/QuickReplySet.js+6 -28
@@ -1,15 +1,14 @@
11import { getRequestHeaders, substituteParams } from '../../../../script.js';
22import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
33import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4-import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
54import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
65import { debounceAsync, log, warnSlashCommandParser } from '../index../../slash-commands/SlashCommandParser.js';
6+import { debounceAsync, warn } from '../index.js';
77import { QuickReply } from './QuickReply.js';
88
99export class QuickReplySet {
1010 /**@type {QuickReplySet[]}*/ static list = [];
1111
12-
1312 static from(props) {
1413 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
1514 const instance = Object.assign(new this(), props);
@@ -24,9 +23,6 @@ export class QuickReplySet {
2423 return this.list.find(it=>it.name == name);
2524 }
2625
27-
28-
29-
3026 /**@type {string}*/ name;
3127 /**@type {boolean}*/ disableSend = false;
3228 /**@type {boolean}*/ placeBeforeInput = false;
@@ -34,19 +30,12 @@ export class QuickReplySet {
3430 /**@type {string}*/ color = 'transparent';
3531 /**@type {boolean}*/ onlyBorderColor = false;
3632 /**@type {QuickReply[]}*/ qrList = [];
37-
3833 /**@type {number}*/ idIndex = 0;
39-
4034 /**@type {boolean}*/ isDeleted = false;
41-
4235 /**@type {function}*/ save;
43-
4436 /**@type {HTMLElement}*/ dom;
4537 /**@type {HTMLElement}*/ settingsDom;
4638
47-
48-
49-
5039 constructor() {
5140 this.save = debounceAsync(()=>this.performSave(), 200);
5241 }
@@ -55,9 +44,6 @@ export class QuickReplySet {
5544 this.qrList.forEach(qr=>this.hookQuickReply(qr));
5645 }
5746
58-
59-
60-
6147 unrender() {
6248 this.dom?.remove();
6349 this.dom = null;
@@ -100,9 +86,6 @@ export class QuickReplySet {
10086 }
10187 }
10288
103-
104-
105-
10689 renderSettings() {
10790 if (!this.settingsDom) {
10891 this.settingsDom = document.createElement('div'); {
@@ -123,9 +106,6 @@ export class QuickReplySet {
123106 this.settingsDom.append(qr.renderSettings(idx));
124107 }
125108
126-
127-
128-
129109 /**
130110 *
131111 * @param {QuickReply} qr
@@ -138,6 +118,7 @@ export class QuickReplySet {
138118 closure.scope.setMacro('arg::*', '');
139119 return (await closure.execute())?.pipe;
140120 }
121+
141122 /**
142123 *
143124 * @param {QuickReply} qr The QR to execute.
@@ -207,6 +188,7 @@ export class QuickReplySet {
207188 document.querySelector('#send_but').click();
208189 }
209190 }
191+
210192 /**
211193 * @param {QuickReply} qr
212194 * @param {string} [message] - optional altered message to be used
@@ -220,9 +202,6 @@ export class QuickReplySet {
220202 });
221203 }
222204
223-
224-
225-
226205 addQuickReply(data = {}) {
227206 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228207 data.id =
@@ -239,6 +218,7 @@ export class QuickReplySet {
239218 this.save();
240219 return qr;
241220 }
221+
242222 addQuickReplyFromText(qrJson) {
243223 let data;
244224 if (qrJson) {
@@ -371,7 +351,6 @@ export class QuickReplySet {
371351 this.save();
372352 }
373353
374-
375354 toJSON() {
376355 return {
377356 version: 2,
@@ -386,7 +365,6 @@ export class QuickReplySet {
386365 };
387366 }
388367
389-
390368 async performSave() {
391369 const response = await fetch('/api/quick-replies/save', {
392370 method: 'POST',
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+4 -0
@@ -883,6 +883,10 @@ export class SlashCommandHandler {
883883 }
884884 }
885885 getQuickReply(args) {
886+ if (!args.id && !args.label) {
887+ toastr.error('Please provide a valid id or label.');
888+ return '';
889+ }
886890 try {
887891 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
888892 } catch (ex) {
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+1 -1
@@ -346,7 +346,7 @@ export class SettingsUi {
346346 }
347347
348348 async addQrSet() {
349349 const name = await Popup.show.input('Create a new WorldQuick InfoReply Set', 'Enter a name for the new Quick Reply Set:');
350350 if (name && name.length > 0) {
351351 const oldQrs = QuickReplySet.get(name);
352352 if (oldQrs) {
public/scripts/extensions/regex/editor.html+6 -0
@@ -94,6 +94,12 @@
9494 <span data-i18n="World Info">World Info</span>
9595 </label>
9696 </div>
97+ <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
98+ <label class="checkbox flex-container">
99+ <input type="checkbox" name="replace_position" value="6">
100+ <span data-i18n="Reasoning">Reasoning</span>
101+ </label>
102+ </div>
97103 <div class="flex-container wide100p marginTop5">
98104 <div class="flex1 flex-container flexNoGap">
99105 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
public/scripts/extensions/regex/engine.js+2 -1
@@ -20,6 +20,7 @@ const regex_placement = {
2020 SLASH_COMMAND: 3,
2121 // 4 - sendAs (legacy)
2222 WORLD_INFO: 5,
23+ REASONING: 6,
2324};
2425
2526export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
9495 // Script applies to Generate and input is Generate
9596 (script.promptOnly && isPrompt) ||
9697 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
9798 (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
9899 ) {
99100 if (isEdit && !script.runOnEdit) {
100101 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
public/scripts/extensions/regex/index.js+4 -3
@@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
1111import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
1212import { t } from '../../i18n.js';
13+import { accountStorage } from '../../util/AccountStorage.js';
1314
1415/**
1516 * @typedef {object} RegexScript
@@ -18,7 +19,7 @@ import { t } from '../../i18n.js';
1819 * @property {string} replaceString - The replace string
1920 * @property {string[]} trimStrings - The trim strings
2021 * @property {string?} findRegex - The find regex
2122 * @property {stringnumber?} substituteRegex - The substitute regex
2223 */
2324
2425/**
@@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() {
440441 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
441442 const checkKey = `AlertRegex_${characters[chid].avatar}`;
442443
443444 if (!localStorageaccountStorage.getItem(checkKey)) {
444445 localStorageaccountStorage.setItem(checkKey, 'true');
445446 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
446447 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
447448
public/scripts/extensions/stable-diffusion/index.js+67 -0
@@ -81,6 +81,7 @@ const sources = {
8181 huggingface: 'huggingface',
8282 nanogpt: 'nanogpt',
8383 bfl: 'bfl',
84+ falai: 'falai',
8485};
8586
8687const initiators = {
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
11691170 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
11701171}
11711172
1173+async function onFalaiKeyClick() {
1174+ return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175+}
1176+
11721177function onBflUpsamplingInput() {
11731178 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
11741179 saveSettingsDebounced();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
12991304 sources.huggingface,
13001305 sources.nanogpt,
13011306 sources.bfl,
1307+ sources.falai,
13021308 ];
13031309
13041310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
17071713 case sources.bfl:
17081714 models = await loadBflModels();
17091715 break;
1716+ case sources.falai:
1717+ models = await loadFalaiModels();
1718+ break;
17101719 }
17111720
17121721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
17441753 ];
17451754}
17461755
1756+async function loadFalaiModels() {
1757+ $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
1758+
1759+ const result = await fetch('/api/sd/falai/models', {
1760+ method: 'POST',
1761+ headers: getRequestHeaders(),
1762+ });
1763+
1764+ if (result.ok) {
1765+ return await result.json();
1766+ }
1767+
1768+ return [];
1769+}
1770+
17471771async function loadPollinationsModels() {
17481772 const result = await fetch('/api/sd/pollinations/models', {
17491773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
20812105 case sources.bfl:
20822106 schedulers = ['N/A'];
20832107 break;
2108+ case sources.falai:
2109+ schedulers = ['N/A'];
2110+ break;
20842111 }
20852112
20862113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27352762 case sources.bfl:
27362763 result = await generateBflImage(prefixedPrompt, signal);
27372764 break;
2765+ case sources.falai:
2766+ result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767+ break;
27382768 }
27392769
27402770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
34963526 }
34973527}
34983528
3529+/**
3530+ * Generates an image using the FAL.AI API.
3531+ * @param {string} prompt - The main instruction used to guide the image generation.
3532+ * @param {string} negativePrompt - The negative prompt used to guide the image generation.
3533+ * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3534+ * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3535+ */
3536+async function generateFalaiImage(prompt, negativePrompt, signal) {
3537+ const result = await fetch('/api/sd/falai/generate', {
3538+ method: 'POST',
3539+ headers: getRequestHeaders(),
3540+ signal: signal,
3541+ body: JSON.stringify({
3542+ prompt: prompt,
3543+ negative_prompt: negativePrompt,
3544+ model: extension_settings.sd.model,
3545+ steps: clamp(extension_settings.sd.steps, 1, 50),
3546+ guidance: clamp(extension_settings.sd.scale, 1.5, 5),
3547+ width: clamp(extension_settings.sd.width, 256, 1440),
3548+ height: clamp(extension_settings.sd.height, 256, 1440),
3549+ seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3550+ }),
3551+ });
3552+
3553+ if (result.ok) {
3554+ const data = await result.json();
3555+ return { format: 'jpg', data: data.image };
3556+ } else {
3557+ const text = await result.text();
3558+ console.log(text);
3559+ throw new Error(text);
3560+ }
3561+}
3562+
34993563async function onComfyOpenWorkflowEditorClick() {
35003564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
35013565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
37823846 return secret_state[SECRET_KEYS.NANOGPT];
37833847 case sources.bfl:
37843848 return secret_state[SECRET_KEYS.BFL];
3849+ case sources.falai:
3850+ return secret_state[SECRET_KEYS.FALAI];
37853851 }
37863852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
44434509 $('#sd_function_tool').on('input', onFunctionToolInput);
44444510 $('#sd_bfl_key').on('click', onBflKeyClick);
44454511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512+ $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
44474514 if (!CSS.supports('field-sizing', 'content')) {
44484515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+16 -1
@@ -41,7 +41,8 @@
4141 <option value="blockentropy">Block Entropy</option>
4242 <option value="comfy">ComfyUI</option>
4343 <option value="drawthings">DrawThings HTTP API</option>
4444 <option value="extras">Extras API (local / remotedeprecated)</option>
45+ <option value="falai">FAL.AI</option>
4546 <option value="huggingface">HuggingFace Inference API (serverless)</option>
4647 <option value="nanogpt">NanoGPT</option>
4748 <option value="novel">NovelAI Diffusion</option>
@@ -256,6 +257,20 @@
256257 </label>
257258 </div>
258259
260+ <div data-sd-source="falai">
261+ <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
262+ <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
263+ <strong data-i18n="API Key">API Key</strong>
264+ <i class="fa-solid fa-share-from-square"></i>
265+ </a>
266+ <span class="expander"></span>
267+ <div id="sd_falai_key" class="menu_button menu_button_icon">
268+ <i class="fa-fw fa-solid fa-key"></i>
269+ <span data-i18n="Click to set">Click to set</span>
270+ </div>
271+ </div>
272+ </div>
273+
259274 <div class="flex-container">
260275 <div class="flex1">
261276 <label for="sd_model" data-i18n="Model">Model</label>
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/extensions/vectors/index.js+4 -6
@@ -1621,14 +1621,14 @@ jQuery(async () => {
16211621 const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
16221622 const collectionIds = await ingestDataBankAttachments(String(source));
16231623 const queryResults = await queryMultipleCollections(collectionIds, String(query), count, threshold);
1624-
1624+
16251625 // Get URLs
16261626 const urls = Object
16271627 .keys(queryResults)
16281628 .map(x => attachments.find(y => getFileCollectionId(y.url) === x))
16291629 .filter(x => x)
16301630 .map(x => x.url);
1631-
1631+
16321632 // Gets the actual text content of chunks
16331633 const getChunksText = () => {
16341634 let textResult = '';
@@ -1638,14 +1638,12 @@ jQuery(async () => {
16381638 }
16391639 return textResult;
16401640 };
1641-
16421641 if (args.return === 'chunks') {
16431642 return getChunksText();
16441643 }
16451644
16461645 // @ts-ignore
16471646 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
1648-
16491647 },
16501648 aliases: ['databank-search', 'data-bank-search'],
16511649 helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',
@@ -1660,10 +1658,10 @@ jQuery(async () => {
16601658 defaultValue: 'object',
16611659 enumList: [
16621660 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
16631661 ...slashCommandReturnHelper.enumList({ allowObject: true }),
16641662 ],
16651663 forceEnum: true,
16661664 }),
16671665 ],
16681666 unnamedArgumentList: [
16691667 new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),
public/scripts/extensions/vectors/settings.html+1 -1
@@ -11,7 +11,7 @@
1111 </label>
1212 <select id="vectors_source" class="text_pole">
1313 <option value="cohere">Cohere</option>
1414 <option value="extras">Extras (deprecated)</option>
1515 <option value="palm">Google AI Studio</option>
1616 <option value="llamacpp">llama.cpp</option>
1717 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
public/scripts/f-localStorage.js+15 -0
@@ -1,18 +1,30 @@
11////////////////// LOCAL STORAGE HANDLING /////////////////////
22
3+/**
4+ * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
5+ */
36export function SaveLocal(target, val) {
47 localStorage.setItem(target, val);
58 console.debug('SaveLocal -- ' + target + ' : ' + val);
69}
10+/**
11+ * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
12+ */
713export function LoadLocal(target) {
814 console.debug('LoadLocal -- ' + target);
915 return localStorage.getItem(target);
1016
1117}
18+/**
19+ * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
20+ */
1221export function LoadLocalBool(target) {
1322 let result = localStorage.getItem(target) === 'true';
1423 return result;
1524}
25+/**
26+ * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
27+ */
1628export function CheckLocal() {
1729 console.log('----------local storage---------');
1830 var i;
@@ -22,6 +34,9 @@ export function CheckLocal() {
2234 console.log('------------------------------');
2335}
2436
37+/**
38+ * @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
39+ */
2540export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }
2641
2742/////////////////////////////////////////////////////////////////////////
public/scripts/group-chats.js+42 -37
@@ -78,9 +78,11 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
7878import { isExternalMediaAllowed } from './chats.js';
7979import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
8080import { t } from './i18n.js';
81+import { accountStorage } from './util/AccountStorage.js';
8182
8283export {
8384 selected_group,
85+ openGroupId,
8486 is_group_automode_enabled,
8587 hideMutedSprites,
8688 is_group_generating,
@@ -291,10 +293,11 @@ export function getGroupNames() {
291293
292294/**
293295 * Finds the character ID for a group member.
294296 * @param {number|string} arg 0-based member index or character name
295- * @returns {number} 0-based character ID
297+ * @param {Boolean} full Whether to return a key-value object containing extra data
298+ * @returns {number|Object} 0-based character ID or key-value object if full is true
296299 */
297300export function findGroupMemberId(arg, full = false) {
298301 arg = arg?.trim();
299302
300303 if (!arg) {
@@ -310,15 +313,19 @@ export function findGroupMemberId(arg) {
310313 }
311314
312315 const index = parseInt(arg);
313316 const searchByNamesearchByString = isNaN(index);
314317
315318 if (searchByNamesearchByString) {
316- const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) }));
319+ const memberNames = group.members.map(x => ({
317- const fuse = new Fuse(memberNames, { keys: ['name'] });
320+ avatar: x,
321+ name: characters.find(y => y.avatar === x)?.name,
322+ index: characters.findIndex(y => y.avatar === x),
323+ }));
324+ const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] });
318325 const result = fuse.search(arg);
319326
320327 if (!result.length) {
321328 console.warn(`WARN: No group member found withusing namestring ${arg}`);
322329 return;
323330 }
324331
@@ -329,9 +336,11 @@ export function findGroupMemberId(arg) {
329336 return;
330337 }
331338
332339 console.log(`TriggeringTargeting group member ${chid} (${arg}) from search result`, result[0]);
333- return chid;
340+
334- } else {
341+ return !full ? chid : { ...{ id: chid }, ...result[0].item };
342+ }
343+ else {
335344 const memberAvatar = group.members[index];
336345
337346 if (memberAvatar === undefined) {
@@ -346,8 +355,14 @@ export function findGroupMemberId(arg) {
346355 return;
347356 }
348357
349358 console.log(`TriggeringTargeting group member ${memberAvatar} at index ${index}`);
350- return chid;
359+
360+ return !full ? chid : {
361+ id: chid,
362+ avatar: memberAvatar,
363+ name: characters.find(y => y.avatar === memberAvatar)?.name,
364+ index: index,
365+ };
351366 }
352367}
353368
@@ -804,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
804819
805820 /** @type {any} Caution: JS war crimes ahead */
806821 let textResult = '';
807- let typingIndicator = $('#chat .typing_indicator');
808822 const group = groups.find((x) => x.id === selected_group);
809823
810824 if (!group || !Array.isArray(group.members) || !group.members.length) {
@@ -820,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
820834 setCharacterId(undefined);
821835 const userInput = String($('#send_textarea').val());
822836
823- if (typingIndicator.length === 0 && !isStreamingEnabled()) {
824- typingIndicator = $(
825- '#typing_indicator_template .typing_indicator',
826- ).clone();
827- typingIndicator.hide();
828- $('#chat').append(typingIndicator);
829- }
830-
831837 // id of this specific batch for regeneration purposes
832838 group_generation_id = Date.now();
833839 const lastMessage = chat[chat.length - 1];
@@ -905,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
905911 }
906912 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
907913
908- if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
909- // update indicator and scroll down
910- typingIndicator
911- .find('.typing_indicator_name')
912- .text(characters[chId].name);
913- typingIndicator.show();
914- }
915-
916914 // Wait for generation to finish
917915 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
918916 let messageChunk = textResult?.messageChunk;
@@ -929,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
929927 }
930928 }
931929 } finally {
932- typingIndicator.hide();
933-
934930 is_group_generating = false;
935931 setSendButtonState(false);
936932 setCharacterId(undefined);
@@ -1314,10 +1310,10 @@ function printGroupCandidates() {
13141310 formatNavigator: PAGINATION_TEMPLATE,
13151311 showNavigator: true,
13161312 showSizeChanger: true,
13171313 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5,
13181314 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
13191315 afterSizeSelectorChange: function (e) {
13201316 localStorageaccountStorage.setItem(storageKey, e.target.value);
13211317 },
13221318 callback: function (data) {
13231319 $('#rm_group_add_members').empty();
@@ -1341,10 +1337,10 @@ function printGroupMembers() {
13411337 formatNavigator: PAGINATION_TEMPLATE,
13421338 showNavigator: true,
13431339 showSizeChanger: true,
13441340 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5,
13451341 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
13461342 afterSizeSelectorChange: function (e) {
13471343 localStorageaccountStorage.setItem(storageKey, e.target.value);
13481344 },
13491345 callback: function (data) {
13501346 $('.rm_group_members').empty();
@@ -1367,6 +1363,15 @@ function getGroupCharacterBlock(character) {
13671363 template.find('.ch_fav').val(isFav);
13681364 template.toggleClass('is_fav', isFav);
13691365
1366+ const auxFieldName = power_user.aux_field || 'character_version';
1367+ const auxFieldValue = (character.data && character.data[auxFieldName]) || '';
1368+ if (auxFieldValue) {
1369+ template.find('.character_version').text(auxFieldValue);
1370+ }
1371+ else {
1372+ template.find('.character_version').hide();
1373+ }
1374+
13701375 let queuePosition = groupChatQueueOrder.get(character.avatar);
13711376 if (queuePosition) {
13721377 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/loader.js+33 -12
@@ -27,24 +27,45 @@ export async function hideLoader() {
2727 }
2828
2929 return new Promise((resolve) => {
30- // Spinner blurs/fades out
30+ const spinner = $('#load-spinner');
31- $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
31+ if (!spinner.length) {
32+ console.warn('Spinner element not found, skipping animation');
33+ cleanup();
34+ return;
35+ }
36+
37+ // Check if transitions are enabled
38+ const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
39+ const hasTransitions = parseFloat(transitionDuration) > 0;
40+
41+ if (hasTransitions) {
42+ Promise.race([
43+ new Promise((r) => setTimeout(r, 500)), // Fallback timeout
44+ new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
45+ ]).finally(cleanup);
46+ } else {
47+ cleanup();
48+ }
49+
50+ function cleanup() {
3251 $('#loader').remove();
3352 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
3453 // If it's present, we remove it once and then it's gone.
3554 yoinkPreloader();
3655
3756 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
38- loaderPopup = null;
57+ .catch((err) => console.error('Error completing loaderPopup:', err))
39- resolve();
58+ .finally(() => {
40- });
59+ loaderPopup = null;
41- });
60+ resolve();
61+ });
62+ }
4263
43- $('#load-spinner')
64+ // Apply the styles
4465 spinner.css({
4566 'filter': 'blur(15px)',
4667 'opacity': '0',
4768 });
4869 });
4970}
5071
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+113 -64
@@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js
7373import { Popup, POPUP_RESULT } from './popup.js';
7474import { t } from './i18n.js';
7575import { ToolManager } from './tool-calling.js';
76+import { accountStorage } from './util/AccountStorage.js';
7677
7778export {
7879 openai_messages_count,
@@ -82,7 +83,6 @@ export {
8283 setOpenAIMessageExamples,
8384 setupChatCompletionPromptManager,
8485 sendOpenAIRequest,
85- getChatCompletionModel,
8686 TokenHandler,
8787 IdentifierNotFoundError,
8888 Message,
@@ -258,8 +258,8 @@ const default_settings = {
258258 ai21_model: 'jamba-1.5-large',
259259 mistralai_model: 'mistral-large-latest',
260260 cohere_model: 'command-r-plus',
261261 perplexity_model: 'llama-3.1-70bsonar-instructpro',
262262 groq_model: 'llama-3.13-70b-versatile',
263263 nanogpt_model: 'gpt-4o-mini',
264264 zerooneai_model: 'yi-large',
265265 blockentropy_model: 'be-70b-base-llama3.1',
@@ -298,7 +298,8 @@ const default_settings = {
298298 names_behavior: character_names_behavior.DEFAULT,
299299 continue_postfix: continue_postfix_types.SPACE,
300300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301301 show_thoughts: falsetrue,
302+ reasoning_effort: 'medium',
302303 seed: -1,
303304 n: 1,
304305};
@@ -337,7 +338,7 @@ const oai_settings = {
337338 ai21_model: 'jamba-1.5-large',
338339 mistralai_model: 'mistral-large-latest',
339340 cohere_model: 'command-r-plus',
340341 perplexity_model: 'llama-3.1-70bsonar-instructpro',
341342 groq_model: 'llama-3.1-70b-versatile',
342343 nanogpt_model: 'gpt-4o-mini',
343344 zerooneai_model: 'yi-large',
@@ -377,7 +378,8 @@ const oai_settings = {
377378 names_behavior: character_names_behavior.DEFAULT,
378379 continue_postfix: continue_postfix_types.SPACE,
379380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380381 show_thoughts: falsetrue,
382+ reasoning_effort: 'medium',
381383 seed: -1,
382384 n: 1,
383385};
@@ -412,7 +414,7 @@ async function validateReverseProxy() {
412414 throw err;
413415 }
414416 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
415417 const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true';
416418
417419 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
418420
@@ -423,7 +425,7 @@ async function validateReverseProxy() {
423425 throw new Error('Proxy connection denied.');
424426 }
425427
426428 localStorageaccountStorage.setItem(rememberKey, String(true));
427429}
428430
429431/**
@@ -1096,8 +1098,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
10961098 // Unordered prompts without marker
10971099 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
10981100 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099- { role: 'system', content: bias, identifier: 'bias' },
11001101 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1102+ { role: 'assistant', content: bias, identifier: 'bias' },
11011103 ];
11021104
11031105 // Tavern Extras - Summary
@@ -1443,9 +1445,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
14431445 }
14441446
14451447 const onStreamResult = (res, err) => {
14461448 if (err) {return;
1447- return;
1448- }
14491449
14501450 const thisContent = res?.message?.content;
14511451
@@ -1497,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
14971497 }
14981498}
14991499
15001500export function getChatCompletionModel() {
15011501 switch (oai_settings.chat_completion_source) {
15021502 case chat_completion_sources.CLAUDE:
15031503 return oai_settings.claude_model;
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
18691869 const isQuiet = type === 'quiet';
18701870 const isImpersonate = type === 'impersonate';
18711871 const isContinue = type === 'continue';
18721872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogleisOAI && oai_settings.google_model.includes(['bisono1-2024-12-17')) &&, !'o1'].includes(isOAI && oai_settings.openai_model.startsWith('o1-'));
18731873 const useLogprobs = !!power_user.request_token_probabilities;
18741874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1913,16 +1913,21 @@ async function sendOpenAIRequest(type, messages, signal) {
19131913 'user_name': name1,
19141914 'char_name': name2,
19151915 'group_names': getGroupNames(),
19161916 'show_thoughtsinclude_reasoning': Boolean(oai_settings.show_thoughts),
1917+ 'reasoning_effort': String(oai_settings.reasoning_effort),
19171918 };
19181919
1920+ if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
1921+ await ToolManager.registerFunctionToolsOpenAI(generate_data);
1922+ }
1923+
19191924 // Empty array will produce a validation error
19201925 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
19211926 delete generate_data.stop;
19221927 }
19231928
19241929 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
19251930 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)) {
19261931 await validateReverseProxy();
19271932 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
19281933 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -2030,17 +2035,25 @@ async function sendOpenAIRequest(type, messages, signal) {
20302035 // https://api-docs.deepseek.com/api/create-chat-completion
20312036 if (isDeepSeek) {
20322037 generate_data.top_p = generate_data.top_p || Number.EPSILON;
2038+
2039+ if (generate_data.model.endsWith('-reasoner')) {
2040+ delete generate_data.top_p;
2041+ delete generate_data.temperature;
2042+ delete generate_data.frequency_penalty;
2043+ delete generate_data.presence_penalty;
2044+ delete generate_data.top_logprobs;
2045+ delete generate_data.logprobs;
2046+ delete generate_data.logit_bias;
2047+ delete generate_data.tools;
2048+ delete generate_data.tool_choice;
2049+ }
20332050 }
20342051
20352052 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
20362053 generate_data['seed'] = oai_settings.seed;
20372054 }
20382055
2039- if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
2056+ if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
2040- await ToolManager.registerFunctionToolsOpenAI(generate_data);
2041- }
2042-
2043- if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
20442057 generate_data.messages.forEach((msg) => {
20452058 if (msg.role === 'system') {
20462059 msg.role = 'user';
@@ -2048,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) {
20482061 });
20492062 generate_data.max_completion_tokens = generate_data.max_tokens;
20502063 delete generate_data.max_tokens;
2051- delete generate_data.stream;
20522064 delete generate_data.logprobs;
20532065 delete generate_data.top_logprobs;
20542066 delete generate_data.n;
@@ -2059,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20592071 delete generate_data.tools;
20602072 delete generate_data.tool_choice;
20612073 delete generate_data.stop;
2062- // It does support logit_bias, but the tokenizer used and its effect is yet unknown.
2074+ delete generate_data.logit_bias;
2063- // delete generate_data.logit_bias;
20642075 }
20652076
20662077 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -2085,6 +2096,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20852096 let text = '';
20862097 const swipes = [];
20872098 const toolCalls = [];
2099+ const state = { reasoning: '' };
20882100 while (true) {
20892101 const { done, value } = await reader.read();
20902102 if (done) return;
@@ -2095,14 +2107,14 @@ async function sendOpenAIRequest(type, messages, signal) {
20952107
20962108 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
20972109 const swipeIndex = parsed.choices[0].index - 1;
20982110 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
20992111 } else {
21002112 text += getStreamingReply(parsed, state);
21012113 }
21022114
21032115 ToolManager.parseToolCalls(toolCalls, parsed);
21042116
21052117 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
21062118 }
21072119 };
21082120 }
@@ -2129,13 +2141,32 @@ async function sendOpenAIRequest(type, messages, signal) {
21292141 }
21302142}
21312143
2132-function getStreamingReply(data) {
2144+/**
2145+ * Extracts the reply from the response data from a chat completions-like source
2146+ * @param {object} data Response data from the chat completions-like source
2147+ * @param {object} state Additional state to keep track of
2148+ * @returns {string} The reply extracted from the response data
2149+ */
2150+function getStreamingReply(data, state) {
21332151 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
21342152 return data?.delta?.text || '';
21352153 } 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') || '';
2154+ if (oai_settings.show_thoughts) {
2155+ state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2156+ }
2157+ return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
21372158 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
21382159 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2160+ } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2161+ if (oai_settings.show_thoughts) {
2162+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2163+ }
2164+ return data.choices?.[0]?.delta?.content || '';
2165+ } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2166+ if (oai_settings.show_thoughts) {
2167+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2168+ }
2169+ return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21392170 } else {
21402171 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21412172 }
@@ -3094,6 +3125,7 @@ function loadOpenAISettings(data, settings) {
30943125 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
30953126 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
30963127 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
3128+ oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
30973129 oai_settings.seed = settings.seed ?? default_settings.seed;
30983130 oai_settings.n = settings.n ?? default_settings.n;
30993131
@@ -3223,6 +3255,9 @@ function loadOpenAISettings(data, settings) {
32233255 $('#n_openai').val(oai_settings.n);
32243256 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
32253257
3258+ $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
3259+ $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
3260+
32263261 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
32273262 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
32283263
@@ -3346,7 +3381,7 @@ async function getStatusOpen() {
33463381 chat_completion_source: oai_settings.chat_completion_source,
33473382 };
33483383
33493384 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)) {
33503385 await validateReverseProxy();
33513386 }
33523387
@@ -3483,6 +3518,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
34833518 continue_postfix: settings.continue_postfix,
34843519 function_calling: settings.function_calling,
34853520 show_thoughts: settings.show_thoughts,
3521+ reasoning_effort: settings.reasoning_effort,
34863522 seed: settings.seed,
34873523 n: settings.n,
34883524 };
@@ -3941,6 +3977,7 @@ function onSettingsPresetChange() {
39413977 continue_postfix: ['#continue_postfix', 'continue_postfix', false],
39423978 function_calling: ['#openai_function_calling', 'function_calling', true],
39433979 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
3980+ reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
39443981 seed: ['#seed_openai', 'seed', false],
39453982 n: ['#n_openai', 'n', false],
39463983 };
@@ -3997,7 +4034,7 @@ function getMaxContextOpenAI(value) {
39974034 if (oai_settings.max_context_unlocked) {
39984035 return unlocked_max;
39994036 }
40004037 else if (value.startsWith('o1-') || value.startsWith('o3')) {
40014038 return max_128k;
40024039 }
40034040 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
@@ -4202,9 +4239,9 @@ async function onModelChange() {
42024239 $('#openai_max_context').attr('max', max_2mil);
42034240 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
42044241 $('#openai_max_context').attr('max', max_32k);
42054242 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
42064243 $('#openai_max_context').attr('max', max_2mil);
42074244 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) {
42084245 $('#openai_max_context').attr('max', max_1mil);
42094246 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
42104247 $('#openai_max_context').attr('max', max_32k);
@@ -4350,28 +4387,19 @@ async function onModelChange() {
43504387 if (oai_settings.max_context_unlocked) {
43514388 $('#openai_max_context').attr('max', unlocked_max);
43524389 }
4390+ else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
4391+ $('#openai_max_context').attr('max', 127000);
4392+ }
4393+ else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
4394+ $('#openai_max_context').attr('max', 200000);
4395+ }
43534396 else if (oai_settings.perplexity_model.includes('llama-3.1')) {
43544397 const isOnline = oai_settings.perplexity_model.includes('online');
43554398 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
43564399 $('#openai_max_context').attr('max', contextSize);
43574400 }
4358- else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
4359- $('#openai_max_context').attr('max', max_32k);
4360- }
4361- else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
4362- $('#openai_max_context').attr('max', 28000);
4363- }
4364- else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
4365- $('#openai_max_context').attr('max', max_16k);
4366- }
4367- else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
4368- $('#openai_max_context').attr('max', max_8k);
4369- }
4370- else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
4371- $('#openai_max_context').attr('max', 12000);
4372- }
43734401 else {
43744402 $('#openai_max_context').attr('max', max_4kmax_128k);
43754403 }
43764404 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
43774405 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4382,24 +4410,30 @@ async function onModelChange() {
43824410 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
43834411 if (oai_settings.max_context_unlocked) {
43844412 $('#openai_max_context').attr('max', unlocked_max);
4385- }
4413+ } else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
4386- else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
43874414 $('#openai_max_context').attr('max', max_8k);
4388- }
4415+ } else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
4389- else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
43904416 $('#openai_max_context').attr('max', max_128k);
4391- }
4417+ } else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
4392- else if (oai_settings.groq_model.includes('llama3-groq')) {
4418+ $('#openai_max_context').attr('max', max_128k);
4419+ } else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
43934420 $('#openai_max_context').attr('max', max_8k);
4394- }
4421+ } else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
4395- else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
43964422 $('#openai_max_context').attr('max', max_8k);
4397- }
4423+ } else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
4398- else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
43994424 $('#openai_max_context').attr('max', max_32k);
4400- }
4425+ } else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
4401- else {
4426+ $('#openai_max_context').attr('max', max_128k);
4402- $('#openai_max_context').attr('max', max_4k);
4427+ } else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
4428+ $('#openai_max_context').attr('max', max_8k);
4429+ } else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
4430+ $('#openai_max_context').attr('max', max_128k);
4431+ } else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
4432+ $('#openai_max_context').attr('max', max_128k);
4433+ } else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
4434+ $('#openai_max_context').attr('max', max_128k);
4435+ } else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
4436+ $('#openai_max_context').attr('max', max_128k);
44034437 }
44044438 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
44054439 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4488,7 +4522,7 @@ async function onModelChange() {
44884522 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
44894523 if (oai_settings.max_context_unlocked) {
44904524 $('#openai_max_context').attr('max', unlocked_max);
44914525 } else if (oai_settings.deepseek_model ==['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
44924526 $('#openai_max_context').attr('max', max_64k);
44934527 } else if (oai_settings.deepseek_model == 'deepseek-coder') {
44944528 $('#openai_max_context').attr('max', max_16k);
@@ -4725,7 +4759,7 @@ async function onConnectButtonClick(e) {
47254759 await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);
47264760 }
47274761
47284762 if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) {
47294763 console.log('No secret key saved for DeepSeek');
47304764 return;
47314765 }
@@ -4900,7 +4934,15 @@ export function isImageInliningSupported() {
49004934 // gultra just isn't being offered as multimodal, thanks google.
49014935 const visionSupportedModels = [
49024936 'gpt-4-vision',
4937+ 'gemini-2.0-pro-exp',
4938+ 'gemini-2.0-pro-exp-02-05',
4939+ 'gemini-2.0-flash-lite-preview',
4940+ 'gemini-2.0-flash-lite-preview-02-05',
4941+ 'gemini-2.0-flash',
4942+ 'gemini-2.0-flash-001',
49034943 'gemini-2.0-flash-thinking-exp-1219',
4944+ 'gemini-2.0-flash-thinking-exp-01-21',
4945+ 'gemini-2.0-flash-thinking-exp',
49044946 'gemini-2.0-flash-exp',
49054947 'gemini-1.5-flash',
49064948 'gemini-1.5-flash-latest',
@@ -4925,6 +4967,8 @@ export function isImageInliningSupported() {
49254967 'gpt-4-turbo',
49264968 'gpt-4o',
49274969 'gpt-4o-mini',
4970+ 'o1',
4971+ 'o1-2024-12-17',
49284972 'chatgpt-4o-latest',
49294973 'yi-vision',
49304974 'pixtral-latest',
@@ -5483,6 +5527,11 @@ export function initOpenAI() {
54835527 saveSettingsDebounced();
54845528 });
54855529
5530+ $('#openai_reasoning_effort').on('input', function () {
5531+ oai_settings.reasoning_effort = String($(this).val());
5532+ saveSettingsDebounced();
5533+ });
5534+
54865535 if (!CSS.supports('field-sizing', 'content')) {
54875536 $(document).on('input', '#openai_settings .autoSetHeight', function () {
54885537 resetScrollHeight($(this));
public/scripts/personas.js+6 -5
@@ -25,6 +25,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
2525import { t } from './i18n.js';
2626import { openWorldInfoEditor, world_names } from './world-info.js';
2727import { renderTemplateAsync } from './templates.js';
28+import { accountStorage } from './util/AccountStorage.js';
2829
2930let savePersonasPage = 0;
3031const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -34,7 +35,7 @@ export let user_avatar = '';
3435export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
3536
3637function switchPersonaGridView() {
3738 const state = localStorageaccountStorage.getItem(GRID_STORAGE_KEY) === 'true';
3839 $('#user_avatar_block').toggleClass('gridView', state);
3940}
4041
@@ -182,7 +183,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
182183
183184 const storageKey = 'Personas_PerPage';
184185 const listId = '#user_avatar_block';
185186 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 5;
186187
187188 $('#persona_pagination_container').pagination({
188189 dataSource: entities,
@@ -205,7 +206,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
205206 highlightSelectedAvatar();
206207 },
207208 afterSizeSelectorChange: function (e) {
208209 localStorageaccountStorage.setItem(storageKey, e.target.value);
209210 },
210211 afterPaging: function (e) {
211212 savePersonasPage = e;
@@ -1132,8 +1133,8 @@ export function initPersonas() {
11321133 saveSettingsDebounced();
11331134 });
11341135 $('#persona_grid_toggle').on('click', () => {
11351136 const state = localStorageaccountStorage.getItem(GRID_STORAGE_KEY) === 'true';
11361137 localStorageaccountStorage.setItem(GRID_STORAGE_KEY, String(!state));
11371138 switchPersonaGridView();
11381139 });
11391140
public/scripts/popup.js+12 -1
@@ -24,6 +24,15 @@ export const POPUP_RESULT = {
2424 AFFIRMATIVE: 1,
2525 NEGATIVE: 0,
2626 CANCELLED: null,
27+ CUSTOM1: 1001,
28+ CUSTOM2: 1002,
29+ CUSTOM3: 1003,
30+ CUSTOM4: 1004,
31+ CUSTOM5: 1005,
32+ CUSTOM6: 1006,
33+ CUSTOM7: 1007,
34+ CUSTOM8: 1008,
35+ CUSTOM9: 1009,
2736};
2837
2938/**
@@ -37,6 +46,7 @@ export const POPUP_RESULT = {
3746 * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
3847 * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
3948 * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
49+ * @property {boolean?} [leftAlign=false] - Whether the popup content should be left-aligned by default
4050 * @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
4151 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
4252 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
@@ -164,7 +174,7 @@ export class Popup {
164174 * @param {string} [inputValue=''] - The initial value of the input field
165175 * @param {PopupOptions} [options={}] - Additional options for the popup
166176 */
167177 constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
168178 Popup.util.popups.push(this);
169179
170180 // Make this popup uniquely identifiable
@@ -209,6 +219,7 @@ export class Popup {
209219 if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
210220 if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
211221 if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
222+ if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup');
212223 if (animation) this.dlg.classList.add('popup--animation-' + animation);
213224
214225 // If custom button captions are provided, we set them beforehand
public/scripts/power-user.js+99 -6
@@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
5454import { POPUP_TYPE, callGenericPopup } from './popup.js';
5555import { loadSystemPrompts } from './sysprompt.js';
5656import { fuzzySearchCategories } from './filters.js';
57+import { accountStorage } from './util/AccountStorage.js';
5758
5859export {
5960 loadPowerUserSettings,
@@ -253,6 +254,17 @@ let power_user = {
253254 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254255 },
255256
257+ reasoning: {
258+ auto_parse: false,
259+ add_to_prompts: false,
260+ auto_expand: false,
261+ show_hidden: false,
262+ prefix: '<think>\n',
263+ suffix: '\n</think>',
264+ separator: '\n\n',
265+ max_additions: 1,
266+ },
267+
256268 personas: {},
257269 default_persona: null,
258270 persona_descriptions: {},
@@ -2009,7 +2021,7 @@ export function renderStoryString(params) {
20092021 */
20102022function validateStoryString(storyString, params) {
20112023 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
20122024 const cache = JSON.parse(localStorageaccountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
20132025
20142026 const hash = getStringHash(storyString);
20152027
@@ -2046,7 +2058,7 @@ function validateStoryString(storyString, params) {
20462058 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
20472059 }
20482060
20492061 localStorageaccountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
20502062}
20512063
20522064
@@ -2441,7 +2453,7 @@ async function resetMovablePanels(type) {
24412453 }
24422454
24432455 saveSettingsDebounced();
24442456 await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
24452457
24462458 eventSource.once(event_types.SETTINGS_UPDATED, () => {
24472459 $('.resizing').removeClass('resizing');
@@ -2534,7 +2546,7 @@ async function loadUntilMesId(mesId) {
25342546 let target;
25352547
25362548 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
25372549 await showMoreMessages();
25382550 await delay(1);
25392551 target = $('#chat').find(`.mes[mesid=${mesId}]`);
25402552
@@ -2909,6 +2921,46 @@ export function flushEphemeralStoppingStrings() {
29092921}
29102922
29112923/**
2924+ * Checks if the generated text should be filtered based on the auto-swipe settings.
2925+ * @param {string} text The text to check
2926+ * @returns {boolean} If the generated text should be filtered
2927+ */
2928+export function generatedTextFiltered(text) {
2929+ /**
2930+ * Checks if the given text contains any of the blacklisted words.
2931+ * @param {string} text The text to check
2932+ * @param {string[]} blacklist The list of blacklisted words
2933+ * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
2934+ * @returns {boolean} Whether the text contains blacklisted words
2935+ */
2936+ function containsBlacklistedWords(text, blacklist, threshold) {
2937+ const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
2938+ const matches = text.match(regex) || [];
2939+ return matches.length >= threshold;
2940+ }
2941+
2942+ // Make sure a generated text is non-empty
2943+ // Otherwise we might get in a loop with a broken API
2944+ text = text.trim();
2945+ if (text.length > 0) {
2946+ if (power_user.auto_swipe_minimum_length) {
2947+ if (text.length < power_user.auto_swipe_minimum_length) {
2948+ console.log('Generated text size too small');
2949+ return true;
2950+ }
2951+ }
2952+ if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
2953+ if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
2954+ console.log('Generated text has blacklisted words');
2955+ return true;
2956+ }
2957+ }
2958+ }
2959+
2960+ return false;
2961+}
2962+
2963+/**
29122964 * Gets the custom stopping strings from the power user settings.
29132965 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
29142966 * @returns {string[]} An array of custom stopping strings
@@ -3879,9 +3931,9 @@ $(document).ready(() => {
38793931 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
38803932 }));
38813933 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
38823934 name: 'delmodedel',
38833935 callback: doDelMode,
38843936 aliases: ['deldelete', 'delmode'],
38853937 unnamedArgumentList: [
38863938 new SlashCommandArgument(
38873939 'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4064,4 +4116,45 @@ $(document).ready(() => {
40644116 ],
40654117 helpString: 'activates a movingUI preset by name',
40664118 }));
4119+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4120+ name: 'stop-strings',
4121+ aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4122+ helpString: `
4123+ <div>
4124+ Sets a list of custom stopping strings. Gets the list if no value is provided.
4125+ </div>
4126+ <div>
4127+ <strong>Examples:</strong>
4128+ </div>
4129+ <ul>
4130+ <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4131+ <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4132+ </ul>
4133+ `,
4134+ returns: ARGUMENT_TYPE.LIST,
4135+ unnamedArgumentList: [
4136+ SlashCommandArgument.fromProps({
4137+ description: 'list of strings',
4138+ typeList: [ARGUMENT_TYPE.LIST],
4139+ acceptsMultiple: false,
4140+ isRequired: false,
4141+ }),
4142+ ],
4143+ callback: (_, value) => {
4144+ if (String(value ?? '').trim()) {
4145+ const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4146+ if (!parsedValue || !Array.isArray(parsedValue)) {
4147+ throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4148+ }
4149+ parsedValue.forEach((item, index) => {
4150+ parsedValue[index] = String(item);
4151+ });
4152+ power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4153+ $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4154+ saveSettingsDebounced();
4155+ }
4156+
4157+ return power_user.custom_stopping_strings;
4158+ },
4159+ }));
40674160});
public/scripts/preset-manager.js+3 -0
@@ -586,6 +586,9 @@ class PresetManager {
586586 'tabby_model',
587587 'derived',
588588 'generic_model',
589+ 'include_reasoning',
590+ 'global_banned_tokens',
591+ 'send_banned_tokens',
589592 ];
590593 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
591594
public/scripts/reasoning.js+913 -0
@@ -0,0 +1,913 @@
1+import {
2+ moment,
3+} from '../lib.js';
4+import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
5+import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
6+import { getCurrentLocale, t } from './i18n.js';
7+import { MacrosParser } from './macros.js';
8+import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
9+import { Popup } from './popup.js';
10+import { power_user } from './power-user.js';
11+import { SlashCommand } from './slash-commands/SlashCommand.js';
12+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
13+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15+import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
16+import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';
17+
18+/**
19+ * Gets a message from a jQuery element.
20+ * @param {Element} element
21+ * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
22+ */
23+function getMessageFromJquery(element) {
24+ const messageBlock = $(element).closest('.mes');
25+ const messageId = Number(messageBlock.attr('mesid'));
26+ const message = chat[messageId];
27+ return { messageId: messageId, message, messageBlock };
28+}
29+
30+/**
31+ * Toggles the auto-expand state of reasoning blocks.
32+ */
33+function toggleReasoningAutoExpand() {
34+ const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details');
35+ reasoningBlocks.forEach((block) => {
36+ if (block instanceof HTMLDetailsElement) {
37+ block.open = power_user.reasoning.auto_expand;
38+ }
39+ });
40+}
41+
42+/**
43+ * Extracts the reasoning from the response data.
44+ * @param {object} data Response data
45+ * @returns {string} Extracted reasoning
46+ */
47+export function extractReasoningFromData(data) {
48+ switch (main_api) {
49+ case 'textgenerationwebui':
50+ switch (textgenerationwebui_settings.type) {
51+ case textgen_types.OPENROUTER:
52+ return data?.choices?.[0]?.reasoning ?? '';
53+ }
54+ break;
55+
56+ case 'openai':
57+ if (!oai_settings.show_thoughts) break;
58+
59+ switch (oai_settings.chat_completion_source) {
60+ case chat_completion_sources.DEEPSEEK:
61+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
62+ case chat_completion_sources.OPENROUTER:
63+ return data?.choices?.[0]?.message?.reasoning ?? '';
64+ case chat_completion_sources.MAKERSUITE:
65+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
66+ }
67+ break;
68+ }
69+
70+ return '';
71+}
72+
73+/**
74+ * Check if the model supports reasoning, but does not send back the reasoning
75+ * @returns {boolean} True if the model supports reasoning
76+ */
77+export function isHiddenReasoningModel() {
78+ if (main_api !== 'openai') {
79+ return false;
80+ }
81+
82+ /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
83+ /** @type {Record.<string, MatchingFunc>} */
84+ const FUNCS = {
85+ equals: (currentModel, supportedModel) => currentModel === supportedModel,
86+ startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
87+ };
88+
89+ /** @type {{ name: string; func: MatchingFunc; }[]} */
90+ const hiddenReasoningModels = [
91+ { name: 'o1', func: FUNCS.startsWith },
92+ { name: 'o3', func: FUNCS.startsWith },
93+ { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
94+ { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
95+ ];
96+
97+ const model = getChatCompletionModel();
98+
99+ const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
100+ return isHidden;
101+}
102+
103+/**
104+ * Updates the Reasoning UI for a specific message
105+ * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
106+ * @param {Object} [options={}] - Optional arguments
107+ * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
108+ */
109+export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
110+ const handler = new ReasoningHandler();
111+ handler.initHandleMessage(messageIdOrElement, { reset });
112+}
113+
114+
115+/**
116+ * Enum for representing the state of reasoning
117+ * @enum {string}
118+ * @readonly
119+ */
120+export const ReasoningState = {
121+ None: 'none',
122+ Thinking: 'thinking',
123+ Done: 'done',
124+ Hidden: 'hidden',
125+};
126+
127+/**
128+ * Handles reasoning-specific logic and DOM updates for messages.
129+ * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
130+ */
131+export class ReasoningHandler {
132+ #isHiddenReasoningModel;
133+
134+ /**
135+ * @param {Date?} [timeStarted=null] - When the generation started
136+ */
137+ constructor(timeStarted = null) {
138+ /** @type {ReasoningState} The current state of the reasoning process */
139+ this.state = ReasoningState.None;
140+ /** @type {string} The reasoning output */
141+ this.reasoning = '';
142+ /** @type {Date} When the reasoning started */
143+ this.startTime = null;
144+ /** @type {Date} When the reasoning ended */
145+ this.endTime = null;
146+
147+ /** @type {Date} Initial starting time of the generation */
148+ this.initialTime = timeStarted ?? new Date();
149+
150+ /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151+ this.#isHiddenReasoningModel = isHiddenReasoningModel();
152+
153+ // Cached DOM elements for reasoning
154+ /** @type {HTMLElement} Main message DOM element `.mes` */
155+ this.messageDom = null;
156+ /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */
157+ this.messageReasoningDetailsDom = null;
158+ /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
159+ this.messageReasoningContentDom = null;
160+ /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
161+ this.messageReasoningHeaderDom = null;
162+ }
163+
164+ /**
165+ * Initializes the reasoning handler for a specific message.
166+ *
167+ * Can be used to update the DOM elements or read other reasoning states.
168+ * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
169+ * The state will always be either done/hidden or none.
170+ *
171+ * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
172+ * @param {Object} [options={}] - Optional arguments
173+ * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
174+ */
175+ initHandleMessage(messageIdOrElement, { reset = false } = {}) {
176+ /** @type {HTMLElement} */
177+ const messageElement = typeof messageIdOrElement === 'number'
178+ ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
179+ : messageIdOrElement instanceof HTMLElement
180+ ? messageIdOrElement
181+ : $(messageIdOrElement)[0];
182+ const messageId = Number(messageElement.getAttribute('mesid'));
183+
184+ if (isNaN(messageId) || !chat[messageId]) return;
185+
186+ if (!chat[messageId].extra) {
187+ chat[messageId].extra = {};
188+ }
189+ const extra = chat[messageId].extra;
190+
191+ if (extra.reasoning) {
192+ this.state = ReasoningState.Done;
193+ } else if (extra.reasoning_duration) {
194+ this.state = ReasoningState.Hidden;
195+ }
196+
197+ this.reasoning = extra?.reasoning ?? '';
198+
199+ if (this.state !== ReasoningState.None) {
200+ this.initialTime = new Date(chat[messageId].gen_started);
201+ this.startTime = this.initialTime;
202+ this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
203+ }
204+
205+ // Prefill main dom element, as message might not have been rendered yet
206+ this.messageDom = messageElement;
207+
208+ // Make sure reset correctly clears all relevant states
209+ if (reset) {
210+ this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
211+ this.reasoning = '';
212+ this.initialTime = new Date();
213+ this.startTime = null;
214+ this.endTime = null;
215+ }
216+
217+ this.updateDom(messageId);
218+
219+ if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
220+ this.messageReasoningDetailsDom.open = true;
221+ }
222+ }
223+
224+ /**
225+ * Gets the duration of the reasoning in milliseconds.
226+ *
227+ * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
228+ */
229+ getDuration() {
230+ if (this.startTime && this.endTime) {
231+ return this.endTime.getTime() - this.startTime.getTime();
232+ }
233+ return null;
234+ }
235+
236+ /**
237+ * Updates the reasoning text/string for a message.
238+ *
239+ * @param {number} messageId - The ID of the message to update
240+ * @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
241+ * @param {Object} [options={}] - Optional arguments
242+ * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
243+ * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
244+ */
245+ updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
246+ if (messageId == -1 || !chat[messageId]) {
247+ return false;
248+ }
249+
250+ reasoning = reasoning ?? this.reasoning;
251+ reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;
252+
253+ // Ensure the chat extra exists
254+ if (!chat[messageId].extra) {
255+ chat[messageId].extra = {};
256+ }
257+ const extra = chat[messageId].extra;
258+
259+ const reasoningChanged = extra.reasoning !== reasoning;
260+ this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
261+
262+ if (persist) {
263+ // Build and save the reasoning data to message extras
264+ extra.reasoning = this.reasoning;
265+ extra.reasoning_duration = this.getDuration();
266+ }
267+
268+ return reasoningChanged;
269+ }
270+
271+
272+ /**
273+ * Handles processing of reasoning for a message.
274+ *
275+ * This is usually called by the message processor when a message is changed.
276+ *
277+ * @param {number} messageId - The ID of the message to process
278+ * @param {boolean} mesChanged - Whether the message has changed
279+ * @returns {Promise<void>}
280+ */
281+ async process(messageId, mesChanged) {
282+ if (!this.reasoning && !this.#isHiddenReasoningModel) return;
283+
284+ // Ensure reasoning string is updated and regexes are applied correctly
285+ const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
286+
287+ if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
288+ this.state = ReasoningState.Thinking;
289+ this.startTime = this.initialTime;
290+ }
291+ if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
292+ this.endTime = new Date();
293+ await this.finish(messageId);
294+ }
295+ }
296+
297+ /**
298+ * Completes the reasoning process for a message.
299+ *
300+ * Records the finish time if it was not set during streaming and updates the reasoning state.
301+ * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
302+ *
303+ * @param {number} messageId - The ID of the message to complete reasoning for
304+ * @returns {Promise<void>}
305+ */
306+ async finish(messageId) {
307+ if (this.state === ReasoningState.None) return;
308+
309+ // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
310+ if (this.startTime !== null && this.endTime === null) {
311+ this.endTime = new Date();
312+ }
313+
314+ if (this.state === ReasoningState.Thinking) {
315+ this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
316+ this.updateReasoning(messageId, null, { persist: true });
317+ await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
318+ }
319+
320+ this.updateDom(messageId);
321+ }
322+
323+ /**
324+ * Updates the reasoning UI elements for a message.
325+ *
326+ * Toggles the CSS class, updates states, reasoning message, and duration.
327+ *
328+ * @param {number} messageId - The ID of the message to update
329+ */
330+ updateDom(messageId) {
331+ this.#checkDomElements(messageId);
332+
333+ // Main CSS class to show this message includes reasoning
334+ this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
335+
336+ // Update states to the relevant DOM elements
337+ setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
338+ setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
339+
340+ // Update the reasoning message
341+ const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
342+ const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
343+ this.messageReasoningContentDom.innerHTML = displayReasoning;
344+
345+ // Update tooltip for hidden reasoning edit
346+ /** @type {HTMLElement} */
347+ const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
348+ button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
349+
350+ // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button
351+ if (this.state === ReasoningState.Hidden) {
352+ this.messageReasoningDetailsDom.open = false;
353+ }
354+
355+ // Update the reasoning duration in the UI
356+ this.#updateReasoningTimeUI();
357+ }
358+
359+ /**
360+ * Finds and caches reasoning-related DOM elements for the given message.
361+ *
362+ * @param {number} messageId - The ID of the message to cache the DOM elements for
363+ */
364+ #checkDomElements(messageId) {
365+ // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
366+ if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
367+ this.messageDom = null;
368+ }
369+
370+ // Cache the DOM elements once
371+ if (this.messageDom === null) {
372+ this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
373+ if (this.messageDom === null) throw new Error('message dom does not exist');
374+ }
375+ if (this.messageReasoningDetailsDom === null) {
376+ this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
377+ }
378+ if (this.messageReasoningContentDom === null) {
379+ this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
380+ }
381+ if (this.messageReasoningHeaderDom === null) {
382+ this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
383+ }
384+ }
385+
386+ /**
387+ * Updates the reasoning time display in the UI.
388+ *
389+ * Shows the duration in a human-readable format with a tooltip for exact seconds.
390+ * Displays "Thinking..." if still processing, or a generic message otherwise.
391+ */
392+ #updateReasoningTimeUI() {
393+ const element = this.messageReasoningHeaderDom;
394+ const duration = this.getDuration();
395+ let data = null;
396+ if (duration) {
397+ const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
398+ const secondsStr = moment.duration(duration).asSeconds();
399+
400+ const span = document.createElement('span');
401+ span.title = t`${secondsStr} seconds`;
402+ span.textContent = durationStr;
403+
404+ element.textContent = t`Thought for `;
405+ element.appendChild(span);
406+ data = String(secondsStr);
407+ } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
408+ element.textContent = t`Thought for some time`;
409+ data = 'unknown';
410+ } else {
411+ element.textContent = t`Thinking...`;
412+ data = null;
413+ }
414+
415+ setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
416+ setDatasetProperty(element, 'duration', data);
417+ }
418+}
419+
420+/**
421+ * Helper class for adding reasoning to messages.
422+ * Keeps track of the number of reasoning additions.
423+ */
424+export class PromptReasoning {
425+ static REASONING_PLACEHOLDER = '\u200B';
426+
427+ constructor() {
428+ this.counter = 0;
429+ }
430+
431+ /**
432+ * Checks if the limit of reasoning additions has been reached.
433+ * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
434+ */
435+ isLimitReached() {
436+ if (!power_user.reasoning.add_to_prompts) {
437+ return true;
438+ }
439+
440+ return this.counter >= power_user.reasoning.max_additions;
441+ }
442+
443+ /**
444+ * Add reasoning to a message according to the power user settings.
445+ * @param {string} content Message content
446+ * @param {string} reasoning Message reasoning
447+ * @param {boolean} isPrefix Whether this is the last message prefix
448+ * @returns {string} Message content with reasoning
449+ */
450+ addToMessage(content, reasoning, isPrefix) {
451+ // Disabled or reached limit of additions
452+ if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
453+ return content;
454+ }
455+
456+ // No reasoning provided or a legacy placeholder
457+ if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
458+ return content;
459+ }
460+
461+ // Increment the counter
462+ this.counter++;
463+
464+ // Substitute macros in variable parts
465+ const prefix = substituteParams(power_user.reasoning.prefix || '');
466+ const separator = substituteParams(power_user.reasoning.separator || '');
467+ const suffix = substituteParams(power_user.reasoning.suffix || '');
468+
469+ // Combine parts with reasoning only
470+ if (isPrefix && !content) {
471+ return `${prefix}${reasoning}`;
472+ }
473+
474+ // Combine parts with reasoning and content
475+ return `${prefix}${reasoning}${suffix}${separator}${content}`;
476+ }
477+}
478+
479+function loadReasoningSettings() {
480+ $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
481+ $('#reasoning_add_to_prompts').on('change', function () {
482+ power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
483+ saveSettingsDebounced();
484+ });
485+
486+ $('#reasoning_prefix').val(power_user.reasoning.prefix);
487+ $('#reasoning_prefix').on('input', function () {
488+ power_user.reasoning.prefix = String($(this).val());
489+ saveSettingsDebounced();
490+ });
491+
492+ $('#reasoning_suffix').val(power_user.reasoning.suffix);
493+ $('#reasoning_suffix').on('input', function () {
494+ power_user.reasoning.suffix = String($(this).val());
495+ saveSettingsDebounced();
496+ });
497+
498+ $('#reasoning_separator').val(power_user.reasoning.separator);
499+ $('#reasoning_separator').on('input', function () {
500+ power_user.reasoning.separator = String($(this).val());
501+ saveSettingsDebounced();
502+ });
503+
504+ $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
505+ $('#reasoning_max_additions').on('input', function () {
506+ power_user.reasoning.max_additions = Number($(this).val());
507+ saveSettingsDebounced();
508+ });
509+
510+ $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
511+ $('#reasoning_auto_parse').on('change', function () {
512+ power_user.reasoning.auto_parse = !!$(this).prop('checked');
513+ saveSettingsDebounced();
514+ });
515+
516+ $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
517+ $('#reasoning_auto_expand').on('change', function () {
518+ power_user.reasoning.auto_expand = !!$(this).prop('checked');
519+ toggleReasoningAutoExpand();
520+ saveSettingsDebounced();
521+ });
522+ toggleReasoningAutoExpand();
523+
524+ $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
525+ $('#reasoning_show_hidden').on('change', function () {
526+ power_user.reasoning.show_hidden = !!$(this).prop('checked');
527+ $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
528+ saveSettingsDebounced();
529+ });
530+ $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
531+}
532+
533+function registerReasoningSlashCommands() {
534+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
535+ name: 'reasoning-get',
536+ aliases: ['get-reasoning'],
537+ returns: ARGUMENT_TYPE.STRING,
538+ 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.`,
539+ unnamedArgumentList: [
540+ SlashCommandArgument.fromProps({
541+ description: 'Message ID. If not provided, the message ID of the last message is used.',
542+ typeList: ARGUMENT_TYPE.NUMBER,
543+ enumProvider: commonEnumProviders.messages(),
544+ }),
545+ ],
546+ callback: (_args, value) => {
547+ const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
548+ const message = chat[messageId];
549+ const reasoning = String(message?.extra?.reasoning ?? '');
550+ return reasoning;
551+ },
552+ }));
553+
554+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
555+ name: 'reasoning-set',
556+ aliases: ['set-reasoning'],
557+ returns: ARGUMENT_TYPE.STRING,
558+ helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
559+ namedArgumentList: [
560+ SlashCommandNamedArgument.fromProps({
561+ name: 'at',
562+ description: 'Message ID. If not provided, the message ID of the last message is used.',
563+ typeList: ARGUMENT_TYPE.NUMBER,
564+ enumProvider: commonEnumProviders.messages(),
565+ }),
566+ ],
567+ unnamedArgumentList: [
568+ SlashCommandArgument.fromProps({
569+ description: 'Reasoning block content.',
570+ typeList: ARGUMENT_TYPE.STRING,
571+ }),
572+ ],
573+ callback: async (args, value) => {
574+ const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
575+ const message = chat[messageId];
576+ if (!message?.extra) {
577+ return '';
578+ }
579+
580+ message.extra.reasoning = String(value ?? '');
581+ await saveChatConditional();
582+
583+ closeMessageEditor('reasoning');
584+ updateMessageBlock(messageId, message);
585+ return message.extra.reasoning;
586+ },
587+ }));
588+
589+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
590+ name: 'reasoning-parse',
591+ aliases: ['parse-reasoning'],
592+ returns: 'reasoning string',
593+ helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
594+ namedArgumentList: [
595+ SlashCommandNamedArgument.fromProps({
596+ name: 'regex',
597+ description: 'Whether to apply regex scripts to the reasoning content.',
598+ typeList: [ARGUMENT_TYPE.BOOLEAN],
599+ defaultValue: 'true',
600+ isRequired: false,
601+ enumProvider: commonEnumProviders.boolean('trueFalse'),
602+ }),
603+ ],
604+ unnamedArgumentList: [
605+ SlashCommandArgument.fromProps({
606+ description: 'input string',
607+ typeList: [ARGUMENT_TYPE.STRING],
608+ }),
609+ ],
610+ callback: (args, value) => {
611+ if (!value) {
612+ return '';
613+ }
614+
615+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
616+ toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
617+ return String(value);
618+ }
619+
620+ const parsedReasoning = parseReasoningFromString(String(value));
621+
622+ if (!parsedReasoning) {
623+ return '';
624+ }
625+
626+ const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
627+ return applyRegex
628+ ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
629+ : parsedReasoning.reasoning;
630+ },
631+ }));
632+}
633+
634+function registerReasoningMacros() {
635+ MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
636+ MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
637+ MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
638+}
639+
640+function setReasoningEventHandlers() {
641+ $(document).on('click', '.mes_reasoning_details', function (e) {
642+ if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
643+ e.preventDefault();
644+ }
645+ });
646+
647+ $(document).on('click', '.mes_reasoning_header', function (e) {
648+ const details = $(this).closest('.mes_reasoning_details');
649+ // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited
650+ if (details.find('.mes_reasoning').is(':empty')) {
651+ e.preventDefault();
652+ return;
653+ }
654+
655+ // If we are in message edit mode and reasoning area is closed, a click opens and edits it
656+ const mes = $(this).closest('.mes');
657+ const mesEditArea = mes.find('#curEditTextarea');
658+ if (mesEditArea.length) {
659+ const summary = $(mes).find('.mes_reasoning_summary');
660+ if (!summary.attr('open')) {
661+ summary.find('.mes_reasoning_edit').trigger('click');
662+ }
663+ }
664+ });
665+
666+ $(document).on('click', '.mes_reasoning_copy', (e) => {
667+ e.stopPropagation();
668+ e.preventDefault();
669+ });
670+
671+ $(document).on('click', '.mes_reasoning_edit', function (e) {
672+ e.stopPropagation();
673+ e.preventDefault();
674+ const { message, messageBlock } = getMessageFromJquery(this);
675+ if (!message?.extra) {
676+ return;
677+ }
678+
679+ const reasoning = String(message?.extra?.reasoning ?? '');
680+ const chatElement = document.getElementById('chat');
681+ const textarea = document.createElement('textarea');
682+ const reasoningBlock = messageBlock.find('.mes_reasoning');
683+ textarea.classList.add('reasoning_edit_textarea');
684+ textarea.value = reasoning;
685+ $(textarea).insertBefore(reasoningBlock);
686+
687+ if (!CSS.supports('field-sizing', 'content')) {
688+ const resetHeight = function () {
689+ const scrollTop = chatElement.scrollTop;
690+ textarea.style.height = '0px';
691+ textarea.style.height = `${textarea.scrollHeight}px`;
692+ chatElement.scrollTop = scrollTop;
693+ };
694+
695+ textarea.addEventListener('input', resetHeight);
696+ resetHeight();
697+ }
698+
699+ textarea.focus();
700+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
701+
702+ const textareaRect = textarea.getBoundingClientRect();
703+ const chatRect = chatElement.getBoundingClientRect();
704+
705+ // Scroll if textarea bottom is below visible area
706+ if (textareaRect.bottom > chatRect.bottom) {
707+ const scrollOffset = textareaRect.bottom - chatRect.bottom;
708+ chatElement.scrollTop += scrollOffset;
709+ }
710+ });
711+
712+ $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
713+ e.stopPropagation();
714+ e.preventDefault();
715+ const { message, messageId, messageBlock } = getMessageFromJquery(this);
716+ if (!message?.extra) {
717+ return;
718+ }
719+
720+ const textarea = messageBlock.find('.reasoning_edit_textarea');
721+ const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
722+ message.extra.reasoning = reasoning;
723+ await saveChatConditional();
724+ updateMessageBlock(messageId, message);
725+ textarea.remove();
726+
727+ messageBlock.find('.mes_edit_done:visible').trigger('click');
728+ });
729+
730+ $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
731+ e.stopPropagation();
732+ e.preventDefault();
733+
734+ const { messageBlock } = getMessageFromJquery(this);
735+ const textarea = messageBlock.find('.reasoning_edit_textarea');
736+ textarea.remove();
737+
738+ messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
739+
740+ updateReasoningUI(messageBlock);
741+ });
742+
743+ $(document).on('click', '.mes_edit_add_reasoning', async function () {
744+ const { message, messageBlock } = getMessageFromJquery(this);
745+ if (!message?.extra) {
746+ return;
747+ }
748+
749+ if (message.extra.reasoning) {
750+ toastr.info(t`Reasoning already exists.`, t`Edit Message`);
751+ return;
752+ }
753+
754+ messageBlock.addClass('reasoning');
755+
756+ // To make hidden reasoning blocks editable, we just set them to "Done" here already.
757+ // They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
758+ if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
759+ messageBlock.attr('data-reasoning-state', ReasoningState.Done);
760+ }
761+
762+ // Open the reasoning area so we can actually edit it
763+ messageBlock.find('.mes_reasoning_details').attr('open', '');
764+ messageBlock.find('.mes_reasoning_edit').trigger('click');
765+ await saveChatConditional();
766+ });
767+
768+ $(document).on('click', '.mes_reasoning_delete', async function (e) {
769+ e.stopPropagation();
770+ e.preventDefault();
771+
772+ const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`);
773+
774+ if (!confirm) {
775+ return;
776+ }
777+
778+ const { message, messageId, messageBlock } = getMessageFromJquery(this);
779+ if (!message?.extra) {
780+ return;
781+ }
782+ message.extra.reasoning = '';
783+ await saveChatConditional();
784+ updateMessageBlock(messageId, message);
785+ const textarea = messageBlock.find('.reasoning_edit_textarea');
786+ textarea.remove();
787+ });
788+
789+ $(document).on('pointerup', '.mes_reasoning_copy', async function () {
790+ const { message } = getMessageFromJquery(this);
791+ const reasoning = String(message?.extra?.reasoning ?? '');
792+
793+ if (!reasoning) {
794+ return;
795+ }
796+
797+ await copyText(reasoning);
798+ toastr.info(t`Copied!`, '', { timeOut: 2000 });
799+ });
800+}
801+
802+/**
803+ * Removes reasoning from a string if auto-parsing is enabled.
804+ * @param {string} str Input string
805+ * @returns {string} Output string
806+ */
807+export function removeReasoningFromString(str) {
808+ if (!power_user.reasoning.auto_parse) {
809+ return str;
810+ }
811+
812+ const parsedReasoning = parseReasoningFromString(str);
813+ return parsedReasoning?.content ?? str;
814+}
815+
816+/**
817+ * Parses reasoning from a string using the power user reasoning settings.
818+ * @typedef {Object} ParsedReasoning
819+ * @property {string} reasoning Reasoning block
820+ * @property {string} content Message content
821+ * @param {string} str Content of the message
822+ * @returns {ParsedReasoning|null} Parsed reasoning block and message content
823+ */
824+function parseReasoningFromString(str) {
825+ // Both prefix and suffix must be defined
826+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
827+ return null;
828+ }
829+
830+ try {
831+ const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
832+
833+ let didReplace = false;
834+ let reasoning = '';
835+ let content = String(str).replace(regex, (_match, captureGroup) => {
836+ didReplace = true;
837+ reasoning = captureGroup;
838+ return '';
839+ });
840+
841+ if (didReplace && power_user.trim_spaces) {
842+ reasoning = reasoning.trim();
843+ content = content.trim();
844+ }
845+
846+ return { reasoning, content };
847+ } catch (error) {
848+ console.error('[Reasoning] Error parsing reasoning block', error);
849+ return null;
850+ }
851+}
852+
853+function registerReasoningAppEvents() {
854+ eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
855+ if (!power_user.reasoning.auto_parse) {
856+ return;
857+ }
858+
859+ console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
860+ const message = chat[idx];
861+
862+ if (!message) {
863+ console.warn('[Reasoning] Message not found', idx);
864+ return null;
865+ }
866+
867+ if (!message.mes || message.mes === '...') {
868+ console.debug('[Reasoning] Message content is empty or a placeholder', idx);
869+ return null;
870+ }
871+
872+ const parsedReasoning = parseReasoningFromString(message.mes);
873+
874+ // No reasoning block found
875+ if (!parsedReasoning) {
876+ return;
877+ }
878+
879+ // Make sure the message has an extra object
880+ if (!message.extra || typeof message.extra !== 'object') {
881+ message.extra = {};
882+ }
883+
884+ const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
885+
886+ // If reasoning was found, add it to the message
887+ if (parsedReasoning.reasoning) {
888+ message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
889+ }
890+
891+ // Update the message text if it was changed
892+ if (parsedReasoning.content !== message.mes) {
893+ message.mes = parsedReasoning.content;
894+ }
895+
896+ // Find if a message already exists in DOM and must be updated
897+ if (contentUpdated) {
898+ const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
899+ if (messageRendered) {
900+ console.debug('[Reasoning] Updating message block', idx);
901+ updateMessageBlock(idx, message);
902+ }
903+ }
904+ });
905+}
906+
907+export function initReasoning() {
908+ loadReasoningSettings();
909+ setReasoningEventHandlers();
910+ registerReasoningSlashCommands();
911+ registerReasoningMacros();
912+ registerReasoningAppEvents();
913+}
public/scripts/secrets.js+2 -0
@@ -40,6 +40,8 @@ export const SECRET_KEYS = {
4040 BFL: 'api_key_bfl',
4141 GENERIC: 'api_key_generic',
4242 DEEPSEEK: 'api_key_deepseek',
43+ SERPER: 'api_key_serper',
44+ FALAI: 'api_key_falai',
4345};
4446
4547const INPUT_MAP = {
public/scripts/slash-commands.js+133 -49
@@ -42,6 +42,7 @@ import {
4242 showMoreMessages,
4343 stopGeneration,
4444 substituteParams,
45+ syncCurrentSwipeInfoExtras,
4546 system_avatar,
4647 system_message_types,
4748 this_chid,
@@ -58,7 +59,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
5859import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5960import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
6061import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
6162import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
6263import { registerVariableCommands, resolveVariable } from './variables.js';
6364import { background_settings } from './backgrounds.js';
6465import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -74,6 +75,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
7475import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7576import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7677import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78+import { accountStorage } from './util/AccountStorage.js';
7779export {
7880 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
7981};
@@ -234,7 +236,6 @@ export function initDefaultSlashCommands() {
234236 description: 'Character name - or unique character identifier (avatar key)',
235237 typeList: [ARGUMENT_TYPE.STRING],
236238 enumProvider: commonEnumProviders.characters('character'),
237- forceEnum: false,
238239 }),
239240 ],
240241 helpString: `
@@ -273,7 +274,6 @@ export function initDefaultSlashCommands() {
273274 typeList: [ARGUMENT_TYPE.STRING],
274275 isRequired: true,
275276 enumProvider: commonEnumProviders.characters('character'),
276- forceEnum: false,
277277 }),
278278 SlashCommandNamedArgument.fromProps({
279279 name: 'avatar',
@@ -517,7 +517,6 @@ export function initDefaultSlashCommands() {
517517 typeList: [ARGUMENT_TYPE.STRING],
518518 isRequired: true,
519519 enumProvider: commonEnumProviders.characters('all'),
520- forceEnum: true,
521520 }),
522521 ],
523522 helpString: 'Opens up a chat with the character or group by its name',
@@ -733,6 +732,57 @@ export function initDefaultSlashCommands() {
733732 helpString: 'Unhides a message from the prompt.',
734733 }));
735734 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
735+ name: 'member-get',
736+ aliases: ['getmember', 'memberget'],
737+ callback: (async ({ field = 'name' }, arg) => {
738+ if (!selected_group) {
739+ toastr.warning('Cannot run /member-get command outside of a group chat.');
740+ return '';
741+ }
742+ if (field === '') {
743+ toastr.warning('\'/member-get field=\' argument required!');
744+ return '';
745+ }
746+ field = field.toString();
747+ arg = arg.toString();
748+ if (!['name', 'index', 'id', 'avatar'].includes(field)) {
749+ toastr.warning('\'/member-get field=\' argument required!');
750+ return '';
751+ }
752+ const isId = !isNaN(parseInt(arg));
753+ const groupMember = findGroupMemberId(arg, true);
754+ if (!groupMember) {
755+ toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
756+ return '';
757+ }
758+ return groupMember[field];
759+ }),
760+ namedArgumentList: [
761+ SlashCommandNamedArgument.fromProps({
762+ name: 'field',
763+ description: 'Whether to retrieve the name, index, id, or avatar.',
764+ typeList: [ARGUMENT_TYPE.STRING],
765+ isRequired: true,
766+ defaultValue: 'name',
767+ enumList: [
768+ new SlashCommandEnumValue('name', 'Character name'),
769+ new SlashCommandEnumValue('index', 'Group member index'),
770+ new SlashCommandEnumValue('avatar', 'Character avatar'),
771+ new SlashCommandEnumValue('id', 'Character index'),
772+ ],
773+ }),
774+ ],
775+ unnamedArgumentList: [
776+ SlashCommandArgument.fromProps({
777+ description: 'member index (starts with 0), name, or avatar',
778+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
779+ isRequired: true,
780+ enumProvider: commonEnumProviders.groupMembers(),
781+ }),
782+ ],
783+ helpString: 'Retrieves a group member\'s name, index, id, or avatar.',
784+ }));
785+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
736786 name: 'member-disable',
737787 callback: disableGroupMemberCallback,
738788 aliases: ['disable', 'disablemember', 'memberdisable'],
@@ -842,7 +892,8 @@ export function initDefaultSlashCommands() {
842892 helpString: 'Moves a group member down in the group chat list.',
843893 }));
844894 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
845895 name: 'member-peek',
896+ aliases: ['peek', 'memberpeek', 'peekmember'],
846897 callback: peekCallback,
847898 unnamedArgumentList: [
848899 SlashCommandArgument.fromProps({
@@ -1008,7 +1059,6 @@ export function initDefaultSlashCommands() {
10081059 typeList: [ARGUMENT_TYPE.STRING],
10091060 defaultValue: 'System',
10101061 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
1011- forceEnum: false,
10121062 }),
10131063 new SlashCommandNamedArgument(
10141064 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@@ -1902,7 +1952,7 @@ export function initDefaultSlashCommands() {
19021952 returns: 'uppercase string',
19031953 unnamedArgumentList: [
19041954 new SlashCommandArgument(
19051955 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19061956 ),
19071957 ],
19081958 helpString: 'Converts the provided string to uppercase.',
@@ -1914,7 +1964,7 @@ export function initDefaultSlashCommands() {
19141964 returns: 'lowercase string',
19151965 unnamedArgumentList: [
19161966 new SlashCommandArgument(
19171967 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19181968 ),
19191969 ],
19201970 helpString: 'Converts the provided string to lowercase.',
@@ -1934,7 +1984,7 @@ export function initDefaultSlashCommands() {
19341984 ],
19351985 unnamedArgumentList: [
19361986 new SlashCommandArgument(
19371987 'stringtext to affect', [ARGUMENT_TYPE.STRING], true, false,
19381988 ),
19391989 ],
19401990 helpString: `
@@ -1968,8 +2018,8 @@ export function initDefaultSlashCommands() {
19682018 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
19692019 name: 'chat-render',
19702020 helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
19712021 callback: async (args, number) => {
19722022 await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
19732023 if (isTrueBoolean(String(args?.scroll ?? ''))) {
19742024 $('#chat').scrollTop(0);
19752025 }
@@ -1998,6 +2048,62 @@ export function initDefaultSlashCommands() {
19982048 return '';
19992049 },
20002050 }));
2051+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2052+ name: 'replace',
2053+ aliases: ['re'],
2054+ callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2055+ if (pattern === '')
2056+ throw new Error('Argument of \'pattern=\' cannot be empty');
2057+ switch (mode) {
2058+ case 'literal':
2059+ return text.replaceAll(pattern, replacer);
2060+ case 'regex':
2061+ return text.replace(regexFromString(pattern), replacer);
2062+ default:
2063+ throw new Error('Invalid \'/replace mode=\' argument specified!');
2064+ }
2065+ }),
2066+ returns: 'replaced text',
2067+ namedArgumentList: [
2068+ SlashCommandNamedArgument.fromProps({
2069+ name: 'mode',
2070+ description: 'Replaces occurrence(s) of a pattern',
2071+ typeList: [ARGUMENT_TYPE.STRING],
2072+ defaultValue: 'literal',
2073+ enumList: ['literal', 'regex'],
2074+ }),
2075+ new SlashCommandNamedArgument(
2076+ 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
2077+ ),
2078+ new SlashCommandNamedArgument(
2079+ 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
2080+ ),
2081+ ],
2082+ unnamedArgumentList: [
2083+ new SlashCommandArgument(
2084+ 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
2085+ ),
2086+ ],
2087+ helpString: `
2088+ <div>
2089+ Replaces text within the provided string based on the pattern.
2090+ </div>
2091+ <div>
2092+ If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />
2093+ If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />
2094+ The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />
2095+ If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />
2096+ </div>
2097+ <div>
2098+ <strong>Example:</strong>
2099+ <pre>/let x Blue house and blue car || </pre>
2100+ <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
2101+ <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
2102+ <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
2103+ <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
2104+ </div>
2105+ `,
2106+ }));
20012107
20022108 registerVariableCommands();
20032109}
@@ -2814,8 +2920,11 @@ async function addSwipeCallback(args, value) {
28142920 const newSwipeId = lastMessage.swipes.length - 1;
28152921
28162922 if (isTrueBoolean(args.switch)) {
2923+ // Make sure ad-hoc changes to extras are saved before swiping away
2924+ syncCurrentSwipeInfoExtras();
28172925 lastMessage.swipe_id = newSwipeId;
28182926 lastMessage.mes = lastMessage.swipes[newSwipeId];
2927+ lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
28192928 }
28202929
28212930 await saveChatConditional();
@@ -2987,7 +3096,7 @@ function performGroupMemberAction(chid, action) {
29873096
29883097async function disableGroupMemberCallback(_, arg) {
29893098 if (!selected_group) {
29903099 toastr.warning('Cannot run /member-disable command outside of a group chat.');
29913100 return '';
29923101 }
29933102
@@ -3004,7 +3113,7 @@ async function disableGroupMemberCallback(_, arg) {
30043113
30053114async function enableGroupMemberCallback(_, arg) {
30063115 if (!selected_group) {
30073116 toastr.warning('Cannot run /member-enable command outside of a group chat.');
30083117 return '';
30093118 }
30103119
@@ -3021,7 +3130,7 @@ async function enableGroupMemberCallback(_, arg) {
30213130
30223131async function moveGroupMemberUpCallback(_, arg) {
30233132 if (!selected_group) {
30243133 toastr.warning('Cannot run /memberupmember-up command outside of a group chat.');
30253134 return '';
30263135 }
30273136
@@ -3038,7 +3147,7 @@ async function moveGroupMemberUpCallback(_, arg) {
30383147
30393148async function moveGroupMemberDownCallback(_, arg) {
30403149 if (!selected_group) {
30413150 toastr.warning('Cannot run /memberdownmember-down command outside of a group chat.');
30423151 return '';
30433152 }
30443153
@@ -3055,12 +3164,12 @@ async function moveGroupMemberDownCallback(_, arg) {
30553164
30563165async function peekCallback(_, arg) {
30573166 if (!selected_group) {
30583167 toastr.warning('Cannot run /member-peek command outside of a group chat.');
30593168 return '';
30603169 }
30613170
30623171 if (is_group_generating) {
30633172 toastr.warning('Cannot run /member-peek command while the group reply is generating.');
30643173 return '';
30653174 }
30663175
@@ -3077,12 +3186,7 @@ async function peekCallback(_, arg) {
30773186
30783187async function removeGroupMemberCallback(_, arg) {
30793188 if (!selected_group) {
30803189 toastr.warning('Cannot run /memberremovemember-remove command outside of a group chat.');
3081- return '';
3082- }
3083-
3084- if (is_group_generating) {
3085- toastr.warning('Cannot run /memberremove command while the group reply is generating.');
30863190 return '';
30873191 }
30883192
@@ -3190,12 +3294,7 @@ function findPersonaByName(name) {
31903294}
31913295
31923296async function sendUserMessageCallback(args, text) {
3193- if (!text) {
3297+ text = String(text ?? '').trim();
3194- toastr.warning('You must specify text to send');
3195- return;
3196- }
3197-
3198- text = text.trim();
31993298 const compact = isTrueBoolean(args?.compact);
32003299 const bias = extractMessageBias(text);
32013300
@@ -3504,24 +3603,18 @@ export function getNameAndAvatarForMessage(character, name = null) {
35043603}
35053604
35063605export async function sendMessageAs(args, text) {
3507- if (!text) {
3508- toastr.warning('You must specify text to send as');
3509- return '';
3510- }
3511-
35123606 let name = args.name?.trim();
3513- let mesText;
35143607
35153608 if (!name) {
35163609 const namelessWarningKey = 'sendAsNamelessWarningShown';
35173610 if (localStorageaccountStorage.getItem(namelessWarningKey) !== 'true') {
35183611 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
35193612 localStorageaccountStorage.setItem(namelessWarningKey, 'true');
35203613 }
35213614 name = name2;
35223615 }
35233616
35243617 let mesText = String(text ?? '').trim();
35253618
35263619 // Requires a regex check after the slash command is pushed to output
35273620 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3599,11 +3692,7 @@ export async function sendMessageAs(args, text) {
35993692}
36003693
36013694export async function sendNarratorMessage(args, text) {
3602- if (!text) {
3695+ text = String(text ?? '');
3603- toastr.warning('You must specify text to send');
3604- return '';
3605- }
3606-
36073696 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
36083697 // Messages that do nothing but set bias will be hidden from the context
36093698 const bias = extractMessageBias(text);
@@ -3694,18 +3783,13 @@ export async function promptQuietForLoudResponse(who, text) {
36943783}
36953784
36963785async function sendCommentMessage(args, text) {
3697- if (!text) {
3698- toastr.warning('You must specify text to send');
3699- return '';
3700- }
3701-
37023786 const compact = isTrueBoolean(args?.compact);
37033787 const message = {
37043788 name: COMMENT_NAME_DEFAULT,
37053789 is_user: false,
37063790 is_system: true,
37073791 send_date: getMessageTimeStamp(),
37083792 mes: substituteParams(String(text ?? '').trim()),
37093793 force_avatar: comment_avatar,
37103794 extra: {
37113795 type: system_message_types.COMMENT,
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+28 -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,10 +68,14 @@ 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 { timestampToMomentaccountStorage } from './utilsutil/AccountStorage.js';
72+import { timestampToMoment, uuidv4 } from './utils.js';
73+import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
74+import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
6975
7076export function getContext() {
7177 return {
78+ accountStorage,
7279 chat,
7380 characters,
7481 groups,
@@ -167,6 +174,25 @@ export function getContext() {
167174 chatCompletionSettings: oai_settings,
168175 textCompletionSettings: textgenerationwebui_settings,
169176 powerUserSettings: power_user,
177+ getCharacters,
178+ uuidv4,
179+ humanizedDateTime,
180+ updateMessageBlock,
181+ appendMediaToMessage,
182+ variables: {
183+ local: {
184+ get: getLocalVariable,
185+ set: setLocalVariable,
186+ },
187+ global: {
188+ get: getGlobalVariable,
189+ set: setGlobalVariable,
190+ },
191+ },
192+ loadWorldInfo,
193+ saveWorldInfo,
194+ updateWorldInfoList,
195+ convertCharacterBook,
170196 };
171197}
172198
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/templates/itemizationChat.html+1 -1
@@ -146,5 +146,5 @@
146146</div>
147147<hr>
148148<div id="rawPromptPopup" class="list-group">
149149 <div id="rawPromptWrapper" class="tokenItemizingSubclasstokenItemizingMaintext"></div>
150150</div>
public/scripts/textgen-models.js+15 -3
@@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js';
66import { renderTemplateAsync } from './templates.js';
77import { POPUP_TYPE, callGenericPopup } from './popup.js';
88import { t } from './i18n.js';
9+import { accountStorage } from './util/AccountStorage.js';
910
1011let mancerModels = [];
1112let togetherModels = [];
@@ -54,6 +55,17 @@ const OPENROUTER_PROVIDERS = [
5455 'xAI',
5556 'Cloudflare',
5657 'SF Compute',
58+ 'Minimax',
59+ 'Nineteen',
60+ 'Liquid',
61+ 'InferenceNet',
62+ 'Friendli',
63+ 'AionLabs',
64+ 'Alibaba',
65+ 'Nebius',
66+ 'Chutes',
67+ 'Kluster',
68+ 'Targon',
5769 '01.AI',
5870 'HuggingFace',
5971 'Mancer',
@@ -330,7 +342,7 @@ export async function loadFeatherlessModels(data) {
330342 populateClassSelection(data);
331343
332344 // Retrieve the stored number of items per page or default to 10
333345 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 10;
334346
335347 // Initialize pagination
336348 applyFiltersAndSort();
@@ -406,7 +418,7 @@ export async function loadFeatherlessModels(data) {
406418 },
407419 afterSizeSelectorChange: function (e) {
408420 const newPerPage = e.target.value;
409421 localStorageaccountStorage.setItem('Models_PerPage'storageKey, newPerPage);
410422 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
411423 },
412424 });
@@ -507,7 +519,7 @@ export async function loadFeatherlessModels(data) {
507519 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
508520 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
509521
510522 setupPagination(filteredModels, Number(localStorageaccountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
511523 }
512524
513525 // Required to keep the /model command function
public/scripts/textgen-settings.js+45 -9
@@ -10,6 +10,7 @@ import {
1010 setOnlineStatus,
1111 substituteParams,
1212} from '../script.js';
13+import { t } from './i18n.js';
1314import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
1415
1516import { power_user, registerDebugFunction } from './power-user.js';
@@ -172,6 +173,7 @@ const settings = {
172173 //truncation_length: 2048,
173174 ban_eos_token: false,
174175 skip_special_tokens: true,
176+ include_reasoning: true,
175177 streaming: false,
176178 mirostat_mode: 0,
177179 mirostat_tau: 5,
@@ -181,6 +183,8 @@ const settings = {
181183 grammar_string: '',
182184 json_schema: {},
183185 banned_tokens: '',
186+ global_banned_tokens: '',
187+ send_banned_tokens: true,
184188 sampler_priority: OOBA_DEFAULT_ORDER,
185189 samplers: LLAMACPP_DEFAULT_ORDER,
186190 samplers_priorities: APHRODITE_DEFAULT_ORDER,
@@ -263,6 +267,7 @@ export const setting_names = [
263267 'add_bos_token',
264268 'ban_eos_token',
265269 'skip_special_tokens',
270+ 'include_reasoning',
266271 'streaming',
267272 'mirostat_mode',
268273 'mirostat_tau',
@@ -272,6 +277,8 @@ export const setting_names = [
272277 'grammar_string',
273278 'json_schema',
274279 'banned_tokens',
280+ 'global_banned_tokens',
281+ 'send_banned_tokens',
275282 'ignore_eos_token',
276283 'spaces_between_special_tokens',
277284 'speculative_ngram',
@@ -392,7 +399,7 @@ function getTokenizerForTokenIds() {
392399 * @returns {TokenBanResult} String with comma-separated banned token IDs
393400 */
394401function getCustomTokenBans() {
395402 if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) {
396403 return {
397404 banned_tokens: '',
398405 banned_strings: [],
@@ -402,8 +409,9 @@ function getCustomTokenBans() {
402409 const tokenizer = getTokenizerForTokenIds();
403410 const banned_tokens = [];
404411 const banned_strings = [];
405412 const sequences = settings.banned_tokens[]
406413 .concat(settings.banned_tokens.split('\n'))
414+ .concat(settings.global_banned_tokens.split('\n'))
407415 .concat(textgenerationwebui_banned_in_macros)
408416 .filter(x => x.length > 0)
409417 .filter(onlyUnique);
@@ -451,6 +459,18 @@ function getCustomTokenBans() {
451459}
452460
453461/**
462+ * Sets the banned strings kill switch toggle.
463+ * @param {boolean} isEnabled Kill switch state
464+ * @param {string} title Label title
465+ */
466+function toggleBannedStringsKillSwitch(isEnabled, title) {
467+ $('#send_banned_tokens_textgenerationwebui').prop('checked', isEnabled);
468+ $('#send_banned_tokens_label').find('.menu_button').toggleClass('toggleEnabled', isEnabled).prop('title', title);
469+ settings.send_banned_tokens = isEnabled;
470+ saveSettingsDebounced();
471+}
472+
473+/**
454474 * Calculates logit bias object from the logit bias list.
455475 * @returns {object} Logit bias object
456476 */
@@ -501,7 +521,7 @@ export function loadTextGenSettings(data, loadedSettings) {
501521 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
502522 const control = $(selector);
503523 control.val(settings.server_urls[type] ?? '').on('input', function () {
504524 settings.server_urls[type] = String($(this).val()).trim();
505525 saveSettingsDebounced();
506526 });
507527 }
@@ -592,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) {
592612}
593613
594614jQuery(function () {
615+ $('#send_banned_tokens_textgenerationwebui').on('change', function () {
616+ const checked = !!$(this).prop('checked');
617+ toggleBannedStringsKillSwitch(checked,
618+ checked
619+ ? t`Banned tokens/strings are being sent in the request.`
620+ : t`Banned tokens/strings are NOT being sent in the request.`);
621+ });
622+
595623 $('#koboldcpp_order').sortable({
596624 delay: getSortableDelay(),
597625 stop: function () {
@@ -740,6 +768,7 @@ jQuery(function () {
740768 'add_bos_token_textgenerationwebui': true,
741769 'temperature_last_textgenerationwebui': true,
742770 'skip_special_tokens_textgenerationwebui': true,
771+ 'include_reasoning_textgenerationwebui': true,
743772 'top_a_textgenerationwebui': 0,
744773 'top_a_counter_textgenerationwebui': 0,
745774 'mirostat_mode_textgenerationwebui': 0,
@@ -929,6 +958,10 @@ function setSettingByName(setting, value, trigger) {
929958 if (isCheckbox) {
930959 const val = Boolean(value);
931960 $(`#${setting}_textgenerationwebui`).prop('checked', val);
961+
962+ if ('send_banned_tokens' === setting) {
963+ $(`#${setting}_textgenerationwebui`).trigger('change');
964+ }
932965 }
933966 else if (isText) {
934967 $(`#${setting}_textgenerationwebui`).val(value);
@@ -986,6 +1019,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
9861019 let logprobs = null;
9871020 const swipes = [];
9881021 const toolCalls = [];
1022+ const state = { reasoning: '' };
9891023 while (true) {
9901024 const { done, value } = await reader.read();
9911025 if (done) return;
@@ -1002,9 +1036,10 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
10021036 const newText = data?.choices?.[0]?.text || data?.content || '';
10031037 text += newText;
10041038 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1039+ state.reasoning += data?.choices?.[0]?.reasoning ?? '';
10051040 }
10061041
10071042 yield { text, swipes, logprobs, toolCalls, state };
10081043 }
10091044 };
10101045}
@@ -1216,7 +1251,7 @@ function replaceMacrosInList(str) {
12161251 }
12171252}
12181253
12191254export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
12201255 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
12211256 const dynatemp = isDynamicTemperatureSupported();
12221257 const { banned_tokens, banned_strings } = getCustomTokenBans();
@@ -1231,7 +1266,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12311266 'top_p': settings.top_p,
12321267 'typical_p': settings.typical_p,
12331268 'typical': settings.typical_p,
12341269 'sampler_seed': settings.seed >= 0 ? settings.seed : undefined,
12351270 'min_p': settings.min_p,
12361271 'repetition_penalty': settings.rep_pen,
12371272 'frequency_penalty': settings.freq_pen,
@@ -1265,6 +1300,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12651300 'truncation_length': max_context,
12661301 'ban_eos_token': settings.ban_eos_token,
12671302 'skip_special_tokens': settings.skip_special_tokens,
1303+ 'include_reasoning': settings.include_reasoning,
12681304 'top_a': settings.top_a,
12691305 'tfs': settings.tfs,
12701306 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,
@@ -1294,7 +1330,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12941330 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
12951331 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
12961332 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
12971333 'seed': settings.seed >= 0 ? settings.seed : undefined,
12981334 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
12991335 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
13001336 'grammar_string': settings.grammar_string,
@@ -1443,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
14431479 }
14441480 }
14451481
14461482 await eventSource.emitAndWaitemit(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
14471483
14481484 // Grammar conflicts with with json_schema
14491485 if (settings.type === LLAMACPP) {
public/scripts/tokenizers.js+3 -0
@@ -679,6 +679,9 @@ export function getTokenizerModel() {
679679 }
680680
681681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
682+ if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
683+ return deepseekTokenizer;
684+ }
682685 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
683686 return llama3Tokenizer;
684687 }
public/scripts/tool-calling.js+1 -0
@@ -563,6 +563,7 @@ export class ToolManager {
563563 chat_completion_sources.OPENROUTER,
564564 chat_completion_sources.GROQ,
565565 chat_completion_sources.COHERE,
566+ chat_completion_sources.DEEPSEEK,
566567 ];
567568 return supportedSources.includes(oai_settings.chat_completion_source);
568569 }
public/scripts/user.js+8 -0
@@ -44,6 +44,14 @@ export function isAdmin() {
4444}
4545
4646/**
47+ * Gets the handle string of the current user.
48+ * @returns {string} User handle
49+ */
50+export function getCurrentUserHandle() {
51+ return currentUser?.handle || 'default-user';
52+}
53+
54+/**
4755 * Get the current user.
4856 * @returns {Promise<void>}
4957 */
public/scripts/util/AccountStorage.js+139 -0
@@ -0,0 +1,139 @@
1+import { saveSettingsDebounced } from '../../script.js';
2+
3+const MIGRATED_MARKER = '__migrated';
4+const MIGRATABLE_KEYS = [
5+ /^AlertRegex_/,
6+ /^AlertWI_/,
7+ /^Assets_SkipConfirm_/,
8+ /^Characters_PerPage$/,
9+ /^DataBank_sortField$/,
10+ /^DataBank_sortOrder$/,
11+ /^extension_update_nag$/,
12+ /^extensions_sortByName$/,
13+ /^FeatherlessModels_PerPage$/,
14+ /^GroupMembers_PerPage$/,
15+ /^GroupCandidates_PerPage$/,
16+ /^LNavLockOn$/,
17+ /^LNavOpened$/,
18+ /^mediaWarningShown:/,
19+ /^NavLockOn$/,
20+ /^NavOpened$/,
21+ /^Personas_PerPage$/,
22+ /^Personas_GridView$/,
23+ /^Proxy_SkipConfirm_/,
24+ /^qr--executeShortcut$/,
25+ /^qr--syntax$/,
26+ /^qr--tabSize$/,
27+ /^qr--wrap$/,
28+ /^RegenerateWithCtrlEnter$/,
29+ /^SelectedNavTab$/,
30+ /^sendAsNamelessWarningShown$/,
31+ /^StoryStringValidationCache$/,
32+ /^WINavOpened$/,
33+ /^WI_PerPage$/,
34+ /^world_info_sort_order$/,
35+];
36+
37+/**
38+ * Provides access to account storage of arbitrary key-value pairs.
39+ */
40+class AccountStorage {
41+ /**
42+ * @type {Record<string, string>} Storage state
43+ */
44+ #state = {};
45+
46+ /**
47+ * @type {boolean} If the storage was initialized
48+ */
49+ #ready = false;
50+
51+ #migrateLocalStorage() {
52+ const localStorageKeys = [];
53+ for (let i = 0; i < globalThis.localStorage.length; i++) {
54+ localStorageKeys.push(globalThis.localStorage.key(i));
55+ }
56+ for (const key of localStorageKeys) {
57+ if (MIGRATABLE_KEYS.some(k => k.test(key))) {
58+ const value = globalThis.localStorage.getItem(key);
59+ this.#state[key] = value;
60+ globalThis.localStorage.removeItem(key);
61+ }
62+ }
63+ }
64+
65+ /**
66+ * Initialize the account storage.
67+ * @param {Object} state Initial state
68+ */
69+ init(state) {
70+ if (state && typeof state === 'object') {
71+ this.#state = Object.assign(this.#state, state);
72+ }
73+
74+ if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
75+ this.#migrateLocalStorage();
76+ this.#state[MIGRATED_MARKER] = '1';
77+ saveSettingsDebounced();
78+ }
79+
80+ this.#ready = true;
81+ }
82+
83+ /**
84+ * Get the value of a key in account storage.
85+ * @param {string} key Key to get
86+ * @returns {string|null} Value of the key
87+ */
88+ getItem(key) {
89+ if (!this.#ready) {
90+ console.warn(`AccountStorage not ready (trying to read from ${key})`);
91+ }
92+
93+ return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
94+ }
95+
96+ /**
97+ * Set a key in account storage.
98+ * @param {string} key Key to set
99+ * @param {string} value Value to set
100+ */
101+ setItem(key, value) {
102+ if (!this.#ready) {
103+ console.warn(`AccountStorage not ready (trying to write to ${key})`);
104+ }
105+
106+ this.#state[key] = String(value);
107+ saveSettingsDebounced();
108+ }
109+
110+ /**
111+ * Remove a key from account storage.
112+ * @param {string} key Key to remove
113+ */
114+ removeItem(key) {
115+ if (!this.#ready) {
116+ console.warn(`AccountStorage not ready (trying to remove ${key})`);
117+ }
118+
119+ if (!Object.hasOwn(this.#state, key)) {
120+ return;
121+ }
122+
123+ delete this.#state[key];
124+ saveSettingsDebounced();
125+ }
126+
127+ /**
128+ * Gets a snapshot of the storage state.
129+ * @returns {Record<string, string>} A deep clone of the storage state
130+ */
131+ getState() {
132+ return structuredClone(this.#state);
133+ }
134+}
135+
136+/**
137+ * Account storage instance.
138+ */
139+export const accountStorage = new AccountStorage();
public/scripts/utils.js+24 -7
@@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
17331733
17341734/**
17351735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
1736- *
1736+ * The action will be executed after the animation ends or after the timeout, whichever comes first.
17371737 * @param {HTMLElement} control - The control element to listen for animation end event
17381738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
1739+ * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
17391740 */
17401741export function runAfterAnimation(control, callback, timeout = 500) {
17411742 if (hasAnimation(control)) {
1742- const onAnimationEnd = () => {
1743+ Promise.race([
1743- control.removeEventListener('animationend', onAnimationEnd);
1744+ new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
1744- callback(control);
1745+ new Promise((r) => control.addEventListener('animationend', r, { once: true })),
1745- };
1746+ ]).finally(() => callback(control));
1746- control.addEventListener('animationend', onAnimationEnd);
17471747 } else {
17481748 callback(control);
17491749 }
@@ -2059,6 +2059,23 @@ export function toggleDrawer(drawer, expand = true) {
20592059 }
20602060}
20612061
2062+/**
2063+ * Sets or removes a dataset property on an HTMLElement
2064+ *
2065+ * Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
2066+ *
2067+ * @param {HTMLElement} element - The element to modify
2068+ * @param {string} name - The name of the dataset property
2069+ * @param {string|null} value - The value to set - If null, the dataset property will be removed
2070+ */
2071+export function setDatasetProperty(element, name, value) {
2072+ if (value === null) {
2073+ delete element.dataset[name];
2074+ } else {
2075+ element.dataset[name] = value;
2076+ }
2077+}
2078+
20622079export async function fetchFaFile(name) {
20632080 const style = document.createElement('style');
20642081 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/scripts/variables.js+4 -4
@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1919
2020const MAX_LOOPS = 100;
2121
2222export function getLocalVariable(name, args = {}) {
2323 if (!chat_metadata.variables) {
2424 chat_metadata.variables = {};
2525 }
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
4545 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
4646}
4747
4848export function setLocalVariable(name, value, args = {}) {
4949 if (!name) {
5050 throw new Error('Variable name cannot be empty or undefined.');
5151 }
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
8080 return value;
8181}
8282
8383export function getGlobalVariable(name, args = {}) {
8484 let globalVariable = extension_settings.variables.global[args.key ?? name];
8585 if (args.index !== undefined) {
8686 try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
102102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
103103}
104104
105105export function setGlobalVariable(name, value, args = {}) {
106106 if (!name) {
107107 throw new Error('Variable name cannot be empty or undefined.');
108108 }
public/scripts/world-info.js+38 -20
@@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
2121import { StructuredCloneMap } from './util/StructuredCloneMap.js';
2222import { renderTemplateAsync } from './templates.js';
2323import { t } from './i18n.js';
24+import { accountStorage } from './util/AccountStorage.js';
2425
2526export const world_info_insertion_strategy = {
2627 evenly: 0,
@@ -400,6 +401,12 @@ class WorldInfoTimedEffects {
400401 #entries = [];
401402
402403 /**
404+ * Is this a dry run?
405+ * @type {boolean}
406+ */
407+ #isDryRun = false;
408+
409+ /**
403410 * Buffer for active timed effects.
404411 * @type {Record<TimedEffectType, WIScanEntry[]>}
405412 */
@@ -448,10 +455,12 @@ class WorldInfoTimedEffects {
448455 * Initialize the timed effects with the given messages.
449456 * @param {string[]} chat Array of chat messages
450457 * @param {WIScanEntry[]} entries Array of entries
458+ * @param {boolean} isDryRun Whether the operation is a dry run
451459 */
452460 constructor(chat, entries, isDryRun = false) {
453461 this.#chat = chat;
454462 this.#entries = entries;
463+ this.#isDryRun = isDryRun;
455464 this.#ensureChatMetadata();
456465 }
457466
@@ -583,8 +592,10 @@ class WorldInfoTimedEffects {
583592 * Checks for timed effects on chat messages.
584593 */
585594 checkTimedEffects() {
586- this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
595+ if (!this.#isDryRun) {
587596 this.#checkTimedEffectOfType('cooldownsticky', this.#buffer.cooldownsticky, this.#onEnded.cooldownsticky.bind(this));
597+ this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
598+ }
588599 this.#checkDelayEffect(this.#buffer.delay);
589600 }
590601
@@ -629,6 +640,7 @@ class WorldInfoTimedEffects {
629640 * @param {WIScanEntry[]} activatedEntries Entries that were activated
630641 */
631642 setTimedEffects(activatedEntries) {
643+ if (this.#isDryRun) return;
632644 for (const entry of activatedEntries) {
633645 this.#setTimedEffectOfType('sticky', entry);
634646 this.#setTimedEffectOfType('cooldown', entry);
@@ -645,6 +657,9 @@ class WorldInfoTimedEffects {
645657 if (!this.isValidEffectType(type)) {
646658 return;
647659 }
660+ if (this.#isDryRun && type !== 'delay') {
661+ return;
662+ }
648663
649664 const key = this.#getEntryKey(entry);
650665 delete chat_metadata.timedWorldInfo[type][key];
@@ -858,7 +873,7 @@ export function setWorldInfoSettings(settings, data) {
858873 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
859874 });
860875
861876 $('#world_info_sort_order').val(localStorageaccountStorage.getItem(SORT_ORDER_KEY) || '0');
862877 $('#world_info').trigger('change');
863878 $('#world_editor_select').trigger('change');
864879
@@ -1708,7 +1723,7 @@ export async function loadWorldInfo(name) {
17081723 return null;
17091724}
17101725
17111726export async function updateWorldInfoList() {
17121727 const result = await fetch('/api/settings/get', {
17131728 method: 'POST',
17141729 headers: getRequestHeaders(),
@@ -1933,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19331948 if (typeof navigation === 'number' && Number(navigation) >= 0) {
19341949 const data = getDataArray();
19351950 const uidIndex = data.findIndex(x => x.uid === navigation);
19361951 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || perPageDefault;
19371952 startPage = Math.floor(uidIndex / perPage) + 1;
19381953 }
19391954
19401955 $('#world_info_pagination').pagination({
19411956 dataSource: getDataArray,
19421957 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || perPageDefault,
19431958 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
19441959 showSizeChanger: true,
19451960 pageRange: 1,
@@ -1969,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19691984 worldEntriesList.append(blocks);
19701985 },
19711986 afterSizeSelectorChange: function (e) {
19721987 localStorageaccountStorage.setItem(storageKey, e.target.value);
19731988 },
19741989 afterPaging: function () {
19751990 $('#world_popup_entries_list textarea[name="comment"]').each(function () {
@@ -2174,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() {
21742189 // If search got cleared, we make sure to hide the option and go back to the one before
21752190 if (!searchTerm && !isHidden) {
21762191 searchOption.attr('hidden', '');
21772192 selector.val(localStorageaccountStorage.getItem(SORT_ORDER_KEY) || '0');
21782193 }
21792194}
21802195
@@ -2423,7 +2438,9 @@ export async function getWorldEntry(name, data, entry) {
24232438 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
24242439 await saveWorldInfo(name, data);
24252440 }
2441+ $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
24262442 });
2443+ input.toggleClass('empty', !entry[entryPropName].length);
24272444 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
24282445 input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
24292446
@@ -2458,6 +2475,7 @@ export async function getWorldEntry(name, data, entry) {
24582475 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
24592476 setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
24602477 await saveWorldInfo(name, data);
2478+ $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
24612479 }
24622480 });
24632481 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@@ -3435,7 +3453,7 @@ async function _save(name, data) {
34353453 headers: getRequestHeaders(),
34363454 body: JSON.stringify({ name: name, data: data }),
34373455 });
34383456 await eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
34393457}
34403458
34413459
@@ -3847,7 +3865,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
38473865 const context = getContext();
38483866 const buffer = new WorldInfoBuffer(chat);
38493867
38503868 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
38513869
38523870 // Combine the chat
38533871
@@ -3879,9 +3897,9 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
38793897
38803898 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
38813899 const sortedEntries = await getSortedEntries();
38823900 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries, isDryRun);
38833901
38843902 !isDryRun && timedEffects.checkTimedEffects();
38853903
38863904 if (sortedEntries.length === 0) {
38873905 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
@@ -4324,12 +4342,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
43244342 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
43254343 }
43264344
43274345 !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
43284346 buffer.resetExternalEffects();
43294347 timedEffects.cleanUp();
43304348
43314349 console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
43324350 console.debug('`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---'`);
43334351
43344352 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
43354353}
@@ -4658,7 +4676,7 @@ function convertNovelLorebook(inputObj) {
46584676 return outputObj;
46594677}
46604678
46614679export function convertCharacterBook(characterBook) {
46624680 const result = { entries: {}, originalData: characterBook };
46634681
46644682 characterBook.entries.forEach((entry, index) => {
@@ -4736,8 +4754,8 @@ export function checkEmbeddedWorld(chid) {
47364754 // Only show the alert once per character
47374755 const checkKey = `AlertWI_${characters[chid].avatar}`;
47384756 const worldName = characters[chid]?.data?.extensions?.world;
47394757 if (!localStorageaccountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
47404758 localStorageaccountStorage.setItem(checkKey, 'true');
47414759
47424760 if (power_user.world_import_dialog) {
47434761 const html = `<h3>This character has an embedded World/Lorebook.</h3>
@@ -5181,7 +5199,7 @@ jQuery(() => {
51815199 $('#world_info_sort_order').on('change', function () {
51825200 const value = String($(this).find(':selected').val());
51835201 // Save sort order, but do not save search sorting, as this is a temporary sorting option
51845202 if (value !== 'search') localStorageaccountStorage.setItem(SORT_ORDER_KEY, value);
51855203 updateEditor(navigation_option.none);
51865204 });
51875205
public/style.css+240 -47
@@ -106,6 +106,8 @@
106106 --tool-cool-color-picker-btn-bg: transparent;
107107 --tool-cool-color-picker-btn-border-color: transparent;
108108
109+ --mes-right-spacing: 30px;
110+
109111 --avatar-base-height: 50px;
110112 --avatar-base-width: 50px;
111113 --avatar-base-border-radius: 2px;
@@ -260,6 +262,10 @@ input[type='checkbox']:focus-visible {
260262 color: var(--SmartThemeEmColor);
261263}
262264
265+.tokenItemizingMaintext {
266+ font-size: calc(var(--mainFontSize) * 0.8);
267+}
268+
263269.tokenGraph {
264270 border-radius: 10px;
265271 border: 1px solid var(--SmartThemeBorderColor);
@@ -292,36 +298,44 @@ input[type='checkbox']:focus-visible {
292298 filter: grayscale(25%);
293299}
294300
295301.mes_text table {,
302+.mes_reasoning table {
296303 border-spacing: 0;
297304 border-collapse: collapse;
298305 margin-bottom: 10px;
299306}
300307
301308.mes_text td,
302309.mes_text th {,
310+.mes_reasoning td,
311+.mes_reasoning th {
303312 border: 1px solid;
304313 border-collapse: collapse;
305314 padding: 0.25em;
306315}
307316
308317.mes_text p {,
318+.mes_reasoning p {
309319 margin-top: 0;
310320 margin-bottom: 10px;
311321}
312322
313323.mes_text li tt {,
324+.mes_reasoning li tt {
314325 display: inline-block;
315326}
316327
317328.mes_text ol,
318329.mes_text ul {,
330+.mes_reasoning ol,
331+.mes_reasoning ul {
319332 margin-top: 5px;
320333 margin-bottom: 5px;
321334}
322335
323336.mes_text br,
324337.mes_bias br {,
338+.mes_reasoning br {
325339 content: ' ';
326340}
327341
@@ -332,25 +346,150 @@ input[type='checkbox']:focus-visible {
332346 color: var(--SmartThemeQuoteColor);
333347}
334348
349+.mes_reasoning {
350+ display: block;
351+ border-left: 2px solid var(--SmartThemeEmColor);
352+ border-radius: 2px;
353+ padding: 5px;
354+ padding-left: 14px;
355+ margin-bottom: 0.5em;
356+ overflow-y: auto;
357+ color: var(--SmartThemeEmColor);
358+}
359+
360+.mes_reasoning_details {
361+ margin-right: var(--mes-right-spacing);
362+}
363+
364+.mes_reasoning_details .mes_reasoning_summary {
365+ list-style: none;
366+ margin-right: calc(var(--mes-right-spacing) * -1);
367+}
368+
369+.mes_reasoning_details summary::-webkit-details-marker {
370+ display: none;
371+}
372+
373+.mes_reasoning *:last-child {
374+ margin-bottom: 0;
375+}
376+
377+.mes_reasoning em,
378+.mes_reasoning i,
379+.mes_reasoning u,
380+.mes_reasoning q,
381+.mes_reasoning blockquote {
382+ filter: saturate(0.5);
383+}
384+
385+.mes_reasoning_details .mes_reasoning em {
386+ color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
387+}
388+
389+.mes_reasoning_header_block {
390+ flex-grow: 1;
391+}
392+
393+.mes_reasoning_header {
394+ cursor: pointer;
395+ position: relative;
396+ user-select: none;
397+ margin: 0.5em 2px;
398+ padding: 7px 14px;
399+ padding-right: calc(0.7em + 14px);
400+ border-radius: 5px;
401+ background-color: var(--grey30);
402+ font-size: calc(var(--mainFontSize) * 0.9);
403+ align-items: baseline;
404+}
405+
406+.mes:has(.mes_reasoning:empty) .mes_reasoning_header {
407+ cursor: default;
408+}
409+
410+/* TWIMC: Remove with custom CSS to show the icon */
411+.mes_reasoning_header>.icon-svg {
412+ display: none;
413+}
414+
415+@supports not selector(:has(*)) {
416+ .mes_reasoning_details {
417+ display: none !important;
418+ }
419+}
420+
421+.mes_bias:empty,
422+.mes:not(.reasoning) .mes_reasoning_details,
423+.mes_reasoning_details:not([open]) .mes_reasoning_actions,
424+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
425+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
426+.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
427+.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
428+.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
429+.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
430+.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
431+.mes:has(.mes_reasoning:empty) .mes_reasoning,
432+.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
433+ display: none;
434+}
435+
436+.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
437+ background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
438+}
439+
440+/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
441+#chat:not([data-show-hidden-reasoning="true"]) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
442+ display: none;
443+}
444+
445+.mes_reasoning_details .mes_reasoning_arrow {
446+ position: absolute;
447+ top: 50%;
448+ right: 7px;
449+ transform: translateY(-50%);
450+ font-size: calc(var(--mainFontSize) * 0.7);
451+ width: calc(var(--mainFontSize) * 0.7);
452+ height: calc(var(--mainFontSize) * 0.7);
453+}
454+
455+.mes_reasoning_details:not([open]) .mes_reasoning_arrow {
456+ transform: translateY(-50%) rotate(180deg);
457+}
458+
459+.mes_reasoning_summary>span {
460+ margin-left: 0.5em;
461+}
462+
335463.mes_text i,
336464.mes_text em {,
465+.mes_reasoning i,
466+.mes_reasoning em {
337467 color: var(--SmartThemeEmColor);
338468}
339469
340470.mes_text uq {i,
471+.mes_text q em {
472+ color: inherit;
473+}
474+
475+.mes_text u,
476+.mes_reasoning u {
341477 color: var(--SmartThemeUnderlineColor);
342478}
343479
344480.mes_text q {,
481+.mes_reasoning q {
345482 color: var(--SmartThemeQuoteColor);
346483}
347484
348485.mes_text font[color] em,
349486.mes_text font[color] i {,
350- color: inherit;
487+.mes_text font[color] u,
351-}
488+.mes_text font[color] q,
352-
489+.mes_reasoning font[color] em,
353490.mes_textmes_reasoning font[color] q {i,
491+.mes_reasoning font[color] u,
492+.mes_reasoning font[color] q {
354493 color: inherit;
355494}
356495
@@ -358,7 +497,8 @@ input[type='checkbox']:focus-visible {
358497 display: block;
359498}
360499
361500.mes_text blockquote {,
501+.mes_reasoning blockquote {
362502 border-left: 3px solid var(--SmartThemeQuoteColor);
363503 padding-left: 10px;
364504 background-color: var(--black30a);
@@ -368,18 +508,24 @@ input[type='checkbox']:focus-visible {
368508.mes_text strong em,
369509.mes_text strong,
370510.mes_text h2,
371511.mes_text h1 {,
512+.mes_reasoning strong em,
513+.mes_reasoning strong,
514+.mes_reasoning h2,
515+.mes_reasoning h1 {
372516 font-weight: bold;
373517}
374518
375519.mes_text pre code {,
520+.mes_reasoning pre code {
376521 position: relative;
377522 display: block;
378523 overflow-x: auto;
379524 padding: 1em;
380525}
381526
382527.mes_text img:not(.mes_img) {,
528+.mes_reasoning img:not(.mes_img) {
383529 max-width: 100%;
384530 max-height: var(--doc-height);
385531}
@@ -1022,8 +1168,8 @@ body .panelControlBar {
10221168 /*only affects bubblechat to make it sit nicely at the bottom*/
10231169}
10241170
1025-.last_mes .mes_text {
1171+.last_mes:has(.mes_text:empty):has(.mes_reasoning_details) .mes_reasoning:not(:empty) {
1026- padding-right: 30px;
1172+ margin-bottom: var(--mes-right-spacing);
10271173}
10281174
10291175/* SWIPE RELATED STYLES*/
@@ -1235,14 +1381,19 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12351381 overflow-y: clip;
12361382}
12371383
12381384.mes_text {,
1385+.mes_reasoning {
12391386 font-weight: 500;
12401387 line-height: calc(var(--mainFontSize) + .5rem);
1388+ max-width: 100%;
1389+ overflow-wrap: anywhere;
1390+}
1391+
1392+.mes_text {
12411393 padding-left: 0;
12421394 padding-top: 5px;
12431395 padding-bottom: 5px;
1244- max-width: 100%;
1396+ padding-right: var(--mes-right-spacing);
1245- overflow-wrap: anywhere;
12461397}
12471398
12481399br {
@@ -2728,9 +2879,8 @@ select option:not(:checked) {
27282879 color: var(--active) !important;
27292880}
27302881
27312882#instruct_enabled_label .menu_button.togglable:not(.toggleEnabled), {
2732-#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
2883+ color: red;
2733- color: Red;
27342884}
27352885
27362886.displayBlock {
@@ -2913,6 +3063,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
29133063.mes_block .ch_name {
29143064 max-width: 100%;
29153065 min-height: 22px;
3066+ align-items: flex-start;
29163067}
29173068
29183069/*applies to both groups and solos chars in the char list*/
@@ -2921,7 +3072,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
29213072 position: relative;
29223073}
29233074
29243075#rm_print_characters_block.character_name_block .ch_name,
29253076.avatar-container .ch_name {
29263077 flex: 1 1 auto;
29273078 white-space: nowrap;
@@ -2931,6 +3082,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
29313082 display: block;
29323083}
29333084
3085+.character_name_block .character_version {
3086+ text-overflow: ellipsis;
3087+ overflow: hidden;
3088+ text-wrap: nowrap;
3089+ max-width: 50%;
3090+}
3091+
29343092#rm_print_characters_block .character_name_block> :last-child {
29353093 flex: 0 100000 auto;
29363094 /* Force shrinking first */
@@ -4130,7 +4288,13 @@ input[type="range"]::-webkit-slider-thumb {
41304288 transition: 0.3s ease-in-out;
41314289}
41324290
41334291.mes_edit_buttons .menu_buttonmes_reasoning_actions {
4292+ margin: 0;
4293+ margin-top: 0.5em;
4294+}
4295+
4296+.mes_edit_buttons .menu_button,
4297+.mes_reasoning_actions .edit_button {
41344298 opacity: 0.5;
41354299 padding: 0px;
41364300 font-size: 1rem;
@@ -4143,10 +4307,18 @@ input[type="range"]::-webkit-slider-thumb {
41434307 align-items: center;
41444308}
41454309
4310+.mes_reasoning_actions .edit_button {
4311+ margin-bottom: 0.5em;
4312+ opacity: 1;
4313+ filter: brightness(0.7);
4314+}
4315+
4316+.mes_reasoning_edit_cancel,
41464317.mes_edit_cancel.menu_button {
41474318 background-color: var(--crimson70a);
41484319}
41494320
4321+.mes_reasoning_edit_done,
41504322.mes_edit_done.menu_button {
41514323 background-color: var(--okGreen70a);
41524324}
@@ -4155,6 +4327,7 @@ input[type="range"]::-webkit-slider-thumb {
41554327 opacity: 1;
41564328}
41574329
4330+.reasoning_edit_textarea,
41584331.edit_textarea {
41594332 padding: 5px;
41604333 margin: 0;
@@ -4166,6 +4339,14 @@ input[type="range"]::-webkit-slider-thumb {
41664339 field-sizing: content;
41674340}
41684341
4342+body[data-generating="true"] #send_but,
4343+body[data-generating="true"] #mes_continue,
4344+body[data-generating="true"] #mes_impersonate,
4345+body[data-generating="true"] #chat .last_mes .mes_buttons,
4346+body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
4347+ display: none;
4348+}
4349+
41694350#anchor_order {
41704351 margin-bottom: 15px;
41714352}
@@ -4505,23 +4686,6 @@ body .ui-widget-content li:hover {
45054686 opacity: 1;
45064687}
45074688
4508-.typing_indicator {
4509- position: sticky;
4510- bottom: 10px;
4511- margin: 10px;
4512- opacity: 0.85;
4513- text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
4514- order: 9999;
4515-}
4516-
4517-.typing_indicator:after {
4518- display: inline-block;
4519- vertical-align: bottom;
4520- animation: ellipsis steps(4, end) 1500ms infinite;
4521- content: "";
4522- width: 0px;
4523-}
4524-
45254689#group_avatar_preview .missing-avatar {
45264690 display: inline;
45274691 vertical-align: middle;
@@ -5610,11 +5774,13 @@ body:not(.movingUI) .drawer-content.maximized {
56105774 overflow-wrap: anywhere;
56115775}
56125776
5777+#SystemPromptColumn summary,
56135778#InstructSequencesColumn summary {
56145779 font-size: 0.95em;
56155780 cursor: pointer;
56165781}
56175782
5783+#SystemPromptColumn details,
56185784#InstructSequencesColumn details:not(:last-of-type) {
56195785 margin-bottom: 5px;
56205786}
@@ -5643,6 +5809,7 @@ body:not(.movingUI) .drawer-content.maximized {
56435809
56445810.model-card .details-container {
56455811 text-align: right;
5812+ line-height: 0.9;
56465813}
56475814
56485815.model-card:hover {
@@ -5665,7 +5832,7 @@ body:not(.movingUI) .drawer-content.maximized {
56655832}
56665833
56675834.model-title {
5668- font-size: 13px;
5835+ font-size: calc(var(--mainFontSize) * 0.95);
56695836 font-weight: bold;
56705837 overflow: hidden;
56715838}
@@ -5681,7 +5848,7 @@ body:not(.movingUI) .drawer-content.maximized {
56815848.model-class,
56825849.model-context-length,
56835850.model-date-added {
5684- font-size: 10px;
5851+ font-size: calc(var(--mainFontSize) * 0.75);
56855852}
56865853
56875854.model-class,
@@ -5763,3 +5930,29 @@ body:not(.movingUI) .drawer-content.maximized {
57635930.alternate_greetings_list {
57645931 overflow-y: scroll;
57655932}
5933+
5934+.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) {
5935+ display: flex;
5936+ flex-direction: row;
5937+ flex-wrap: nowrap;
5938+ justify-content: space-between;
5939+ align-items: center;
5940+ gap: 10px;
5941+ padding: 0 2px;
5942+}
5943+
5944+.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
5945+ flex: 1;
5946+}
5947+
5948+.oneline-dropdown label {
5949+ margin-top: 3px;
5950+ margin-bottom: 5px;
5951+ flex-grow: 1;
5952+ text-align: left;
5953+}
5954+
5955+.oneline-dropdown select {
5956+ min-width: fit-content;
5957+ width: 40%;
5958+}
server.js+269 -60
@@ -4,6 +4,7 @@
44import fs from 'node:fs';
55import http from 'node:http';
66import https from 'node:https';
7+import os from 'os';
78import path from 'node:path';
89import util from 'node:util';
910import net from 'node:net';
@@ -18,10 +19,9 @@ import { hideBin } from 'yargs/helpers';
1819
1920// express/server related library imports
2021import cors from 'cors';
2122import { doubleCsrfcsrfSync } from 'csrf-csrfsync';
2223import express from 'express';
2324import compression from 'compression';
24-import cookieParser from 'cookie-parser';
2525import cookieSession from 'cookie-session';
2626import multer from 'multer';
2727import responseTime from 'response-time';
@@ -30,6 +30,7 @@ import bodyParser from 'body-parser';
3030
3131// net related library imports
3232import fetch from 'node-fetch';
33+import ipRegex from 'ip-regex';
3334
3435// Unrestrict console logs display limit
3536util.inspect.defaultOptions.maxArrayLength = null;
@@ -40,7 +41,6 @@ util.inspect.defaultOptions.depth = 4;
4041import { loadPlugins } from './src/plugin-loader.js';
4142import {
4243 initUserStorage,
43- getCsrfSecret,
4444 getCookieSecret,
4545 getCookieSessionName,
4646 getAllEnabledUsers,
@@ -60,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js';
6060import whitelistMiddleware from './src/middleware/whitelist.js';
6161import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
6262import initRequestProxy from './src/request-proxy.js';
63+import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
6364import {
6465 getVersion,
6566 getConfigValue,
@@ -67,6 +68,11 @@ import {
6768 forwardFetchResponse,
6869 removeColorFormatting,
6970 getSeparator,
71+ stringToBool,
72+ urlHostnameToIPv6,
73+ canResolve,
74+ safeReadFileSync,
75+ setupLogLevel,
7076} from './src/util.js';
7177import { UPLOADS_DIRECTORY } from './src/constants.js';
7278import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -126,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
126132const DEFAULT_PORT = 8000;
127133const DEFAULT_AUTORUN = false;
128134const DEFAULT_LISTEN = false;
135+const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
136+const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
129137const DEFAULT_CORS_PROXY = false;
130138const DEFAULT_WHITELIST = true;
131139const DEFAULT_ACCOUNTS = false;
@@ -150,11 +158,11 @@ const DEFAULT_PROXY_BYPASS = [];
150158const cliArguments = yargs(hideBin(process.argv))
151159 .usage('Usage: <your-start-script> <command> [options]')
152160 .option('enableIPv6', {
153161 type: 'booleanstring',
154162 default: null,
155163 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
156164 }).option('enableIPv4', {
157165 type: 'booleanstring',
158166 default: null,
159167 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
160168 }).option('port', {
@@ -181,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
181189 type: 'boolean',
182190 default: null,
183191 describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
192+ }).option('listenAddressIPv6', {
193+ type: 'string',
194+ default: null,
195+ describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
196+ }).option('listenAddressIPv4', {
197+ type: 'string',
198+ default: null,
199+ describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]',
184200 }).option('corsProxy', {
185201 type: 'boolean',
186202 default: null,
@@ -243,27 +259,46 @@ app.use(helmet({
243259app.use(compression());
244260app.use(responseTime());
245261
262+
263+/** @type {number} */
246264const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
265+/** @type {boolean} */
247266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
267+/** @type {boolean} */
248268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
269+/** @type {string} */
270+const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271+/** @type {string} */
272+const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
273+/** @type {boolean} */
249274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
250275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
276+/** @type {string} */
251277const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278+/** @type {boolean} */
252279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
253280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
254281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
282+/** @type {boolean} */
255283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
256284
257285const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
258286
259-const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
260-const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
261287
288+/** @type {boolean | "auto"} */
289+let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
290+/** @type {boolean | "auto"} */
291+let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
292+
293+/** @type {string} */
262294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295+/** @type {number} */
263296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
264297
298+/** @type {boolean} */
265299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
266300
301+/** @type {boolean} */
267302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
268303
269304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -280,7 +315,19 @@ if (dnsPreferIPv6) {
280315 console.log('Preferring IPv4 for DNS resolution');
281316}
282317
283-if (!enableIPv6 && !enableIPv4) {
318+
319+const ipOptions = [true, 'auto', false];
320+
321+if (!ipOptions.includes(enableIPv6)) {
322+ console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6);
323+ enableIPv6 = DEFAULT_ENABLE_IPV6;
324+}
325+if (!ipOptions.includes(enableIPv4)) {
326+ console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4);
327+ enableIPv4 = DEFAULT_ENABLE_IPV4;
328+}
329+
330+if (enableIPv6 === false && enableIPv4 === false) {
284331 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
285332 process.exit(1);
286333}
@@ -347,8 +394,8 @@ if (enableCorsProxy) {
347394}
348395
349396function getSessionCookieAge() {
350397 // Defaults to 24 hours in"no secondsexpiration" if not set
351398 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60-1);
352399
353400 // Convert to milliseconds
354401 if (configValue > 0) {
@@ -365,6 +412,55 @@ function getSessionCookieAge() {
365412 return undefined;
366413}
367414
415+/**
416+ * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses.
417+ *
418+ * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing:
419+ * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`).
420+ * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`).
421+ * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`).
422+ * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`).
423+ */
424+async function getHasIP() {
425+ let hasIPv6 = false;
426+ let hasIPv6Local = false;
427+
428+ let hasIPv4 = false;
429+ let hasIPv4Local = false;
430+
431+ const interfaces = os.networkInterfaces();
432+
433+ for (const iface of Object.values(interfaces)) {
434+ if (iface === undefined) {
435+ continue;
436+ }
437+
438+ for (const info of iface) {
439+ if (info.family === 'IPv6') {
440+ hasIPv6 = true;
441+ if (info.address === '::1') {
442+ hasIPv6Local = true;
443+ }
444+ }
445+
446+ if (info.family === 'IPv4') {
447+ hasIPv4 = true;
448+ if (info.address === '127.0.0.1') {
449+ hasIPv4Local = true;
450+ }
451+ }
452+ if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
453+ }
454+ if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
455+ }
456+ return [
457+ hasIPv6,
458+ hasIPv4,
459+ hasIPv6Local,
460+ hasIPv4Local,
461+ ];
462+}
463+
368464app.use(cookieSession({
369465 name: getCookieSessionName(),
370466 sameSite: 'strict',
@@ -377,27 +473,38 @@ app.use(setUserDataMiddleware);
377473
378474// CSRF Protection //
379475if (!disableCsrf) {
380476 const COOKIES_SECRETcsrfSyncProtection = getCookieSecretcsrfSync();{
381-
477+ getTokenFromState: (req) => {
382- const { generateToken, doubleCsrfProtection } = doubleCsrf({
478+ if (!req.session) {
383- getSecret: getCsrfSecret,
479+ console.error('(CSRF error) getTokenFromState: Session object not initialized');
384- cookieName: 'X-CSRF-Token',
480+ return;
385- cookieOptions: {
481+ }
386- sameSite: 'strict',
482+ return req.session.csrfToken;
387- secure: false,
483+ },
484+ getTokenFromRequest: (req) => {
485+ return req.headers['x-csrf-token']?.toString();
486+ },
487+ storeTokenInState: (req, token) => {
488+ if (!req.session) {
489+ console.error('(CSRF error) storeTokenInState: Session object not initialized');
490+ return;
491+ }
492+ req.session.csrfToken = token;
388493 },
389494 size: 6432,
390- getTokenFromRequest: (req) => req.headers['x-csrf-token'],
391495 });
392496
393497 app.get('/csrf-token', (req, res) => {
394498 res.json({
395499 'token': csrfSyncProtection.generateToken(res, req),
396500 });
397501 });
398502
399- app.use(cookieParser(COOKIES_SECRET));
503+ // Customize the error message
400- app.use(doubleCsrfProtection);
504+ csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
505+ csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
506+
507+ app.use(csrfSyncProtection.csrfSynchronisedProtection);
401508} else {
402509 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
403510 app.get('/csrf-token', (req, res) => {
@@ -409,7 +516,7 @@ if (!disableCsrf) {
409516
410517// Static files
411518// Host index page
412519app.get('/', getCacheBusterMiddleware(), (request, response) => {
413520 if (shouldRedirectToLogin(request)) {
414521 const query = request.url.split('?')[1];
415522 const redirectUrl = query ? `/login?${query}` : '/login';
@@ -617,13 +724,13 @@ app.use('/api/azure', azureRouter);
617724
618725const tavernUrlV6 = new URL(
619726 (cliArguments.ssl ? 'https://' : 'http://') +
620727 (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
621728 (':' + server_port),
622729);
623730
624731const tavernUrl = new URL(
625732 (cliArguments.ssl ? 'https://' : 'http://') +
626733 (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
627734 (':' + server_port),
628735);
629736
@@ -683,20 +790,23 @@ const preSetupTasks = async function () {
683790
684791/**
685792 * Gets the hostname to use for autorun in the browser.
686793 * @returnsparam {stringboolean} The hostnameuseIPv6 toIf use for autorunIPv6
794+ * @param {boolean} useIPv4 If use IPv4
795+ * @returns Promise<string> The hostname to use for autorun
687796 */
688797async function getAutorunHostname(useIPv6, useIPv4) {
689798 if (autorunHostname === 'auto') {
690- if (enableIPv6 && enableIPv4) {
799+ let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
691- if (avoidLocalhost) return '[::1]';
800+
692- return 'localhost';
801+ if (useIPv6 && useIPv4) {
802+ return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
693803 }
694804
695805 if (enableIPv6useIPv6) {
696806 return '[::1]';
697807 }
698808
699809 if (enableIPv4useIPv4) {
700810 return '127.0.0.1';
701811 }
702812 }
@@ -708,11 +818,13 @@ function getAutorunHostname() {
708818 * Tasks that need to be run after the server starts listening.
709819 * @param {boolean} v6Failed If the server failed to start on IPv6
710820 * @param {boolean} v4Failed If the server failed to start on IPv4
821+ * @param {boolean} useIPv6 If the server is using IPv6
822+ * @param {boolean} useIPv4 If the server is using IPv4
711823 */
712824const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
713825 const autorunUrl = new URL(
714826 (cliArguments.ssl ? 'https://' : 'http://') +
715827 (await getAutorunHostname(useIPv6, useIPv4)) +
716828 (':') +
717829 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
718830 );
@@ -725,36 +837,48 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
725837
726838 let logListen = 'SillyTavern is listening on';
727839
728840 if (enableIPv6useIPv6 && !v6Failed) {
729841 logListen += color.green(' IPv6: ' + tavernUrlV6.host);
842+ ' IPv6: ' + tavernUrlV6.host,
843+ );
730844 }
731845
732846 if (enableIPv4useIPv4 && !v4Failed) {
733847 logListen += color.green(' IPv4: ' + tavernUrl.host);
848+ ' IPv4: ' + tavernUrl.host,
849+ );
734850 }
735851
736852 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
737853 const plainGoToLog = removeColorFormatting(goToLog);
738854
739855 console.log(logListen);
856+ if (listen) {
857+ console.log();
858+ console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
859+ console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.');
860+ }
740861 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
741862 console.log(goToLog);
742863 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
743864
744- if (listen) {
745- console.log('[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n');
746- }
747865
748866 if (basicAuthMode) {
749867 if (perUserBasicAuth && !enableAccounts) {
750- console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));
868+ console.error(color.red(
869+ 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
870+ ));
751871 } else if (!perUserBasicAuth) {
752872 const basicAuthUser = getConfigValue('basicAuthUser', {});
753873 if (!basicAuthUser?.username || !basicAuthUser?.password) {
754- console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
874+ console.warn(color.yellow(
875+ 'Basic Authentication is enabled, but username or password is not set or empty!',
876+ ));
755877 }
756878 }
757879 }
880+
881+ setupLogLevel();
758882};
759883
760884/**
@@ -804,14 +928,16 @@ function logSecurityAlert(message) {
804928 * Handles the case where the server failed to start on one or both protocols.
805929 * @param {boolean} v6Failed If the server failed to start on IPv6
806930 * @param {boolean} v4Failed If the server failed to start on IPv4
931+ * @param {boolean} useIPv6 If use IPv6
932+ * @param {boolean} useIPv4 If use IPv4
807933 */
808934function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
809935 if (v6Failed && !enableIPv4useIPv4) {
810936 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
811937 process.exit(1);
812938 }
813939
814940 if (v4Failed && !enableIPv6useIPv6) {
815941 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
816942 process.exit(1);
817943 }
@@ -825,10 +951,11 @@ function handleServerListenFail(v6Failed, v4Failed) {
825951/**
826952 * Creates an HTTPS server.
827953 * @param {URL} url The URL to listen on
954+ * @param {number} ipVersion the ip version to use
828955 * @returns {Promise<void>} A promise that resolves when the server is listening
829956 * @throws {Error} If the server fails to start
830957 */
831958function createHttpsServer(url, ipVersion) {
832959 return new Promise((resolve, reject) => {
833960 const server = https.createServer(
834961 {
@@ -837,34 +964,56 @@ function createHttpsServer(url) {
837964 }, app);
838965 server.on('error', reject);
839966 server.on('listening', resolve);
840- server.listen(Number(url.port || 443), url.hostname);
967+
968+ let host = url.hostname;
969+ if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
970+ server.listen({
971+ host: host,
972+ port: Number(url.port || 443),
973+ // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
974+ ipv6Only: true,
975+ });
841976 });
842977}
843978
844979/**
845980 * Creates an HTTP server.
846981 * @param {URL} url The URL to listen on
982+ * @param {number} ipVersion the ip version to use
847983 * @returns {Promise<void>} A promise that resolves when the server is listening
848984 * @throws {Error} If the server fails to start
849985 */
850986function createHttpServer(url, ipVersion) {
851987 return new Promise((resolve, reject) => {
852988 const server = http.createServer(app);
853989 server.on('error', reject);
854990 server.on('listening', resolve);
855- server.listen(Number(url.port || 80), url.hostname);
991+
992+ let host = url.hostname;
993+ if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
994+ server.listen({
995+ host: host,
996+ port: Number(url.port || 80),
997+ // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
998+ ipv6Only: true,
999+ });
8561000 });
8571001}
8581002
859-async function startHTTPorHTTPS() {
1003+/**
1004+ * Starts the server using http or https depending on config
1005+ * @param {boolean} useIPv6 If use IPv6
1006+ * @param {boolean} useIPv4 If use IPv4
1007+ */
1008+async function startHTTPorHTTPS(useIPv6, useIPv4) {
8601009 let v6Failed = false;
8611010 let v4Failed = false;
8621011
8631012 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
8641013
8651014 if (enableIPv6useIPv6) {
8661015 try {
8671016 await createFunc(tavernUrlV6, 6);
8681017 } catch (error) {
8691018 console.error('non-fatal error: failed to start server on IPv6');
8701019 console.error(error);
@@ -873,9 +1022,9 @@ async function startHTTPorHTTPS() {
8731022 }
8741023 }
8751024
8761025 if (enableIPv4useIPv4) {
8771026 try {
8781027 await createFunc(tavernUrl, 4);
8791028 } catch (error) {
8801029 console.error('non-fatal error: failed to start server on IPv4');
8811030 console.error(error);
@@ -888,10 +1037,59 @@ async function startHTTPorHTTPS() {
8881037}
8891038
8901039async function startServer() {
891- const [v6Failed, v4Failed] = await startHTTPorHTTPS();
1040+ let useIPv6 = (enableIPv6 === true);
1041+ let useIPv4 = (enableIPv4 === true);
1042+
1043+ let hasIPv6 = false,
1044+ hasIPv4 = false,
1045+ hasIPv6Local = false,
1046+ hasIPv4Local = false,
1047+ hasIPv6Any = false,
1048+ hasIPv4Any = false;
1049+
1050+ if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
1051+ [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
1052+
1053+ hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
1054+ if (enableIPv6 === 'auto') {
1055+ useIPv6 = hasIPv6;
1056+ }
1057+ if (hasIPv6) {
1058+ if (useIPv6) {
1059+ console.log(color.green('IPv6 support detected'));
1060+ } else {
1061+ console.log('IPv6 support detected (but disabled)');
1062+ }
1063+ }
1064+
1065+ hasIPv4 = listen ? hasIPv4Any : hasIPv4Local;
1066+ if (enableIPv4 === 'auto') {
1067+ useIPv4 = hasIPv4;
1068+ }
1069+ if (hasIPv4) {
1070+ if (useIPv4) {
1071+ console.log(color.green('IPv4 support detected'));
1072+ } else {
1073+ console.log('IPv4 support detected (but disabled)');
1074+ }
1075+ }
1076+
1077+ if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
1078+ if (!hasIPv6 && !hasIPv4) {
1079+ console.error('Both IPv6 and IPv4 are not detected');
1080+ process.exit(1);
1081+ }
1082+ }
1083+ }
8921084
893- handleServerListenFail(v6Failed, v4Failed);
1085+ if (!useIPv6 && !useIPv4) {
894- postSetupTasks(v6Failed, v4Failed);
1086+ console.error('Both IPv6 and IPv4 are disabled,\nP.S. you should never see this error, at least at one point it was checked for before this, with the rest of the config options');
1087+ process.exit(1);
1088+ }
1089+
1090+ const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
1091+ handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
1092+ postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
8951093}
8961094
8971095async function verifySecuritySettings() {
@@ -901,7 +1099,7 @@ async function verifySecuritySettings() {
9011099 }
9021100
9031101 if (!enableAccounts) {
9041102 logSecurityAlert('Your current SillyTavern isconfiguration currentlyis insecurelyinsecure open(listening to the publicnon-localhost). Enable whitelisting, basic authentication or user accounts.');
9051103 }
9061104
9071105 const users = await getAllEnabledUsers();
@@ -921,6 +1119,16 @@ async function verifySecuritySettings() {
9211119 }
9221120}
9231121
1122+/**
1123+ * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
1124+ */
1125+function apply404Middleware() {
1126+ const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
1127+ app.use((req, res) => {
1128+ res.status(404).send(notFoundWebpage);
1129+ });
1130+}
1131+
9241132// User storage module needs to be initialized before starting the server
9251133initUserStorage(dataRoot)
9261134 .then(ensurePublicDirectoriesExist)
@@ -928,4 +1136,5 @@ initUserStorage(dataRoot)
9281136 .then(migrateSystemPrompts)
9291137 .then(verifySecuritySettings)
9301138 .then(preSetupTasks)
1139+ .then(apply404Middleware)
9311140 .finally(startServer);
src/constants.js+13 -4
@@ -139,19 +139,19 @@ export const UNSAFE_EXTENSIONS = [
139139export const GEMINI_SAFETY = [
140140 {
141141 category: 'HARM_CATEGORY_HARASSMENT',
142142 threshold: 'BLOCK_NONEOFF',
143143 },
144144 {
145145 category: 'HARM_CATEGORY_HATE_SPEECH',
146146 threshold: 'BLOCK_NONEOFF',
147147 },
148148 {
149149 category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
150150 threshold: 'BLOCK_NONEOFF',
151151 },
152152 {
153153 category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
154154 threshold: 'BLOCK_NONEOFF',
155155 },
156156 {
157157 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
@@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [
304304export const OLLAMA_KEYS = [
305305 'num_predict',
306306 'num_ctx',
307+ 'num_batch',
307308 'stop',
308309 'temperature',
309310 'repeat_penalty',
@@ -369,6 +370,7 @@ export const OPENROUTER_KEYS = [
369370 'prompt',
370371 'stop',
371372 'provider',
373+ 'include_reasoning',
372374];
373375
374376// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
@@ -413,3 +415,10 @@ export const VLLM_KEYS = [
413415 'guided_decoding_backend',
414416 'guided_whitespace_pattern',
415417];
418+
419+export const LOG_LEVELS = {
420+ DEBUG: 0,
421+ INFO: 1,
422+ WARN: 2,
423+ ERROR: 3,
424+};
src/endpoints/anthropic.js+3 -3
@@ -32,7 +32,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
3232 max_tokens: 4096,
3333 };
3434
3535 console.logdebug('Multimodal captioning request', body);
3636
3737 const result = await fetch(url, {
3838 body: JSON.stringify(body),
@@ -46,14 +46,14 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4646
4747 if (!result.ok) {
4848 const text = await result.text();
4949 console.logwarn(`Claude API returned error: ${result.status} ${result.statusText}`, text);
5050 return response.status(result.status).send({ error: true });
5151 }
5252
5353 /** @type {any} */
5454 const generateResponseJson = await result.json();
5555 const caption = generateResponseJson.content[0].text;
5656 console.logdebug('Claude response:', generateResponseJson);
5757
5858 if (!caption) {
5959 return response.status(500).send('No caption found');
src/endpoints/assets.js+13 -12
@@ -176,7 +176,7 @@ router.post('/get', jsonParser, async (request, response) => {
176176 }
177177 }
178178 catch (err) {
179179 console.logerror(err);
180180 }
181181 return response.send(output);
182182});
@@ -200,7 +200,7 @@ router.post('/download', jsonParser, async (request, response) => {
200200 category = i;
201201
202202 if (category === null) {
203203 console.debugerror('Bad request: unsupported asset category.');
204204 return response.sendStatus(400);
205205 }
206206
@@ -212,7 +212,7 @@ router.post('/download', jsonParser, async (request, response) => {
212212
213213 const temp_path = path.join(request.user.directories.assets, 'temp', request.body.filename);
214214 const file_path = path.join(request.user.directories.assets, category, request.body.filename);
215215 console.debuginfo('Request received to download', url, 'to', file_path);
216216
217217 try {
218218 // Download to temp
@@ -241,13 +241,13 @@ router.post('/download', jsonParser, async (request, response) => {
241241 }
242242
243243 // Move into asset place
244244 console.debuginfo('Download finished, moving file from', temp_path, 'to', file_path);
245245 fs.copyFileSync(temp_path, file_path);
246246 fs.rmSync(temp_path);
247247 response.sendStatus(200);
248248 }
249249 catch (error) {
250250 console.logerror(error);
251251 response.sendStatus(500);
252252 }
253253});
@@ -270,7 +270,7 @@ router.post('/delete', jsonParser, async (request, response) => {
270270 category = i;
271271
272272 if (category === null) {
273273 console.debugerror('Bad request: unsupported asset category.');
274274 return response.sendStatus(400);
275275 }
276276
@@ -280,7 +280,7 @@ router.post('/delete', jsonParser, async (request, response) => {
280280 return response.status(400).send(validation.message);
281281
282282 const file_path = path.join(request.user.directories.assets, category, request.body.filename);
283283 console.debuginfo('Request received to delete', category, file_path);
284284
285285 try {
286286 // Delete if previous download failed
@@ -288,17 +288,17 @@ router.post('/delete', jsonParser, async (request, response) => {
288288 fs.unlink(file_path, (err) => {
289289 if (err) throw err;
290290 });
291291 console.debuginfo('Asset deleted.');
292292 }
293293 else {
294294 console.debugerror('Asset not found.');
295295 response.sendStatus(400);
296296 }
297297 // Move into asset place
298298 response.sendStatus(200);
299299 }
300300 catch (error) {
301301 console.logerror(error);
302302 response.sendStatus(500);
303303 }
304304});
@@ -314,6 +314,7 @@ router.post('/delete', jsonParser, async (request, response) => {
314314 */
315315router.post('/character', jsonParser, async (request, response) => {
316316 if (request.query.name === undefined) return response.sendStatus(400);
317+
317318 // For backwards compatibility, don't reject invalid character names, just sanitize them
318319 const name = sanitize(request.query.name.toString());
319320 const inputCategory = request.query.category;
@@ -325,7 +326,7 @@ router.post('/character', jsonParser, async (request, response) => {
325326 category = i;
326327
327328 if (category === null) {
328329 console.debugerror('Bad request: unsupported asset category.');
329330 return response.sendStatus(400);
330331 }
331332
@@ -364,7 +365,7 @@ router.post('/character', jsonParser, async (request, response) => {
364365 return response.send(output);
365366 }
366367 catch (err) {
367368 console.logerror(err);
368369 return response.sendStatus(500);
369370 }
370371});
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/azure.js+6 -6
@@ -11,14 +11,14 @@ router.post('/list', jsonParser, async (req, res) => {
1111 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
1212
1313 if (!key) {
1414 console.errorwarn('Azure TTS API Key not set');
1515 return res.sendStatus(403);
1616 }
1717
1818 const region = req.body.region;
1919
2020 if (!region) {
2121 console.errorwarn('Azure TTS region not set');
2222 return res.sendStatus(400);
2323 }
2424
@@ -32,7 +32,7 @@ router.post('/list', jsonParser, async (req, res) => {
3232 });
3333
3434 if (!response.ok) {
3535 console.errorwarn('Azure Request failed', response.status, response.statusText);
3636 return res.sendStatus(500);
3737 }
3838
@@ -49,13 +49,13 @@ router.post('/generate', jsonParser, async (req, res) => {
4949 const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
5050
5151 if (!key) {
5252 console.errorwarn('Azure TTS API Key not set');
5353 return res.sendStatus(403);
5454 }
5555
5656 const { text, voice, region } = req.body;
5757 if (!text || !voice || !region) {
5858 console.errorwarn('Missing required parameters');
5959 return res.sendStatus(400);
6060 }
6161
@@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async (req, res) => {
7575 });
7676
7777 if (!response.ok) {
7878 console.errorwarn('Azure Request failed', response.status, response.statusText);
7979 return res.sendStatus(500);
8080 }
8181
src/endpoints/backends/chat-completions.js+180 -77
@@ -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 }
@@ -109,7 +114,7 @@ async function sendClaudeRequest(request, response) {
109114 }
110115
111116 if (!apiKey) {
112117 console.logwarn(color.red(`Claude API key is missing.\n${divider}`));
113118 return response.status(400).send({ error: true });
114119 }
115120
@@ -174,7 +179,7 @@ async function sendClaudeRequest(request, response) {
174179 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
175180 }
176181
177182 console.logdebug('Claude request:', requestBody);
178183
179184 const generateResponse = await fetch(apiUrl + '/messages', {
180185 method: 'POST',
@@ -194,21 +199,21 @@ async function sendClaudeRequest(request, response) {
194199 } else {
195200 if (!generateResponse.ok) {
196201 const generateResponseText = await generateResponse.text();
197202 console.logwarn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
198203 return response.status(generateResponse.status).send({ error: true });
199204 }
200205
201206 /** @type {any} */
202207 const generateResponseJson = await generateResponse.json();
203208 const responseText = generateResponseJson?.content?.[0]?.text || '';
204209 console.logdebug('Claude response:', generateResponseJson);
205210
206211 // Wrap it back to OAI format + save the original content
207212 const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };
208213 return response.send(reply);
209214 }
210215 } catch (error) {
211216 console.logerror(color.red(`Error communicating with Claude: ${error}\n${divider}`));
212217 if (!response.headersSent) {
213218 return response.status(500).send({ error: true });
214219 }
@@ -225,12 +230,12 @@ async function sendScaleRequest(request, response) {
225230 const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);
226231
227232 if (!apiKey) {
228233 console.logwarn('Scale API key is missing.');
229234 return response.status(400).send({ error: true });
230235 }
231236
232237 const requestPrompt = convertTextCompletionPrompt(request.body.messages);
233238 console.logdebug('Scale request:', requestPrompt);
234239
235240 try {
236241 const controller = new AbortController();
@@ -249,18 +254,18 @@ async function sendScaleRequest(request, response) {
249254 });
250255
251256 if (!generateResponse.ok) {
252257 console.logwarn(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
253258 return response.status(500).send({ error: true });
254259 }
255260
256261 /** @type {any} */
257262 const generateResponseJson = await generateResponse.json();
258263 console.logdebug('Scale response:', generateResponseJson);
259264
260265 const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };
261266 return response.send(reply);
262267 } catch (error) {
263268 console.logerror(error);
264269 if (!response.headersSent) {
265270 return response.status(500).send({ error: true });
266271 }
@@ -277,13 +282,13 @@ async function sendMakerSuiteRequest(request, response) {
277282 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
278283
279284 if (!request.body.reverse_proxy && !apiKey) {
280285 console.logwarn('Google AI Studio API key is missing.');
281286 return response.status(400).send({ error: true });
282287 }
283288
284289 const model = String(request.body.model);
285290 const stream = Boolean(request.body.stream);
286291 const showThoughtsisThinking = Boolean(request.bodymodel.show_thoughtsincludes('thinking');
287292
288293 const generationConfig = {
289294 stopSequences: request.body.stop,
@@ -300,8 +305,9 @@ async function sendMakerSuiteRequest(request, response) {
300305 }
301306
302307 const should_use_system_prompt = (
308+ model.includes('gemini-2.0-pro') ||
309+ model.includes('gemini-2.0-flash') ||
303310 model.includes('gemini-2.0-flash-thinking-exp') ||
304- model.includes('gemini-2.0-flash-exp') ||
305311 model.includes('gemini-1.5-flash') ||
306312 model.includes('gemini-1.5-pro') ||
307313 model.startsWith('gemini-exp')
@@ -310,9 +316,15 @@ async function sendMakerSuiteRequest(request, response) {
310316 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
311317 let safetySettings = GEMINI_SAFETY;
312318
313- if (model.includes('gemini-2.0-flash-exp')) {
319+ // These old models do not support setting the threshold to OFF at all.
320+ if (['gemini-1.5-pro-001', 'gemini-1.5-flash-001', 'gemini-1.5-flash-8b-exp-0827', 'gemini-1.5-flash-8b-exp-0924', 'gemini-pro', 'gemini-1.0-pro', 'gemini-1.0-pro-001'].includes(model)) {
321+ safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));
322+ }
323+ // Interestingly, Gemini 2.0 Flash does support setting the threshold for HARM_CATEGORY_CIVIC_INTEGRITY to OFF.
324+ else if (['gemini-2.0-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash-exp'].includes(model)) {
314325 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));
315326 }
327+ // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
316328
317329 let body = {
318330 contents: prompt.contents,
@@ -328,7 +340,7 @@ async function sendMakerSuiteRequest(request, response) {
328340 }
329341
330342 const body = getGeminiBody();
331343 console.logdebug('Google AI Studio request:', body);
332344
333345 try {
334346 const controller = new AbortController();
@@ -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
@@ -355,14 +366,14 @@ async function sendMakerSuiteRequest(request, response) {
355366 // Pipe remote SSE stream to Express response
356367 forwardFetchResponse(generateResponse, response);
357368 } catch (error) {
358369 console.logerror('Error forwarding streaming response:', error);
359370 if (!response.headersSent) {
360371 return response.status(500).send({ error: true });
361372 }
362373 }
363374 } else {
364375 if (!generateResponse.ok) {
365376 console.logwarn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
366377 return response.status(generateResponse.status).send({ error: true });
367378 }
368379
@@ -372,7 +383,7 @@ async function sendMakerSuiteRequest(request, response) {
372383 const candidates = generateResponseJson?.candidates;
373384 if (!candidates || candidates.length === 0) {
374385 let message = 'Google AI Studio API returned no candidate';
375386 console.logwarn(message, generateResponseJson);
376387 if (generateResponseJson?.promptFeedback?.blockReason) {
377388 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
378389 }
@@ -380,25 +391,21 @@ async function sendMakerSuiteRequest(request, response) {
380391 }
381392
382393 const responseContent = candidates[0].content ?? candidates[0].output;
383394 console.logwarn('Google AI Studio response:', responseContent);
384-
385- if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {
386- responseContent.parts = responseContent.parts.filter(part => !part.thought);
387- }
388395
389396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
390397 if (!responseText) {
391398 let message = 'Google AI Studio Candidate text empty';
392399 console.logwarn(message, generateResponseJson);
393400 return response.send({ error: { message } });
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) {
401408 console.logerror('Error communicating with Google AI Studio API: ', error);
402409 if (!response.headersSent) {
403410 return response.status(500).send({ error: true });
404411 }
@@ -412,8 +419,9 @@ async function sendMakerSuiteRequest(request, response) {
412419 */
413420async function sendAI21Request(request, response) {
414421 if (!request.body) return response.sendStatus(400);
422+
415423 const controller = new AbortController();
416424 console.logdebug(request.body.messages);
417425 request.socket.removeAllListeners('close');
418426 request.socket.on('close', function () {
419427 controller.abort();
@@ -439,7 +447,7 @@ async function sendAI21Request(request, response) {
439447 signal: controller.signal,
440448 };
441449
442450 console.logdebug('AI21 request:', body);
443451
444452 try {
445453 const generateResponse = await fetch(API_AI21 + '/chat/completions', options);
@@ -448,16 +456,16 @@ async function sendAI21Request(request, response) {
448456 } else {
449457 if (!generateResponse.ok) {
450458 const errorText = await generateResponse.text();
451459 console.logwarn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
452460 const errorJson = tryParse(errorText) ?? { error: true };
453461 return response.status(500).send(errorJson);
454462 }
455463 const generateResponseJson = await generateResponse.json();
456464 console.logdebug('AI21 response:', generateResponseJson);
457465 return response.send(generateResponseJson);
458466 }
459467 } catch (error) {
460468 console.logerror('Error communicating with AI21 API: ', error);
461469 if (!response.headersSent) {
462470 response.send({ error: true });
463471 } else {
@@ -476,7 +484,7 @@ async function sendMistralAIRequest(request, response) {
476484 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
477485
478486 if (!apiKey) {
479487 console.logwarn('MistralAI API key is missing.');
480488 return response.status(400).send({ error: true });
481489 }
482490
@@ -517,7 +525,7 @@ async function sendMistralAIRequest(request, response) {
517525 timeout: 0,
518526 };
519527
520528 console.logdebug('MisralAI request:', requestBody);
521529
522530 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
523531 if (request.body.stream) {
@@ -525,16 +533,16 @@ async function sendMistralAIRequest(request, response) {
525533 } else {
526534 if (!generateResponse.ok) {
527535 const errorText = await generateResponse.text();
528536 console.logwarn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
529537 const errorJson = tryParse(errorText) ?? { error: true };
530538 return response.status(500).send(errorJson);
531539 }
532540 const generateResponseJson = await generateResponse.json();
533541 console.logdebug('MistralAI response:', generateResponseJson);
534542 return response.send(generateResponseJson);
535543 }
536544 } catch (error) {
537545 console.logerror('Error communicating with MistralAI API: ', error);
538546 if (!response.headersSent) {
539547 response.send({ error: true });
540548 } else {
@@ -557,7 +565,7 @@ async function sendCohereRequest(request, response) {
557565 });
558566
559567 if (!apiKey) {
560568 console.logwarn('Cohere API key is missing.');
561569 return response.status(400).send({ error: true });
562570 }
563571
@@ -596,7 +604,7 @@ async function sendCohereRequest(request, response) {
596604 requestBody.safety_mode = 'OFF';
597605 }
598606
599607 console.logdebug('Cohere request:', requestBody);
600608
601609 const config = {
602610 method: 'POST',
@@ -618,16 +626,16 @@ async function sendCohereRequest(request, response) {
618626 const generateResponse = await fetch(apiUrl, config);
619627 if (!generateResponse.ok) {
620628 const errorText = await generateResponse.text();
621629 console.logwarn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
622630 const errorJson = tryParse(errorText) ?? { error: true };
623631 return response.status(500).send(errorJson);
624632 }
625633 const generateResponseJson = await generateResponse.json();
626634 console.logdebug('Cohere response:', generateResponseJson);
627635 return response.send(generateResponseJson);
628636 }
629637 } catch (error) {
630638 console.logerror('Error communicating with Cohere API: ', error);
631639 if (!response.headersSent) {
632640 response.send({ error: true });
633641 } else {
@@ -636,6 +644,94 @@ async function sendCohereRequest(request, response) {
636644 }
637645}
638646
647+/**
648+ * Sends a request to DeepSeek API.
649+ * @param {express.Request} request Express request
650+ * @param {express.Response} response Express response
651+ */
652+async function sendDeepSeekRequest(request, response) {
653+ const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();
654+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
655+
656+ if (!apiKey && !request.body.reverse_proxy) {
657+ console.warn('DeepSeek API key is missing.');
658+ return response.status(400).send({ error: true });
659+ }
660+
661+ const controller = new AbortController();
662+ request.socket.removeAllListeners('close');
663+ request.socket.on('close', function () {
664+ controller.abort();
665+ });
666+
667+ try {
668+ let bodyParams = {};
669+
670+ if (request.body.logprobs > 0) {
671+ bodyParams['top_logprobs'] = request.body.logprobs;
672+ bodyParams['logprobs'] = true;
673+ }
674+
675+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
676+ bodyParams['tools'] = request.body.tools;
677+ bodyParams['tool_choice'] = request.body.tool_choice;
678+ }
679+
680+ const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
681+ const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
682+
683+ const requestBody = {
684+ 'messages': processedMessages,
685+ 'model': request.body.model,
686+ 'temperature': request.body.temperature,
687+ 'max_tokens': request.body.max_tokens,
688+ 'stream': request.body.stream,
689+ 'presence_penalty': request.body.presence_penalty,
690+ 'frequency_penalty': request.body.frequency_penalty,
691+ 'top_p': request.body.top_p,
692+ 'stop': request.body.stop,
693+ 'seed': request.body.seed,
694+ ...bodyParams,
695+ };
696+
697+ const config = {
698+ method: 'POST',
699+ headers: {
700+ 'Content-Type': 'application/json',
701+ 'Authorization': 'Bearer ' + apiKey,
702+ },
703+ body: JSON.stringify(requestBody),
704+ signal: controller.signal,
705+ };
706+
707+ console.debug('DeepSeek request:', requestBody);
708+
709+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
710+
711+ if (request.body.stream) {
712+ forwardFetchResponse(generateResponse, response);
713+ } else {
714+ if (!generateResponse.ok) {
715+ const errorText = await generateResponse.text();
716+ console.warn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
717+ const errorJson = tryParse(errorText) ?? { error: true };
718+ return response.status(500).send(errorJson);
719+ }
720+ const generateResponseJson = await generateResponse.json();
721+ console.debug('DeepSeek response:', generateResponseJson);
722+ return response.send(generateResponseJson);
723+ }
724+ } catch (error) {
725+ console.error('Error communicating with DeepSeek API: ', error);
726+ if (!response.headersSent) {
727+ response.send({ error: true });
728+ } else {
729+ response.end();
730+ }
731+ }
732+}
733+
734+
639735export const router = express.Router();
640736
641737router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@@ -680,16 +776,16 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
680776 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
681777 headers = {};
682778 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {
683779 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
684780 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
685781 headers = {};
686782 } else {
687783 console.logwarn('This chat completion source is not supported yet.');
688784 return response_getstatus_openai.status(400).send({ error: true });
689785 }
690786
691787 if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
692788 console.logwarn('Chat Completion API key is missing.');
693789 return response_getstatus_openai.status(400).send({ error: true });
694790 }
695791
@@ -724,23 +820,23 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
724820 };
725821 });
726822
727823 console.loginfo('Available OpenRouter models:', models);
728824 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
729825 const models = data?.data;
730826 console.loginfo(models);
731827 } else {
732828 const models = data?.data;
733829
734830 if (Array.isArray(models)) {
735831 const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
736832 console.loginfo('Available models:', modelIds);
737833 } else {
738834 console.logwarn('Chat Completion endpoint did not return a list of models.');
739835 }
740836 }
741837 }
742838 else {
743839 console.logerror('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');
744840 response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });
745841 }
746842 } catch (e) {
@@ -773,10 +869,18 @@ router.post('/bias', jsonParser, async function (request, response) {
773869 const tokenizer = getSentencepiceTokenizer(model);
774870 const instance = await tokenizer?.get();
775871 if (!instance) {
776872 console.warnerror('Tokenizer not initialized:', model);
777873 return response.send({});
778874 }
779875 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
876+ } else if (webTokenizers.includes(model)) {
877+ const tokenizer = getWebTokenizer(model);
878+ const instance = await tokenizer?.get();
879+ if (!instance) {
880+ console.warn('Tokenizer not initialized:', model);
881+ return response.send({});
882+ }
883+ encodeFunction = (text) => new Uint32Array(instance.encode(text));
780884 } else {
781885 const tokenizer = getTiktokenTokenizer(model);
782886 encodeFunction = (tokenizer.encode.bind(tokenizer));
@@ -841,6 +945,7 @@ router.post('/generate', jsonParser, function (request, response) {
841945 case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
842946 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
843947 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
948+ case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
844949 }
845950
846951 let apiUrl;
@@ -899,6 +1004,10 @@ router.post('/generate', jsonParser, function (request, response) {
8991004 bodyParams['route'] = 'fallback';
9001005 }
9011006
1007+ if (request.body.include_reasoning) {
1008+ bodyParams['include_reasoning'] = true;
1009+ }
1010+
9021011 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
9031012 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
9041013 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -922,7 +1031,7 @@ router.post('/generate', jsonParser, function (request, response) {
9221031 mergeObjectWithYaml(headers, request.body.custom_include_headers);
9231032
9241033 if (request.body.custom_prompt_post_processing) {
9251034 console.loginfo('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
9261035 request.body.messages = postProcessPrompt(
9271036 request.body.messages,
9281037 request.body.custom_prompt_post_processing,
@@ -954,25 +1063,20 @@ router.post('/generate', jsonParser, function (request, response) {
9541063 apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
9551064 headers = {};
9561065 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));
9691066 } else {
9701067 console.logwarn('This chat completion source is not supported yet.');
9711068 return response.status(400).send({ error: true });
9721069 }
9731070
1071+ // A few of OpenAIs reasoning models support reasoning effort
1072+ if ([CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
1073+ if (['o1', 'o3-mini', 'o3-mini-2025-01-31'].includes(request.body.model)) {
1074+ bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1075+ }
1076+ }
1077+
9741078 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
9751079 console.logwarn('OpenAI API key is missing.');
9761080 return response.status(400).send({ error: true });
9771081 }
9781082
@@ -1032,7 +1136,7 @@ router.post('/generate', jsonParser, function (request, response) {
10321136 signal: controller.signal,
10331137 };
10341138
10351139 console.logdebug(requestBody);
10361140
10371141 makeRequest(config, response, request);
10381142
@@ -1049,7 +1153,7 @@ router.post('/generate', jsonParser, function (request, response) {
10491153 const fetchResponse = await fetch(endpointUrl, config);
10501154
10511155 if (request.body.stream) {
10521156 console.loginfo('Streaming request in progress');
10531157 forwardFetchResponse(fetchResponse, response);
10541158 return;
10551159 }
@@ -1058,10 +1162,10 @@ router.post('/generate', jsonParser, function (request, response) {
10581162 /** @type {any} */
10591163 let json = await fetchResponse.json();
10601164 response.send(json);
10611165 console.logdebug(json);
10621166 console.logdebug(json?.choices?.[0]?.message);
10631167 } else if (fetchResponse.status === 429 && retries > 0) {
10641168 console.logwarn(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
10651169 setTimeout(() => {
10661170 timeout *= 2;
10671171 makeRequest(config, response, request, retries - 1, timeout);
@@ -1070,7 +1174,7 @@ router.post('/generate', jsonParser, function (request, response) {
10701174 await handleErrorResponse(fetchResponse);
10711175 }
10721176 } catch (error) {
10731177 console.logerror('Generation failed', error);
10741178 const message = error.code === 'ECONNREFUSED'
10751179 ? `Connection refused: ${error.message}`
10761180 : error.message || 'Unknown error occurred';
@@ -1092,7 +1196,7 @@ router.post('/generate', jsonParser, function (request, response) {
10921196
10931197 const message = errorResponse.statusText || 'Unknown error occurred';
10941198 const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';
10951199 console.logerror('Chat completion request error: ', message, responseText);
10961200
10971201 if (!response.headersSent) {
10981202 response.send({ error: { message }, quota_error: quota_error });
@@ -1103,4 +1207,3 @@ router.post('/generate', jsonParser, function (request, response) {
11031207 }
11041208 }
11051209});
1106-
src/endpoints/backends/kobold.js+14 -14
@@ -22,17 +22,17 @@ router.post('/generate', jsonParser, async function (request, response_generate)
2222 request.socket.on('close', async function () {
2323 if (request.body.can_abort && !response_generate.writableEnded) {
2424 try {
2525 console.loginfo('Aborting Kobold generation...');
2626 // send abort signal to koboldcpp
2727 const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {
2828 method: 'POST',
2929 });
3030
3131 if (!abortResponse.ok) {
3232 console.logerror('Error sending abort request to Kobold:', abortResponse.status);
3333 }
3434 } catch (error) {
3535 console.logerror(error);
3636 }
3737 }
3838 controller.abort();
@@ -81,7 +81,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
8181 }
8282 }
8383
8484 console.logdebug(this_settings);
8585 const args = {
8686 body: JSON.stringify(this_settings),
8787 headers: Object.assign(
@@ -105,7 +105,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
105105 } else {
106106 if (!response.ok) {
107107 const errorText = await response.text();
108108 console.logwarn(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);
109109
110110 try {
111111 const errorJson = JSON.parse(errorText);
@@ -117,7 +117,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
117117 }
118118
119119 const data = await response.json();
120120 console.logdebug('Endpoint response:', data);
121121 return response_generate.send(data);
122122 }
123123 } catch (error) {
@@ -125,19 +125,19 @@ router.post('/generate', jsonParser, async function (request, response_generate)
125125 switch (error?.status) {
126126 case 403:
127127 case 503: // retry in case of temporary service issue, possibly caused by a queue failure?
128128 console.debugwarn(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);
129129 await delay(delayAmount);
130130 break;
131131 default:
132132 if ('status' in error) {
133133 console.logerror('Status Code from Kobold:', error.status);
134134 }
135135 return response_generate.send({ error: true });
136136 }
137137 }
138138 }
139139
140140 console.logerror('Max retries exceeded. Giving up.');
141141 return response_generate.send({ error: true });
142142});
143143
@@ -193,16 +193,16 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
193193 const server = request.body.server;
194194
195195 if (!server) {
196196 console.logerror('Server is not set');
197197 return response.sendStatus(400);
198198 }
199199
200200 if (!request.file) {
201201 console.logerror('No audio file found');
202202 return response.sendStatus(400);
203203 }
204204
205205 console.logdebug('Transcribing audio with KoboldCpp', server);
206206
207207 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
208208 fs.rmSync(request.file.path);
@@ -226,12 +226,12 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
226226
227227 if (!result.ok) {
228228 const text = await result.text();
229229 console.logerror('KoboldCpp request failed', result.statusText, text);
230230 return response.status(500).send(text);
231231 }
232232
233233 const data = await result.json();
234234 console.logdebug('KoboldCpp transcription response', data);
235235 return response.json(data);
236236 } catch (error) {
237237 console.error('KoboldCpp transcription failed', error);
src/endpoints/backends/scale-alt.js+5 -5
@@ -13,7 +13,7 @@ router.post('/generate', jsonParser, async function (request, response) {
1313 const cookie = readSecret(request.user.directories, SECRET_KEYS.SCALE_COOKIE);
1414
1515 if (!cookie) {
1616 console.logerror('No Scale cookie found');
1717 return response.sendStatus(400);
1818 }
1919
@@ -62,7 +62,7 @@ router.post('/generate', jsonParser, async function (request, response) {
6262 },
6363 };
6464
6565 console.logdebug('Scale request:', body);
6666
6767 const result = await fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {
6868 method: 'POST',
@@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async function (request, response) {
7575
7676 if (!result.ok) {
7777 const text = await result.text();
7878 console.logerror('Scale request failed', result.statusText, text);
7979 return response.status(500).send({ error: { message: result.statusText } });
8080 }
8181
@@ -83,7 +83,7 @@ router.post('/generate', jsonParser, async function (request, response) {
8383 const data = await result.json();
8484 const output = data?.result?.data?.json?.outputs?.[0] || '';
8585
8686 console.logdebug('Scale response:', data);
8787
8888 if (!output) {
8989 console.warn('Scale response is empty');
@@ -92,7 +92,7 @@ router.post('/generate', jsonParser, async function (request, response) {
9292
9393 return response.json({ output });
9494 } catch (error) {
9595 console.logerror(error);
9696 return response.sendStatus(500);
9797 }
9898});
src/endpoints/backends/text-completions.js+43 -45
@@ -58,12 +58,12 @@ async function parseOllamaStream(jsonStream, request, response) {
5858 });
5959
6060 jsonStream.body.on('end', () => {
6161 console.loginfo('Streaming request finished');
6262 response.write('data: [DONE]\n\n');
6363 response.end();
6464 });
6565 } catch (error) {
6666 console.logerror('Error forwarding streaming response:', error);
6767 if (!response.headersSent) {
6868 return response.status(500).send({ error: true });
6969 } else {
@@ -79,16 +79,16 @@ async function parseOllamaStream(jsonStream, request, response) {
7979 */
8080async function abortKoboldCppRequest(url) {
8181 try {
8282 console.loginfo('Aborting Kobold generation...');
8383 const abortResponse = await fetch(`${url}/api/extra/abort`, {
8484 method: 'POST',
8585 });
8686
8787 if (!abortResponse.ok) {
8888 console.logerror('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
8989 }
9090 } catch (error) {
9191 console.logerror(error);
9292 }
9393}
9494
@@ -101,7 +101,7 @@ router.post('/status', jsonParser, async function (request, response) {
101101 request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
102102 }
103103
104104 console.logdebug('Trying to connect to API:', request.body);
105105 const baseUrl = trimV1(request.body.api_server);
106106
107107 const args = {
@@ -123,6 +123,7 @@ router.post('/status', jsonParser, async function (request, response) {
123123 case TEXTGEN_TYPES.LLAMACPP:
124124 case TEXTGEN_TYPES.INFERMATICAI:
125125 case TEXTGEN_TYPES.OPENROUTER:
126+ case TEXTGEN_TYPES.FEATHERLESS:
126127 url += '/v1/models';
127128 break;
128129 case TEXTGEN_TYPES.DREAMGEN:
@@ -140,9 +141,6 @@ router.post('/status', jsonParser, async function (request, response) {
140141 case TEXTGEN_TYPES.OLLAMA:
141142 url += '/api/tags';
142143 break;
143- case TEXTGEN_TYPES.FEATHERLESS:
144- url += '/v1/models';
145- break;
146144 case TEXTGEN_TYPES.HUGGINGFACE:
147145 url += '/info';
148146 break;
@@ -152,7 +150,7 @@ router.post('/status', jsonParser, async function (request, response) {
152150 const isPossiblyLmStudio = modelsReply.headers.get('x-powered-by') === 'Express';
153151
154152 if (!modelsReply.ok) {
155153 console.logerror('Models endpoint is offline.');
156154 return response.sendStatus(400);
157155 }
158156
@@ -173,12 +171,12 @@ router.post('/status', jsonParser, async function (request, response) {
173171 }
174172
175173 if (!Array.isArray(data.data)) {
176174 console.logerror('Models response is not an array.');
177175 return response.sendStatus(400);
178176 }
179177
180178 const modelIds = data.data.map(x => x.id);
181179 console.loginfo('Models available:', modelIds);
182180
183181 // Set result to the first model ID
184182 result = modelIds[0] || 'Valid';
@@ -191,7 +189,7 @@ router.post('/status', jsonParser, async function (request, response) {
191189 if (modelInfoReply.ok) {
192190 /** @type {any} */
193191 const modelInfo = await modelInfoReply.json();
194192 console.logdebug('Ooba model info:', modelInfo);
195193
196194 const modelName = modelInfo?.model_name;
197195 result = modelName || result;
@@ -208,7 +206,7 @@ router.post('/status', jsonParser, async function (request, response) {
208206 if (modelInfoReply.ok) {
209207 /** @type {any} */
210208 const modelInfo = await modelInfoReply.json();
211209 console.logdebug('Tabby model info:', modelInfo);
212210
213211 const modelName = modelInfo?.id;
214212 result = modelName || result;
@@ -255,7 +253,7 @@ router.post('/props', jsonParser, async function (request, response) {
255253 props['chat_template'] = props['chat_template'].slice(0, -1) + '\n';
256254 }
257255 props['chat_template_hash'] = createHash('sha256').update(props['chat_template']).digest('hex');
258256 console.logdebug(`Model properties: ${JSON.stringify(props)}`);
259257 return response.send(props);
260258 } catch (error) {
261259 console.error(error);
@@ -273,7 +271,7 @@ router.post('/generate', jsonParser, async function (request, response) {
273271
274272 const apiType = request.body.api_type;
275273 const baseUrl = request.body.api_server;
276274 console.logdebug(request.body);
277275
278276 const controller = new AbortController();
279277 request.socket.removeAllListeners('close');
@@ -375,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) {
375373
376374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
377375 const keepAlive = getConfigValue('ollama.keepAlive', -1);
376+ const numBatch = getConfigValue('ollama.batchSize', -1);
377+ if (numBatch > 0) {
378+ request.body['num_batch'] = numBatch;
379+ }
378380 args.body = JSON.stringify({
379381 model: request.body.model,
380382 prompt: request.body.prompt,
@@ -399,7 +401,7 @@ router.post('/generate', jsonParser, async function (request, response) {
399401 if (completionsReply.ok) {
400402 /** @type {any} */
401403 const data = await completionsReply.json();
402404 console.logdebug('Endpoint response:', data);
403405
404406 // Map InfermaticAI response to OAI completions format
405407 if (apiType === TEXTGEN_TYPES.INFERMATICAI) {
@@ -411,24 +413,20 @@ router.post('/generate', jsonParser, async function (request, response) {
411413 const text = await completionsReply.text();
412414 const errorBody = { error: true, status: completionsReply.status, response: text };
413415
414416 ifreturn (!response.headersSent) {
415417 return? response.send(errorBody);
416- }
418+ : response.end();
417-
418- return response.end();
419419 }
420420 }
421421 } catch (error) {
422422 const status = error?.status ?? error?.code ?? 'UNKNOWN';
423423 const text = error?.error ?? error?.statusText ?? error?.message ?? 'Unknown error on /generate endpoint';
424424 let value = { error: true, status: status, response: text };
425425 console.logerror('Endpoint error:', error);
426-
427- if (!response.headersSent) {
428- return response.send(value);
429- }
430426
431427 return !response.end();headersSent
428+ ? response.send(value)
429+ : response.end();
432430 }
433431});
434432
@@ -451,7 +449,7 @@ ollama.post('/download', jsonParser, async function (request, response) {
451449 });
452450
453451 if (!fetchResponse.ok) {
454452 console.logerror('Download error:', fetchResponse.status, fetchResponse.statusText);
455453 return response.status(fetchResponse.status).send({ error: true });
456454 }
457455
@@ -468,7 +466,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
468466 return response.sendStatus(400);
469467 }
470468
471469 console.logdebug('Ollama caption request:', request.body);
472470 const baseUrl = trimV1(request.body.server_url);
473471
474472 const fetchResponse = await fetch(`${baseUrl}/api/generate`, {
@@ -483,18 +481,18 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
483481 });
484482
485483 if (!fetchResponse.ok) {
486484 console.logerror('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);
487485 return response.status(500).send({ error: true });
488486 }
489487
490488 /** @type {any} */
491489 const data = await fetchResponse.json();
492490 console.logdebug('Ollama caption response:', data);
493491
494492 const caption = data?.response || '';
495493
496494 if (!caption) {
497495 console.logerror('Ollama caption is empty.');
498496 return response.status(500).send({ error: true });
499497 }
500498
@@ -513,7 +511,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
513511 return response.sendStatus(400);
514512 }
515513
516514 console.logdebug('LlamaCpp caption request:', request.body);
517515 const baseUrl = trimV1(request.body.server_url);
518516
519517 const fetchResponse = await fetch(`${baseUrl}/completion`, {
@@ -529,18 +527,18 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
529527 });
530528
531529 if (!fetchResponse.ok) {
532530 console.logerror('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);
533531 return response.status(500).send({ error: true });
534532 }
535533
536534 /** @type {any} */
537535 const data = await fetchResponse.json();
538536 console.logdebug('LlamaCpp caption response:', data);
539537
540538 const caption = data?.content || '';
541539
542540 if (!caption) {
543541 console.logerror('LlamaCpp caption is empty.');
544542 return response.status(500).send({ error: true });
545543 }
546544
@@ -558,7 +556,7 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
558556 return response.sendStatus(400);
559557 }
560558
561559 console.logdebug('LlamaCpp props request:', request.body);
562560 const baseUrl = trimV1(request.body.server_url);
563561
564562 const fetchResponse = await fetch(`${baseUrl}/props`, {
@@ -566,12 +564,12 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
566564 });
567565
568566 if (!fetchResponse.ok) {
569567 console.logerror('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);
570568 return response.status(500).send({ error: true });
571569 }
572570
573571 const data = await fetchResponse.json();
574572 console.logdebug('LlamaCpp props response:', data);
575573
576574 return response.send(data);
577575
@@ -590,7 +588,7 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
590588 return response.sendStatus(400);
591589 }
592590
593591 console.logdebug('LlamaCpp slots request:', request.body);
594592 const baseUrl = trimV1(request.body.server_url);
595593
596594 let fetchResponse;
@@ -616,12 +614,12 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
616614 }
617615
618616 if (!fetchResponse.ok) {
619617 console.logerror('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);
620618 return response.status(500).send({ error: true });
621619 }
622620
623621 const data = await fetchResponse.json();
624622 console.logdebug('LlamaCpp slots response:', data);
625623
626624 return response.send(data);
627625
@@ -659,14 +657,14 @@ tabby.post('/download', jsonParser, async function (request, response) {
659657 return response.status(403).send({ error: true });
660658 }
661659 } else {
662660 console.logerror('API Permission error:', permissionResponse.status, permissionResponse.statusText);
663661 return response.status(permissionResponse.status).send({ error: true });
664662 }
665663
666664 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
667665
668666 if (!fetchResponse.ok) {
669667 console.logerror('Download error:', fetchResponse.status, fetchResponse.statusText);
670668 return response.status(fetchResponse.status).send({ error: true });
671669 }
672670
src/endpoints/backgrounds.js+5 -4
@@ -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)) {
@@ -26,7 +27,7 @@ router.post('/delete', jsonParser, function (request, response) {
2627 const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));
2728
2829 if (!fs.existsSync(fileName)) {
2930 console.logerror('BG file not found');
3031 return response.sendStatus(400);
3132 }
3233
@@ -42,12 +43,12 @@ router.post('/rename', jsonParser, function (request, response) {
4243 const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));
4344
4445 if (!fs.existsSync(oldFileName)) {
4546 console.logerror('BG file not found');
4647 return response.sendStatus(400);
4748 }
4849
4950 if (fs.existsSync(newFileName)) {
5051 console.logerror('New BG file already exists');
5152 return response.sendStatus(400);
5253 }
5354
src/endpoints/caption.js+4 -4
@@ -2,10 +2,10 @@ import express from 'express';
22import { jsonParser } from '../express-common.js';
33import { getPipeline, getRawImage } from '../transformers.js';
44
5-const TASK = 'image-to-text';
6-
75export const router = express.Router();
86
7+const TASK = 'image-to-text';
8+
99router.post('/', jsonParser, async (req, res) => {
1010 try {
1111 const { image } = req.body;
@@ -13,14 +13,14 @@ router.post('/', jsonParser, async (req, res) => {
1313 const rawImage = await getRawImage(image);
1414
1515 if (!rawImage) {
1616 console.logwarn('Failed to parse captioned image');
1717 return res.sendStatus(400);
1818 }
1919
2020 const pipe = await getPipeline(TASK);
2121 const result = await pipe(rawImage);
2222 const text = result[0].generated_text;
2323 console.loginfo('Image caption:', text);
2424
2525 return res.json({ caption: text });
2626 } catch (error) {
src/endpoints/characters.js+58 -49
@@ -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();
@@ -90,7 +97,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
9097 writeFileAtomicSync(outputImagePath, outputImage);
9198 return true;
9299 } catch (err) {
93100 console.logerror(err);
94101 return false;
95102 }
96103}
@@ -159,7 +166,7 @@ async function tryReadImage(imgPath, crop) {
159166 }
160167 // If it's an unsupported type of image (APNG) - just read the file as buffer
161168 catch (error) {
162169 console.logerror(`Failed to read image: ${imgPath}`, error);
163170 return fs.readFileSync(imgPath);
164171 }
165172}
@@ -222,12 +229,12 @@ const processCharacter = async (item, directories) => {
222229 return character;
223230 }
224231 catch (err) {
225232 console.logerror(`Could not process character: ${item}`);
226233
227234 if (err instanceof SyntaxError) {
228235 console.logerror(`${item} does not contain a valid JSON object.`);
229236 } else {
230237 console.logerror('An unexpected error occurred: ', err);
231238 }
232239
233240 return {
@@ -315,7 +322,7 @@ function readFromV2(char) {
315322 };
316323
317324 _.forEach(fieldMappings, (v2Path, charField) => {
318325 //console.loginfo(`Migrating field: ${charField} from ${v2Path}`);
319326 const v2Value = _.get(char.data, v2Path);
320327 if (_.isUndefined(v2Value)) {
321328 let defaultValue = undefined;
@@ -330,15 +337,15 @@ function readFromV2(char) {
330337 }
331338
332339 if (!_.isUndefined(defaultValue)) {
333340 //console.debugwarn(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
334341 char[charField] = defaultValue;
335342 } else {
336343 console.debugwarn(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
337344 return;
338345 }
339346 }
340347 if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {
341348 console.debugwarn(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
342349 }
343350 char[charField] = v2Value;
344351 });
@@ -435,7 +442,7 @@ function charaFormatData(data, directories) {
435442 }
436443
437444 } catch {
438445 console.debugwarn(`Failed to read world info file: ${data.world}. Character book will not be available.`);
439446 }
440447 }
441448
@@ -445,7 +452,7 @@ function charaFormatData(data, directories) {
445452 // Deep merge the extensions object
446453 _.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));
447454 } catch {
448455 console.debugwarn(`Failed to parse extensions JSON: ${data.extensions}`);
449456 }
450457 }
451458
@@ -519,7 +526,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
519526 const fileText = fs.readFileSync(uploadPath, 'utf8');
520527 fs.rmSync(uploadPath);
521528 const yamlData = yaml.parse(fileText);
522529 console.loginfo('Importing from YAML');
523530 yamlData.name = sanitize(yamlData.name);
524531 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
525532 let char = convertToV2({
@@ -552,7 +559,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
552559async function importFromCharX(uploadPath, { request }, preservedFileName) {
553560 const data = fs.readFileSync(uploadPath).buffer;
554561 fs.rmSync(uploadPath);
555562 console.loginfo('Importing from CharX');
556563 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
557564
558565 if (!cardBuffer) {
@@ -601,7 +608,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
601608 let jsonData = JSON.parse(data);
602609
603610 if (jsonData.spec !== undefined) {
604611 console.loginfo(`Importing from ${jsonData.spec} json`);
605612 importRisuSprites(request.user.directories, jsonData);
606613 unsetFavFlag(jsonData);
607614 jsonData = readFromV2(jsonData);
@@ -611,7 +618,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
611618 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
612619 return result ? pngName : '';
613620 } else if (jsonData.name !== undefined) {
614621 console.loginfo('Importing from v1 json');
615622 jsonData.name = sanitize(jsonData.name);
616623 if (jsonData.creator_notes) {
617624 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -637,7 +644,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
637644 const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
638645 return result ? pngName : '';
639646 } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
640647 console.loginfo('Importing from gradio json');
641648 jsonData.char_name = sanitize(jsonData.char_name);
642649 if (jsonData.creator_notes) {
643650 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -684,7 +691,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
684691 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
685692
686693 if (jsonData.spec !== undefined) {
687694 console.loginfo(`Found a ${jsonData.spec} character file.`);
688695 importRisuSprites(request.user.directories, jsonData);
689696 unsetFavFlag(jsonData);
690697 jsonData = readFromV2(jsonData);
@@ -694,7 +701,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
694701 fs.unlinkSync(uploadPath);
695702 return result ? pngName : '';
696703 } else if (jsonData.name !== undefined) {
697704 console.loginfo('Found a v1 character file.');
698705
699706 if (jsonData.creator_notes) {
700707 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -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,15 +810,15 @@ 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.errorwarn('Error: no response body detected');
809816 response.status(400).send('Error: no response body detected');
810817 return;
811818 }
812819
813820 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
814821 console.errorwarn('Error: invalid name.');
815822 response.status(400).send('Error: invalid name.');
816823 return;
817824 }
@@ -832,6 +839,9 @@ router.post('/edit', urlencodedParser, async function (request, response) {
832839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
833840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
834841 fs.unlinkSync(newAvatarPath);
842+
843+ // Bust cache to reload the new avatar
844+ response.setHeader('Clear-Site-Data', '"cache"');
835845 }
836846
837847 return response.sendStatus(200);
@@ -852,15 +862,15 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852862 * @param {Object} response - The HTTP response object.
853863 * @returns {void}
854864 */
855865router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856866 console.logdebug(request.body);
857867 if (!request.body) {
858868 console.errorwarn('Error: no response body detected');
859869 return response.status(400).send('Error: no response body detected');
860870 }
861871
862872 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
863873 console.errorwarn('Error: invalid name.');
864874 return response.status(400).send('Error: invalid name.');
865875 }
866876
@@ -872,7 +882,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
872882 const char = JSON.parse(charJSON);
873883 //check if the field exists
874884 if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
875885 console.errorwarn('Error: invalid field.');
876886 response.status(400).send('Error: invalid field.');
877887 return;
878888 }
@@ -898,7 +908,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898908 *
899909 * @returns {void}
900910 * */
901911router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902912 try {
903913 const update = request.body;
904914 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -921,7 +931,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
921931 await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);
922932 response.sendStatus(200);
923933 } else {
924934 console.logwarn(validator.lastValidationError);
925935 response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });
926936 }
927937 } catch (exception) {
@@ -929,7 +939,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929939 }
930940});
931941
932942router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933943 if (!request.body || !request.body.avatar_url) {
934944 return response.sendStatus(400);
935945 }
@@ -992,7 +1002,7 @@ router.post('/all', jsonParser, async function (request, response) {
9921002 }
9931003});
9941004
9951005router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
9961006 try {
9971007 if (!request.body) return response.sendStatus(400);
9981008 const item = request.body.avatar_url;
@@ -1011,7 +1021,7 @@ router.post('/get', jsonParser, async function (request, response) {
10111021 }
10121022});
10131023
10141024router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
10151025 if (!request.body) return response.sendStatus(400);
10161026
10171027 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1043,7 +1053,7 @@ router.post('/chats', jsonParser, async function (request, response) {
10431053 const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
10441054
10451055 if (stats.size === 0) {
10461056 console.logwarn(`Found an empty chat file: ${pathToFile}`);
10471057 res({});
10481058 return;
10491059 }
@@ -1075,7 +1085,7 @@ router.post('/chats', jsonParser, async function (request, response) {
10751085
10761086 res(chatData);
10771087 } else {
10781088 console.logwarn('Found an invalid or corrupted chat file:', pathToFile);
10791089 res({});
10801090 }
10811091 }
@@ -1088,7 +1098,7 @@ router.post('/chats', jsonParser, async function (request, response) {
10881098
10891099 return response.send(validFiles);
10901100 } catch (error) {
10911101 console.logerror(error);
10921102 return response.send({ error: true });
10931103 }
10941104});
@@ -1145,7 +1155,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11451155 const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);
11461156
11471157 if (!fileName) {
11481158 console.errorwarn('Failed to import character');
11491159 return response.sendStatus(400);
11501160 }
11511161
@@ -1155,22 +1165,21 @@ router.post('/import', urlencodedParser, async function (request, response) {
11551165
11561166 response.send({ file_name: fileName });
11571167 } catch (err) {
11581168 console.logerror(err);
11591169 response.send({ error: true });
11601170 }
11611171});
11621172
11631173router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
11641174 try {
11651175 if (!request.body.avatar_url) {
11661176 console.logwarn('avatar URL not found in request body');
11671177 console.logdebug(request.body);
11681178 return response.sendStatus(400);
11691179 }
11701180 let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
11711181 if (!fs.existsSync(filename)) {
11721182 console.logerror('file for dupe not found', filename);
1173- console.log(filename);
11741183 return response.sendStatus(404);
11751184 }
11761185 let suffix = 1;
@@ -1198,7 +1207,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
11981207 }
11991208
12001209 fs.copyFileSync(filename, newFilename);
12011210 console.loginfo(`${filename} was copied to ${newFilename}`);
12021211 response.send({ path: path.parse(newFilename).base });
12031212 }
12041213 catch (error) {
@@ -1207,7 +1216,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
12071216 }
12081217});
12091218
12101219router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
12111220 try {
12121221 if (!request.body.format || !request.body.avatar_url) {
12131222 return response.sendStatus(400);
src/endpoints/chats.js+25 -25
@@ -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,
@@ -49,7 +50,7 @@ function backupChat(directory, name, chat) {
4950
5051 removeOldBackups(directory, 'chat_', maxTotalChatBackups);
5152 } catch (err) {
5253 console.logerror(`Could not backup chat for ${name}`, err);
5354 }
5455}
5556
@@ -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;
@@ -305,12 +306,12 @@ router.post('/save', jsonParser, function (request, response) {
305306 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);
306307 return response.send({ result: 'ok' });
307308 } catch (error) {
308309 responseconsole.senderror(error);
309310 return consoleresponse.logsend(error);
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 }
@@ -358,37 +359,37 @@ router.post('/rename', jsonParser, async function (request, response) {
358359 const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));
359360 const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));
360361 const sanitizedFileName = path.parse(pathToRenamedFile).name;
361362 console.loginfo('Old chat name', pathToOriginalFile);
362363 console.loginfo('New chat name', pathToRenamedFile);
363364
364365 if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {
365366 console.logerror('Either Source or Destination files are not available');
366367 return response.status(400).send({ error: true });
367368 }
368369
369370 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
370371 fs.rmSync(pathToOriginalFile);
371372 console.loginfo('Successfully renamed.');
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));
379380 const chatFileExists = fs.existsSync(filePath);
380381
381382 if (!chatFileExists) {
382383 console.logerror(`Chat file not found '${filePath}'`);
383384 return response.sendStatus(400);
384385 }
385386
386387 fs.rmSync(filePath);
387388 console.loginfo('`Deleted chat file: ' + ${filePath}`);
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 }
@@ -401,7 +402,7 @@ router.post('/export', jsonParser, async function (request, response) {
401402 const errorMessage = {
402403 message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
403404 };
404405 console.logerror(errorMessage.message);
405406 return response.status(404).json(errorMessage);
406407 }
407408 try {
@@ -414,14 +415,14 @@ router.post('/export', jsonParser, async function (request, response) {
414415 result: rawFile,
415416 };
416417
417418 console.loginfo(`Chat exported as ${exportfilename}`);
418419 return response.status(200).json(successMessage);
419420 } catch (err) {
420421 console.error(err);
421422 const errorMessage = {
422423 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
423424 };
424425 console.logerror(errorMessage.message);
425426 return response.status(500).json(errorMessage);
426427 }
427428 }
@@ -448,12 +449,11 @@ router.post('/export', jsonParser, async function (request, response) {
448449 message: `Chat saved to ${exportfilename}`,
449450 result: buffer,
450451 };
451452 console.loginfo(`Chat exported as ${exportfilename}`);
452453 return response.status(200).json(successMessage);
453454 });
454455 } catch (err) {
455456 console.logerror('chat export failed.', err);
456- console.log(err);
457457 return response.sendStatus(400);
458458 }
459459});
@@ -478,7 +478,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
478478 }
479479});
480480
481481router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482482 if (!request.body) return response.sendStatus(400);
483483
484484 const format = request.body.file_type;
@@ -512,7 +512,7 @@ router.post('/import', urlencodedParser, function (request, response) {
512512 } else if (jsonData.type === 'risuChat') { // RisuAI format
513513 importFunc = importRisuChat;
514514 } else { // Unknown format
515515 console.logerror('Incorrect chat format .json');
516516 return response.send({ error: true });
517517 }
518518
@@ -540,7 +540,7 @@ router.post('/import', urlencodedParser, function (request, response) {
540540 const jsonData = JSON.parse(header);
541541
542542 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {
543543 console.logerror('Incorrect chat format .jsonl');
544544 return response.send({ error: true });
545545 }
546546
@@ -626,7 +626,7 @@ router.post('/group/save', jsonParser, (request, response) => {
626626 return response.send({ ok: true });
627627});
628628
629629router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
630630 try {
631631 const { query, avatar_url, group_id } = request.body;
632632 let chatFiles = [];
@@ -646,7 +646,7 @@ router.post('/search', jsonParser, function (request, response) {
646646 break;
647647 }
648648 } catch (error) {
649649 console.errorwarn(groupFile, 'group file is corrupted:', error);
650650 }
651651 }
652652
src/endpoints/classify.js+2 -2
@@ -44,9 +44,9 @@ router.post('/', jsonParser, async (req, res) => {
4444 }
4545 }
4646
4747 console.logdebug('Classify input:', text);
4848 const result = await getResult(text);
4949 console.logdebug('Classify output:', result);
5050
5151 return res.json({ classification: result });
5252 } catch (error) {
src/endpoints/content-manager.js+26 -26
@@ -71,7 +71,7 @@ export function getDefaultPresets(directories) {
7171
7272 return presets;
7373 } catch (err) {
7474 console.logwarn('Failed to get default presets', err);
7575 return [];
7676 }
7777}
@@ -92,7 +92,7 @@ export function getDefaultPresetFile(filename) {
9292 const fileContent = fs.readFileSync(contentPath, 'utf8');
9393 return JSON.parse(fileContent);
9494 } catch (err) {
9595 console.logwarn(`Failed to get default file ${filename}`, err);
9696 return null;
9797 }
9898}
@@ -121,21 +121,21 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
121121 }
122122
123123 if (!contentItem.folder) {
124124 console.logwarn(`Content file ${contentItem.filename} has no parent folder`);
125125 continue;
126126 }
127127
128128 const contentPath = path.join(contentItem.folder, contentItem.filename);
129129
130130 if (!fs.existsSync(contentPath)) {
131131 console.logwarn(`Content file ${contentItem.filename} is missing`);
132132 continue;
133133 }
134134
135135 const contentTarget = getTargetByType(contentItem.type, directories);
136136
137137 if (!contentTarget) {
138138 console.logwarn(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);
139139 continue;
140140 }
141141
@@ -144,12 +144,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
144144 contentLog.push(contentItem.filename);
145145
146146 if (fs.existsSync(targetPath)) {
147147 console.logwarn(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
148148 continue;
149149 }
150150
151151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
152152 console.loginfo(`Content file ${contentItem.filename} copied to ${contentTarget}`);
153153 anyContentAdded = true;
154154 }
155155
@@ -182,12 +182,12 @@ export async function checkForNewContent(directoriesList, forceCategories = [])
182182 }
183183
184184 if (anyContentAdded && !contentCheckSkip && forceCategories?.length === 0) {
185185 console.loginfo();
186186 console.loginfo(`${color.blue('If you don\'t want to receive content updates in the future, set')} ${color.yellow('skipContentCheck')} ${color.blue('to true in the config.yaml file.')}`);
187187 console.loginfo();
188188 }
189189 } catch (err) {
190190 console.logerror('Content check failed', err);
191191 }
192192}
193193
@@ -331,7 +331,7 @@ async function downloadChubLorebook(id) {
331331
332332 if (!result.ok) {
333333 const text = await result.text();
334334 console.logerror('Chub returned error', result.statusText, text);
335335 throw new Error('Failed to download lorebook');
336336 }
337337
@@ -355,7 +355,7 @@ async function downloadChubCharacter(id) {
355355
356356 if (!result.ok) {
357357 const text = await result.text();
358358 console.logerror('Chub returned error', result.statusText, text);
359359 throw new Error('Failed to download character');
360360 }
361361
@@ -376,7 +376,7 @@ async function downloadPygmalionCharacter(id) {
376376
377377 if (!result.ok) {
378378 const text = await result.text();
379379 console.logerror('Pygsite returned error', result.status, text);
380380 throw new Error('Failed to download character');
381381 }
382382
@@ -485,7 +485,7 @@ async function downloadJannyCharacter(uuid) {
485485 }
486486 }
487487
488488 console.logerror('Janny returned error', result.statusText, await result.text());
489489 throw new Error('Failed to download character');
490490}
491491
@@ -577,7 +577,7 @@ async function downloadRisuCharacter(uuid) {
577577
578578 if (!result.ok) {
579579 const text = await result.text();
580580 console.logerror('RisuAI returned error', result.statusText, text);
581581 throw new Error('Failed to download character');
582582 }
583583
@@ -673,11 +673,11 @@ router.post('/importURL', jsonParser, async (request, response) => {
673673 type = chubParsed?.type;
674674
675675 if (chubParsed?.type === 'character') {
676676 console.loginfo('Downloading chub character:', chubParsed.id);
677677 result = await downloadChubCharacter(chubParsed.id);
678678 }
679679 else if (chubParsed?.type === 'lorebook') {
680680 console.loginfo('Downloading chub lorebook:', chubParsed.id);
681681 result = await downloadChubLorebook(chubParsed.id);
682682 }
683683 else {
@@ -692,7 +692,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
692692 type = 'character';
693693 result = await downloadRisuCharacter(uuid);
694694 } else if (isGeneric) {
695695 console.loginfo('Downloading from generic url.');
696696 type = 'character';
697697 result = await downloadGenericPng(url);
698698 } else {
@@ -708,7 +708,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
708708 response.set('X-Custom-Content-Type', type);
709709 return response.send(result.buffer);
710710 } catch (error) {
711711 console.logerror('Importing custom content failed', error);
712712 return response.sendStatus(500);
713713 }
714714});
@@ -728,22 +728,22 @@ router.post('/importUUID', jsonParser, async (request, response) => {
728728 const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
729729
730730 if (isPygmalion) {
731731 console.loginfo('Downloading Pygmalion character:', uuid);
732732 result = await downloadPygmalionCharacter(uuid);
733733 } else if (isJannny) {
734734 console.loginfo('Downloading Janitor character:', uuid.split('_')[0]);
735735 result = await downloadJannyCharacter(uuid.split('_')[0]);
736736 } else if (isAICC) {
737737 const [, author, card] = uuid.split('/');
738738 console.loginfo('Downloading AICC character:', `${author}/${card}`);
739739 result = await downloadAICCCharacter(`${author}/${card}`);
740740 } else {
741741 if (uuidType === 'character') {
742742 console.loginfo('Downloading chub character:', uuid);
743743 result = await downloadChubCharacter(uuid);
744744 }
745745 else if (uuidType === 'lorebook') {
746746 console.loginfo('Downloading chub lorebook:', uuid);
747747 result = await downloadChubLorebook(uuid);
748748 }
749749 else {
@@ -756,7 +756,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
756756 response.set('X-Custom-Content-Type', uuidType);
757757 return response.send(result.buffer);
758758 } catch (error) {
759759 console.logerror('Importing custom content failed', error);
760760 return response.sendStatus(500);
761761 }
762762});
src/endpoints/extensions.js+16 -16
@@ -80,7 +80,7 @@ router.post('/install', jsonParser, async (request, response) => {
8080 const { url, global } = request.body;
8181
8282 if (global && !request.user.profile.admin) {
8383 console.warnerror(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
8484 return response.status(403).send('Forbidden: No permission to install global extensions.');
8585 }
8686
@@ -92,13 +92,13 @@ router.post('/install', jsonParser, async (request, response) => {
9292 }
9393
9494 await git.clone(url, extensionPath, { '--depth': 1 });
9595 console.loginfo(`Extension has been cloned at ${extensionPath}`);
9696
9797 const { version, author, display_name } = await getManifest(extensionPath);
9898
9999 return response.send({ version, author, display_name, extensionPath });
100100 } catch (error) {
101101 console.logerror('Importing custom content failed', error);
102102 return response.status(500).send(`Server Error: ${error.message}`);
103103 }
104104});
@@ -124,7 +124,7 @@ router.post('/update', jsonParser, async (request, response) => {
124124 const { extensionName, global } = request.body;
125125
126126 if (global && !request.user.profile.admin) {
127127 console.warnerror(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
128128 return response.status(403).send('Forbidden: No permission to update global extensions.');
129129 }
130130
@@ -139,9 +139,9 @@ router.post('/update', jsonParser, async (request, response) => {
139139 const currentBranch = await git.cwd(extensionPath).branch();
140140 if (!isUpToDate) {
141141 await git.cwd(extensionPath).pull('origin', currentBranch.current);
142142 console.loginfo(`Extension has been updated at ${extensionPath}`);
143143 } else {
144144 console.loginfo(`Extension is up to date at ${extensionPath}`);
145145 }
146146 await git.cwd(extensionPath).fetch('origin');
147147 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
@@ -150,7 +150,7 @@ router.post('/update', jsonParser, async (request, response) => {
150150 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
151151
152152 } catch (error) {
153153 console.logerror('Updating custom content failed', error);
154154 return response.status(500).send(`Server Error: ${error.message}`);
155155 }
156156});
@@ -164,7 +164,7 @@ router.post('/move', jsonParser, async (request, response) => {
164164 }
165165
166166 if (!request.user.profile.admin) {
167167 console.warnerror(`User ${request.user.profile.handle} does not have permission to move extensions.`);
168168 return response.status(403).send('Forbidden: No permission to move extensions.');
169169 }
170170
@@ -190,11 +190,11 @@ router.post('/move', jsonParser, async (request, response) => {
190190
191191 fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
192192 fs.rmSync(sourcePath, { recursive: true, force: true });
193193 console.loginfo(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
194194
195195 return response.sendStatus(204);
196196 } catch (error) {
197197 console.logerror('Moving extension failed', error);
198198 return response.status(500).send('Internal Server Error. Try again later.');
199199 }
200200});
@@ -237,13 +237,13 @@ router.post('/version', jsonParser, async (request, response) => {
237237 // get only the working branch
238238 const currentBranchName = currentBranch.current;
239239 await git.cwd(extensionPath).fetch('origin');
240240 console.logdebug(extensionName, currentBranchName, currentCommitHash);
241241 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
242242
243243 return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
244244
245245 } catch (error) {
246246 console.logerror('Getting extension version failed', error);
247247 return response.status(500).send(`Server Error: ${error.message}`);
248248 }
249249});
@@ -265,7 +265,7 @@ router.post('/delete', jsonParser, async (request, response) => {
265265 const { extensionName, global } = request.body;
266266
267267 if (global && !request.user.profile.admin) {
268268 console.warnerror(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
269269 return response.status(403).send('Forbidden: No permission to delete global extensions.');
270270 }
271271
@@ -277,12 +277,12 @@ router.post('/delete', jsonParser, async (request, response) => {
277277 }
278278
279279 await fs.promises.rm(extensionPath, { recursive: true });
280280 console.loginfo(`Extension has been deleted at ${extensionPath}`);
281281
282282 return response.send(`Extension has been deleted at ${extensionPath}`);
283283
284284 } catch (error) {
285285 console.logerror('Deleting custom content failed', error);
286286 return response.status(500).send(`Server Error: ${error.message}`);
287287 }
288288});
@@ -323,7 +323,7 @@ router.get('/discover', jsonParser, function (request, response) {
323323
324324 // Combine all extensions
325325 const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];
326326 console.loginfo('Extensions available for', request.user.profile.handle, allExtensions);
327327
328328 return response.send(allExtensions);
329329});
src/endpoints/files.js+6 -6
@@ -21,7 +21,7 @@ router.post('/sanitize-filename', jsonParser, async (request, response) => {
2121 const sanitizedFilename = sanitize(fileName);
2222 return response.send({ fileName: sanitizedFilename });
2323 } catch (error) {
2424 console.logerror(error);
2525 return response.sendStatus(500);
2626 }
2727});
@@ -44,10 +44,10 @@ router.post('/upload', jsonParser, async (request, response) => {
4444 const pathToUpload = path.join(request.user.directories.files, request.body.name);
4545 writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
4646 const url = clientRelativePath(request.user.directories.root, pathToUpload);
4747 console.loginfo(`Uploaded file: ${url} from ${request.user.profile.handle}`);
4848 return response.send({ path: url });
4949 } catch (error) {
5050 console.logerror(error);
5151 return response.sendStatus(500);
5252 }
5353});
@@ -68,10 +68,10 @@ router.post('/delete', jsonParser, async (request, response) => {
6868 }
6969
7070 fs.rmSync(pathToDelete);
7171 console.loginfo(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
7272 return response.sendStatus(200);
7373 } catch (error) {
7474 console.logerror(error);
7575 return response.sendStatus(500);
7676 }
7777});
@@ -87,7 +87,7 @@ router.post('/verify', jsonParser, async (request, response) => {
8787 for (const url of request.body.urls) {
8888 const pathToVerify = path.join(request.user.directories.root, url);
8989 if (!pathToVerify.startsWith(request.user.directories.files)) {
9090 console.debugwarn(`File verification: Invalid path: ${pathToVerify}`);
9191 continue;
9292 }
9393 const fileExists = fs.existsSync(pathToVerify);
@@ -96,7 +96,7 @@ router.post('/verify', jsonParser, async (request, response) => {
9696
src/endpoints/google.js+0 -0
src/endpoints/groups.js+0 -0
src/endpoints/horde.js+0 -0
src/endpoints/images.js+0 -0
src/endpoints/novelai.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/openrouter.js+0 -0
src/endpoints/presets.js+0 -0
src/endpoints/search.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/settings.js+0 -0
src/endpoints/speech.js+0 -0
src/endpoints/sprites.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/stats.js+0 -0
src/endpoints/thumbnails.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/translate.js+0 -0
src/endpoints/users-admin.js+0 -0
src/endpoints/users-private.js+0 -0
src/endpoints/users-public.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/worldinfo.js+0 -0
src/middleware/basicAuth.js+0 -0
src/middleware/cacheBuster.js+0 -0
src/middleware/validateFileName.js+0 -0
src/middleware/whitelist.js+0 -0
src/plugin-loader.js+0 -0
src/prompt-converters.js+0 -0
src/request-proxy.js+0 -0
src/transformers.js+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/cohere-vectors.js+0 -0
src/vectors/extras-vectors.js+0 -0
src/vectors/makersuite-vectors.js+0 -0
src/vectors/nomicai-vectors.js+0 -0
src/vectors/openai-vectors.js+0 -0
Diff truncated