Merge branch 'staging' into support-multiple-expressions

c12f26441e65082c2b63a60c139e750c53ea938d

Wolfsblvt <wolfsblvt@gmail.com>

124 files changed, +3077 -1347Ignore whitespace
.github/ISSUE_TEMPLATE/bug-report.yml+2 -0
@@ -80,6 +80,8 @@ body:
8080 required: true
8181 - label: I have checked the [docs](https://docs.sillytavern.app/) ![important](https://img.shields.io/badge/Important!-F6094E)
8282 required: true
83+ - label: I confirm that my issue is not related to third-party content, unofficial extension or patch. If in doubt, check with a new [user account](https://docs.sillytavern.app/administration/multi-user/) and with extensions disabled
84+ required: true
8385
8486 - type: markdown
8587 attributes:
default/config.yaml+14 -0
@@ -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,6 +71,8 @@ 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
@@ -179,6 +187,10 @@ ollama:
179187 # * 0: Unload the model immediately after the request
180188 # * N (any positive number): Keep the model loaded for N seconds after the request.
181189 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
182194# -- ANTHROPIC CLAUDE API CONFIGURATION --
183195claude:
184196 # Enables caching of the system prompt (if supported).
@@ -198,3 +210,5 @@ claude:
198210 cachingAtDepth: -1
199211# -- SERVER PLUGIN CONFIGURATION --
200212enableServerPlugins: false
213+# Attempt to automatically update server plugins on startup
214+enableServerPluginsAutoUpdate: true
default/content/index.json+0 -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 },
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
package-lock.json+30 -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": {
@@ -28,7 +28,7 @@
2828 "cors": "^2.8.5",
2929 "csrf-sync": "^4.0.3",
3030 "diff-match-patch": "^1.0.5",
3131 "dompurify": "^3.12.74",
3232 "droll": "^0.2.1",
3333 "express": "^4.21.0",
3434 "form-data": "^4.0.0",
@@ -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",
@@ -1462,6 +1463,13 @@
14621463 "@types/jquery": "*"
14631464 }
14641465 },
1466+ "node_modules/@types/trusted-types": {
1467+ "version": "2.0.7",
1468+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1469+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1470+ "license": "MIT",
1471+ "optional": true
1472+ },
14651473 "node_modules/@types/write-file-atomic": {
14661474 "version": "4.0.3",
14671475 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
@@ -3217,10 +3225,13 @@
32173225 }
32183226 },
32193227 "node_modules/dompurify": {
32203228 "version": "3.12.74",
32213229 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.12.74.tgz",
32223230 "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTaysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+eNpxSl5gmzn+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQMs/mg==",
32233231 "license": "(MPL-2.0 OR Apache-2.0)",
3232+ "optionalDependencies": {
3233+ "@types/trusted-types": "^2.0.7"
3234+ }
32243235 },
32253236 "node_modules/domutils": {
32263237 "version": "3.1.0",
@@ -4610,6 +4621,18 @@
46104621 "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
46114622 "license": "LGPL-3.0-only"
46124623 },
4624+ "node_modules/ip-regex": {
4625+ "version": "5.0.0",
4626+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
4627+ "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
4628+ "license": "MIT",
4629+ "engines": {
4630+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4631+ },
4632+ "funding": {
4633+ "url": "https://github.com/sponsors/sindresorhus"
4634+ }
4635+ },
46134636 "node_modules/ipaddr.js": {
46144637 "version": "2.1.0",
46154638 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
package.json+4 -2
@@ -18,7 +18,7 @@
1818 "cors": "^2.8.5",
1919 "csrf-sync": "^4.0.3",
2020 "diff-match-patch": "^1.0.5",
2121 "dompurify": "^3.12.74",
2222 "droll": "^0.2.1",
2323 "express": "^4.21.0",
2424 "form-data": "^4.0.0",
@@ -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",
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}']);
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+4 -0
@@ -493,3 +493,7 @@ label[for="trim_spaces"]:not(:has(input:checked)) small {
493493#mistralai_other_models:empty {
494494 display: none;
495495}
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+163 -120
@@ -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>
@@ -1621,17 +1621,34 @@
16211621 </div>
16221622 <div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">
16231623 <hr class="width100p">
16241624 <h4div class="range-block-title justifyCentertitle_restorable">
1625- <span data-i18n="Banned Tokens">Banned Tokens/Strings</span>
1625+ <div>
1626- <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>
1627- </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>
1628- <div class="wide100p">
1628+ </div>
1629- <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>
16301647 </div>
16311648 </div>
16321649 <div class="range-block wide100p">
16331650 <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
16341651 <spanstrong data-i18n="Logit Bias">Logit Bias</spanstrong>
16351652 <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
16361653 <i class="fa-xs fa-solid fa-plus"></i>
16371654 <small data-i18n="Add">Add</small>
@@ -1934,7 +1951,7 @@
19341951 </span>
19351952 </div>
19361953 </div>
19371954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
19381955 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19391956 <input id="openai_function_calling" type="checkbox" />
19401957 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1957,14 +1974,16 @@
19571974 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
19581975 </div>
19591976 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
1960- <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1977+ <div class="flex-container oneline-dropdown">
1961- Inline Image Quality
1978+ <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1962- </label>
1979+ Inline Image Quality
1963- <select id="openai_inline_image_quality">
1980+ </label>
1964- <option data-i18n="openai_inline_image_quality_auto" value="auto">Auto</option>
1981+ <select id="openai_inline_image_quality">
19651982 <option data-i18n="openai_inline_image_quality_lowopenai_inline_image_quality_auto" value="lowauto">LowAuto</option>
19661983 <option data-i18n="openai_inline_image_quality_highopenai_inline_image_quality_low" value="highlow">HighLow</option>
1967- </select>
1984+ <option data-i18n="openai_inline_image_quality_high" value="high">High</option>
1985+ </select>
1986+ </div>
19681987 </div>
19691988 </div>
19701989 <div class="range-block" data-source="makersuite">
@@ -1981,12 +2000,12 @@
19812000 </span>
19822001 </div>
19832002 </div>
19842003 <div class="range-block" data-source="makersuite,deepseek,openrouter">
19852004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19862005 <input id="openai_show_thoughts" type="checkbox" />
19872006 <span>
19882007 <span data-i18n="Request model reasoning">Request model reasoning</span>
19892008 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
19902009 </span>
19912010 </label>
19922011 <div class="toggle-description justifyLeft marginBot5">
@@ -1995,6 +2014,18 @@
19952014 </span>
19962015 </div>
19972016 </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." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
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>
19982029 <div class="range-block" data-source="claude">
19992030 <div class="wide100p">
20002031 <div class="flex-container alignItemsCenter">
@@ -2809,27 +2840,6 @@
28092840 <div>
28102841 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
28112842 <select id="model_openai_select">
2812- <optgroup label="GPT-3.5 Turbo">
2813- <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
2814- <option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
2815- <option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
2816- <option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option>
2817- <option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option>
2818- <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
2819- <option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option>
2820- </optgroup>
2821- <optgroup label="GPT-3.5 Turbo Instruct">
2822- <option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
2823- <option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option>
2824- </optgroup>
2825- <optgroup label="GPT-4">
2826- <option value="gpt-4">gpt-4</option>
2827- <option value="gpt-4-0613">gpt-4-0613 (2023)</option>
2828- <option value="gpt-4-0314">gpt-4-0314 (2023)</option>
2829- <option value="gpt-4-32k">gpt-4-32k</option>
2830- <option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
2831- <option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
2832- </optgroup>
28332843 <optgroup label="GPT-4o">
28342844 <option value="gpt-4o">gpt-4o</option>
28352845 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@@ -2837,29 +2847,44 @@
28372847 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
28382848 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
28392849 </optgroup>
28402850 <optgroup label="gptGPT-4o- mini">
28412851 <option value="gpt-4o-mini">gpt-4o-mini</option>
28422852 <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>
28432856 </optgroup>
28442857 <optgroup label="GPT-4o1 Turboand 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>
2868+ </optgroup>
2869+ <optgroup label="GPT-4 Turbo and GPT-4">
28452870 <option value="gpt-4-turbo">gpt-4-turbo</option>
28462871 <option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
28472872 <option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>
2848- <option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
28492873 <option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>
28502874 <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>
28512878 </optgroup>
28522879 <optgroup label="o1GPT-3.5 Turbo">
28532880 <option value="o1gpt-preview3.5-turbo">o1gpt-preview3.5-turbo</option>
28542881 <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>
28552884 </optgroup>
28562885 <optgroup label="Other">
28572886 <option value="text-davincibabbage-003002">text-davincibabbage-003002</option>
28582887 <option value="text-davinci-002">text-davinci-002</option>
2859- <option value="text-curie-001">text-curie-001</option>
2860- <option value="text-babbage-001">text-babbage-001</option>
2861- <option value="text-ada-001">text-ada-001</option>
2862- <option value="code-davinci-002">code-davinci-002</option>
28632888 </optgroup>
28642889 <optgroup id="openai_external_category" label="External">
28652890 </optgroup>
@@ -3058,6 +3083,7 @@
30583083 <h4 data-i18n="Google Model">Google Model</h4>
30593084 <select id="model_google_select">
30603085 <optgroup label="Primary">
3086+ <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
30613087 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
30623088 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
30633089 <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
@@ -3066,6 +3092,11 @@
30663092 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
30673093 </optgroup>
30683094 <optgroup label="Subversions">
3095+ <option value="gemini-2.0-pro-exp">Gemini 2.0 Pro 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>
30693100 <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
30703101 <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
30713102 <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
@@ -3155,34 +3186,22 @@
31553186 </div>
31563187 <h4 data-i18n="Groq Model">Groq Model</h4>
31573188 <select id="model_groq_select">
31583189 <optgroup label="LlamaProduction 3.3Models">
3190+ <option value="gemma2-9b-it">gemma2-9b-it</option>
31593191 <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>
31603196 </optgroup>
31613197 <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>
31623200 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
31633201 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
31643202 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
31653203 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
31663204 </optgroup>
3167- <optgroup label="Llama 3.1">
3168- <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3169- <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
3170- <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
3171- </optgroup>
3172- <optgroup label="Llama 3">
3173- <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
3174- <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
3175- <option value="llama3-8b-8192">llama3-8b-8192</option>
3176- <option value="llama3-70b-8192">llama3-70b-8192</option>
3177- </optgroup>
3178- <optgroup label="Gemma">
3179- <option value="gemma-7b-it">gemma-7b-it</option>
3180- <option value="gemma2-9b-it">gemma2-9b-it</option>
3181- </optgroup>
3182- <optgroup label="Other">
3183- <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3184- <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
3185- </optgroup>
31863205 </select>
31873206 </div>
31883207 <div id="nanogpt_form" data-source="nanogpt">
@@ -3515,7 +3534,7 @@
35153534 </label>
35163535 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
35173536 <input id="instruct_enabled" type="checkbox" style="display:none;" />
35183537 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
35193538 </label>
35203539 </div>
35213540 </h4>
@@ -3693,7 +3712,7 @@
36933712 <div class="flex-container">
36943713 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
36953714 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
36963715 <small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
36973716 </label>
36983717 </div>
36993718 </h4>
@@ -3747,8 +3766,8 @@
37473766 </div>
37483767 <label class="checkbox_label" for="custom_stopping_strings_macro">
37493768 <input id="custom_stopping_strings_macro" type="checkbox" checked>
37503769 <small data-i18n="Replace Macro in Custom StoppingStop Strings">
37513770 Replace Macro in Custom StoppingStop Strings
37523771 </small>
37533772 </label>
37543773 </div>
@@ -3795,38 +3814,59 @@
37953814 <span data-i18n="Reasoning">Reasoning</span>
37963815 </h4>
37973816 <div>
3798- <label class="checkbox_label" 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">
3817+ <div class="flex-container alignItemsBaseline">
3799- <input id="reasoning_auto_parse" type="checkbox" />
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">
3800- <small data-i18n="Auto-Parse Reasoning">
3819+ <input id="reasoning_auto_parse" type="checkbox" />
3801- Auto-Parse Reasoning
3820+ <small data-i18n="Auto-Parse">
3802- </small>
3821+ Auto-Parse
38033822 </labelsmall>
3804- <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3823+ </label>
3805- <input id="reasoning_add_to_prompts" type="checkbox" />
3824+ <label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand">
3806- <small data-i18n="Add Reasoning to Prompts">
3825+ <input id="reasoning_auto_expand" type="checkbox" />
3807- Add Reasoning to Prompts
3826+ <small data-i18n="Auto-Expand">
3808- </small>
3827+ Auto-Expand
38093828 </labelsmall>
3810- <div class="flex-container">
3829+ </label>
38113830 <divlabel class="checkbox_label flex1" for="reasoning_show_hidden" title="InsertedShow beforereasoning thetime reasoningfor contentmodels with hidden reasoning." data-i18n="[title]reasoning_prefixreasoning_show_hidden">
3812- <small data-i18n="Prefix">Prefix</small>
3831+ <input id="reasoning_show_hidden" type="checkbox" />
3813- <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3832+ <small data-i18n="Show Hidden">
3814- </div>
3833+ Show Hidden
3815- <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3834+ </small>
3816- <small data-i18n="Suffix">Suffix</small>
3835+ </label>
3817- <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
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>
38183847 </div>
38193848 </div>
3820- <div class="flex-container">
3849+ <details>
3821- <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3850+ <summary data-i18n="Reasoning Formatting">
3822- <small data-i18n="Separator">Separator</small>
3851+ Reasoning Formatting
3823- <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
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>
38243862 </div>
3825- <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3863+ <div class="flex-container">
3826- <small data-i18n="Max Additions">Max Additions</small>
3864+ <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3827- <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3865+ <small data-i18n="Separator">Separator</small>
3866+ <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3867+ </div>
38283868 </div>
38293869 </divdetails>
38303870 </div>
38313871 </div>
38323872 <div>
@@ -3961,7 +4001,7 @@
39614001 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
39624002 <small>
39634003 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
39644004 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
39654005 </small>
39664006 <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
39674007 <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
@@ -4842,6 +4882,7 @@
48424882 </div>
48434883 <div id="extensions_settings" class="flex1 wide50p">
48444884 <div id="assets_container" class="extension_container"></div>
4885+ <div id="typing_indicator_container" class="extension_container"></div>
48454886 <div id="expressions_container" class="extension_container"></div>
48464887 <div id="sd_container" class="extension_container"></div>
48474888 <div id="tts_container" class="extension_container"></div>
@@ -5837,7 +5878,7 @@
58375878 <div class="inline-drawer-content flex-container paddingBottom5px wide100p">
58385879 <div class="flex-container wide100p alignitemscenter">
58395880 <div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">
58405881 <div class="world_entry_form_control keyprimary flex1">
58415882 <small class="displayNone">
58425883 <span data-i18n="Comma separated (required)">
58435884 Comma separated (required)
@@ -6259,14 +6300,19 @@
62596300 </div>
62606301 </div>
62616302 <details class="mes_reasoning_details">
62626303 <summary class="mes_reasoning_summary flex-container">
6263- <span data-i18n="Reasoning">Reasoning</span>
6304+ <div class="mes_reasoning_header_block flex-container">
62646305 <div class="mes_reasoning_actionsmes_reasoning_header flex-container">
6265- <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6306+ <span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span>
62666307 <div class="mes_reasoning_edit_cancel mes_buttonmes_reasoning_arrow fa-solid fa-xmark" title="Cancel edit" datachevron-i18n="[title]Cancel editup"></div>
6267- <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></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>
62686314 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
62696315 <div class="mes_reasoning_deletemes_reasoning_edit mes_button fa-solid fa-trash-canpencil" title="RemoveEdit reasoning" data-i18n="[title]RemoveEdit reasoning"></div>
62706316 </div>
62716317 </summary>
62726318 <div class="mes_reasoning"></div>
@@ -6485,9 +6531,6 @@
64856531 </div>
64866532
64876533 <!-- chat and input bar -->
6488- <div id="typing_indicator_template" class="template_element">
6489- <div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
6490- </div>
64916534 <div id="message_file_template" class="template_element">
64926535 <div class="mes_file_container">
64936536 <div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
@@ -6847,8 +6890,8 @@
68476890 </div>
68486891 <div id="form_sheld">
68496892 <div id="dialogue_del_mes">
68506893 <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
68516894 <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
68526895 </div>
68536896 <div id="send_form" class="no-connection">
68546897 <form id="file_form" class="wide100p displayNone">
public/locales/ar-sa.json+1 -1
@@ -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)": "الطول المستهدف (رموز)",
public/locales/de-de.json+1 -1
@@ -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)",
public/locales/es-es.json+1 -1
@@ -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)",
public/locales/fr-fr.json+2 -2
@@ -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)",
@@ -1485,7 +1485,7 @@
14851485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
14861486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
14871487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
14881488 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",
14891489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
14901490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
14911491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",
public/locales/is-is.json+1 -1
@@ -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)",
public/locales/it-it.json+1 -1
@@ -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)",
public/locales/ja-jp.json+1 -1
@@ -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)": "ターゲット長さ(トークン)",
public/locales/ko-kr.json+10 -10
@@ -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.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",
@@ -1537,7 +1537,7 @@
15371537 "Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
15381538 "qr--colorClear": "색상 지우기",
15391539 "Color": "색상",
15401540 "[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
15411541 "Select TTS Provider": "TTS 공급자 선택",
15421542 "tts_enabled": "활성화",
15431543 "Narrate user messages": "사용자 메시지 나레이션",
@@ -1582,15 +1582,15 @@
15821582 "Prompt Content": "프롬프트 내용",
15831583 "Instruct Sequences": "지시 시퀀스",
15841584 "Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",
15851585 "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
15861586 "Auto-select Input Text": "입력 텍스트 자동 선택",
15871587 "[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.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
15881588 "Markdown Hotkeys": "마크다운 입력 단축키",
15891589 "[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
15901590 "Show group chat queue": "그룹 채팅 대기열 표시",
15911591 "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
15921592 "Quick 'Impersonate' button": "빠른 '사칭' 버튼",
15931593 "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
15941594 "Injection Template": "삽입 템플릿",
15951595 "Query messages": "쿼리 메시지 수",
15961596 "Score threshold": "점수 임계값",
public/locales/nl-nl.json+1 -1
@@ -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)",
public/locales/pt-pt.json+1 -1
@@ -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)",
public/locales/ru-ru.json+74 -14
@@ -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": "Начало чата",
@@ -195,7 +195,7 @@
195195 "Yes": "Да",
196196 "No": "Нет",
197197 "Context %": "Процент контекста",
198198 "Budget Cap": "БюджетныйЛимит лимитбюджета",
199199 "(0 = disabled)": "(0 = отключено)",
200200 "None": "Отсутствует",
201201 "User Settings": "Настройки пользователя",
@@ -575,10 +575,10 @@
575575 "Characters sorting order": "Порядок сортировки персонажей",
576576 "Remove": "Убрать",
577577 "Select a World Info file for": "Выбрать файл с миром для",
578578 "Primary Lorebook": "ОсновногоОсновной лорбукалорбук",
579579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
580580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
581581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
582582 "Additional Lorebooks": "Вспомогательные лорбуки",
583583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
584584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@@ -593,7 +593,7 @@
593593 "Prompt": "Промпт",
594594 "Copy": "Скопировать",
595595 "Confirm": "Подтвердить",
596596 "Copy this message": "СкопироватьПродублировать сообщение",
597597 "Delete this message": "Удалить сообщение",
598598 "Move message up": "Переместить сообщение вверх",
599599 "Move message down": "Переместить сообщение вниз",
@@ -612,7 +612,7 @@
612612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
613613 "Continue the last message": "Продолжить текущее сообщение",
614614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",
615615 "Select this as default persona for the new chats.": "ВыбератьВыбирать эту Персону в качестве персоныперсону по умолчанию для всех новых чатов.",
616616 "Change persona image": "Сменить аватар персоны",
617617 "Delete persona": "Удалить персону",
618618 "Reduced Motion": "Сокращение анимаций",
@@ -640,7 +640,7 @@
640640 "Token Probabilities": "Вероятности токенов",
641641 "Close chat": "Закрыть чат",
642642 "Manage chat files": "Все чаты",
643643 "Import Extension From Git Repo": "Импортировать расширение из Git Repository-репозитория.",
644644 "Install extension": "Установить расширение",
645645 "Manage extensions": "Управление расширениями",
646646 "Tokens persona description": "Токенов",
@@ -1122,7 +1122,7 @@
11221122 "help_hotkeys_0": "Горячие клавиши",
11231123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
11241124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",
11251125 "menu within": "внутри этихв кубиковменю",
11261126 "Assets URL": "URL с описанием ресурсов",
11271127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
11281128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@@ -1943,7 +1943,7 @@
19431943 "and connect to an": "и подключитесь к",
19441944 "You can add more": "Можете добавить больше",
19451945 "from other websites": "с других сайтов.",
19461946 "Go to the": "ЗаглянитеЗаходите в",
19471947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
19481948 "or_welcome": "; также доступен",
19491949 "Claude API Key": "Ключ от API Claude",
@@ -1958,7 +1958,7 @@
19581958 "Save": "Сохранить",
19591959 "Chat Lorebook": "Лорбук для чата",
19601960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
19611961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + кликЛКМ, чтобы открыть диалог привязки мира",
19621962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
19631963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",
19641964 "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2144,5 +2144,65 @@
21442144 "Not connected to the API!": "Нет соединения с API!",
21452145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
21462146 "Update all": "Обновить все",
21472147 "Close": "Закрыть",
2148-}
2148+ "Optional modules:": "Необязательные модули:",
2149+ "Sort: Display Name": "Сортировать: по названию",
2150+ "Sort: Loading Order": "Сортировать: в порядке загрузки",
2151+ "Click to toggle": "Нажмите, чтобы включить или выключить",
2152+ "Loading Asset List": "Загрузить список ресурсов",
2153+ "Don't ask again for this URL": "Запомнить выбор для этого адреса",
2154+ "Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
2155+ "All": "Всё",
2156+ "Characters": "Персонажи",
2157+ "Ambient sounds": "Звуковой эмбиент",
2158+ "Blip sounds": "Звуки уведомлений",
2159+ "Background music": "Фоновая музыка",
2160+ "Search": "Поиск",
2161+ "extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
2162+ "extension_install_2": ".",
2163+ "extension_install_3": "Нажмите на иконку ",
2164+ "extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
2165+ "Extension repo/guide:": "Репозиторий расширения:",
2166+ "Preview in browser": "Предпросмотр",
2167+ "Adds a function tool": "Частично или полностью работает через вызов функций",
2168+ "Tool": "Функции",
2169+ "Move extension": "Переместить расширение",
2170+ "ext_type_local": "Это локальное расширение, доступно только вам",
2171+ "ext_type_global": "Это глобальное расширение, доступно всем пользователям",
2172+ "Move": "Переместить",
2173+ "Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
2174+ "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.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
2175+ "Disclaimer:": "Внимание:",
2176+ "Example:": "Пример:",
2177+ "context_derived": "Считывать из метаданных модели (по возможности)",
2178+ "instruct_derived": "Считывать из метаданных модели (по возможности)",
2179+ "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
2180+ "Reasoning Effort": "Рассуждения",
2181+ "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
2182+ "openai_reasoning_effort_low": "Поверхностные",
2183+ "openai_reasoning_effort_medium": "Обычные",
2184+ "openai_reasoning_effort_high": "Подробные",
2185+ "Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
2186+ "Persona Lorebook for": "Лорбук для персоны",
2187+ "persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
2188+ "Global list": "Глобальный список",
2189+ "Preset-specific list": "Список для данного пресета",
2190+ "Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
2191+ "Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
2192+ "Add a reasoning block": "Добавить блок рассуждений",
2193+ "Create a copy of this message?": "Продублировать это сообщение?",
2194+ "Max Recursion Steps": "Макс. глубина рекурсии",
2195+ "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
2196+ "(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
2197+ "Enter a valid API URL": "Введите корректный адрес API",
2198+ "No Ollama model selected.": "Не выбрана модель Ollama",
2199+ "Background Fitting": "Способ подгонки фона под разрешение",
2200+ "Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
2201+ "Token Counter": "Подсчитать токены",
2202+ "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
2203+ "Selected tokenizer:": "Выбранный токенайзер:",
2204+ "Input:": "Входные данные:",
2205+ "Tokenized text:": "Токенизированный текст:",
2206+ "Token IDs:": "Идентификаторы токенов:",
2207+ "Tokens:": "Токенов:"
2208+}
2208 \ No newline at end of file
public/locales/uk-ua.json+1 -1
@@ -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)": "Цільова довжина (токени)",
public/locales/vi-vn.json+1 -1
@@ -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)",
public/locales/zh-cn.json+12 -12
@@ -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,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
@@ -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 字符串",
@@ -584,7 +584,7 @@
584584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",
585585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",
586586 "Max Recursion Steps": "最大递归深度",
587587 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)",
588588 "Insertion Strategy": "插入策略",
589589 "Sorted Evenly": "均匀排序",
590590 "Character Lore First": "角色世界书优先",
@@ -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 提示词级联",
@@ -1485,7 +1485,7 @@
14851485 "ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",
14861486 "Trim Out": "修剪掉",
14871487 "ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",
14881488 "ext_regex_affects": "影响作用范围",
14891489 "ext_regex_user_input_desc": "用户发送的消息",
14901490 "ext_regex_user_input": "用户输入",
14911491 "ext_regex_ai_input_desc": "从生成式API中获取的信息。",
@@ -1719,9 +1719,9 @@
17191719 "Chat Lorebook for": "聊天知识书",
17201720 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
17211721 "chat_rename_1": "输入聊天的新名称:",
17221722 "chat_rename_2": "注意!!使用已有文件名会导致错误与其他文件重名会导致错误!!",
17231723 "chat_rename_3": "此举会将次聊天与标记为此举会将此聊天与标记为“检查点”的聊天解绑。",
17241724 "chat_rename_4": "不需要在结尾添加 '.JSONL' 后缀)",
17251725 "Enter Checkpoint Name:": "输入检查点名称:",
17261726 "(Leave empty to auto-generate)": "(留空以自动生成)",
17271727 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",
@@ -1974,7 +1974,7 @@
19741974 "Enter your password below to confirm:": "输入您的密码以确认:",
19751975 "Chat Scenario Override": "聊天场景覆盖",
19761976 "Remove": "移除",
19771977 "Unique to this chat.": "Unique to this chat.仅对此聊天生效。",
19781978 "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.",
19791979 "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.",
19801980 "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.",
public/locales/zh-tw.json+3 -3
@@ -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)": "目標長度(符元)",
@@ -1458,7 +1458,7 @@
14581458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
14591459 "popup-button-crop": "裁剪",
14601460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
14611461 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",
14621462 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。",
14631463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
14641464 "Abort current image generation task": "終止目前的圖片生成任務",
@@ -1805,7 +1805,7 @@
18051805 "context_derived": "若可能,根據模型元數據推導。",
18061806 "instruct_derived": "若可能,根據模型元數據推導。",
18071807 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
18081808 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)",
18091809 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
18101810 "Manual": "手動",
18111811 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)",
public/script.js+210 -152
@@ -269,7 +269,8 @@ import { initSettingsSearch } from './scripts/setting-search.js';
269269import { initBulkEdit } from './scripts/bulk-edit.js';
270270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271271import { getContext } from './scripts/st-context.js';
272272import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
273+import { accountStorage } from './scripts/util/AccountStorage.js';
273274
274275// API OBJECT FOR EXTERNAL WIRING
275276globalThis.SillyTavern = {
@@ -365,6 +366,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
365366 return;
366367 }
367368
369+ if (!(node instanceof Element)) {
370+ return;
371+ }
372+
368373 let mediaBlocked = false;
369374
370375 switch (node.tagName) {
@@ -419,7 +424,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
419424 const entityId = getCurrentEntityId();
420425 const warningShownKey = `mediaWarningShown:${entityId}`;
421426
422427 if (localStorageaccountStorage.getItem(warningShownKey) === null) {
423428 const warningToast = toastr.warning(
424429 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
425430 t`External media has been blocked`,
@@ -430,7 +435,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
430435 },
431436 );
432437
433438 localStorageaccountStorage.setItem(warningShownKey, 'true');
434439 }
435440 }
436441});
@@ -492,9 +497,11 @@ export const event_types = {
492497 // TODO: Naming convention is inconsistent with other events
493498 CHARACTER_DELETED: 'characterDeleted',
494499 CHARACTER_DUPLICATED: 'character_duplicated',
500+ CHARACTER_RENAMED: 'character_renamed',
495501 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
496502 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
497503 STREAM_TOKEN_RECEIVED: 'stream_token_received',
504+ STREAM_REASONING_DONE: 'stream_reasoning_done',
498505 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
499506 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
500507 OPEN_CHARACTER_LIBRARY: 'open_character_library',
@@ -1025,12 +1032,22 @@ export function setAnimationDuration(ms = null) {
10251032 document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
10261033}
10271034
1035+/**
1036+ * Sets the currently active character
1037+ * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`.
1038+ */
10281039export function setActiveCharacter(entityOrKey) {
10291040 active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
1041+ if (active_character) active_group = null;
10301042}
10311043
1044+/**
1045+ * Sets the currently active group.
1046+ * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`.
1047+ */
10321048export function setActiveGroup(entityOrKey) {
10331049 active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
1050+ if (active_group) active_character = null;
10341051}
10351052
10361053/**
@@ -1489,7 +1506,7 @@ export async function printCharacters(fullRefresh = false) {
14891506
14901507 $('#rm_print_characters_pagination').pagination({
14911508 dataSource: entities,
14921509 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || per_page_default,
14931510 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
14941511 pageRange: 1,
14951512 pageNumber: saveCharactersPage || 1,
@@ -1533,7 +1550,7 @@ export async function printCharacters(fullRefresh = false) {
15331550 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
15341551 },
15351552 afterSizeSelectorChange: function (e) {
15361553 localStorageaccountStorage.setItem(storageKey, e.target.value);
15371554 },
15381555 afterPaging: function (e) {
15391556 saveCharactersPage = e;
@@ -2189,26 +2206,29 @@ function insertSVGIcon(mes, extra) {
21892206 modelName = extra.api;
21902207 }
21912208
2192- const image = new Image();
2209+ const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
2193- // Add classes for styling and identification
2210+ image.onload = async function () {
2194- image.classList.add('icon-svg', 'timestamp-icon');
2211+ let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`);
2195- image.src = `/img/${modelName}.svg`;
2212+ if (existingSVG.length) {
2196- image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2213+ existingSVG.replaceWith(image);
2197-
2214+ } else {
2198- image.onload = async function () {
2215+ if (insertBefore) mes.find(targetSelector).before(image);
2199- // Check if an SVG already exists adjacent to the timestamp
2216+ else mes.find(targetSelector).after(image);
2200- let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
2217+ }
2201-
2218+ await SVGInject(image);
2202- if (existingSVG.length) {
2219+ };
2203- // Replace existing SVG
2220+ };
2204- existingSVG.replaceWith(image);
2205- } else {
2206- // Append the new SVG if none exists
2207- mes.find('.timestamp').after(image);
2208- }
22092221
2210- await SVGInject(image);
2222+ const createModelImage = (className, targetSelector, insertBefore) => {
2223+ const image = new Image();
2224+ image.classList.add('icon-svg', className);
2225+ image.src = `/img/${modelName}.svg`;
2226+ image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
2227+ insertOrReplaceSVG(image, className, targetSelector, insertBefore);
22112228 };
2229+
2230+ createModelImage('timestamp-icon', '.timestamp');
2231+ createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
22122232}
22132233
22142234
@@ -2219,7 +2239,6 @@ function getMessageFromTemplate({
22192239 isUser,
22202240 avatarImg,
22212241 bias,
2222- reasoning,
22232242 isSystem,
22242243 title,
22252244 timerValue,
@@ -2244,7 +2263,6 @@ function getMessageFromTemplate({
22442263 mes.find('.avatar img').attr('src', avatarImg);
22452264 mes.find('.ch_name .name_text').text(characterName);
22462265 mes.find('.mes_bias').html(bias);
2247- mes.find('.mes_reasoning').html(reasoning);
22482266 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
22492267 mes.find('.mesIDDisplay').text(`#${mesId}`);
22502268 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2252,6 +2270,8 @@ function getMessageFromTemplate({
22522270 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
22532271 bookmarkLink && updateBookmarkDisplay(mes);
22542272
2273+ updateReasoningUI(mes);
2274+
22552275 if (power_user.timestamp_model_icon && extra?.api) {
22562276 insertSVGIcon(mes, extra);
22572277 }
@@ -2263,12 +2283,18 @@ function getMessageFromTemplate({
22632283 * Re-renders a message block with updated content.
22642284 * @param {number} messageId Message ID
22652285 * @param {object} message Message object
2286+ * @param {object} [options={}] Optional arguments
2287+ * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
22662288 */
22672289export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
22682290 const messageElement = $(`#chat [mesid="${messageId}"]`);
2269- const text = message?.extra?.display_text ?? message.mes;
2291+ if (rerenderMessage) {
2270- messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2292+ const text = message?.extra?.display_text ?? message.mes;
22712293 messageElement.find('.mes_reasoningmes_text').html(messageFormatting(message.extra?.reasoning ?? ''text, ''message.name, falsemessage.is_system, falsemessage.is_user, messageId, {}, truefalse));
2294+ }
2295+
2296+ updateReasoningUI(messageElement);
2297+
22722298 addCopyToCodeBlocks(messageElement);
22732299 appendMediaToMessage(message, messageElement);
22742300}
@@ -2428,7 +2454,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24282454 false,
24292455 );
24302456 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2431- const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
24322457 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24332458
24342459 let params = {
@@ -2438,7 +2463,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24382463 isUser: mes.is_user,
24392464 avatarImg: avatarImg,
24402465 bias: bias,
2441- reasoning: reasoning,
24422466 isSystem: isSystem,
24432467 title: title,
24442468 bookmarkLink: bookmarkLink,
@@ -2446,7 +2470,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24462470 timestamp: timestamp,
24472471 extra: mes.extra,
24482472 tokenCount: mes.extra?.token_count ?? 0,
24492473 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
24502474 };
24512475
24522476 const renderedMessage = getMessageFromTemplate(params);
@@ -2498,8 +2522,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24982522 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
24992523 swipeMessage.attr('swipeid', params.swipeId);
25002524 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2501- swipeMessage.find('.mes_reasoning').html(reasoning);
25022525 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2526+ updateReasoningUI(swipeMessage);
25032527 appendMediaToMessage(mes, swipeMessage);
25042528 if (power_user.timestamp_model_icon && params.extra?.api) {
25052529 insertSVGIcon(swipeMessage, params.extra);
@@ -2566,13 +2590,14 @@ export function formatCharacterAvatar(characterAvatar) {
25662590 * @param {Date} gen_started Date when generation was started
25672591 * @param {Date} gen_finished Date when generation was finished
25682592 * @param {number} tokenCount Number of tokens generated (0 if not available)
2593+ * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
25692594 * @returns {Object} Object containing the formatted timer value and title
25702595 * @example
25712596 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
25722597 * console.log(timerValue); // 1.2s
25732598 * 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
25742599 */
25752600function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
25762601 if (!gen_started || !gen_finished) {
25772602 return {};
25782603 }
@@ -2586,8 +2611,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
25862611 `Generation queued: ${start.format(dateFormat)}`,
25872612 `Reply received: ${finish.format(dateFormat)}`,
25882613 `Time to generate: ${seconds} seconds`,
2614+ reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
25892615 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
25902616 ].filter(x => x).join('\n').trim();
25912617
25922618 if (isNaN(seconds) || seconds < 0) {
25932619 return { timerValue: '', timerTitle };
@@ -2775,7 +2801,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
27752801 TempResponseLength.save(main_api, responseLength);
27762802 eventHook = TempResponseLength.setupEventHook(main_api);
27772803 }
27782804 returnconst result = await Generate('quiet', options);
2805+ return removeReasoningFromString(result);
27792806 } finally {
27802807 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
27812808 TempResponseLength.restore(main_api);
@@ -3075,8 +3102,8 @@ export function isStreamingEnabled() {
30753102 (main_api == 'openai' &&
30763103 oai_settings.stream_openai &&
30773104 !noStreamSources.includes(oai_settings.chat_completion_source) &&
30783105 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith(['o1-2024-12-17') ||, 'o1'].includes(oai_settings.openai_model.startsWith('o3'))) &&
3079- !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
3106+ )
30803107 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
30813108 || (main_api == 'novel' && nai_settings.streaming_novel)
30823109 || (main_api == 'textgenerationwebui' && textgen_settings.streaming));
@@ -3105,11 +3132,14 @@ class StreamingProcessor {
31053132 constructor(type, forceName2, timeStarted, continueMessage) {
31063133 this.result = '';
31073134 this.messageId = -1;
3135+ /** @type {HTMLElement} */
31083136 this.messageDom = null;
3137+ /** @type {HTMLElement} */
31093138 this.messageTextDom = null;
3139+ /** @type {HTMLElement} */
31103140 this.messageTimerDom = null;
3141+ /** @type {HTMLElement} */
31113142 this.messageTokenCounterDom = null;
3112- this.messageReasoningDom = null;
31133143 /** @type {HTMLTextAreaElement} */
31143144 this.sendTextarea = document.querySelector('#send_textarea');
31153145 this.type = type;
@@ -3125,7 +3155,8 @@ class StreamingProcessor {
31253155 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
31263156 this.messageLogprobs = [];
31273157 this.toolCalls = [];
3128- this.reasoning = '';
3158+ // Initialize reasoning in its own handler
3159+ this.reasoningHandler = new ReasoningHandler(timeStarted);
31293160 }
31303161
31313162 #checkDomElements(messageId) {
@@ -3134,8 +3165,8 @@ class StreamingProcessor {
31343165 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
31353166 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
31363167 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3137- this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
31383168 }
3169+ this.reasoningHandler.updateDom(messageId);
31393170 }
31403171
31413172 #updateMessageBlockVisibility() {
@@ -3145,22 +3176,12 @@ class StreamingProcessor {
31453176 }
31463177 }
31473178
31483179 showMessageButtonsmarkUIGenStarted(messageId) {
3149- if (messageId == -1) {
3180+ deactivateSendButtons();
3150- return;
3151- }
3152-
3153- showStopButton();
3154- $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
31553181 }
31563182
31573183 hideMessageButtonsmarkUIGenStopped(messageId) {
3158- if (messageId == -1) {
3184+ activateSendButtons();
3159- return;
3160- }
3161-
3162- hideStopButton();
3163- $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });
31643185 }
31653186
31663187 async onStartStreaming(text) {
@@ -3169,20 +3190,18 @@ class StreamingProcessor {
31693190 if (this.type == 'impersonate') {
31703191 this.sendTextarea.value = '';
31713192 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
31723193 } else {
3173- else {
31743194 await saveReply(this.type, text, true, '', [], '');
31753195 messageId = chat.length - 1;
31763196 this.#checkDomElements(messageId);
31773197 this.showMessageButtonsmarkUIGenStarted(messageId);
31783198 }
3179-
31803199 hideSwipeButtons();
31813200 scrollChatToBottom();
31823201 return messageId;
31833202 }
31843203
31853204 async onProgressStreaming(messageId, text, isFinal) {
31863205 const isImpersonate = this.type == 'impersonate';
31873206 const isContinue = this.type == 'continue';
31883207
@@ -3194,11 +3213,9 @@ class StreamingProcessor {
31943213
31953214 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
31963215
3197- // Predict unbalanced asterisks / quotes during streaming
31983216 const charsToBalance = ['*', '"', '```'];
31993217 for (const char of charsToBalance) {
32003218 if (!isFinal && isOdd(countOccurrences(processedText, char))) {
3201- // Add character at the end to balance it
32023219 const separator = char.length > 1 ? '\n' : '';
32033220 processedText = processedText.trimEnd() + separator + char;
32043221 }
@@ -3207,31 +3224,25 @@ class StreamingProcessor {
32073224 if (isImpersonate) {
32083225 this.sendTextarea.value = processedText;
32093226 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
32103227 } else {
3211- else {
3228+ const mesChanged = chat[messageId]['mes'] !== processedText;
32123229 this.#checkDomElements(messageId);
32133230 this.#updateMessageBlockVisibility();
32143231 const currentTime = new Date();
32153232 chat[messageId]['mes'] = processedText;
32163233 chat[messageId]['gen_started'] = this.timeStarted;
32173234 chat[messageId]['gen_finished'] = currentTime;
3218-
32193235 if (!chat[messageId]['extra']) {
32203236 chat[messageId]['extra'] = {};
32213237 }
32223238
3223- if (this.reasoning) {
3239+ // Update reasoning
3224- chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
3240+ await this.reasoningHandler.process(messageId, mesChanged);
3225- if (this.messageReasoningDom instanceof HTMLElement) {
3241+ processedText = chat[messageId]['mes'];
3226- const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3227- this.messageReasoningDom.innerHTML = formattedReasoning;
3228- }
3229- }
32303242
3231- // Don't waste time calculating token count for streaming
3243+ // Token count update.
32323244 const tokenCountText = (this.reasoningHandler.reasoning || '') + processedText;
32333245 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3234-
32353246 if (currentTokenCount) {
32363247 chat[messageId]['extra']['token_count'] = currentTokenCount;
32373248 if (this.messageTokenCounterDom instanceof HTMLElement) {
@@ -3257,7 +3268,7 @@ class StreamingProcessor {
32573268 this.messageTextDom.innerHTML = formattedText;
32583269 }
32593270
32603271 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
32613272 if (this.messageTimerDom instanceof HTMLElement) {
32623273 this.messageTimerDom.textContent = timePassed.timerValue;
32633274 this.messageTimerDom.title = timePassed.timerTitle;
@@ -3272,10 +3283,12 @@ class StreamingProcessor {
32723283 }
32733284
32743285 async onFinishStreaming(messageId, text) {
32753286 this.hideMessageButtonsmarkUIGenStopped(this.messageId);
32763287 await this.onProgressStreaming(messageId, text, true);
32773288 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
32783289
3290+ await this.reasoningHandler.finish(messageId);
3291+
32793292 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
32803293 const message = chat[messageId];
32813294 const swipeInfo = {
@@ -3315,7 +3328,7 @@ class StreamingProcessor {
33153328 this.abortController.abort();
33163329 this.isStopped = true;
33173330
33183331 this.hideMessageButtonsmarkUIGenStopped(this.messageId);
33193332 generatedPromptCache = '';
33203333 unblockGeneration();
33213334
@@ -3366,7 +3379,7 @@ class StreamingProcessor {
33663379 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
33673380 timestamps.push(Date.now());
33683381 if (this.isStopped || this.abortController.signal.aborted) {
33693382 return this.result;
33703383 }
33713384
33723385 this.toolCalls = toolCalls;
@@ -3375,9 +3388,10 @@ class StreamingProcessor {
33753388 if (logprobs) {
33763389 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33773390 }
3378- this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
3391+ // Get the updated reasoning string into the handler
3392+ this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
33793393 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33803394 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
33813395 }
33823396 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
33833397 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3453,7 +3467,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
34533467 break;
34543468 }
34553469 case 'textgenerationwebui':
34563470 generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
34573471 TempResponseLength.restore(api);
34583472 break;
34593473 case 'openai': {
@@ -4440,7 +4454,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44404454 // For prompt bit itemization
44414455 let mesSendString = '';
44424456
44434457 async function getCombinedPrompt(isNegative) {
44444458 // Only return if the guidance scale doesn't exist or the value is 1
44454459 // Also don't return if constructing the neutral prompt
44464460 if (isNegative && !useCfgPrompt) {
@@ -4467,7 +4481,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44674481 // TODO: Make all extension prompts use an array/splice method
44684482 const lengthDiff = mesSend.length - cfgPrompt.depth;
44694483 const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
4470- finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4484+ const cfgMessage = finalMesSend[cfgDepth];
4485+ if (cfgMessage) {
4486+ if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
4487+ finalMesSend[cfgDepth].extensionPrompts = [];
4488+ }
4489+ finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
4490+ }
44714491 }
44724492 }
44734493 }
@@ -4543,13 +4563,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45434563 };
45444564
45454565 // Before returning the combined prompt, give available context related information to all subscribers.
45464566 await eventSource.emitAndWaitemit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
45474567
45484568 // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
45494569 return !data.combinedPrompt ? combine() : data.combinedPrompt;
45504570 }
45514571
45524572 let finalPrompt = await getCombinedPrompt(false);
45534573
45544574 const eventData = { prompt: finalPrompt, dryRun: dryRun };
45554575 await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);
@@ -4583,8 +4603,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45834603 }
45844604 break;
45854605 case 'textgenerationwebui': {
45864606 const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null;
45874607 generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
45884608 break;
45894609 }
45904610 case 'novel': {
@@ -5510,7 +5530,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
55105530 toastr.info(t`Copied!`);
55115531 });
55125532
55135533 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
55145534 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
55155535 console.log(PromptArrayItemForRawPromptDisplay);
55165536 console.log(itemizedPrompts);
@@ -5518,6 +5538,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
55185538
55195539 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
55205540
5541+ // Mobile needs special handholding. The side-view on the popup wouldn't work,
5542+ // so we just show an additional popup for this.
5543+ if (isMobile()) {
5544+ const content = document.createElement('div');
5545+ content.classList.add('tokenItemizingMaintext');
5546+ content.innerText = rawPrompt;
5547+ const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5548+ await popup.show();
5549+ return;
5550+ }
5551+
55215552 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
55225553 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
55235554 rawPromptWrapper.innerText = rawPrompt;
@@ -5701,37 +5732,6 @@ function extractMessageFromData(data) {
57015732}
57025733
57035734/**
5704- * Extracts the reasoning from the response data.
5705- * @param {object} data Response data
5706- * @returns {string} Extracted reasoning
5707- */
5708-function extractReasoningFromData(data) {
5709- switch (main_api) {
5710- case 'textgenerationwebui':
5711- switch (textgen_settings.type) {
5712- case textgen_types.OPENROUTER:
5713- return data?.choices?.[0]?.reasoning ?? '';
5714- }
5715- break;
5716-
5717- case 'openai':
5718- if (!oai_settings.show_thoughts) break;
5719-
5720- switch (oai_settings.chat_completion_source) {
5721- case chat_completion_sources.DEEPSEEK:
5722- return data?.choices?.[0]?.message?.reasoning_content ?? '';
5723- case chat_completion_sources.OPENROUTER:
5724- return data?.choices?.[0]?.message?.reasoning ?? '';
5725- case chat_completion_sources.MAKERSUITE:
5726- return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5727- }
5728- break;
5729- }
5730-
5731- return '';
5732-}
5733-
5734-/**
57355735 * Extracts multiswipe swipes from the response data.
57365736 * @param {Object} data Response data
57375737 * @param {string} type Type of generation
@@ -5921,6 +5921,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
59215921 chat[chat.length - 1]['extra'] = {};
59225922 }
59235923
5924+ // Coerce null/undefined to empty string
5925+ if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {
5926+ chat[chat.length - 1]['extra']['reasoning'] = '';
5927+ }
5928+
5929+ if (!reasoning) {
5930+ reasoning = '';
5931+ }
5932+
59245933 let oldMessage = '';
59255934 const generationFinished = new Date();
59265935 const img = extractImageFromMessage(getMessage);
@@ -5937,6 +5946,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
59375946 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59385947 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
59395948 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5949+ chat[chat.length - 1]['extra']['reasoning_duration'] = null;
59405950 if (power_user.message_token_count_enabled) {
59415951 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
59425952 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5958,7 +5968,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
59585968 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
59595969 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59605970 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
59615971 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5972+ chat[chat.length - 1]['extra']['reasoning_duration'] = null;
59625973 if (power_user.message_token_count_enabled) {
59635974 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
59645975 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5978,6 +5989,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
59785989 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59795990 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
59805991 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5992+ // We don't know if the reasoning duration extended, so we don't update it here on purpose.
59815993 if (power_user.message_token_count_enabled) {
59825994 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
59835995 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@@ -5997,6 +6009,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
59976009 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
59986010 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
59996011 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
6012+ chat[chat.length - 1]['extra']['reasoning_duration'] = null;
60006013 if (power_user.trim_spaces) {
60016014 getMessage = getMessage.trim();
60026015 }
@@ -6137,20 +6150,21 @@ function extractImageFromMessage(getMessage) {
61376150 return { getMessage, image, title };
61386151}
61396152
6153+/**
6154+ * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
6155+ */
61406156export function activateSendButtons() {
61416157 is_send_press = false;
6142- $('#send_but').removeClass('displayNone');
6143- $('#mes_continue').removeClass('displayNone');
6144- $('#mes_impersonate').removeClass('displayNone');
6145- $('.mes_buttons:last').show();
61466158 hideStopButton();
6159+ delete document.body.dataset.generating;
61476160}
61486161
6162+/**
6163+ * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
6164+ */
61496165export function deactivateSendButtons() {
6150- $('#send_but').addClass('displayNone');
6151- $('#mes_continue').addClass('displayNone');
6152- $('#mes_impersonate').addClass('displayNone');
61536166 showStopButton();
6167+ document.body.dataset.generating = 'true';
61546168}
61556169
61566170export function resetChatState() {
@@ -6243,9 +6257,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats
62436257 const data = await response.json();
62446258 const newAvatar = data.avatar;
62456259
6246- // Replace tags list
6260+ const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar });
6261+ const newName = getCharaFilename(null, { manualAvatarKey: newAvatar });
6262+
6263+ // Replace other auxillery fields where was referenced by avatar key
6264+ // Tag List
62476265 renameTagKey(oldAvatar, newAvatar);
62486266
6267+ // Addtional lore books
6268+ const charLore = world_info.charLore?.find(x => x.name == oldName);
6269+ if (charLore) {
6270+ charLore.name = newName;
6271+ saveSettingsDebounced();
6272+ }
6273+
6274+ // Char-bound Author's Notes
6275+ const charNote = extension_settings.note.chara?.find(x => x.name == oldName);
6276+ if (charNote) {
6277+ charNote.name = newName;
6278+ saveSettingsDebounced();
6279+ }
6280+
6281+ // Update active character, if the current one was the currently active one
6282+ if (active_character === oldAvatar) {
6283+ active_character = newAvatar;
6284+ saveSettingsDebounced();
6285+ }
6286+
6287+ await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
6288+
62496289 // Reload characters list
62506290 await getCharacters();
62516291
@@ -6846,10 +6886,11 @@ export async function getSettings() {
68466886 $('#your_name').val(name1);
68476887 }
68486888
6889+ accountStorage.init(settings?.accountStorage);
68496890 await setUserControls(data.enable_accounts);
68506891
68516892 // Allow subscribers to mutate settings
68526893 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
68536894
68546895 //Load KoboldAI settings
68556896 koboldai_setting_names = data.koboldai_setting_names;
@@ -6946,7 +6987,7 @@ export async function getSettings() {
69466987 loadProxyPresets(settings);
69476988
69486989 // Allow subscribers to mutate settings
69496990 await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
69506991
69516992 // Set context size after loading power user (may override the max value)
69526993 $('#max_context').val(max_context);
@@ -7006,7 +7047,7 @@ export async function getSettings() {
70067047 }
70077048 await validateDisabledSamplers();
70087049 settingsReady = true;
70097050 await eventSource.emit(event_types.SETTINGS_LOADED);
70107051}
70117052
70127053function selectKoboldGuiPreset() {
@@ -7017,7 +7058,8 @@ function selectKoboldGuiPreset() {
70177058
70187059export async function saveSettings(loopCounter = 0) {
70197060 if (!settingsReady) {
70207061 console.warn('Settings not ready, abortingscheduling another save');
7062+ saveSettingsDebounced();
70217063 return;
70227064 }
70237065
@@ -7038,6 +7080,7 @@ export async function saveSettings(loopCounter = 0) {
70387080 url: '/api/settings/save',
70397081 data: JSON.stringify({
70407082 firstRun: firstRun,
7083+ accountStorage: accountStorage.getState(),
70417084 currentVersion: currentVersion,
70427085 username: name1,
70437086 active_character: active_character,
@@ -7103,8 +7146,10 @@ export function setGenerationParamsFromPreset(preset) {
71037146// Common code for message editor done and auto-save
71047147function updateMessage(div) {
71057148 const mesBlock = div.closest('.mes_block');
71067149 let text = mesBlock.find('.edit_textarea').val();
7107- const mes = chat[this_edit_mes_id];
7150+ ?? mesBlock.find('.mes_text').text();
7151+ const mesElement = div.closest('.mes');
7152+ const mes = chat[mesElement.attr('mesid')];
71087153
71097154 let regexPlacement;
71107155 if (mes.is_user) {
@@ -7223,6 +7268,11 @@ async function messageEditDone(div) {
72237268 appendMediaToMessage(mes, div.closest('.mes'));
72247269 addCopyToCodeBlocks(div.closest('.mes'));
72257270
7271+ const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible');
7272+ if (reasoningEditDone.length > 0) {
7273+ reasoningEditDone.trigger('click');
7274+ }
7275+
72267276 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
72277277 this_edit_mes_id = undefined;
72287278 await saveChatConditional();
@@ -7486,7 +7536,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
74867536 }
74877537
74887538 try {
74897539 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
74907540 const page = Math.floor(charIndex / perPage) + 1;
74917541 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
74927542 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7518,7 +7568,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
75187568 return;
75197569 }
75207570
75217571 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
75227572 const page = Math.floor(charIndex / perPage) + 1;
75237573 $('#rm_print_characters_pagination').pagination('go', page);
75247574 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -8723,11 +8773,6 @@ const swipe_right = () => {
87238773 easing: animation_easing,
87248774 queue: false,
87258775 complete: async function () {
8726- /*if (!selected_group) {
8727- var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
8728- typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
8729- } */
8730- /* $("#chat").append(typingIndicator); */
87318776 const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
87328777 //console.log(parseInt(chat[chat.length-1]['swipe_id']));
87338778 //console.log(chat[chat.length-1]['swipes'].length);
@@ -8738,7 +8783,7 @@ const swipe_right = () => {
87388783 // resets the timer
87398784 swipeMessage.find('.mes_timer').html('');
87408785 swipeMessage.find('.tokenCounterDisplay').text('');
8741- swipeMessage.find('.mes_reasoning').html('');
8786+ updateReasoningUI(swipeMessage, { reset: true });
87428787 } else {
87438788 //console.log('showing previously generated swipe candidate, or "..."');
87448789 //console.log('onclick right swipe calling addOneMessage');
@@ -8788,7 +8833,6 @@ const swipe_right = () => {
87888833 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
87898834 console.debug('caught here 2');
87908835 is_send_press = true;
8791- $('.mes_buttons:last').hide();
87928836 await Generate('swipe');
87938837 } else {
87948838 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
@@ -9390,6 +9434,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
93909434 continue;
93919435 }
93929436
9437+ accountStorage.removeItem(`AlertWI_${character.avatar}`);
9438+ accountStorage.removeItem(`AlertRegex_${character.avatar}`);
9439+ accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
93939440 delete tag_map[character.avatar];
93949441 select_rm_info('char_delete', character.name);
93959442
@@ -9592,8 +9639,8 @@ function addDebugFunctions() {
95929639 });
95939640
95949641 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
95959642 localStorageaccountStorage.setItem('RegenerateWithCtrlEnter', localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
95969643 toastr.info('Regenerate warning is now ' + (localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
95979644 });
95989645
95999646 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
@@ -10735,6 +10782,12 @@ jQuery(async function () {
1073510782 var edit_mes_id = $(this).closest('.mes').attr('mesid');
1073610783 this_edit_mes_id = edit_mes_id;
1073710784
10785+ // Also edit reasoning, if it exists
10786+ const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible');
10787+ if (reasoningEdit.length > 0) {
10788+ reasoningEdit.trigger('click');
10789+ }
10790+
1073810791 var text = chat[edit_mes_id]['mes'];
1073910792 if (chat[edit_mes_id]['is_user']) {
1074010793 this_edit_mes_chname = name1;
@@ -10868,6 +10921,11 @@ jQuery(async function () {
1086810921 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
1086910922 addCopyToCodeBlocks($(this).closest('.mes'));
1087010923
10924+ const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible');
10925+ if (reasoningEditDone.length > 0) {
10926+ reasoningEditDone.trigger('click');
10927+ }
10928+
1087110929 await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
1087210930 this_edit_mes_id = undefined;
1087310931 });
@@ -10931,7 +10989,7 @@ jQuery(async function () {
1093110989 });
1093210990
1093310991 $(document).on('click', '.mes_edit_copy', async function () {
1093410992 const confirmation = await callGenericPopup('t`Create a copy of this message?'`, POPUP_TYPE.CONFIRM);
1093510993 if (!confirmation) {
1093610994 return;
1093710995 }
@@ -11410,7 +11468,7 @@ jQuery(async function () {
1141011468 );
1141111469 break;*/
1141211470 default:
1141311471 await eventSource.emit('charManagementDropdown', target);
1141411472 }
1141511473 $('#char-management-dropdown').prop('selectedIndex', 0);
1141611474 });
@@ -11572,7 +11630,7 @@ jQuery(async function () {
1157211630
1157311631 $(document).on('click', '.open_characters_library', async function () {
1157411632 await getCharacters();
1157511633 await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
1157611634 });
1157711635
1157811636 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
public/scripts/RossAscends-mods.js+58 -30
@@ -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');
@@ -279,17 +280,32 @@ async function RA_autoloadchat() {
279280 // active character is the name, we should look it up in the character list and get the id
280281 if (active_character !== null && active_character !== undefined) {
281282 const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
282283 if (active_character_id !== null-1) {
283284 await selectCharacterById(String(active_character_id));
284285
285286 // Do a little tomfoolery to spoof the tag selector
286287 const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
287288 applyTagsOnCharacterSelect.call(selectedCharElement);
289+ } else {
290+ setActiveCharacter(null);
291+ saveSettingsDebounced();
292+ console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
288293 }
289294 }
290295
291296 if (active_group !== null && active_group !== undefined) {
292- await openGroupById(String(active_group));
297+ if (active_character) {
298+ console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
299+ setActiveGroup(null);
300+ saveSettingsDebounced();
301+ } else {
302+ const result = await openGroupById(String(active_group));
303+ if (!result) {
304+ setActiveGroup(null);
305+ saveSettingsDebounced();
306+ console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
307+ }
308+ }
293309 }
294310
295311 // if the character list hadn't been loaded yet, try again.
@@ -409,32 +425,34 @@ function RA_autoconnect(PrevApi) {
409425function OpenNavPanels() {
410426 if (!isMobile()) {
411427 //auto-open R nav if locked and previously open
412428 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('NavOpened') == 'true') {
413429 //console.log("RA -- clicking right nav to open");
414430 $('#rightNavDrawerIcon').click();
415431 }
416432
417433 //auto-open L nav if locked and previously open
418434 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('LNavOpened') == 'true') {
419435 console.debug('RA -- clicking left nav to open');
420436 $('#leftNavDrawerIcon').click();
421437 }
422438
423439 //auto-open WI if locked and previously open
424440 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('WINavOpened') == 'true') {
425441 console.debug('RA -- clicking WI to open');
426442 $('#WIDrawerIcon').click();
427443 }
428444 }
429445}
430446
447+const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
448+
431449function restoreUserInput() {
432450 if (!power_user.restore_user_input) {
433451 console.debug('restoreUserInput disabled');
434452 return;
435453 }
436454
437455 const userInput = LoadLocallocalStorage.getItem('userInput'getUserInputKey());
438456 if (userInput) {
439457 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440458 }
@@ -442,7 +460,8 @@ function restoreUserInput() {
442460
443461function saveUserInput() {
444462 const userInput = String($('#send_textarea').val());
445463 SaveLocallocalStorage.setItem('userInput'getUserInputKey(), userInput);
464+ console.debug('User Input -- ', userInput);
446465}
447466const saveUserInputDebounced = debounce(saveUserInput);
448467
@@ -739,7 +758,7 @@ export function initRossMods() {
739758
740759 //toggle pin class when lock toggle clicked
741760 $(RPanelPin).on('click', function () {
742761 SaveLocalaccountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743762 if ($(RPanelPin).prop('checked') == true) {
744763 //console.log('adding pin class to right nav');
745764 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +776,7 @@ export function initRossMods() {
757776 }
758777 });
759778 $(LPanelPin).on('click', function () {
760779 SaveLocalaccountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761780 if ($(LPanelPin).prop('checked') == true) {
762781 //console.log('adding pin class to Left nav');
763782 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +795,7 @@ export function initRossMods() {
776795 });
777796
778797 $(WIPanelPin).on('click', function () {
779798 SaveLocalaccountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780799 if ($(WIPanelPin).prop('checked') == true) {
781800 console.debug('adding pin class to WI');
782801 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +815,8 @@ export function initRossMods() {
796815 });
797816
798817 // read the state of right Nav Lock and apply to rightnav classlist
799818 $(RPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true');
800819 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true') {
801820 //console.log('setting pin class via local var');
802821 $(RightNavPanel).addClass('pinnedOpen');
803822 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +827,8 @@ export function initRossMods() {
808827 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809828 }
810829 // read the state of left Nav Lock and apply to leftnav classlist
811830 $(LPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('LNavLockOn') === 'true');
812831 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true') {
813832 //console.log('setting pin class via local var');
814833 $(LeftNavPanel).addClass('pinnedOpen');
815834 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +840,8 @@ export function initRossMods() {
821840 }
822841
823842 // read the state of left Nav Lock and apply to leftnav classlist
824843 $(WIPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('WINavLockOn') === 'true');
825844 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true') {
826845 //console.log('setting pin class via local var');
827846 $(WorldInfo).addClass('pinnedOpen');
828847 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +856,22 @@ export function initRossMods() {
837856 //save state of Right nav being open or closed
838857 $('#rightNavDrawerIcon').on('click', function () {
839858 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840859 SaveLocalaccountStorage.setItem('NavOpened', 'true');
841860 } else { SaveLocalaccountStorage.setItem('NavOpened', 'false'); }
842861 });
843862
844863 //save state of Left nav being open or closed
845864 $('#leftNavDrawerIcon').on('click', function () {
846865 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847866 SaveLocalaccountStorage.setItem('LNavOpened', 'true');
848867 } else { SaveLocalaccountStorage.setItem('LNavOpened', 'false'); }
849868 });
850869
851870 //save state of Left nav being open or closed
852871 $('#WorldInfo').on('click', function () {
853872 if (!$('#WorldInfo').hasClass('openIcon')) {
854873 SaveLocalaccountStorage.setItem('WINavOpened', 'true');
855874 } else { SaveLocalaccountStorage.setItem('WINavOpened', 'false'); }
856875 });
857876
858877 var chatbarInFocus = false;
@@ -868,8 +887,8 @@ export function initRossMods() {
868887 OpenNavPanels();
869888 }, 300);
870889
871890 $(SelectedCharacterTab).click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872891 $('#rm_button_characters').click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873892
874893 // when a char is selected from the list, save them as the auto-load character for next page load
875894
@@ -1063,14 +1082,21 @@ export function initRossMods() {
10631082 // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
10641083 if (event.ctrlKey && event.key == 'Enter') {
10651084 const editMesDone = $('.mes_edit_done:visible');
1085+ const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
10661086 if (editMesDone.length > 0) {
10671087 console.debug('Accepting edits with Ctrl+Enter');
10681088 $('#send_textarea').focustrigger('focus');
10691089 editMesDone.trigger('click');
10701090 return;
10711091 } else if (is_send_pressreasoningMesDone.length ==> false0) {
1092+ console.debug('Accepting edits with Ctrl+Enter');
1093+ $('#send_textarea').trigger('focus');
1094+ reasoningMesDone.trigger('click');
1095+ return;
1096+ }
1097+ else if (is_send_press == false) {
10721098 const skipConfirmKey = 'RegenerateWithCtrlEnter';
10731099 const skipConfirm = LoadLocalBoolaccountStorage.getItem(skipConfirmKey) === 'true';
10741100 function doRegenerate() {
10751101 console.debug('Regenerating with Ctrl+Enter');
10761102 $('#option_regenerate').trigger('click');
@@ -1082,13 +1108,15 @@ export function initRossMods() {
10821108 let regenerateWithCtrlEnter = false;
10831109 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
10841110 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
10851111 onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,{
1112+ regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
1113+ },
10861114 });
10871115 if (!result) {
10881116 return;
10891117 }
10901118
10911119 SaveLocalaccountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
10921120 doRegenerate();
10931121 }
10941122 return;
public/scripts/chat-templates.js+2 -1
@@ -69,6 +69,7 @@ const hash_derivations = {
6969 // DeepSeek R1
7070 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
7171 'DeepSeek-V2.5'
72+ ,
7273};
7374
7475const substr_derivations = {
@@ -97,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
9798 }
9899 }
99100
100101 console.logwarn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
101102 return null;
102103}
public/scripts/chats.js+48 -12
@@ -45,6 +45,7 @@ import { DragAndDropHandler } from './dragdrop.js';
4545import { renderTemplateAsync } from './templates.js';
4646import { t } from './i18n.js';
4747import { humanizedDateTime } from './RossAscends-mods.js';
48+import { accountStorage } from './util/AccountStorage.js';
4849
4950/**
5051 * @typedef {Object} FileAttachment
@@ -621,21 +622,56 @@ async function enlargeMessageImage() {
621622}
622623
623624async function deleteMessageImage() {
624625 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+ });
625640
626641 if (value !== POPUP_RESULT.AFFIRMATIVEvalue) {
627642 return;
628643 }
629644
630645 const mesBlock = $(this).closest('.mes');
631646 const mesId = mesBlock.attr('mesid');
632647 const message = chat[mesId];
633- delete message.extra.image;
648+
634- delete message.extra.inline_image;
649+ let isLastImage = true;
635- delete message.extra.title;
650+
636- delete message.extra.append_title;
651+ if (Array.isArray(message.extra.image_swipes)) {
637- mesBlock.find('.mes_img_container').removeClass('img_extra');
652+ const indexOf = message.extra.image_swipes.indexOf(message.extra.image);
638- 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+
639675 await saveChatConditional();
640676}
641677
@@ -1043,8 +1079,8 @@ async function openAttachmentManager() {
10431079 renderAttachments();
10441080 });
10451081
10461082 let sortField = localStorageaccountStorage.getItem('DataBank_sortField') || 'created';
10471083 let sortOrder = localStorageaccountStorage.getItem('DataBank_sortOrder') || 'desc';
10481084 let filterString = '';
10491085
10501086 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@@ -1060,8 +1096,8 @@ async function openAttachmentManager() {
10601096
10611097 sortField = this.selectedOptions[0].dataset.sortField;
10621098 sortOrder = this.selectedOptions[0].dataset.sortOrder;
10631099 localStorageaccountStorage.setItem('DataBank_sortField', sortField);
10641100 localStorageaccountStorage.setItem('DataBank_sortOrder', sortOrder);
10651101 renderAttachments();
10661102 });
10671103 function handleBulkAction(action) {
public/scripts/extensions.js+9 -8
@@ -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,
@@ -612,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
612613 }
613614
614615 let toggleElement = isActive || isDisabled ?
615616 `'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
616617 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
617618
618619 let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
619620 let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
620621 let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
621622 let modulesInfo = '';
622623
623624 if (isActive && Array.isArray(manifest.optional)) {
@@ -625,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
625626 modules.forEach(x => optional.delete(x));
626627 if (optional.size > 0) {
627628 const optionalString = DOMPurify.sanitize([...optional].join(', '));
628629 modulesInfo = `'<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
629630 }
630631 } else if (!isDisabled) { // Neither active nor disabled
631632 const requirements = new Set(manifest.requires);
@@ -724,7 +725,7 @@ async function showExtensionsDetails() {
724725 htmlExternal.append(htmlLoading);
725726
726727 const sortOrderKey = 'extensions_sortByName';
727728 const sortByName = localStorageaccountStorage.getItem(sortOrderKey) === 'true';
728729 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
729730 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
730731
@@ -755,7 +756,7 @@ async function showExtensionsDetails() {
755756 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
756757 action: async () => {
757758 abortController.abort();
758759 localStorageaccountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
759760 await showExtensionsDetails();
760761 },
761762 };
@@ -1163,11 +1164,11 @@ async function checkForExtensionUpdates(force) {
11631164 const currentDate = new Date().toDateString();
11641165
11651166 // Don't nag more than once a day
11661167 if (localStorageaccountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
11671168 return;
11681169 }
11691170
11701171 localStorageaccountStorage.setItem(STORAGE_NAG_KEY, currentDate);
11711172 }
11721173
11731174 const isCurrentUserAdmin = isAdmin();
public/scripts/extensions/assets/index.js+16 -17
@@ -8,7 +8,9 @@ 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';
13+import { t } from '../../i18n.js';
1214export { MODULE_NAME };
1315
1416const MODULE_NAME = 'assets';
@@ -58,11 +60,11 @@ const KNOWN_TYPES = {
5860 'blip': 'Blip sounds',
5961};
6062
6163async function downloadAssetsList(url) {
6264 updateCurrentAssets().then(async function () {
6365 fetch(url, { cache: 'no-cache' })
6466 .then(response => response.json())
6567 .then(jsonasync =>function(json) {
6668
6769 availableAssets = {};
6870 $('#assets_menu').empty();
@@ -83,10 +85,10 @@ function downloadAssetsList(url) {
8385
8486 $('#assets_type_select').empty();
8587 $('#assets_search').val('');
8688 $('#assets_type_select').append($('<option />', { value: '', text: 't`All'` }));
8789
8890 for (const type of assetTypes) {
8991 const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
9092 $('#assets_type_select').append(option);
9193 }
9294
@@ -103,11 +105,7 @@ function downloadAssetsList(url) {
103105 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
104106
105107 if (assetType == 'extension') {
106- assetTypeMenu.append(`
108+ assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
107- <div class="assets-list-git">
108- To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
109- Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
110- </div>`);
111109 }
112110
113111 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
@@ -183,7 +181,7 @@ function downloadAssetsList(url) {
183181 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
184182 const description = DOMPurify.sanitize(asset['description'] || '');
185183 const url = isValidUrl(asset['url']) ? asset['url'] : '';
186184 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : 't`Preview in browser'`;
187185 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
188186 const toolTag = assetType === 'extension' && asset['tool'];
189187
@@ -194,9 +192,10 @@ function downloadAssetsList(url) {
194192 <b>${displayName}</b>
195193 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
196194 <i class="fa-solid fa-sm ${previewIcon}"></i>
197195 </a>` +
198196 ${(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}+
199- </span>
197+ t`Tool` + '</span>' : '') +
198+ `</span>
200199 <small class="asset-description">
201200 ${description}
202201 </small>
@@ -432,14 +431,14 @@ jQuery(async () => {
432431 connectButton.on('click', async function () {
433432 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
434433 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
435434 const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true';
436435
437436 const confirmation = skipConfirm || await Popup.show.confirm('t`Loading Asset List'`, `'<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
438437 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
439438 onClose: popup => {
440439 if (popup.result) {
441440 const rememberValue = popup.inputResults.get('assets-remember');
442441 localStorageaccountStorage.setItem(rememberKey, String(rememberValue));
443442 }
444443 },
445444 });
public/scripts/extensions/assets/installation.html+4 -0
@@ -0,0 +1,4 @@
1+<div class="assets-list-git">
2+ <span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
3+ <span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
4+</div>
4 \ No newline at end of file
public/scripts/extensions/assets/window.html+1 -1
@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
3333 <div id="assets_filters" class="flex-container">
3434 <select id="assets_type_select" class="text_pole flex1">
3535 </select>
3636 <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
3737 <div id="assets-characters-button" class="menu_button menu_button_icon">
3838 <i class="fa-solid fa-image-portrait"></i>
3939 <span data-i18n="Characters">Characters</span>
public/scripts/extensions/caption/settings.html+7 -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,6 +53,12 @@
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>
5763 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
5864 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
public/scripts/extensions/expressions/index.js+0 -1
@@ -2190,7 +2190,6 @@ function migrateSettings() {
21902190 description: 'Character name - or unique character identifier (avatar key). If not provided, the current character for this chat will be used (does not work in group chats)',
21912191 typeList: [ARGUMENT_TYPE.STRING],
21922192 enumProvider: commonEnumProviders.characters('character'),
2193- forceEnum: true,
21942193 }),
21952194 ],
21962195 helpString: 'Returns the last set 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/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/index.js+3 -2
@@ -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
@@ -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/token-counter/index.js+6 -20
@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
66import { resetScrollHeight, debounce } from '../../utils.js';
77import { debounce_timeout } from '../../constants.js';
88import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
9+import { renderExtensionTemplateAsync } from '../../extensions.js';
10+import { t } from '../../i18n.js';
911
1012function rgb2hex(rgb) {
1113 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
2224
2325async function doTokenCounter() {
2426 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
25- const html = `
27+ const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
26- <div class="wide100p">
27- <h3>Token Counter</h3>
28- <div class="justifyLeft flex-container flexFlowColumn">
29- <h4>Type / paste in the box below to see the number of tokens in the text.</h4>
30- <p>Selected tokenizer: ${tokenizerName}</p>
31- <div>Input:</div>
32- <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
33- <div>Tokens: <span id="token_counter_result">0</span></div>
34- <hr>
35- <div>Tokenized text:</div>
36- <div id="tokenized_chunks_display" class="wide100p">—</div>
37- <hr>
38- <div>Token IDs:</div>
39- <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
40- </div>
41- </div>`;
4228
4329 const dialog = $(html);
4430 const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
131117jQuery(() => {
132118 const buttonHtml = `
133119 <div id="token_counter" class="list-group-item flex-container flexGap5">
134120 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
135121 t`Token Counter` +
136122 '</div>`';
137123 $('#token_counter_wand_container').append(buttonHtml);
138124 $('#token_counter').on('click', doTokenCounter);
139125 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
public/scripts/extensions/token-counter/window.html+16 -0
@@ -0,0 +1,16 @@
1+<div class="wide100p">
2+ <h3 data-i18n="Token Counter">Token Counter</h3>
3+ <div class="justifyLeft flex-container flexFlowColumn">
4+ <h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4>
5+ <p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
6+ <div data-i18n="Input:">Input:</div>
7+ <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
8+ <div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
9+ <hr>
10+ <div data-i18n="Tokenized text:">Tokenized text:</div>
11+ <div id="tokenized_chunks_display" class="wide100p">—</div>
12+ <hr>
13+ <div data-i18n="Token IDs:">Token IDs:</div>
14+ <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
15+ </div>
16+</div>
16 \ No newline at end of file
public/scripts/extensions/tts/openai-compatible.js+3 -3
@@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
2525 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
2626 <div class="flex-container alignItemsCenter">
2727 <div class="flex1">
2828 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250500" value="${this.defaultSettings.provider_endpoint}"/>
2929 </div>
3030 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
3131 <i class="fa-solid fa-key"></i>
@@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
3333 </div>
3434 </div>
3535 <label for="openai_compatible_model">Model:</label>
3636 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250500" value="${this.defaultSettings.model}"/>
3737 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
3838 <input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>
3939 <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
4040 <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
4141 return html;
public/scripts/extensions/vectors/index.js+60 -68
@@ -746,6 +746,44 @@ async function getQueryText(chat, initiator) {
746746}
747747
748748/**
749+ * Gets common body parameters for vector requests.
750+ * @returns {object}
751+ */
752+function getVectorsRequestBody() {
753+ const body = {};
754+ switch (settings.source) {
755+ case 'extras':
756+ body.extrasUrl = extension_settings.apiUrl;
757+ body.extrasKey = extension_settings.apiKey;
758+ break;
759+ case 'togetherai':
760+ body.model = extension_settings.vectors.togetherai_model;
761+ break;
762+ case 'openai':
763+ body.model = extension_settings.vectors.openai_model;
764+ break;
765+ case 'cohere':
766+ body.model = extension_settings.vectors.cohere_model;
767+ break;
768+ case 'ollama':
769+ body.model = extension_settings.vectors.ollama_model;
770+ body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
771+ body.keep = !!extension_settings.vectors.ollama_keep;
772+ break;
773+ case 'llamacpp':
774+ body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
775+ break;
776+ case 'vllm':
777+ body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
778+ body.model = extension_settings.vectors.vllm_model;
779+ break;
780+ default:
781+ break;
782+ }
783+ return body;
784+}
785+
786+/**
749787 * Gets the saved hashes for a collection
750788* @param {string} collectionId
751789* @returns {Promise<number[]>} Saved hashes
@@ -753,8 +791,9 @@ async function getQueryText(chat, initiator) {
753791async function getSavedHashes(collectionId) {
754792 const response = await fetch('/api/vector/list', {
755793 method: 'POST',
756794 headers: getVectorHeadersgetRequestHeaders(),
757795 body: JSON.stringify({
796+ ...getVectorsRequestBody(),
758797 collectionId: collectionId,
759798 source: settings.source,
760799 }),
@@ -768,54 +807,6 @@ async function getSavedHashes(collectionId) {
768807 return hashes;
769808}
770809
771-function getVectorHeaders() {
772- const headers = getRequestHeaders();
773- switch (settings.source) {
774- case 'extras':
775- Object.assign(headers, {
776- 'X-Extras-Url': extension_settings.apiUrl,
777- 'X-Extras-Key': extension_settings.apiKey,
778- });
779- break;
780- case 'togetherai':
781- Object.assign(headers, {
782- 'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
783- });
784- break;
785- case 'openai':
786- Object.assign(headers, {
787- 'X-OpenAI-Model': extension_settings.vectors.openai_model,
788- });
789- break;
790- case 'cohere':
791- Object.assign(headers, {
792- 'X-Cohere-Model': extension_settings.vectors.cohere_model,
793- });
794- break;
795- case 'ollama':
796- Object.assign(headers, {
797- 'X-Ollama-Model': extension_settings.vectors.ollama_model,
798- 'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
799- 'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
800- });
801- break;
802- case 'llamacpp':
803- Object.assign(headers, {
804- 'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
805- });
806- break;
807- case 'vllm':
808- Object.assign(headers, {
809- 'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
810- 'X-Vllm-Model': extension_settings.vectors.vllm_model,
811- });
812- break;
813- default:
814- break;
815- }
816- return headers;
817-}
818-
819810/**
820811 * Inserts vector items into a collection
821812 * @param {string} collectionId - The collection to insert into
@@ -825,12 +816,11 @@ function getVectorHeaders() {
825816async function insertVectorItems(collectionId, items) {
826817 throwIfSourceInvalid();
827818
828- const headers = getVectorHeaders();
829-
830819 const response = await fetch('/api/vector/insert', {
831820 method: 'POST',
832821 headers: headersgetRequestHeaders(),
833822 body: JSON.stringify({
823+ ...getVectorsRequestBody(),
834824 collectionId: collectionId,
835825 items: items,
836826 source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
879869async function deleteVectorItems(collectionId, hashes) {
880870 const response = await fetch('/api/vector/delete', {
881871 method: 'POST',
882872 headers: getVectorHeadersgetRequestHeaders(),
883873 body: JSON.stringify({
874+ ...getVectorsRequestBody(),
884875 collectionId: collectionId,
885876 hashes: hashes,
886877 source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
899890 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
900891 */
901892async function queryCollection(collectionId, searchText, topK) {
902- const headers = getVectorHeaders();
903-
904893 const response = await fetch('/api/vector/query', {
905894 method: 'POST',
906895 headers: headersgetRequestHeaders(),
907896 body: JSON.stringify({
897+ ...getVectorsRequestBody(),
908898 collectionId: collectionId,
909899 searchText: searchText,
910900 topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
929919 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
930920 */
931921async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
932- const headers = getVectorHeaders();
933-
934922 const response = await fetch('/api/vector/query-multi', {
935923 method: 'POST',
936924 headers: headersgetRequestHeaders(),
937925 body: JSON.stringify({
926+ ...getVectorsRequestBody(),
938927 collectionIds: collectionIds,
939928 searchText: searchText,
940929 topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
965954
966955 const response = await fetch('/api/vector/purge', {
967956 method: 'POST',
968957 headers: getVectorHeadersgetRequestHeaders(),
969958 body: JSON.stringify({
959+ ...getVectorsRequestBody(),
970960 collectionId: collectionId,
971961 }),
972962 });
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
994984
995985 const response = await fetch('/api/vector/purge', {
996986 method: 'POST',
997987 headers: getVectorHeadersgetRequestHeaders(),
998988 body: JSON.stringify({
989+ ...getVectorsRequestBody(),
999990 collectionId: collectionId,
1000991 }),
1001992 });
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
10191010 try {
10201011 const response = await fetch('/api/vector/purge-all', {
10211012 method: 'POST',
10221013 headers: getVectorHeadersgetRequestHeaders(),
1014+ body: JSON.stringify({
1015+ ...getVectorsRequestBody(),
1016+ }),
10231017 });
10241018
10251019 if (!response.ok) {
@@ -1621,14 +1615,14 @@ jQuery(async () => {
16211615 const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
16221616 const collectionIds = await ingestDataBankAttachments(String(source));
16231617 const queryResults = await queryMultipleCollections(collectionIds, String(query), count, threshold);
1624-
1618+
16251619 // Get URLs
16261620 const urls = Object
16271621 .keys(queryResults)
16281622 .map(x => attachments.find(y => getFileCollectionId(y.url) === x))
16291623 .filter(x => x)
16301624 .map(x => x.url);
1631-
1625+
16321626 // Gets the actual text content of chunks
16331627 const getChunksText = () => {
16341628 let textResult = '';
@@ -1638,14 +1632,12 @@ jQuery(async () => {
16381632 }
16391633 return textResult;
16401634 };
1641-
16421635 if (args.return === 'chunks') {
16431636 return getChunksText();
16441637 }
16451638
16461639 // @ts-ignore
16471640 return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
1648-
16491641 },
16501642 aliases: ['databank-search', 'data-bank-search'],
16511643 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 +1652,10 @@ jQuery(async () => {
16601652 defaultValue: 'object',
16611653 enumList: [
16621654 new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
16631655 ...slashCommandReturnHelper.enumList({ allowObject: true }),
16641656 ],
16651657 forceEnum: true,
16661658 }),
16671659 ],
16681660 unnamedArgumentList: [
16691661 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+37 -39
@@ -78,6 +78,7 @@ 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,
@@ -292,10 +293,11 @@ export function getGroupNames() {
292293
293294/**
294295 * Finds the character ID for a group member.
295296 * @param {number|string} arg 0-based member index or character name
296- * @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
297299 */
298300export function findGroupMemberId(arg, full = false) {
299301 arg = arg?.trim();
300302
301303 if (!arg) {
@@ -311,15 +313,19 @@ export function findGroupMemberId(arg) {
311313 }
312314
313315 const index = parseInt(arg);
314316 const searchByNamesearchByString = isNaN(index);
315317
316318 if (searchByNamesearchByString) {
317- 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 => ({
318- 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'] });
319325 const result = fuse.search(arg);
320326
321327 if (!result.length) {
322328 console.warn(`WARN: No group member found withusing namestring ${arg}`);
323329 return;
324330 }
325331
@@ -330,9 +336,11 @@ export function findGroupMemberId(arg) {
330336 return;
331337 }
332338
333339 console.log(`TriggeringTargeting group member ${chid} (${arg}) from search result`, result[0]);
334- return chid;
340+
335- } else {
341+ return !full ? chid : { ...{ id: chid }, ...result[0].item };
342+ }
343+ else {
336344 const memberAvatar = group.members[index];
337345
338346 if (memberAvatar === undefined) {
@@ -347,8 +355,14 @@ export function findGroupMemberId(arg) {
347355 return;
348356 }
349357
350358 console.log(`TriggeringTargeting group member ${memberAvatar} at index ${index}`);
351- 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+ };
352366 }
353367}
354368
@@ -805,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
805819
806820 /** @type {any} Caution: JS war crimes ahead */
807821 let textResult = '';
808- let typingIndicator = $('#chat .typing_indicator');
809822 const group = groups.find((x) => x.id === selected_group);
810823
811824 if (!group || !Array.isArray(group.members) || !group.members.length) {
@@ -821,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
821834 setCharacterId(undefined);
822835 const userInput = String($('#send_textarea').val());
823836
824- if (typingIndicator.length === 0 && !isStreamingEnabled()) {
825- typingIndicator = $(
826- '#typing_indicator_template .typing_indicator',
827- ).clone();
828- typingIndicator.hide();
829- $('#chat').append(typingIndicator);
830- }
831-
832837 // id of this specific batch for regeneration purposes
833838 group_generation_id = Date.now();
834839 const lastMessage = chat[chat.length - 1];
@@ -906,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
906911 }
907912 await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
908913
909- if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
910- // update indicator and scroll down
911- typingIndicator
912- .find('.typing_indicator_name')
913- .text(characters[chId].name);
914- typingIndicator.show();
915- }
916-
917914 // Wait for generation to finish
918915 textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
919916 let messageChunk = textResult?.messageChunk;
@@ -930,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
930927 }
931928 }
932929 } finally {
933- typingIndicator.hide();
934-
935930 is_group_generating = false;
936931 setSendButtonState(false);
937932 setCharacterId(undefined);
@@ -1315,10 +1310,10 @@ function printGroupCandidates() {
13151310 formatNavigator: PAGINATION_TEMPLATE,
13161311 showNavigator: true,
13171312 showSizeChanger: true,
13181313 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5,
13191314 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
13201315 afterSizeSelectorChange: function (e) {
13211316 localStorageaccountStorage.setItem(storageKey, e.target.value);
13221317 },
13231318 callback: function (data) {
13241319 $('#rm_group_add_members').empty();
@@ -1342,10 +1337,10 @@ function printGroupMembers() {
13421337 formatNavigator: PAGINATION_TEMPLATE,
13431338 showNavigator: true,
13441339 showSizeChanger: true,
13451340 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5,
13461341 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
13471342 afterSizeSelectorChange: function (e) {
13481343 localStorageaccountStorage.setItem(storageKey, e.target.value);
13491344 },
13501345 callback: function (data) {
13511346 $('.rm_group_members').empty();
@@ -1669,12 +1664,12 @@ function updateFavButtonState(state) {
16691664export async function openGroupById(groupId) {
16701665 if (isChatSaving) {
16711666 toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
16721667 return false;
16731668 }
16741669
16751670 if (!groups.find(x => x.id === groupId)) {
16761671 console.log('Group not found', groupId);
16771672 return false;
16781673 }
16791674
16801675 if (!is_send_press && !is_group_generating) {
@@ -1691,8 +1686,11 @@ export async function openGroupById(groupId) {
16911686 updateChatMetadata({}, true);
16921687 chat.length = 0;
16931688 await getGroupChat(groupId);
1689+ return true;
16941690 }
16951691 }
1692+
1693+ return false;
16961694}
16971695
16981696function openCharacterDefinition(characterSelect) {
public/scripts/openai.js+57 -31
@@ -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,
@@ -259,7 +259,7 @@ const default_settings = {
259259 mistralai_model: 'mistral-large-latest',
260260 cohere_model: 'command-r-plus',
261261 perplexity_model: 'sonar-pro',
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',
@@ -299,6 +299,7 @@ const default_settings = {
299299 continue_postfix: continue_postfix_types.SPACE,
300300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301301 show_thoughts: true,
302+ reasoning_effort: 'medium',
302303 seed: -1,
303304 n: 1,
304305};
@@ -378,6 +379,7 @@ const oai_settings = {
378379 continue_postfix: continue_postfix_types.SPACE,
379380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380381 show_thoughts: true,
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/**
@@ -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')) && !(isOAI &&, (oai_settings.openai_model.startsWith('o1') || ].includes(oai_settings.openai_model.startsWith('o3')));
18731873 const useLogprobs = !!power_user.request_token_probabilities;
18741874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1914,8 +1914,13 @@ async function sendOpenAIRequest(type, messages, signal) {
19141914 'char_name': name2,
19151915 'group_names': getGroupNames(),
19161916 'include_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;
@@ -2039,6 +2044,8 @@ async function sendOpenAIRequest(type, messages, signal) {
20392044 delete generate_data.top_logprobs;
20402045 delete generate_data.logprobs;
20412046 delete generate_data.logit_bias;
2047+ delete generate_data.tools;
2048+ delete generate_data.tool_choice;
20422049 }
20432050 }
20442051
@@ -2046,10 +2053,6 @@ async function sendOpenAIRequest(type, messages, signal) {
20462053 generate_data['seed'] = oai_settings.seed;
20472054 }
20482055
2049- if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
2050- await ToolManager.registerFunctionToolsOpenAI(generate_data);
2051- }
2052-
20532056 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
20542057 generate_data.messages.forEach((msg) => {
20552058 if (msg.role === 'system') {
@@ -2058,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) {
20582061 });
20592062 generate_data.max_completion_tokens = generate_data.max_tokens;
20602063 delete generate_data.max_tokens;
2061- delete generate_data.stream;
20622064 delete generate_data.logprobs;
20632065 delete generate_data.top_logprobs;
20642066 delete generate_data.n;
@@ -2069,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20692071 delete generate_data.tools;
20702072 delete generate_data.tool_choice;
20712073 delete generate_data.stop;
2072- // It does support logit_bias, but the tokenizer used and its effect is yet unknown.
2074+ delete generate_data.logit_bias;
2073- // delete generate_data.logit_bias;
20742075 }
20752076
20762077 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -3124,6 +3125,7 @@ function loadOpenAISettings(data, settings) {
31243125 oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
31253126 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
31263127 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
3128+ oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
31273129 oai_settings.seed = settings.seed ?? default_settings.seed;
31283130 oai_settings.n = settings.n ?? default_settings.n;
31293131
@@ -3253,6 +3255,9 @@ function loadOpenAISettings(data, settings) {
32533255 $('#n_openai').val(oai_settings.n);
32543256 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
32553257
3258+ $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
3259+ $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
3260+
32563261 if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
32573262 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
32583263
@@ -3513,6 +3518,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
35133518 continue_postfix: settings.continue_postfix,
35143519 function_calling: settings.function_calling,
35153520 show_thoughts: settings.show_thoughts,
3521+ reasoning_effort: settings.reasoning_effort,
35163522 seed: settings.seed,
35173523 n: settings.n,
35183524 };
@@ -3971,6 +3977,7 @@ function onSettingsPresetChange() {
39713977 continue_postfix: ['#continue_postfix', 'continue_postfix', false],
39723978 function_calling: ['#openai_function_calling', 'function_calling', true],
39733979 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
3980+ reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
39743981 seed: ['#seed_openai', 'seed', false],
39753982 n: ['#n_openai', 'n', false],
39763983 };
@@ -4232,9 +4239,9 @@ async function onModelChange() {
42324239 $('#openai_max_context').attr('max', max_2mil);
42334240 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
42344241 $('#openai_max_context').attr('max', max_32k);
42354242 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
42364243 $('#openai_max_context').attr('max', max_2mil);
42374244 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {
42384245 $('#openai_max_context').attr('max', max_1mil);
42394246 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
42404247 $('#openai_max_context').attr('max', max_32k);
@@ -4403,24 +4410,30 @@ async function onModelChange() {
44034410 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
44044411 if (oai_settings.max_context_unlocked) {
44054412 $('#openai_max_context').attr('max', unlocked_max);
4406- }
4413+ } else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
4407- else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
44084414 $('#openai_max_context').attr('max', max_8k);
4409- }
4415+ } else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
4410- 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')) {
44114416 $('#openai_max_context').attr('max', max_128k);
4412- }
4417+ } else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
4413- 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')) {
44144420 $('#openai_max_context').attr('max', max_8k);
4415- }
4421+ } else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
4416- else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
44174422 $('#openai_max_context').attr('max', max_8k);
4418- }
4423+ } else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
4419- else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
44204424 $('#openai_max_context').attr('max', max_32k);
4421- }
4425+ } else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
4422- else {
4426+ $('#openai_max_context').attr('max', max_128k);
4423- $('#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);
44244437 }
44254438 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
44264439 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -4921,6 +4934,12 @@ export function isImageInliningSupported() {
49214934 // gultra just isn't being offered as multimodal, thanks google.
49224935 const visionSupportedModels = [
49234936 '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',
49244943 'gemini-2.0-flash-thinking-exp-1219',
49254944 'gemini-2.0-flash-thinking-exp-01-21',
49264945 'gemini-2.0-flash-thinking-exp',
@@ -4948,6 +4967,8 @@ export function isImageInliningSupported() {
49484967 'gpt-4-turbo',
49494968 'gpt-4o',
49504969 'gpt-4o-mini',
4970+ 'o1',
4971+ 'o1-2024-12-17',
49514972 'chatgpt-4o-latest',
49524973 'yi-vision',
49534974 'pixtral-latest',
@@ -5506,6 +5527,11 @@ export function initOpenAI() {
55065527 saveSettingsDebounced();
55075528 });
55085529
5530+ $('#openai_reasoning_effort').on('input', function () {
5531+ oai_settings.reasoning_effort = String($(this).val());
5532+ saveSettingsDebounced();
5533+ });
5534+
55095535 if (!CSS.supports('field-sizing', 'content')) {
55105536 $(document).on('input', '#openai_settings .autoSetHeight', function () {
55115537 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+6 -3
@@ -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,
@@ -256,6 +257,8 @@ let power_user = {
256257 reasoning: {
257258 auto_parse: false,
258259 add_to_prompts: false,
260+ auto_expand: false,
261+ show_hidden: false,
259262 prefix: '<think>\n',
260263 suffix: '\n</think>',
261264 separator: '\n\n',
@@ -2019,7 +2022,7 @@ export function renderStoryString(params) {
20192022 */
20202023function validateStoryString(storyString, params) {
20212024 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
20222025 const cache = JSON.parse(localStorageaccountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
20232026
20242027 const hash = getStringHash(storyString);
20252028
@@ -2056,7 +2059,7 @@ function validateStoryString(storyString, params) {
20562059 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
20572060 }
20582061
20592062 localStorageaccountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
20602063}
20612064
20622065
@@ -2451,7 +2454,7 @@ async function resetMovablePanels(type) {
24512454 }
24522455
24532456 saveSettingsDebounced();
24542457 await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
24552458
24562459 eventSource.once(event_types.SETTINGS_UPDATED, () => {
24572460 $('.resizing').removeClass('resizing');
public/scripts/preset-manager.js+2 -0
@@ -587,6 +587,8 @@ class PresetManager {
587587 'derived',
588588 'generic_model',
589589 'include_reasoning',
590+ 'global_banned_tokens',
591+ 'send_banned_tokens',
590592 ];
591593 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
592594
public/scripts/reasoning.js+615 -28
@@ -1,14 +1,32 @@
1-import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
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';
25import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
36import { getCurrentLocale, t, translate } from './i18n.js';
47import { MacrosParser } from './macros.js';
8+import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
59import { Popup } from './popup.js';
610import { power_user } from './power-user.js';
711import { SlashCommand } from './slash-commands/SlashCommand.js';
812import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
913import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14+import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
1015import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1116import { copyText, escapeRegextextgen_types, isFalseBooleantextgenerationwebui_settings } from './utilstextgen-settings.js';
17+import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
18+
19+/**
20+ * Enum representing the type of the reasoning for a message (where it came from)
21+ * @enum {string}
22+ * @readonly
23+ */
24+export const ReasoningType = {
25+ Model: 'model',
26+ Parsed: 'parsed',
27+ Manual: 'manual',
28+ Edited: 'edited',
29+};
1230
1331/**
1432 * Gets a message from a jQuery element.
@@ -23,12 +41,467 @@ function getMessageFromJquery(element) {
2341}
2442
2543/**
44+ * Toggles the auto-expand state of reasoning blocks.
45+ */
46+function toggleReasoningAutoExpand() {
47+ const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details');
48+ reasoningBlocks.forEach((block) => {
49+ if (block instanceof HTMLDetailsElement) {
50+ block.open = power_user.reasoning.auto_expand;
51+ }
52+ });
53+}
54+
55+/**
56+ * Extracts the reasoning from the response data.
57+ * @param {object} data Response data
58+ * @returns {string} Extracted reasoning
59+ */
60+export function extractReasoningFromData(data) {
61+ switch (main_api) {
62+ case 'textgenerationwebui':
63+ switch (textgenerationwebui_settings.type) {
64+ case textgen_types.OPENROUTER:
65+ return data?.choices?.[0]?.reasoning ?? '';
66+ }
67+ break;
68+
69+ case 'openai':
70+ if (!oai_settings.show_thoughts) break;
71+
72+ switch (oai_settings.chat_completion_source) {
73+ case chat_completion_sources.DEEPSEEK:
74+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
75+ case chat_completion_sources.OPENROUTER:
76+ return data?.choices?.[0]?.message?.reasoning ?? '';
77+ case chat_completion_sources.MAKERSUITE:
78+ return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
79+ }
80+ break;
81+ }
82+
83+ return '';
84+}
85+
86+/**
87+ * Check if the model supports reasoning, but does not send back the reasoning
88+ * @returns {boolean} True if the model supports reasoning
89+ */
90+export function isHiddenReasoningModel() {
91+ if (main_api !== 'openai') {
92+ return false;
93+ }
94+
95+ /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
96+ /** @type {Record.<string, MatchingFunc>} */
97+ const FUNCS = {
98+ equals: (currentModel, supportedModel) => currentModel === supportedModel,
99+ startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
100+ };
101+
102+ /** @type {{ name: string; func: MatchingFunc; }[]} */
103+ const hiddenReasoningModels = [
104+ { name: 'o1', func: FUNCS.startsWith },
105+ { name: 'o3', func: FUNCS.startsWith },
106+ { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
107+ { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
108+ ];
109+
110+ const model = getChatCompletionModel() || '';
111+
112+ const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
113+ return isHidden;
114+}
115+
116+/**
117+ * Updates the Reasoning UI for a specific message
118+ * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
119+ * @param {Object} [options={}] - Optional arguments
120+ * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
121+ */
122+export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
123+ const handler = new ReasoningHandler();
124+ handler.initHandleMessage(messageIdOrElement, { reset });
125+}
126+
127+
128+/**
129+ * Enum for representing the state of reasoning
130+ * @enum {string}
131+ * @readonly
132+ */
133+export const ReasoningState = {
134+ None: 'none',
135+ Thinking: 'thinking',
136+ Done: 'done',
137+ Hidden: 'hidden',
138+};
139+
140+/**
141+ * Handles reasoning-specific logic and DOM updates for messages.
142+ * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
143+ */
144+export class ReasoningHandler {
145+ /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
146+ #isHiddenReasoningModel;
147+ /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
148+ #isParsingReasoning = false;
149+ /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
150+ #parsingReasoningMesStartIndex = null;
151+
152+ /**
153+ * @param {Date?} [timeStarted=null] - When the generation started
154+ */
155+ constructor(timeStarted = null) {
156+ /** @type {ReasoningState} The current state of the reasoning process */
157+ this.state = ReasoningState.None;
158+ /** @type {ReasoningType?} The type of the reasoning (where it came from) */
159+ this.type = null;
160+ /** @type {string} The reasoning output */
161+ this.reasoning = '';
162+ /** @type {Date} When the reasoning started */
163+ this.startTime = null;
164+ /** @type {Date} When the reasoning ended */
165+ this.endTime = null;
166+
167+ /** @type {Date} Initial starting time of the generation */
168+ this.initialTime = timeStarted ?? new Date();
169+
170+ this.#isHiddenReasoningModel = isHiddenReasoningModel();
171+
172+ // Cached DOM elements for reasoning
173+ /** @type {HTMLElement} Main message DOM element `.mes` */
174+ this.messageDom = null;
175+ /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */
176+ this.messageReasoningDetailsDom = null;
177+ /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
178+ this.messageReasoningContentDom = null;
179+ /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
180+ this.messageReasoningHeaderDom = null;
181+ }
182+
183+ /**
184+ * Initializes the reasoning handler for a specific message.
185+ *
186+ * Can be used to update the DOM elements or read other reasoning states.
187+ * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
188+ * The state will always be either done/hidden or none.
189+ *
190+ * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
191+ * @param {Object} [options={}] - Optional arguments
192+ * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
193+ */
194+ initHandleMessage(messageIdOrElement, { reset = false } = {}) {
195+ /** @type {HTMLElement} */
196+ const messageElement = typeof messageIdOrElement === 'number'
197+ ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
198+ : messageIdOrElement instanceof HTMLElement
199+ ? messageIdOrElement
200+ : $(messageIdOrElement)[0];
201+ const messageId = Number(messageElement.getAttribute('mesid'));
202+
203+ if (isNaN(messageId) || !chat[messageId]) return;
204+
205+ if (!chat[messageId].extra) {
206+ chat[messageId].extra = {};
207+ }
208+ const extra = chat[messageId].extra;
209+
210+ if (extra.reasoning) {
211+ this.state = ReasoningState.Done;
212+ } else if (extra.reasoning_duration) {
213+ this.state = ReasoningState.Hidden;
214+ }
215+
216+ this.type = extra?.reasoning_type;
217+ this.reasoning = extra?.reasoning ?? '';
218+
219+ if (this.state !== ReasoningState.None) {
220+ this.initialTime = new Date(chat[messageId].gen_started);
221+ this.startTime = this.initialTime;
222+ this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
223+ }
224+
225+ // Prefill main dom element, as message might not have been rendered yet
226+ this.messageDom = messageElement;
227+
228+ // Make sure reset correctly clears all relevant states
229+ if (reset) {
230+ this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
231+ this.type = null;
232+ this.reasoning = '';
233+ this.initialTime = new Date();
234+ this.startTime = null;
235+ this.endTime = null;
236+ }
237+
238+ this.updateDom(messageId);
239+
240+ if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
241+ this.messageReasoningDetailsDom.open = true;
242+ }
243+ }
244+
245+ /**
246+ * Gets the duration of the reasoning in milliseconds.
247+ *
248+ * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
249+ */
250+ getDuration() {
251+ if (this.startTime && this.endTime) {
252+ return this.endTime.getTime() - this.startTime.getTime();
253+ }
254+ return null;
255+ }
256+
257+ /**
258+ * Updates the reasoning text/string for a message.
259+ *
260+ * @param {number} messageId - The ID of the message to update
261+ * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
262+ * @param {Object} [options={}] - Optional arguments
263+ * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
264+ * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
265+ * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
266+ */
267+ updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
268+ if (messageId == -1 || !chat[messageId]) {
269+ return false;
270+ }
271+
272+ reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
273+ reasoning = trimSpaces(reasoning);
274+
275+ // Ensure the chat extra exists
276+ if (!chat[messageId].extra) {
277+ chat[messageId].extra = {};
278+ }
279+ const extra = chat[messageId].extra;
280+
281+ const reasoningChanged = extra.reasoning !== reasoning;
282+ this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
283+
284+ this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
285+
286+ if (persist) {
287+ // Build and save the reasoning data to message extras
288+ extra.reasoning = this.reasoning;
289+ extra.reasoning_duration = this.getDuration();
290+ extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
291+ }
292+
293+ return reasoningChanged;
294+ }
295+
296+
297+ /**
298+ * Handles processing of reasoning for a message.
299+ *
300+ * This is usually called by the message processor when a message is changed.
301+ *
302+ * @param {number} messageId - The ID of the message to process
303+ * @param {boolean} mesChanged - Whether the message has changed
304+ * @returns {Promise<void>}
305+ */
306+ async process(messageId, mesChanged) {
307+ mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
308+
309+ if (!this.reasoning && !this.#isHiddenReasoningModel)
310+ return;
311+
312+ // Ensure reasoning string is updated and regexes are applied correctly
313+ const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
314+
315+ if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
316+ this.state = ReasoningState.Thinking;
317+ this.startTime = this.initialTime;
318+ }
319+ if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
320+ this.endTime = new Date();
321+ await this.finish(messageId);
322+ }
323+ }
324+
325+ #autoParseReasoningFromMessage(messageId, mesChanged) {
326+ if (!power_user.reasoning.auto_parse)
327+ return;
328+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
329+ return mesChanged;
330+
331+ /** @type {{ mes: string, [key: string]: any}} */
332+ const message = chat[messageId];
333+ if (!message) return mesChanged;
334+
335+ // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
336+ if (this.#parsingReasoningMesStartIndex) {
337+ message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
338+ return mesChanged;
339+ }
340+
341+ if (this.state === ReasoningState.None) {
342+ // If streamed message starts with the opening, cut it out and put all inside reasoning
343+ if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
344+ this.#isParsingReasoning = true;
345+
346+ // Manually set starting state here, as we might already have received the ending suffix
347+ this.state = ReasoningState.Thinking;
348+ this.startTime = this.initialTime;
349+ }
350+ }
351+
352+ if (!this.#isParsingReasoning)
353+ return mesChanged;
354+
355+ // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
356+ const originalMes = message.mes;
357+ this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
358+ message.mes = '';
359+
360+ // If the reasoning contains the ending suffix, we cut that off and continue as message streaming
361+ if (this.reasoning.includes(power_user.reasoning.suffix)) {
362+ this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
363+ this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
364+ message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
365+ this.#isParsingReasoning = false;
366+ }
367+
368+ // Only return the original mesChanged value if we haven't cut off the complete message
369+ return message.mes.length ? mesChanged : false;
370+ }
371+
372+ /**
373+ * Completes the reasoning process for a message.
374+ *
375+ * Records the finish time if it was not set during streaming and updates the reasoning state.
376+ * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
377+ *
378+ * @param {number} messageId - The ID of the message to complete reasoning for
379+ * @returns {Promise<void>}
380+ */
381+ async finish(messageId) {
382+ if (this.state === ReasoningState.None) return;
383+
384+ // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
385+ if (this.startTime !== null && this.endTime === null) {
386+ this.endTime = new Date();
387+ }
388+
389+ if (this.state === ReasoningState.Thinking) {
390+ this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
391+ this.updateReasoning(messageId, null, { persist: true });
392+ await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
393+ }
394+
395+ this.updateDom(messageId);
396+ }
397+
398+ /**
399+ * Updates the reasoning UI elements for a message.
400+ *
401+ * Toggles the CSS class, updates states, reasoning message, and duration.
402+ *
403+ * @param {number} messageId - The ID of the message to update
404+ */
405+ updateDom(messageId) {
406+ this.#checkDomElements(messageId);
407+
408+ // Main CSS class to show this message includes reasoning
409+ this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
410+
411+ // Update states to the relevant DOM elements
412+ setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
413+ setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
414+ setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
415+
416+ // Update the reasoning message
417+ const reasoning = trimSpaces(this.reasoning);
418+ const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
419+ this.messageReasoningContentDom.innerHTML = displayReasoning;
420+
421+ // Update tooltip for hidden reasoning edit
422+ /** @type {HTMLElement} */
423+ const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
424+ button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
425+
426+ // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button
427+ if (this.state === ReasoningState.Hidden) {
428+ this.messageReasoningDetailsDom.open = false;
429+ }
430+
431+ // Update the reasoning duration in the UI
432+ this.#updateReasoningTimeUI();
433+ }
434+
435+ /**
436+ * Finds and caches reasoning-related DOM elements for the given message.
437+ *
438+ * @param {number} messageId - The ID of the message to cache the DOM elements for
439+ */
440+ #checkDomElements(messageId) {
441+ // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
442+ if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
443+ this.messageDom = null;
444+ }
445+
446+ // Cache the DOM elements once
447+ if (this.messageDom === null) {
448+ this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
449+ if (this.messageDom === null) throw new Error('message dom does not exist');
450+ }
451+ if (this.messageReasoningDetailsDom === null) {
452+ this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
453+ }
454+ if (this.messageReasoningContentDom === null) {
455+ this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
456+ }
457+ if (this.messageReasoningHeaderDom === null) {
458+ this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
459+ }
460+ }
461+
462+ /**
463+ * Updates the reasoning time display in the UI.
464+ *
465+ * Shows the duration in a human-readable format with a tooltip for exact seconds.
466+ * Displays "Thinking..." if still processing, or a generic message otherwise.
467+ */
468+ #updateReasoningTimeUI() {
469+ const element = this.messageReasoningHeaderDom;
470+ const duration = this.getDuration();
471+ let data = null;
472+ let title = '';
473+ if (duration) {
474+ const seconds = moment.duration(duration).asSeconds();
475+
476+ const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
477+ element.textContent = t`Thought for ${durationStr}`;
478+ data = String(seconds);
479+ title = `${seconds} seconds`;
480+ } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
481+ element.textContent = t`Thought for some time`;
482+ data = 'unknown';
483+ } else {
484+ element.textContent = t`Thinking...`;
485+ data = null;
486+ }
487+
488+ if (this.type !== ReasoningType.Model) {
489+ title += ` [${translate(this.type)}]`;
490+ title = title.trim();
491+ }
492+ element.title = title;
493+
494+ setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
495+ setDatasetProperty(element, 'duration', data);
496+ }
497+}
498+
499+/**
26500 * Helper class for adding reasoning to messages.
27501 * Keeps track of the number of reasoning additions.
28502 */
29503export class PromptReasoning {
30504 static REASONING_PLACEHOLDER = '\u200B';
31- static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
32505
33506 constructor() {
34507 this.counter = 0;
@@ -59,7 +532,7 @@ export class PromptReasoning {
59532 return content;
60533 }
61534
62535 // No reasoning provided or a legacy placeholder
63536 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
64537 return content;
65538 }
@@ -118,6 +591,22 @@ function loadReasoningSettings() {
118591 power_user.reasoning.auto_parse = !!$(this).prop('checked');
119592 saveSettingsDebounced();
120593 });
594+
595+ $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
596+ $('#reasoning_auto_expand').on('change', function () {
597+ power_user.reasoning.auto_expand = !!$(this).prop('checked');
598+ toggleReasoningAutoExpand();
599+ saveSettingsDebounced();
600+ });
601+ toggleReasoningAutoExpand();
602+
603+ $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
604+ $('#reasoning_show_hidden').on('change', function () {
605+ power_user.reasoning.show_hidden = !!$(this).prop('checked');
606+ $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
607+ saveSettingsDebounced();
608+ });
609+ $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
121610}
122611
123612function registerReasoningSlashCommands() {
@@ -134,10 +623,10 @@ function registerReasoningSlashCommands() {
134623 }),
135624 ],
136625 callback: (_args, value) => {
137626 const messageId = !isNaN(NumberparseInt(value.toString())) ? NumberparseInt(value.toString()) : chat.length - 1;
138627 const message = chat[messageId];
139628 const reasoning = String(message?.extra?.reasoning ?? '');
140- return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
629+ return reasoning;
141630 },
142631 }));
143632
@@ -163,11 +652,16 @@ function registerReasoningSlashCommands() {
163652 callback: async (args, value) => {
164653 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
165654 const message = chat[messageId];
166655 if (!message?.extra) {
167656 return '';
168657 }
658+ // Make sure the message has an extra object
659+ if (!message.extra || typeof message.extra !== 'object') {
660+ message.extra = {};
661+ }
169662
170663 message.extra.reasoning = String(value ?? '');
664+ message.extra.reasoning_type = ReasoningType.Manual;
171665 await saveChatConditional();
172666
173667 closeMessageEditor('reasoning');
@@ -188,7 +682,26 @@ function registerReasoningSlashCommands() {
188682 typeList: [ARGUMENT_TYPE.BOOLEAN],
189683 defaultValue: 'true',
190684 isRequired: false,
191685 enumProviderenumList: commonEnumProviders.boolean('trueFalse')(),
686+ }),
687+ SlashCommandNamedArgument.fromProps({
688+ name: 'return',
689+ description: 'Whether to return the parsed reasoning or the content without reasoning',
690+ typeList: [ARGUMENT_TYPE.STRING],
691+ defaultValue: 'reasoning',
692+ isRequired: false,
693+ enumList: [
694+ new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
695+ new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
696+ ],
697+ }),
698+ SlashCommandNamedArgument.fromProps({
699+ name: 'strict',
700+ description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
701+ typeList: [ARGUMENT_TYPE.BOOLEAN],
702+ defaultValue: 'true',
703+ isRequired: false,
704+ enumList: commonEnumProviders.boolean('trueFalse')(),
192705 }),
193706 ],
194707 unnamedArgumentList: [
@@ -198,19 +711,27 @@ function registerReasoningSlashCommands() {
198711 }),
199712 ],
200713 callback: (args, value) => {
201714 if (!value || typeof value !== 'string') {
202715 return '';
203716 }
204717
205718 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
206719 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
207720 return String(value);
721+ }
722+ if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
723+ toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
208724 }
209725
210- const parsedReasoning = parseReasoningFromString(String(value));
726+ const returnMessage = args.return === 'content';
211727
728+ const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
212729 if (!parsedReasoning) {
213730 return returnMessage ? value : '';
731+ }
732+
733+ if (returnMessage) {
734+ return parsedReasoning.content;
214735 }
215736
216737 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@@ -228,6 +749,31 @@ function registerReasoningMacros() {
228749}
229750
230751function setReasoningEventHandlers() {
752+ $(document).on('click', '.mes_reasoning_details', function (e) {
753+ if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
754+ e.preventDefault();
755+ }
756+ });
757+
758+ $(document).on('click', '.mes_reasoning_header', function (e) {
759+ const details = $(this).closest('.mes_reasoning_details');
760+ // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited
761+ if (details.find('.mes_reasoning').is(':empty')) {
762+ e.preventDefault();
763+ return;
764+ }
765+
766+ // If we are in message edit mode and reasoning area is closed, a click opens and edits it
767+ const mes = $(this).closest('.mes');
768+ const mesEditArea = mes.find('#curEditTextarea');
769+ if (mesEditArea.length) {
770+ const summary = $(mes).find('.mes_reasoning_summary');
771+ if (!summary.attr('open')) {
772+ summary.find('.mes_reasoning_edit').trigger('click');
773+ }
774+ }
775+ });
776+
231777 $(document).on('click', '.mes_reasoning_copy', (e) => {
232778 e.stopPropagation();
233779 e.preventDefault();
@@ -246,7 +792,7 @@ function setReasoningEventHandlers() {
246792 const textarea = document.createElement('textarea');
247793 const reasoningBlock = messageBlock.find('.mes_reasoning');
248794 textarea.classList.add('reasoning_edit_textarea');
249795 textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
250796 $(textarea).insertBefore(reasoningBlock);
251797
252798 if (!CSS.supports('field-sizing', 'content')) {
@@ -285,9 +831,12 @@ function setReasoningEventHandlers() {
285831 const textarea = messageBlock.find('.reasoning_edit_textarea');
286832 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
287833 message.extra.reasoning = reasoning;
834+ message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
288835 await saveChatConditional();
289836 updateMessageBlock(messageId, message);
290837 textarea.remove();
838+
839+ messageBlock.find('.mes_edit_done:visible').trigger('click');
291840 });
292841
293842 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@@ -297,10 +846,14 @@ function setReasoningEventHandlers() {
297846 const { messageBlock } = getMessageFromJquery(this);
298847 const textarea = messageBlock.find('.reasoning_edit_textarea');
299848 textarea.remove();
849+
850+ messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
851+
852+ updateReasoningUI(messageBlock);
300853 });
301854
302855 $(document).on('click', '.mes_edit_add_reasoning', async function () {
303856 const { message, messageIdmessageBlock } = getMessageFromJquery(this);
304857 if (!message?.extra) {
305858 return;
306859 }
@@ -310,34 +863,46 @@ function setReasoningEventHandlers() {
310863 return;
311864 }
312865
313- message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
866+ messageBlock.addClass('reasoning');
867+
868+ // To make hidden reasoning blocks editable, we just set them to "Done" here already.
869+ // They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
870+ if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
871+ messageBlock.attr('data-reasoning-state', ReasoningState.Done);
872+ }
873+
874+ // Open the reasoning area so we can actually edit it
875+ messageBlock.find('.mes_reasoning_details').attr('open', '');
876+ messageBlock.find('.mes_reasoning_edit').trigger('click');
314877 await saveChatConditional();
315- closeMessageEditor();
316- updateMessageBlock(messageId, message);
317878 });
318879
319880 $(document).on('click', '.mes_reasoning_delete', async function (e) {
320881 e.stopPropagation();
321882 e.preventDefault();
322883
323884 const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?`,<br t`/>Visible message contents will stay intact.`);
324885
325886 if (!confirm) {
326887 return;
327888 }
328889
329890 const { message, messageId, messageBlock } = getMessageFromJquery(this);
330891 if (!message?.extra) {
331892 return;
332893 }
333894 message.extra.reasoning = '';
895+ delete message.extra.reasoning_type;
896+ delete message.extra.reasoning_duration;
334897 await saveChatConditional();
335898 updateMessageBlock(messageId, message);
899+ const textarea = messageBlock.find('.reasoning_edit_textarea');
900+ textarea.remove();
336901 });
337902
338903 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
339904 const { message } = getMessageFromJquery(this);
340905 const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
341906
342907 if (!reasoning) {
343908 return;
@@ -349,21 +914,37 @@ function setReasoningEventHandlers() {
349914}
350915
351916/**
917+ * Removes reasoning from a string if auto-parsing is enabled.
918+ * @param {string} str Input string
919+ * @returns {string} Output string
920+ */
921+export function removeReasoningFromString(str) {
922+ if (!power_user.reasoning.auto_parse) {
923+ return str;
924+ }
925+
926+ const parsedReasoning = parseReasoningFromString(str);
927+ return parsedReasoning?.content ?? str;
928+}
929+
930+/**
352931 * Parses reasoning from a string using the power user reasoning settings.
353932 * @typedef {Object} ParsedReasoning
354933 * @property {string} reasoning Reasoning block
355934 * @property {string} content Message content
356935 * @param {string} str Content of the message
936+ * @param {Object} options Optional arguments
937+ * @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
357938 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
358939 */
359940function parseReasoningFromString(str, { strict = true } = {}) {
360941 // Both prefix and suffix must be defined
361942 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
362943 return null;
363944 }
364945
365946 try {
366947 const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
367948
368949 let didReplace = false;
369950 let reasoning = '';
@@ -373,9 +954,9 @@ function parseReasoningFromString(str) {
373954 return '';
374955 });
375956
376957 if (didReplace && power_user.trim_spaces) {
377958 reasoning = reasoning.trimtrimSpaces(reasoning);
378959 content = content.trimtrimSpaces(content);
379960 }
380961
381962 return { reasoning, content };
@@ -404,6 +985,11 @@ function registerReasoningAppEvents() {
404985 return null;
405986 }
406987
988+ if (message.extra?.reasoning) {
989+ console.debug('[Reasoning] Message already has reasoning', idx);
990+ return null;
991+ }
992+
407993 const parsedReasoning = parseReasoningFromString(message.mes);
408994
409995 // No reasoning block found
@@ -421,6 +1007,7 @@ function registerReasoningAppEvents() {
4211007 // If reasoning was found, add it to the message
4221008 if (parsedReasoning.reasoning) {
4231009 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
1010+ message.extra.reasoning_type = ReasoningType.Parsed;
4241011 }
4251012
4261013 // Update the message text if it was changed
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+63 -14
@@ -75,6 +75,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
7575import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7676import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7777import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78+import { accountStorage } from './util/AccountStorage.js';
7879export {
7980 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
8081};
@@ -235,7 +236,6 @@ export function initDefaultSlashCommands() {
235236 description: 'Character name - or unique character identifier (avatar key)',
236237 typeList: [ARGUMENT_TYPE.STRING],
237238 enumProvider: commonEnumProviders.characters('character'),
238- forceEnum: false,
239239 }),
240240 ],
241241 helpString: `
@@ -274,7 +274,6 @@ export function initDefaultSlashCommands() {
274274 typeList: [ARGUMENT_TYPE.STRING],
275275 isRequired: true,
276276 enumProvider: commonEnumProviders.characters('character'),
277- forceEnum: false,
278277 }),
279278 SlashCommandNamedArgument.fromProps({
280279 name: 'avatar',
@@ -518,7 +517,6 @@ export function initDefaultSlashCommands() {
518517 typeList: [ARGUMENT_TYPE.STRING],
519518 isRequired: true,
520519 enumProvider: commonEnumProviders.characters('all'),
521- forceEnum: true,
522520 }),
523521 ],
524522 helpString: 'Opens up a chat with the character or group by its name',
@@ -734,6 +732,57 @@ export function initDefaultSlashCommands() {
734732 helpString: 'Unhides a message from the prompt.',
735733 }));
736734 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({
737786 name: 'member-disable',
738787 callback: disableGroupMemberCallback,
739788 aliases: ['disable', 'disablemember', 'memberdisable'],
@@ -843,7 +892,8 @@ export function initDefaultSlashCommands() {
843892 helpString: 'Moves a group member down in the group chat list.',
844893 }));
845894 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
846895 name: 'member-peek',
896+ aliases: ['peek', 'memberpeek', 'peekmember'],
847897 callback: peekCallback,
848898 unnamedArgumentList: [
849899 SlashCommandArgument.fromProps({
@@ -1009,7 +1059,6 @@ export function initDefaultSlashCommands() {
10091059 typeList: [ARGUMENT_TYPE.STRING],
10101060 defaultValue: 'System',
10111061 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
1012- forceEnum: false,
10131062 }),
10141063 new SlashCommandNamedArgument(
10151064 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@@ -3047,7 +3096,7 @@ function performGroupMemberAction(chid, action) {
30473096
30483097async function disableGroupMemberCallback(_, arg) {
30493098 if (!selected_group) {
30503099 toastr.warning('Cannot run /member-disable command outside of a group chat.');
30513100 return '';
30523101 }
30533102
@@ -3064,7 +3113,7 @@ async function disableGroupMemberCallback(_, arg) {
30643113
30653114async function enableGroupMemberCallback(_, arg) {
30663115 if (!selected_group) {
30673116 toastr.warning('Cannot run /member-enable command outside of a group chat.');
30683117 return '';
30693118 }
30703119
@@ -3081,7 +3130,7 @@ async function enableGroupMemberCallback(_, arg) {
30813130
30823131async function moveGroupMemberUpCallback(_, arg) {
30833132 if (!selected_group) {
30843133 toastr.warning('Cannot run /memberupmember-up command outside of a group chat.');
30853134 return '';
30863135 }
30873136
@@ -3098,7 +3147,7 @@ async function moveGroupMemberUpCallback(_, arg) {
30983147
30993148async function moveGroupMemberDownCallback(_, arg) {
31003149 if (!selected_group) {
31013150 toastr.warning('Cannot run /memberdownmember-down command outside of a group chat.');
31023151 return '';
31033152 }
31043153
@@ -3115,12 +3164,12 @@ async function moveGroupMemberDownCallback(_, arg) {
31153164
31163165async function peekCallback(_, arg) {
31173166 if (!selected_group) {
31183167 toastr.warning('Cannot run /member-peek command outside of a group chat.');
31193168 return '';
31203169 }
31213170
31223171 if (is_group_generating) {
31233172 toastr.warning('Cannot run /member-peek command while the group reply is generating.');
31243173 return '';
31253174 }
31263175
@@ -3137,7 +3186,7 @@ async function peekCallback(_, arg) {
31373186
31383187async function removeGroupMemberCallback(_, arg) {
31393188 if (!selected_group) {
31403189 toastr.warning('Cannot run /memberremovemember-remove command outside of a group chat.');
31413190 return '';
31423191 }
31433192
@@ -3558,9 +3607,9 @@ export async function sendMessageAs(args, text) {
35583607
35593608 if (!name) {
35603609 const namelessWarningKey = 'sendAsNamelessWarningShown';
35613610 if (localStorageaccountStorage.getItem(namelessWarningKey) !== 'true') {
35623611 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
35633612 localStorageaccountStorage.setItem(namelessWarningKey, 'true');
35643613 }
35653614 name = name2;
35663615 }
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -34,6 +34,7 @@ export const enumIcons = {
3434 preset: '⚙️',
3535 file: '📄',
3636 message: '💬',
37+ reasoning: '💡',
3738 voice: '🎤',
3839 server: '🖥️',
3940 popup: '🗔',
public/scripts/st-context.js+7 -0
@@ -68,11 +68,14 @@ import { tag_map, tags } from './tags.js';
6868import { textgenerationwebui_settings } from './textgen-settings.js';
6969import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
7070import { ToolManager } from './tool-calling.js';
71+import { accountStorage } from './util/AccountStorage.js';
7172import { timestampToMoment, uuidv4 } from './utils.js';
7273import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
74+import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
7375
7476export function getContext() {
7577 return {
78+ accountStorage,
7679 chat,
7780 characters,
7881 groups,
@@ -186,6 +189,10 @@ export function getContext() {
186189 set: setGlobalVariable,
187190 },
188191 },
192+ loadWorldInfo,
193+ saveWorldInfo,
194+ updateWorldInfoList,
195+ convertCharacterBook,
189196 };
190197}
191198
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+9 -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 = [];
@@ -57,9 +58,14 @@ const OPENROUTER_PROVIDERS = [
5758 'Minimax',
5859 'Nineteen',
5960 'Liquid',
61+ 'InferenceNet',
62+ 'Friendli',
63+ 'AionLabs',
64+ 'Alibaba',
6065 'Nebius',
6166 'Chutes',
6267 'Kluster',
68+ 'Targon',
6369 '01.AI',
6470 'HuggingFace',
6571 'Mancer',
@@ -336,7 +342,7 @@ export async function loadFeatherlessModels(data) {
336342 populateClassSelection(data);
337343
338344 // Retrieve the stored number of items per page or default to 10
339345 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 10;
340346
341347 // Initialize pagination
342348 applyFiltersAndSort();
@@ -412,7 +418,7 @@ export async function loadFeatherlessModels(data) {
412418 },
413419 afterSizeSelectorChange: function (e) {
414420 const newPerPage = e.target.value;
415421 localStorageaccountStorage.setItem('Models_PerPage'storageKey, newPerPage);
416422 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
417423 },
418424 });
@@ -513,7 +519,7 @@ export async function loadFeatherlessModels(data) {
513519 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
514520 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
515521
516522 setupPagination(filteredModels, Number(localStorageaccountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
517523 }
518524
519525 // Required to keep the /model command function
public/scripts/textgen-settings.js+37 -7
@@ -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';
@@ -182,6 +183,8 @@ const settings = {
182183 grammar_string: '',
183184 json_schema: {},
184185 banned_tokens: '',
186+ global_banned_tokens: '',
187+ send_banned_tokens: true,
185188 sampler_priority: OOBA_DEFAULT_ORDER,
186189 samplers: LLAMACPP_DEFAULT_ORDER,
187190 samplers_priorities: APHRODITE_DEFAULT_ORDER,
@@ -274,6 +277,8 @@ export const setting_names = [
274277 'grammar_string',
275278 'json_schema',
276279 'banned_tokens',
280+ 'global_banned_tokens',
281+ 'send_banned_tokens',
277282 'ignore_eos_token',
278283 'spaces_between_special_tokens',
279284 'speculative_ngram',
@@ -306,7 +311,7 @@ export function validateTextGenUrl() {
306311 const formattedUrl = formatTextGenURL(url);
307312
308313 if (!formattedUrl) {
309314 toastr.error('t`Enter a valid API URL'`, 'Text Completion API');
310315 return;
311316 }
312317
@@ -394,7 +399,7 @@ function getTokenizerForTokenIds() {
394399 * @returns {TokenBanResult} String with comma-separated banned token IDs
395400 */
396401function getCustomTokenBans() {
397402 if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) {
398403 return {
399404 banned_tokens: '',
400405 banned_strings: [],
@@ -404,8 +409,9 @@ function getCustomTokenBans() {
404409 const tokenizer = getTokenizerForTokenIds();
405410 const banned_tokens = [];
406411 const banned_strings = [];
407412 const sequences = settings.banned_tokens[]
408413 .concat(settings.banned_tokens.split('\n'))
414+ .concat(settings.global_banned_tokens.split('\n'))
409415 .concat(textgenerationwebui_banned_in_macros)
410416 .filter(x => x.length > 0)
411417 .filter(onlyUnique);
@@ -453,6 +459,18 @@ function getCustomTokenBans() {
453459}
454460
455461/**
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+/**
456474 * Calculates logit bias object from the logit bias list.
457475 * @returns {object} Logit bias object
458476 */
@@ -594,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) {
594612}
595613
596614jQuery(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+
597623 $('#koboldcpp_order').sortable({
598624 delay: getSortableDelay(),
599625 stop: function () {
@@ -932,6 +958,10 @@ function setSettingByName(setting, value, trigger) {
932958 if (isCheckbox) {
933959 const val = Boolean(value);
934960 $(`#${setting}_textgenerationwebui`).prop('checked', val);
961+
962+ if ('send_banned_tokens' === setting) {
963+ $(`#${setting}_textgenerationwebui`).trigger('change');
964+ }
935965 }
936966 else if (isText) {
937967 $(`#${setting}_textgenerationwebui`).val(value);
@@ -1157,7 +1187,7 @@ export function getTextGenModel() {
11571187 return settings.aphrodite_model;
11581188 case OLLAMA:
11591189 if (!settings.ollama_model) {
11601190 toastr.error('t`No Ollama model selected.'`, 'Text Completion API');
11611191 throw new Error('No Ollama model selected');
11621192 }
11631193 return settings.ollama_model;
@@ -1221,7 +1251,7 @@ function replaceMacrosInList(str) {
12211251 }
12221252}
12231253
12241254export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
12251255 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
12261256 const dynatemp = isDynamicTemperatureSupported();
12271257 const { banned_tokens, banned_strings } = getCustomTokenBans();
@@ -1449,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
14491479 }
14501480 }
14511481
14521482 await eventSource.emitAndWaitemit(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
14531483
14541484 // Grammar conflicts with with json_schema
14551485 if (settings.type === LLAMACPP) {
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+41 -6
@@ -8,7 +8,7 @@ import {
88import { getContext } from './extensions.js';
99import { characters, getRequestHeaders, this_chid } from '../script.js';
1010import { isMobile } from './RossAscends-mods.js';
1111import { collapseNewlines, power_user } from './power-user.js';
1212import { debounce_timeout } from './constants.js';
1313import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
1414import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) {
677677}
678678
679679/**
680+ * Trims leading and trailing whitespace from the input string based on a configuration setting.
681+ * @param {string} input - The string to be trimmed
682+ * @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
683+ */
684+
685+export function trimSpaces(input) {
686+ if (!input || typeof input !== 'string') {
687+ return input;
688+ }
689+ return power_user.trim_spaces ? input.trim() : input;
690+}
691+
692+/**
680693 * Trims a string to the end of a nearest sentence.
681694 * @param {string} input The string to trim.
682695 * @returns {string} The trimmed string.
@@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) {
9941007 });
9951008}
9961009
997-export function getCharaFilename(chid) {
1010+/**
1011+ * Gets the filename of the character avatar without extension
1012+ * @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
1013+ * @param {object} [options={}] - Options arguments
1014+ * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
1015+ * @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
1016+ */
1017+export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
9981018 const context = getContext();
9991019 const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
10001020
1001- if (fileName) {
1021+ return fileName?.replace(/\.[^/.]+$/, '') ?? null;
1002- return fileName.replace(/\.[^/.]+$/, '');
1003- }
10041022}
10051023
10061024/**
@@ -2059,6 +2077,23 @@ export function toggleDrawer(drawer, expand = true) {
20592077 }
20602078}
20612079
2080+/**
2081+ * Sets or removes a dataset property on an HTMLElement
2082+ *
2083+ * Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
2084+ *
2085+ * @param {HTMLElement} element - The element to modify
2086+ * @param {string} name - The name of the dataset property
2087+ * @param {string|null} value - The value to set - If null, the dataset property will be removed
2088+ */
2089+export function setDatasetProperty(element, name, value) {
2090+ if (value === null) {
2091+ delete element.dataset[name];
2092+ } else {
2093+ element.dataset[name] = value;
2094+ }
2095+}
2096+
20622097export async function fetchFaFile(name) {
20632098 const style = document.createElement('style');
20642099 style.innerHTML = await (await fetch(`/css/${name}`)).text();
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+131 -50
@@ -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);
@@ -342,18 +348,68 @@ input[type='checkbox']:focus-visible {
342348
343349.mes_reasoning {
344350 display: block;
345351 border-left: 1px2px solid var(--SmartThemeBorderColorSmartThemeEmColor);
346- background-color: var(--black30a);
352+ border-radius: 2px;
347- border-radius: 5px;
348353 padding: 5px;
349354 marginpadding-left: 5px 014px;
355+ margin-bottom: 0.5em;
350356 overflow-y: auto;
357+ color: var(--SmartThemeEmColor);
351358}
352359
353360.mes_reasoning_summarymes_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 {
354394 cursor: pointer;
355395 position: relative;
356396 marginuser-select: 2pxnone;
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;
357413}
358414
359415@supports not selector(:has(*)) {
@@ -363,29 +419,41 @@ input[type='checkbox']:focus-visible {
363419}
364420
365421.mes_bias:empty,
366-.mes_reasoning:empty,
422+.mes:not(.reasoning) .mes_reasoning_details,
367-.mes_reasoning_details:has(.mes_reasoning:empty),
368-.mes_block:has(.edit_textarea) .mes_reasoning_details,
369423.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370424.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
371425.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_donemes_reasoning_header,
372426.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_canceledit_button),
373427.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_doneedit_button, .mes_reasoning_edit_cancel) {
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 {
374433 display: none;
375434}
376435
377-.mes_reasoning_actions {
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"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
442+ display: none;
443+}
444+
445+.mes_reasoning_details .mes_reasoning_arrow {
378446 position: absolute;
379447 righttop: 050%;
380448 topright: 07px;
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+}
381454
382- display: flex;
455+.mes_reasoning_details:not([open]) .mes_reasoning_arrow {
383- gap: 4px;
456+ transform: translateY(-50%) rotate(180deg);
384- flex-wrap: nowrap;
385- justify-content: flex-end;
386- transition: all 200ms;
387- overflow-x: hidden;
388- padding: 1px;
389457}
390458
391459.mes_reasoning_summary>span {
@@ -1100,13 +1168,8 @@ body .panelControlBar {
11001168 /*only affects bubblechat to make it sit nicely at the bottom*/
11011169}
11021170
11031171.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
11041172 margin-bottom: 30pxvar(--mes-right-spacing);
1105-}
1106-
1107-.last_mes .mes_reasoning,
1108-.last_mes .mes_text {
1109- padding-right: 30px;
11101173}
11111174
11121175/* SWIPE RELATED STYLES*/
@@ -1330,6 +1393,7 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
13301393 padding-left: 0;
13311394 padding-top: 5px;
13321395 padding-bottom: 5px;
1396+ padding-right: var(--mes-right-spacing);
13331397}
13341398
13351399br {
@@ -2815,9 +2879,8 @@ select option:not(:checked) {
28152879 color: var(--active) !important;
28162880}
28172881
28182882#instruct_enabled_label .menu_button.togglable:not(.toggleEnabled), {
2819-#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
2883+ color: red;
2820- color: Red;
28212884}
28222885
28232886.displayBlock {
@@ -3000,6 +3063,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
30003063.mes_block .ch_name {
30013064 max-width: 100%;
30023065 min-height: 22px;
3066+ align-items: flex-start;
30033067}
30043068
30053069/*applies to both groups and solos chars in the char list*/
@@ -4224,7 +4288,13 @@ input[type="range"]::-webkit-slider-thumb {
42244288 transition: 0.3s ease-in-out;
42254289}
42264290
42274291.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 {
42284298 opacity: 0.5;
42294299 padding: 0px;
42304300 font-size: 1rem;
@@ -4237,6 +4307,12 @@ input[type="range"]::-webkit-slider-thumb {
42374307 align-items: center;
42384308}
42394309
4310+.mes_reasoning_actions .edit_button {
4311+ margin-bottom: 0.5em;
4312+ opacity: 1;
4313+ filter: brightness(0.7);
4314+}
4315+
42404316.mes_reasoning_edit_cancel,
42414317.mes_edit_cancel.menu_button {
42424318 background-color: var(--crimson70a);
@@ -4263,6 +4339,14 @@ input[type="range"]::-webkit-slider-thumb {
42634339 field-sizing: content;
42644340}
42654341
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+
42664350#anchor_order {
42674351 margin-bottom: 15px;
42684352}
@@ -4602,23 +4686,6 @@ body .ui-widget-content li:hover {
46024686 opacity: 1;
46034687}
46044688
4605-.typing_indicator {
4606- position: sticky;
4607- bottom: 10px;
4608- margin: 10px;
4609- opacity: 0.85;
4610- text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
4611- order: 9999;
4612-}
4613-
4614-.typing_indicator:after {
4615- display: inline-block;
4616- vertical-align: bottom;
4617- animation: ellipsis steps(4, end) 1500ms infinite;
4618- content: "";
4619- width: 0px;
4620-}
4621-
46224689#group_avatar_preview .missing-avatar {
46234690 display: inline;
46244691 vertical-align: middle;
@@ -5707,11 +5774,13 @@ body:not(.movingUI) .drawer-content.maximized {
57075774 overflow-wrap: anywhere;
57085775}
57095776
5777+#SystemPromptColumn summary,
57105778#InstructSequencesColumn summary {
57115779 font-size: 0.95em;
57125780 cursor: pointer;
57135781}
57145782
5783+#SystemPromptColumn details,
57155784#InstructSequencesColumn details:not(:last-of-type) {
57165785 margin-bottom: 5px;
57175786}
@@ -5875,3 +5944,15 @@ body:not(.movingUI) .drawer-content.maximized {
58755944.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
58765945 flex: 1;
58775946}
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+232 -43
@@ -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';
@@ -29,6 +30,7 @@ import bodyParser from 'body-parser';
2930
3031// net related library imports
3132import fetch from 'node-fetch';
33+import ipRegex from 'ip-regex';
3234
3335// Unrestrict console logs display limit
3436util.inspect.defaultOptions.maxArrayLength = null;
@@ -55,9 +57,10 @@ import {
5557
5658import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
5759import basicAuthMiddleware from './src/middleware/basicAuth.js';
5860import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js';
5961import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
6062import initRequestProxy from './src/request-proxy.js';
63+import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
6164import {
6265 getVersion,
6366 getConfigValue,
@@ -65,7 +68,11 @@ import {
6568 forwardFetchResponse,
6669 removeColorFormatting,
6770 getSeparator,
71+ stringToBool,
72+ urlHostnameToIPv6,
73+ canResolve,
6874 safeReadFileSync,
75+ setupLogLevel,
6976} from './src/util.js';
7077import { UPLOADS_DIRECTORY } from './src/constants.js';
7178import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -125,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
125132const DEFAULT_PORT = 8000;
126133const DEFAULT_AUTORUN = false;
127134const DEFAULT_LISTEN = false;
135+const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
136+const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
128137const DEFAULT_CORS_PROXY = false;
129138const DEFAULT_WHITELIST = true;
130139const DEFAULT_ACCOUNTS = false;
@@ -149,11 +158,11 @@ const DEFAULT_PROXY_BYPASS = [];
149158const cliArguments = yargs(hideBin(process.argv))
150159 .usage('Usage: <your-start-script> <command> [options]')
151160 .option('enableIPv6', {
152161 type: 'booleanstring',
153162 default: null,
154163 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
155164 }).option('enableIPv4', {
156165 type: 'booleanstring',
157166 default: null,
158167 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
159168 }).option('port', {
@@ -180,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
180189 type: 'boolean',
181190 default: null,
182191 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 ]',
183200 }).option('corsProxy', {
184201 type: 'boolean',
185202 default: null,
@@ -242,27 +259,46 @@ app.use(helmet({
242259app.use(compression());
243260app.use(responseTime());
244261
262+
263+/** @type {number} */
245264const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
265+/** @type {boolean} */
246266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
267+/** @type {boolean} */
247268const 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} */
248274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
249275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
276+/** @type {string} */
250277const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278+/** @type {boolean} */
251279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
252280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
253281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
282+/** @type {boolean} */
254283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
255284
256285const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
257286
258-const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
259-const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
260287
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} */
261294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295+/** @type {number} */
262296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
263297
298+/** @type {boolean} */
264299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
265300
301+/** @type {boolean} */
266302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
267303
268304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -279,7 +315,19 @@ if (dnsPreferIPv6) {
279315 console.log('Preferring IPv4 for DNS resolution');
280316}
281317
282-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) {
283331 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
284332 process.exit(1);
285333}
@@ -364,6 +412,55 @@ function getSessionCookieAge() {
364412 return undefined;
365413}
366414
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+
367464app.use(cookieSession({
368465 name: getCookieSessionName(),
369466 sameSite: 'strict',
@@ -419,7 +516,7 @@ if (!disableCsrf) {
419516
420517// Static files
421518// Host index page
422519app.get('/', getCacheBusterMiddleware(), (request, response) => {
423520 if (shouldRedirectToLogin(request)) {
424521 const query = request.url.split('?')[1];
425522 const redirectUrl = query ? `/login?${query}` : '/login';
@@ -627,13 +724,13 @@ app.use('/api/azure', azureRouter);
627724
628725const tavernUrlV6 = new URL(
629726 (cliArguments.ssl ? 'https://' : 'http://') +
630727 (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
631728 (':' + server_port),
632729);
633730
634731const tavernUrl = new URL(
635732 (cliArguments.ssl ? 'https://' : 'http://') +
636733 (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
637734 (':' + server_port),
638735);
639736
@@ -657,6 +754,7 @@ const preSetupTasks = async function () {
657754 await checkForNewContent(directories);
658755 await ensureThumbnailCache();
659756 cleanUploads();
757+ migrateAccessLog();
660758
661759 await settingsInit();
662760 await statsInit();
@@ -693,20 +791,23 @@ const preSetupTasks = async function () {
693791
694792/**
695793 * Gets the hostname to use for autorun in the browser.
696794 * @returnsparam {stringboolean} The hostnameuseIPv6 toIf use for autorunIPv6
795+ * @param {boolean} useIPv4 If use IPv4
796+ * @returns Promise<string> The hostname to use for autorun
697797 */
698798async function getAutorunHostname(useIPv6, useIPv4) {
699799 if (autorunHostname === 'auto') {
700- if (enableIPv6 && enableIPv4) {
800+ let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
701- if (avoidLocalhost) return '[::1]';
801+
702- return 'localhost';
802+ if (useIPv6 && useIPv4) {
803+ return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
703804 }
704805
705806 if (enableIPv6useIPv6) {
706807 return '[::1]';
707808 }
708809
709810 if (enableIPv4useIPv4) {
710811 return '127.0.0.1';
711812 }
712813 }
@@ -718,11 +819,13 @@ function getAutorunHostname() {
718819 * Tasks that need to be run after the server starts listening.
719820 * @param {boolean} v6Failed If the server failed to start on IPv6
720821 * @param {boolean} v4Failed If the server failed to start on IPv4
822+ * @param {boolean} useIPv6 If the server is using IPv6
823+ * @param {boolean} useIPv4 If the server is using IPv4
721824 */
722825const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
723826 const autorunUrl = new URL(
724827 (cliArguments.ssl ? 'https://' : 'http://') +
725828 (await getAutorunHostname(useIPv6, useIPv4)) +
726829 (':') +
727830 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
728831 );
@@ -735,36 +838,48 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
735838
736839 let logListen = 'SillyTavern is listening on';
737840
738841 if (enableIPv6useIPv6 && !v6Failed) {
739842 logListen += color.green(' IPv6: ' + tavernUrlV6.host);
843+ ' IPv6: ' + tavernUrlV6.host,
844+ );
740845 }
741846
742847 if (enableIPv4useIPv4 && !v4Failed) {
743848 logListen += color.green(' IPv4: ' + tavernUrl.host);
849+ ' IPv4: ' + tavernUrl.host,
850+ );
744851 }
745852
746853 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
747854 const plainGoToLog = removeColorFormatting(goToLog);
748855
749856 console.log(logListen);
857+ if (listen) {
858+ console.log();
859+ console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
860+ console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
861+ }
750862 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
751863 console.log(goToLog);
752864 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
753865
754- if (listen) {
755- 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');
756- }
757866
758867 if (basicAuthMode) {
759868 if (perUserBasicAuth && !enableAccounts) {
760- console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));
869+ console.error(color.red(
870+ 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
871+ ));
761872 } else if (!perUserBasicAuth) {
762873 const basicAuthUser = getConfigValue('basicAuthUser', {});
763874 if (!basicAuthUser?.username || !basicAuthUser?.password) {
764- console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
875+ console.warn(color.yellow(
876+ 'Basic Authentication is enabled, but username or password is not set or empty!',
877+ ));
765878 }
766879 }
767880 }
881+
882+ setupLogLevel();
768883};
769884
770885/**
@@ -814,14 +929,16 @@ function logSecurityAlert(message) {
814929 * Handles the case where the server failed to start on one or both protocols.
815930 * @param {boolean} v6Failed If the server failed to start on IPv6
816931 * @param {boolean} v4Failed If the server failed to start on IPv4
932+ * @param {boolean} useIPv6 If use IPv6
933+ * @param {boolean} useIPv4 If use IPv4
817934 */
818935function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
819936 if (v6Failed && !enableIPv4useIPv4) {
820937 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
821938 process.exit(1);
822939 }
823940
824941 if (v4Failed && !enableIPv6useIPv6) {
825942 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
826943 process.exit(1);
827944 }
@@ -835,10 +952,11 @@ function handleServerListenFail(v6Failed, v4Failed) {
835952/**
836953 * Creates an HTTPS server.
837954 * @param {URL} url The URL to listen on
955+ * @param {number} ipVersion the ip version to use
838956 * @returns {Promise<void>} A promise that resolves when the server is listening
839957 * @throws {Error} If the server fails to start
840958 */
841959function createHttpsServer(url, ipVersion) {
842960 return new Promise((resolve, reject) => {
843961 const server = https.createServer(
844962 {
@@ -847,34 +965,56 @@ function createHttpsServer(url) {
847965 }, app);
848966 server.on('error', reject);
849967 server.on('listening', resolve);
850- server.listen(Number(url.port || 443), url.hostname);
968+
969+ let host = url.hostname;
970+ if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
971+ server.listen({
972+ host: host,
973+ port: Number(url.port || 443),
974+ // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
975+ ipv6Only: true,
976+ });
851977 });
852978}
853979
854980/**
855981 * Creates an HTTP server.
856982 * @param {URL} url The URL to listen on
983+ * @param {number} ipVersion the ip version to use
857984 * @returns {Promise<void>} A promise that resolves when the server is listening
858985 * @throws {Error} If the server fails to start
859986 */
860987function createHttpServer(url, ipVersion) {
861988 return new Promise((resolve, reject) => {
862989 const server = http.createServer(app);
863990 server.on('error', reject);
864991 server.on('listening', resolve);
865- server.listen(Number(url.port || 80), url.hostname);
992+
993+ let host = url.hostname;
994+ if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
995+ server.listen({
996+ host: host,
997+ port: Number(url.port || 80),
998+ // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
999+ ipv6Only: true,
1000+ });
8661001 });
8671002}
8681003
869-async function startHTTPorHTTPS() {
1004+/**
1005+ * Starts the server using http or https depending on config
1006+ * @param {boolean} useIPv6 If use IPv6
1007+ * @param {boolean} useIPv4 If use IPv4
1008+ */
1009+async function startHTTPorHTTPS(useIPv6, useIPv4) {
8701010 let v6Failed = false;
8711011 let v4Failed = false;
8721012
8731013 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
8741014
8751015 if (enableIPv6useIPv6) {
8761016 try {
8771017 await createFunc(tavernUrlV6, 6);
8781018 } catch (error) {
8791019 console.error('non-fatal error: failed to start server on IPv6');
8801020 console.error(error);
@@ -883,9 +1023,9 @@ async function startHTTPorHTTPS() {
8831023 }
8841024 }
8851025
8861026 if (enableIPv4useIPv4) {
8871027 try {
8881028 await createFunc(tavernUrl, 4);
8891029 } catch (error) {
8901030 console.error('non-fatal error: failed to start server on IPv4');
8911031 console.error(error);
@@ -898,10 +1038,59 @@ async function startHTTPorHTTPS() {
8981038}
8991039
9001040async function startServer() {
901- const [v6Failed, v4Failed] = await startHTTPorHTTPS();
1041+ let useIPv6 = (enableIPv6 === true);
1042+ let useIPv4 = (enableIPv4 === true);
1043+
1044+ let hasIPv6 = false,
1045+ hasIPv4 = false,
1046+ hasIPv6Local = false,
1047+ hasIPv4Local = false,
1048+ hasIPv6Any = false,
1049+ hasIPv4Any = false;
1050+
1051+ if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
1052+ [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
1053+
1054+ hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
1055+ if (enableIPv6 === 'auto') {
1056+ useIPv6 = hasIPv6;
1057+ }
1058+ if (hasIPv6) {
1059+ if (useIPv6) {
1060+ console.log(color.green('IPv6 support detected'));
1061+ } else {
1062+ console.log('IPv6 support detected (but disabled)');
1063+ }
1064+ }
1065+
1066+ hasIPv4 = listen ? hasIPv4Any : hasIPv4Local;
1067+ if (enableIPv4 === 'auto') {
1068+ useIPv4 = hasIPv4;
1069+ }
1070+ if (hasIPv4) {
1071+ if (useIPv4) {
1072+ console.log(color.green('IPv4 support detected'));
1073+ } else {
1074+ console.log('IPv4 support detected (but disabled)');
1075+ }
1076+ }
1077+
1078+ if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
1079+ if (!hasIPv6 && !hasIPv4) {
1080+ console.error('Both IPv6 and IPv4 are not detected');
1081+ process.exit(1);
1082+ }
1083+ }
1084+ }
1085+
1086+ if (!useIPv6 && !useIPv4) {
1087+ 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');
1088+ process.exit(1);
1089+ }
9021090
903- handleServerListenFail(v6Failed, v4Failed);
1091+ const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
9041092 postSetupTaskshandleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
1093+ postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
9051094}
9061095
9071096async function verifySecuritySettings() {
@@ -911,7 +1100,7 @@ async function verifySecuritySettings() {
9111100 }
9121101
9131102 if (!enableAccounts) {
9141103 logSecurityAlert('Your current SillyTavern isconfiguration currentlyis insecurelyinsecure open(listening to the publicnon-localhost). Enable whitelisting, basic authentication or user accounts.');
9151104 }
9161105
9171106 const users = await getAllEnabledUsers();
src/constants.js+12 -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',
@@ -414,3 +415,10 @@ export const VLLM_KEYS = [
414415 'guided_decoding_backend',
415416 'guided_whitespace_pattern',
416417];
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/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+78 -65
@@ -114,7 +114,7 @@ async function sendClaudeRequest(request, response) {
114114 }
115115
116116 if (!apiKey) {
117117 console.logwarn(color.red(`Claude API key is missing.\n${divider}`));
118118 return response.status(400).send({ error: true });
119119 }
120120
@@ -179,7 +179,7 @@ async function sendClaudeRequest(request, response) {
179179 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
180180 }
181181
182182 console.logdebug('Claude request:', requestBody);
183183
184184 const generateResponse = await fetch(apiUrl + '/messages', {
185185 method: 'POST',
@@ -199,21 +199,21 @@ async function sendClaudeRequest(request, response) {
199199 } else {
200200 if (!generateResponse.ok) {
201201 const generateResponseText = await generateResponse.text();
202202 console.logwarn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
203203 return response.status(generateResponse.status).send({ error: true });
204204 }
205205
206206 /** @type {any} */
207207 const generateResponseJson = await generateResponse.json();
208208 const responseText = generateResponseJson?.content?.[0]?.text || '';
209209 console.logdebug('Claude response:', generateResponseJson);
210210
211211 // Wrap it back to OAI format + save the original content
212212 const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };
213213 return response.send(reply);
214214 }
215215 } catch (error) {
216216 console.logerror(color.red(`Error communicating with Claude: ${error}\n${divider}`));
217217 if (!response.headersSent) {
218218 return response.status(500).send({ error: true });
219219 }
@@ -230,12 +230,12 @@ async function sendScaleRequest(request, response) {
230230 const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);
231231
232232 if (!apiKey) {
233233 console.logwarn('Scale API key is missing.');
234234 return response.status(400).send({ error: true });
235235 }
236236
237237 const requestPrompt = convertTextCompletionPrompt(request.body.messages);
238238 console.logdebug('Scale request:', requestPrompt);
239239
240240 try {
241241 const controller = new AbortController();
@@ -254,18 +254,18 @@ async function sendScaleRequest(request, response) {
254254 });
255255
256256 if (!generateResponse.ok) {
257257 console.logwarn(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
258258 return response.status(500).send({ error: true });
259259 }
260260
261261 /** @type {any} */
262262 const generateResponseJson = await generateResponse.json();
263263 console.logdebug('Scale response:', generateResponseJson);
264264
265265 const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };
266266 return response.send(reply);
267267 } catch (error) {
268268 console.logerror(error);
269269 if (!response.headersSent) {
270270 return response.status(500).send({ error: true });
271271 }
@@ -282,13 +282,12 @@ async function sendMakerSuiteRequest(request, response) {
282282 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
283283
284284 if (!request.body.reverse_proxy && !apiKey) {
285285 console.logwarn('Google AI Studio API key is missing.');
286286 return response.status(400).send({ error: true });
287287 }
288288
289289 const model = String(request.body.model);
290290 const stream = Boolean(request.body.stream);
291- const showThoughts = Boolean(request.body.include_reasoning);
292291 const isThinking = model.includes('thinking');
293292
294293 const generationConfig = {
@@ -306,8 +305,9 @@ async function sendMakerSuiteRequest(request, response) {
306305 }
307306
308307 const should_use_system_prompt = (
308+ model.includes('gemini-2.0-pro') ||
309+ model.includes('gemini-2.0-flash') ||
309310 model.includes('gemini-2.0-flash-thinking-exp') ||
310- model.includes('gemini-2.0-flash-exp') ||
311311 model.includes('gemini-1.5-flash') ||
312312 model.includes('gemini-1.5-pro') ||
313313 model.startsWith('gemini-exp')
@@ -316,9 +316,15 @@ async function sendMakerSuiteRequest(request, response) {
316316 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
317317 let safetySettings = GEMINI_SAFETY;
318318
319- 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)) {
320325 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));
321326 }
327+ // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
322328
323329 let body = {
324330 contents: prompt.contents,
@@ -330,17 +336,11 @@ async function sendMakerSuiteRequest(request, response) {
330336 body.systemInstruction = prompt.system_instruction;
331337 }
332338
333- if (isThinking && showThoughts) {
334- generationConfig.thinkingConfig = {
335- includeThoughts: true,
336- };
337- }
338-
339339 return body;
340340 }
341341
342342 const body = getGeminiBody();
343343 console.logdebug('Google AI Studio request:', body);
344344
345345 try {
346346 const controller = new AbortController();
@@ -366,14 +366,14 @@ async function sendMakerSuiteRequest(request, response) {
366366 // Pipe remote SSE stream to Express response
367367 forwardFetchResponse(generateResponse, response);
368368 } catch (error) {
369369 console.logerror('Error forwarding streaming response:', error);
370370 if (!response.headersSent) {
371371 return response.status(500).send({ error: true });
372372 }
373373 }
374374 } else {
375375 if (!generateResponse.ok) {
376376 console.logwarn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
377377 return response.status(generateResponse.status).send({ error: true });
378378 }
379379
@@ -383,7 +383,7 @@ async function sendMakerSuiteRequest(request, response) {
383383 const candidates = generateResponseJson?.candidates;
384384 if (!candidates || candidates.length === 0) {
385385 let message = 'Google AI Studio API returned no candidate';
386386 console.logwarn(message, generateResponseJson);
387387 if (generateResponseJson?.promptFeedback?.blockReason) {
388388 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
389389 }
@@ -391,12 +391,12 @@ async function sendMakerSuiteRequest(request, response) {
391391 }
392392
393393 const responseContent = candidates[0].content ?? candidates[0].output;
394394 console.logwarn('Google AI Studio response:', responseContent);
395395
396396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
397397 if (!responseText) {
398398 let message = 'Google AI Studio Candidate text empty';
399399 console.logwarn(message, generateResponseJson);
400400 return response.send({ error: { message } });
401401 }
402402
@@ -405,7 +405,7 @@ async function sendMakerSuiteRequest(request, response) {
405405 return response.send(reply);
406406 }
407407 } catch (error) {
408408 console.logerror('Error communicating with Google AI Studio API: ', error);
409409 if (!response.headersSent) {
410410 return response.status(500).send({ error: true });
411411 }
@@ -419,8 +419,9 @@ async function sendMakerSuiteRequest(request, response) {
419419 */
420420async function sendAI21Request(request, response) {
421421 if (!request.body) return response.sendStatus(400);
422+
422423 const controller = new AbortController();
423424 console.logdebug(request.body.messages);
424425 request.socket.removeAllListeners('close');
425426 request.socket.on('close', function () {
426427 controller.abort();
@@ -446,7 +447,7 @@ async function sendAI21Request(request, response) {
446447 signal: controller.signal,
447448 };
448449
449450 console.logdebug('AI21 request:', body);
450451
451452 try {
452453 const generateResponse = await fetch(API_AI21 + '/chat/completions', options);
@@ -455,16 +456,16 @@ async function sendAI21Request(request, response) {
455456 } else {
456457 if (!generateResponse.ok) {
457458 const errorText = await generateResponse.text();
458459 console.logwarn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
459460 const errorJson = tryParse(errorText) ?? { error: true };
460461 return response.status(500).send(errorJson);
461462 }
462463 const generateResponseJson = await generateResponse.json();
463464 console.logdebug('AI21 response:', generateResponseJson);
464465 return response.send(generateResponseJson);
465466 }
466467 } catch (error) {
467468 console.logerror('Error communicating with AI21 API: ', error);
468469 if (!response.headersSent) {
469470 response.send({ error: true });
470471 } else {
@@ -483,7 +484,7 @@ async function sendMistralAIRequest(request, response) {
483484 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
484485
485486 if (!apiKey) {
486487 console.logwarn('MistralAI API key is missing.');
487488 return response.status(400).send({ error: true });
488489 }
489490
@@ -524,7 +525,7 @@ async function sendMistralAIRequest(request, response) {
524525 timeout: 0,
525526 };
526527
527528 console.logdebug('MisralAI request:', requestBody);
528529
529530 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
530531 if (request.body.stream) {
@@ -532,16 +533,16 @@ async function sendMistralAIRequest(request, response) {
532533 } else {
533534 if (!generateResponse.ok) {
534535 const errorText = await generateResponse.text();
535536 console.logwarn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
536537 const errorJson = tryParse(errorText) ?? { error: true };
537538 return response.status(500).send(errorJson);
538539 }
539540 const generateResponseJson = await generateResponse.json();
540541 console.logdebug('MistralAI response:', generateResponseJson);
541542 return response.send(generateResponseJson);
542543 }
543544 } catch (error) {
544545 console.logerror('Error communicating with MistralAI API: ', error);
545546 if (!response.headersSent) {
546547 response.send({ error: true });
547548 } else {
@@ -564,7 +565,7 @@ async function sendCohereRequest(request, response) {
564565 });
565566
566567 if (!apiKey) {
567568 console.logwarn('Cohere API key is missing.');
568569 return response.status(400).send({ error: true });
569570 }
570571
@@ -603,7 +604,7 @@ async function sendCohereRequest(request, response) {
603604 requestBody.safety_mode = 'OFF';
604605 }
605606
606607 console.logdebug('Cohere request:', requestBody);
607608
608609 const config = {
609610 method: 'POST',
@@ -625,16 +626,16 @@ async function sendCohereRequest(request, response) {
625626 const generateResponse = await fetch(apiUrl, config);
626627 if (!generateResponse.ok) {
627628 const errorText = await generateResponse.text();
628629 console.logwarn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
629630 const errorJson = tryParse(errorText) ?? { error: true };
630631 return response.status(500).send(errorJson);
631632 }
632633 const generateResponseJson = await generateResponse.json();
633634 console.logdebug('Cohere response:', generateResponseJson);
634635 return response.send(generateResponseJson);
635636 }
636637 } catch (error) {
637638 console.logerror('Error communicating with Cohere API: ', error);
638639 if (!response.headersSent) {
639640 response.send({ error: true });
640641 } else {
@@ -653,7 +654,7 @@ async function sendDeepSeekRequest(request, response) {
653654 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
654655
655656 if (!apiKey && !request.body.reverse_proxy) {
656657 console.logwarn('DeepSeek API key is missing.');
657658 return response.status(400).send({ error: true });
658659 }
659660
@@ -671,6 +672,11 @@ async function sendDeepSeekRequest(request, response) {
671672 bodyParams['logprobs'] = true;
672673 }
673674
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+
674680 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
675681 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
676682
@@ -698,7 +704,7 @@ async function sendDeepSeekRequest(request, response) {
698704 signal: controller.signal,
699705 };
700706
701707 console.logdebug('DeepSeek request:', requestBody);
702708
703709 const generateResponse = await fetch(apiUrl + '/chat/completions', config);
704710
@@ -707,16 +713,16 @@ async function sendDeepSeekRequest(request, response) {
707713 } else {
708714 if (!generateResponse.ok) {
709715 const errorText = await generateResponse.text();
710716 console.logwarn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
711717 const errorJson = tryParse(errorText) ?? { error: true };
712718 return response.status(500).send(errorJson);
713719 }
714720 const generateResponseJson = await generateResponse.json();
715721 console.logdebug('DeepSeek response:', generateResponseJson);
716722 return response.send(generateResponseJson);
717723 }
718724 } catch (error) {
719725 console.logerror('Error communicating with DeepSeek API: ', error);
720726 if (!response.headersSent) {
721727 response.send({ error: true });
722728 } else {
@@ -774,12 +780,12 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
774780 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
775781 headers = {};
776782 } else {
777783 console.logwarn('This chat completion source is not supported yet.');
778784 return response_getstatus_openai.status(400).send({ error: true });
779785 }
780786
781787 if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
782788 console.logwarn('Chat Completion API key is missing.');
783789 return response_getstatus_openai.status(400).send({ error: true });
784790 }
785791
@@ -814,23 +820,23 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
814820 };
815821 });
816822
817823 console.loginfo('Available OpenRouter models:', models);
818824 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
819825 const models = data?.data;
820826 console.loginfo(models);
821827 } else {
822828 const models = data?.data;
823829
824830 if (Array.isArray(models)) {
825831 const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
826832 console.loginfo('Available models:', modelIds);
827833 } else {
828834 console.logwarn('Chat Completion endpoint did not return a list of models.');
829835 }
830836 }
831837 }
832838 else {
833839 console.logerror('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');
834840 response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });
835841 }
836842 } catch (e) {
@@ -863,7 +869,7 @@ router.post('/bias', jsonParser, async function (request, response) {
863869 const tokenizer = getSentencepiceTokenizer(model);
864870 const instance = await tokenizer?.get();
865871 if (!instance) {
866872 console.warnerror('Tokenizer not initialized:', model);
867873 return response.send({});
868874 }
869875 encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
@@ -1025,7 +1031,7 @@ router.post('/generate', jsonParser, function (request, response) {
10251031 mergeObjectWithYaml(headers, request.body.custom_include_headers);
10261032
10271033 if (request.body.custom_prompt_post_processing) {
10281034 console.loginfo('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
10291035 request.body.messages = postProcessPrompt(
10301036 request.body.messages,
10311037 request.body.custom_prompt_post_processing,
@@ -1058,12 +1064,19 @@ router.post('/generate', jsonParser, function (request, response) {
10581064 headers = {};
10591065 bodyParams = {};
10601066 } else {
10611067 console.logwarn('This chat completion source is not supported yet.');
10621068 return response.status(400).send({ error: true });
10631069 }
10641070
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+
10651078 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
10661079 console.logwarn('OpenAI API key is missing.');
10671080 return response.status(400).send({ error: true });
10681081 }
10691082
@@ -1123,7 +1136,7 @@ router.post('/generate', jsonParser, function (request, response) {
11231136 signal: controller.signal,
11241137 };
11251138
11261139 console.logdebug(requestBody);
11271140
11281141 makeRequest(config, response, request);
11291142
@@ -1140,7 +1153,7 @@ router.post('/generate', jsonParser, function (request, response) {
11401153 const fetchResponse = await fetch(endpointUrl, config);
11411154
11421155 if (request.body.stream) {
11431156 console.loginfo('Streaming request in progress');
11441157 forwardFetchResponse(fetchResponse, response);
11451158 return;
11461159 }
@@ -1149,10 +1162,10 @@ router.post('/generate', jsonParser, function (request, response) {
11491162 /** @type {any} */
11501163 let json = await fetchResponse.json();
11511164 response.send(json);
11521165 console.logdebug(json);
11531166 console.logdebug(json?.choices?.[0]?.message);
11541167 } else if (fetchResponse.status === 429 && retries > 0) {
11551168 console.logwarn(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
11561169 setTimeout(() => {
11571170 timeout *= 2;
11581171 makeRequest(config, response, request, retries - 1, timeout);
@@ -1161,7 +1174,7 @@ router.post('/generate', jsonParser, function (request, response) {
11611174 await handleErrorResponse(fetchResponse);
11621175 }
11631176 } catch (error) {
11641177 console.logerror('Generation failed', error);
11651178 const message = error.code === 'ECONNREFUSED'
11661179 ? `Connection refused: ${error.message}`
11671180 : error.message || 'Unknown error occurred';
@@ -1183,7 +1196,7 @@ router.post('/generate', jsonParser, function (request, response) {
11831196
11841197 const message = errorResponse.statusText || 'Unknown error occurred';
11851198 const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';
11861199 console.logerror('Chat completion request error: ', message, responseText);
11871200
11881201 if (!response.headersSent) {
11891202 response.send({ error: { message }, quota_error: quota_error });
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+3 -3
@@ -27,7 +27,7 @@ router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function
2727 const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));
2828
2929 if (!fs.existsSync(fileName)) {
3030 console.logerror('BG file not found');
3131 return response.sendStatus(400);
3232 }
3333
@@ -43,12 +43,12 @@ router.post('/rename', jsonParser, function (request, response) {
4343 const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));
4444
4545 if (!fs.existsSync(oldFileName)) {
4646 console.logerror('BG file not found');
4747 return response.sendStatus(400);
4848 }
4949
5050 if (fs.existsSync(newFileName)) {
5151 console.logerror('New BG file already exists');
5252 return response.sendStatus(400);
5353 }
5454
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+37 -35
@@ -97,7 +97,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
9797 writeFileAtomicSync(outputImagePath, outputImage);
9898 return true;
9999 } catch (err) {
100100 console.logerror(err);
101101 return false;
102102 }
103103}
@@ -166,7 +166,7 @@ async function tryReadImage(imgPath, crop) {
166166 }
167167 // If it's an unsupported type of image (APNG) - just read the file as buffer
168168 catch (error) {
169169 console.logerror(`Failed to read image: ${imgPath}`, error);
170170 return fs.readFileSync(imgPath);
171171 }
172172}
@@ -229,12 +229,12 @@ const processCharacter = async (item, directories) => {
229229 return character;
230230 }
231231 catch (err) {
232232 console.logerror(`Could not process character: ${item}`);
233233
234234 if (err instanceof SyntaxError) {
235235 console.logerror(`${item} does not contain a valid JSON object.`);
236236 } else {
237237 console.logerror('An unexpected error occurred: ', err);
238238 }
239239
240240 return {
@@ -322,7 +322,7 @@ function readFromV2(char) {
322322 };
323323
324324 _.forEach(fieldMappings, (v2Path, charField) => {
325325 //console.loginfo(`Migrating field: ${charField} from ${v2Path}`);
326326 const v2Value = _.get(char.data, v2Path);
327327 if (_.isUndefined(v2Value)) {
328328 let defaultValue = undefined;
@@ -337,15 +337,15 @@ function readFromV2(char) {
337337 }
338338
339339 if (!_.isUndefined(defaultValue)) {
340340 //console.debugwarn(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
341341 char[charField] = defaultValue;
342342 } else {
343343 console.debugwarn(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
344344 return;
345345 }
346346 }
347347 if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {
348348 console.debugwarn(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
349349 }
350350 char[charField] = v2Value;
351351 });
@@ -442,7 +442,7 @@ function charaFormatData(data, directories) {
442442 }
443443
444444 } catch {
445445 console.debugwarn(`Failed to read world info file: ${data.world}. Character book will not be available.`);
446446 }
447447 }
448448
@@ -452,7 +452,7 @@ function charaFormatData(data, directories) {
452452 // Deep merge the extensions object
453453 _.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));
454454 } catch {
455455 console.debugwarn(`Failed to parse extensions JSON: ${data.extensions}`);
456456 }
457457 }
458458
@@ -526,7 +526,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
526526 const fileText = fs.readFileSync(uploadPath, 'utf8');
527527 fs.rmSync(uploadPath);
528528 const yamlData = yaml.parse(fileText);
529529 console.loginfo('Importing from YAML');
530530 yamlData.name = sanitize(yamlData.name);
531531 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
532532 let char = convertToV2({
@@ -559,7 +559,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
559559async function importFromCharX(uploadPath, { request }, preservedFileName) {
560560 const data = fs.readFileSync(uploadPath).buffer;
561561 fs.rmSync(uploadPath);
562562 console.loginfo('Importing from CharX');
563563 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
564564
565565 if (!cardBuffer) {
@@ -608,7 +608,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
608608 let jsonData = JSON.parse(data);
609609
610610 if (jsonData.spec !== undefined) {
611611 console.loginfo(`Importing from ${jsonData.spec} json`);
612612 importRisuSprites(request.user.directories, jsonData);
613613 unsetFavFlag(jsonData);
614614 jsonData = readFromV2(jsonData);
@@ -618,7 +618,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
618618 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
619619 return result ? pngName : '';
620620 } else if (jsonData.name !== undefined) {
621621 console.loginfo('Importing from v1 json');
622622 jsonData.name = sanitize(jsonData.name);
623623 if (jsonData.creator_notes) {
624624 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -644,7 +644,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
644644 const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
645645 return result ? pngName : '';
646646 } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
647647 console.loginfo('Importing from gradio json');
648648 jsonData.char_name = sanitize(jsonData.char_name);
649649 if (jsonData.creator_notes) {
650650 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -691,7 +691,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
691691 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
692692
693693 if (jsonData.spec !== undefined) {
694694 console.loginfo(`Found a ${jsonData.spec} character file.`);
695695 importRisuSprites(request.user.directories, jsonData);
696696 unsetFavFlag(jsonData);
697697 jsonData = readFromV2(jsonData);
@@ -701,7 +701,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
701701 fs.unlinkSync(uploadPath);
702702 return result ? pngName : '';
703703 } else if (jsonData.name !== undefined) {
704704 console.loginfo('Found a v1 character file.');
705705
706706 if (jsonData.creator_notes) {
707707 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@@ -812,13 +812,13 @@ router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (
812812
813813router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
814814 if (!request.body) {
815815 console.errorwarn('Error: no response body detected');
816816 response.status(400).send('Error: no response body detected');
817817 return;
818818 }
819819
820820 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
821821 console.errorwarn('Error: invalid name.');
822822 response.status(400).send('Error: invalid name.');
823823 return;
824824 }
@@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
839839 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
840840 await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
841841 fs.unlinkSync(newAvatarPath);
842+
843+ // Bust cache to reload the new avatar
844+ response.setHeader('Clear-Site-Data', '"cache"');
842845 }
843846
844847 return response.sendStatus(200);
@@ -860,14 +863,14 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
860863 * @returns {void}
861864 */
862865router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
863866 console.logdebug(request.body);
864867 if (!request.body) {
865868 console.errorwarn('Error: no response body detected');
866869 return response.status(400).send('Error: no response body detected');
867870 }
868871
869872 if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
870873 console.errorwarn('Error: invalid name.');
871874 return response.status(400).send('Error: invalid name.');
872875 }
873876
@@ -879,7 +882,7 @@ router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async fu
879882 const char = JSON.parse(charJSON);
880883 //check if the field exists
881884 if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
882885 console.errorwarn('Error: invalid field.');
883886 response.status(400).send('Error: invalid field.');
884887 return;
885888 }
@@ -928,7 +931,7 @@ router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avat
928931 await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);
929932 response.sendStatus(200);
930933 } else {
931934 console.logwarn(validator.lastValidationError);
932935 response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });
933936 }
934937 } catch (exception) {
@@ -1050,7 +1053,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
10501053 const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
10511054
10521055 if (stats.size === 0) {
10531056 console.logwarn(`Found an empty chat file: ${pathToFile}`);
10541057 res({});
10551058 return;
10561059 }
@@ -1082,7 +1085,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
10821085
10831086 res(chatData);
10841087 } else {
10851088 console.logwarn('Found an invalid or corrupted chat file:', pathToFile);
10861089 res({});
10871090 }
10881091 }
@@ -1095,7 +1098,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
10951098
10961099 return response.send(validFiles);
10971100 } catch (error) {
10981101 console.logerror(error);
10991102 return response.send({ error: true });
11001103 }
11011104});
@@ -1152,7 +1155,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11521155 const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);
11531156
11541157 if (!fileName) {
11551158 console.errorwarn('Failed to import character');
11561159 return response.sendStatus(400);
11571160 }
11581161
@@ -1162,7 +1165,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11621165
11631166 response.send({ file_name: fileName });
11641167 } catch (err) {
11651168 console.logerror(err);
11661169 response.send({ error: true });
11671170 }
11681171});
@@ -1170,14 +1173,13 @@ router.post('/import', urlencodedParser, async function (request, response) {
11701173router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
11711174 try {
11721175 if (!request.body.avatar_url) {
11731176 console.logwarn('avatar URL not found in request body');
11741177 console.logdebug(request.body);
11751178 return response.sendStatus(400);
11761179 }
11771180 let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
11781181 if (!fs.existsSync(filename)) {
11791182 console.logerror('file for dupe not found', filename);
1180- console.log(filename);
11811183 return response.sendStatus(404);
11821184 }
11831185 let suffix = 1;
@@ -1205,7 +1207,7 @@ router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async functio
12051207 }
12061208
12071209 fs.copyFileSync(filename, newFilename);
12081210 console.loginfo(`${filename} was copied to ${newFilename}`);
12091211 response.send({ path: path.parse(newFilename).base });
12101212 }
12111213 catch (error) {
src/endpoints/chats.js+17 -18
@@ -50,7 +50,7 @@ function backupChat(directory, name, chat) {
5050
5151 removeOldBackups(directory, 'chat_', maxTotalChatBackups);
5252 } catch (err) {
5353 console.logerror(`Could not backup chat for ${name}`, err);
5454 }
5555}
5656
@@ -306,8 +306,8 @@ router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request,
306306 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);
307307 return response.send({ result: 'ok' });
308308 } catch (error) {
309309 responseconsole.senderror(error);
310310 return consoleresponse.logsend(error);
311311 }
312312});
313313
@@ -359,17 +359,17 @@ router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (
359359 const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));
360360 const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));
361361 const sanitizedFileName = path.parse(pathToRenamedFile).name;
362362 console.loginfo('Old chat name', pathToOriginalFile);
363363 console.loginfo('New chat name', pathToRenamedFile);
364364
365365 if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {
366366 console.logerror('Either Source or Destination files are not available');
367367 return response.status(400).send({ error: true });
368368 }
369369
370370 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
371371 fs.rmSync(pathToOriginalFile);
372372 console.loginfo('Successfully renamed.');
373373 return response.send({ ok: true, sanitizedFileName });
374374});
375375
@@ -380,12 +380,12 @@ router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (reques
380380 const chatFileExists = fs.existsSync(filePath);
381381
382382 if (!chatFileExists) {
383383 console.logerror(`Chat file not found '${filePath}'`);
384384 return response.sendStatus(400);
385385 }
386386
387387 fs.rmSync(filePath);
388388 console.loginfo('`Deleted chat file: ' + ${filePath}`);
389389 return response.send('ok');
390390});
391391
@@ -402,7 +402,7 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
402402 const errorMessage = {
403403 message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
404404 };
405405 console.logerror(errorMessage.message);
406406 return response.status(404).json(errorMessage);
407407 }
408408 try {
@@ -415,14 +415,14 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
415415 result: rawFile,
416416 };
417417
418418 console.loginfo(`Chat exported as ${exportfilename}`);
419419 return response.status(200).json(successMessage);
420420 } catch (err) {
421421 console.error(err);
422422 const errorMessage = {
423423 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
424424 };
425425 console.logerror(errorMessage.message);
426426 return response.status(500).json(errorMessage);
427427 }
428428 }
@@ -449,12 +449,11 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
449449 message: `Chat saved to ${exportfilename}`,
450450 result: buffer,
451451 };
452452 console.loginfo(`Chat exported as ${exportfilename}`);
453453 return response.status(200).json(successMessage);
454454 });
455455 } catch (err) {
456456 console.logerror('chat export failed.', err);
457- console.log(err);
458457 return response.sendStatus(400);
459458 }
460459});
@@ -513,7 +512,7 @@ router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (
513512 } else if (jsonData.type === 'risuChat') { // RisuAI format
514513 importFunc = importRisuChat;
515514 } else { // Unknown format
516515 console.logerror('Incorrect chat format .json');
517516 return response.send({ error: true });
518517 }
519518
@@ -541,7 +540,7 @@ router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (
541540 const jsonData = JSON.parse(header);
542541
543542 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {
544543 console.logerror('Incorrect chat format .jsonl');
545544 return response.send({ error: true });
546545 }
547546
@@ -647,7 +646,7 @@ router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (reques
647646 break;
648647 }
649648 } catch (error) {
650649 console.errorwarn(groupFile, 'group file is corrupted:', error);
651650 }
652651 }
653652
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+7 -7
@@ -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
9797 return response.send(verified);
9898 } catch (error) {
9999 console.logerror(error);
100100 return response.sendStatus(500);
101101 }
102102});
src/endpoints/google.js+3 -3
@@ -34,7 +34,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
3434 generationConfig: { maxOutputTokens: 1000 },
3535 };
3636
3737 console.logdebug('Multimodal captioning request', model, body);
3838
3939 const result = await fetch(url, {
4040 body: JSON.stringify(body),
@@ -46,13 +46,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4646
4747 if (!result.ok) {
4848 const error = await result.json();
4949 console.logerror(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
5050 return response.status(result.status).send({ error: true });
5151 }
5252
5353 /** @type {any} */
5454 const data = await result.json();
5555 console.loginfo('Multimodal captioning response', data);
5656
5757 const candidates = data?.candidates;
5858 if (!candidates) {
src/endpoints/groups.js+1 -1
@@ -114,7 +114,7 @@ router.post('/delete', jsonParser, async (request, response) => {
114114
115115 if (group && Array.isArray(group.chats)) {
116116 for (const chat of group.chats) {
117117 console.loginfo('Deleting group chat', chat);
118118 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
119119
120120 if (fs.existsSync(pathToFile)) {
src/endpoints/horde.js+15 -15
@@ -155,7 +155,7 @@ router.post('/cancel-task', jsonParser, async (request, response) => {
155155 });
156156
157157 const data = await fetchResult.json();
158158 console.loginfo(`Cancelled Horde task ${taskId}`);
159159 return response.send(data);
160160 } catch (error) {
161161 console.error(error);
@@ -174,7 +174,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
174174 });
175175
176176 const data = await fetchResult.json();
177177 console.loginfo(`Horde task ${taskId} status:`, data);
178178 return response.send(data);
179179 } catch (error) {
180180 console.error(error);
@@ -187,7 +187,7 @@ router.post('/generate-text', jsonParser, async (request, response) => {
187187 const url = 'https://aihorde.net/api/v2/generate/text/async';
188188 const agent = await getClientAgent();
189189
190190 console.logdebug(request.body);
191191 try {
192192 const result = await fetch(url, {
193193 method: 'POST',
@@ -201,14 +201,14 @@ router.post('/generate-text', jsonParser, async (request, response) => {
201201
202202 if (!result.ok) {
203203 const message = await result.text();
204204 console.logerror('Horde returned an error:', message);
205205 return response.send({ error: { message } });
206206 }
207207
208208 const data = await result.json();
209209 return response.send(data);
210210 } catch (error) {
211211 console.logerror(error);
212212 return response.send({ error: true });
213213 }
214214});
@@ -254,7 +254,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
254254 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
255255 await delay(CHECK_INTERVAL);
256256 const status = await ai_horde.getInterrogationStatus(result.id);
257257 console.loginfo(status);
258258
259259 if (status.state === HordeAsyncRequestStates.done) {
260260
@@ -263,7 +263,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
263263 return response.sendStatus(500);
264264 }
265265
266266 console.logdebug('Image interrogation result:', status);
267267 const caption = status?.forms[0]?.result?.caption || '';
268268
269269 if (!caption) {
@@ -275,7 +275,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
275275 }
276276
277277 if (status.state === HordeAsyncRequestStates.faulted || status.state === HordeAsyncRequestStates.cancelled) {
278278 console.logerror('Image interrogation request is not successful.');
279279 return response.sendStatus(503);
280280 }
281281 }
@@ -315,7 +315,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
315315 try {
316316 const maxLength = PROMPT_THRESHOLD - String(request.body.negative_prompt).length - 5;
317317 if (String(request.body.prompt).length > maxLength) {
318318 console.logwarn('Stable Horde prompt is too long, truncating...');
319319 request.body.prompt = String(request.body.prompt).substring(0, maxLength);
320320 }
321321
@@ -324,14 +324,14 @@ router.post('/generate-image', jsonParser, async (request, response) => {
324324 const sanitized = sanitizeHordeImagePrompt(request.body.prompt);
325325
326326 if (request.body.prompt !== sanitized) {
327327 console.loginfo('Stable Horde prompt was sanitized.');
328328 }
329329
330330 request.body.prompt = sanitized;
331331 }
332332
333333 const api_key_horde = readSecret(request.user.directories, SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
334334 console.logdebug('Stable Horde request:', request.body);
335335
336336 const ai_horde = await getHordeClient();
337337 // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan
@@ -360,16 +360,16 @@ router.post('/generate-image', jsonParser, async (request, response) => {
360360 { token: api_key_horde });
361361
362362 if (!generation.id) {
363363 console.errorwarn('Image generation request is not satisfyable:', generation.message || 'unknown error');
364364 return response.sendStatus(400);
365365 }
366366
367367 console.loginfo('Horde image generation request:', generation);
368368
369369 const controller = new AbortController();
370370 request.socket.removeAllListeners('close');
371371 request.socket.on('close', function () {
372372 console.logwarn('Horde image generation request aborted.');
373373 controller.abort();
374374 if (generation.id) ai_horde.deleteImageGenerationRequest(generation.id);
375375 });
@@ -378,7 +378,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
378378 controller.signal.throwIfAborted();
379379 await delay(CHECK_INTERVAL);
380380 const check = await ai_horde.getImageGenerationCheck(generation.id);
381381 console.loginfo(check);
382382
383383 if (check.done) {
384384 const result = await ai_horde.getImageGenerationStatus(generation.id);
src/endpoints/images.js+1 -1
@@ -71,7 +71,7 @@ router.post('/upload', jsonParser, async (request, response) => {
7171 await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer));
7272 response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });
7373 } catch (error) {
7474 console.logerror(error);
7575 response.status(500).send({ error: 'Failed to save the image' });
7676 }
7777});
src/endpoints/novelai.js+16 -16
@@ -120,7 +120,7 @@ router.post('/status', jsonParser, async function (req, res) {
120120 const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);
121121
122122 if (!api_key_novel) {
123123 console.logwarn('NovelAI Access Token is missing.');
124124 return res.sendStatus(400);
125125 }
126126
@@ -137,15 +137,15 @@ router.post('/status', jsonParser, async function (req, res) {
137137 const data = await response.json();
138138 return res.send(data);
139139 } else if (response.status == 401) {
140140 console.logerror('NovelAI Access Token is incorrect.');
141141 return res.send({ error: true });
142142 }
143143 else {
144144 console.logwarn('NovelAI returned an error:', response.statusText);
145145 return res.send({ error: true });
146146 }
147147 } catch (error) {
148148 console.logerror(error);
149149 return res.send({ error: true });
150150 }
151151});
@@ -156,7 +156,7 @@ router.post('/generate', jsonParser, async function (req, res) {
156156 const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);
157157
158158 if (!api_key_novel) {
159159 console.logwarn('NovelAI Access Token is missing.');
160160 return res.sendStatus(400);
161161 }
162162
@@ -241,7 +241,7 @@ router.post('/generate', jsonParser, async function (req, res) {
241241 }
242242 }
243243
244244 console.logdebug(util.inspect(data, { depth: 4 }));
245245
246246 const args = {
247247 body: JSON.stringify(data),
@@ -261,7 +261,7 @@ router.post('/generate', jsonParser, async function (req, res) {
261261 if (!response.ok) {
262262 const text = await response.text();
263263 let message = text;
264264 console.logwarn(`Novel API returned error: ${response.status} ${response.statusText} ${text}`);
265265
266266 try {
267267 const data = JSON.parse(text);
@@ -276,7 +276,7 @@ router.post('/generate', jsonParser, async function (req, res) {
276276
277277 /** @type {any} */
278278 const data = await response.json();
279279 console.loginfo('NovelAI Output', data?.output);
280280 return res.send(data);
281281 }
282282 } catch (error) {
@@ -292,12 +292,12 @@ router.post('/generate-image', jsonParser, async (request, response) => {
292292 const key = readSecret(request.user.directories, SECRET_KEYS.NOVEL);
293293
294294 if (!key) {
295295 console.logwarn('NovelAI Access Token is missing.');
296296 return response.sendStatus(400);
297297 }
298298
299299 try {
300300 console.logdebug('NAI Diffusion request:', request.body);
301301 const generateUrl = `${IMAGE_NOVELAI}/ai/generate-image`;
302302 const generateResult = await fetch(generateUrl, {
303303 method: 'POST',
@@ -358,7 +358,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
358358
359359 if (!generateResult.ok) {
360360 const text = await generateResult.text();
361361 console.logwarn('NovelAI returned an error.', generateResult.statusText, text);
362362 return response.sendStatus(500);
363363 }
364364
@@ -366,7 +366,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
366366 const imageBuffer = await extractFileFromZipBuffer(archiveBuffer, '.png');
367367
368368 if (!imageBuffer) {
369369 console.warnerror('NovelAI generated an image, but the PNG file was not found.');
370370 return response.sendStatus(500);
371371 }
372372
@@ -378,7 +378,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
378378 }
379379
380380 try {
381381 console.debuginfo('Upscaling image...');
382382 const upscaleUrl = `${API_NOVELAI}/ai/upscale`;
383383 const upscaleResult = await fetch(upscaleUrl, {
384384 method: 'POST',
@@ -413,7 +413,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
413413 return response.send(originalBase64);
414414 }
415415 } catch (error) {
416416 console.logerror(error);
417417 return response.sendStatus(500);
418418 }
419419});
@@ -422,7 +422,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
422422 const token = readSecret(request.user.directories, SECRET_KEYS.NOVEL);
423423
424424 if (!token) {
425425 console.logerror('NovelAI Access Token is missing.');
426426 return response.sendStatus(400);
427427 }
428428
@@ -445,7 +445,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
445445
446446 if (!result.ok) {
447447 const errorText = await result.text();
448448 console.logerror('NovelAI returned an error.', result.statusText, errorText);
449449 return response.sendStatus(500);
450450 }
451451
src/endpoints/openai.js+16 -16
@@ -63,7 +63,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
6363 }
6464
6565 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
6666 console.logwarn('No key found for API', request.body.api);
6767 return response.sendStatus(400);
6868 }
6969
@@ -93,7 +93,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
9393 excludeKeysByYaml(body, request.body.custom_exclude_body);
9494 }
9595
9696 console.logdebug('Multimodal captioning request', body);
9797
9898 let apiUrl = '';
9999
@@ -158,13 +158,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
158158
159159 if (!result.ok) {
160160 const text = await result.text();
161161 console.logwarn('Multimodal captioning request failed', result.statusText, text);
162162 return response.status(500).send(text);
163163 }
164164
165165 /** @type {any} */
166166 const data = await result.json();
167167 console.loginfo('Multimodal captioning response', data);
168168 const caption = data?.choices[0]?.message?.content;
169169
170170 if (!caption) {
@@ -184,17 +184,17 @@ router.post('/transcribe-audio', urlencodedParser, async (request, response) =>
184184 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
185185
186186 if (!key) {
187187 console.logwarn('No OpenAI key found');
188188 return response.sendStatus(400);
189189 }
190190
191191 if (!request.file) {
192192 console.logwarn('No audio file found');
193193 return response.sendStatus(400);
194194 }
195195
196196 const formData = new FormData();
197197 console.loginfo('Processing audio file', request.file.path);
198198 formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });
199199 formData.append('model', request.body.model);
200200
@@ -213,13 +213,13 @@ router.post('/transcribe-audio', urlencodedParser, async (request, response) =>
213213
214214 if (!result.ok) {
215215 const text = await result.text();
216216 console.logwarn('OpenAI request failed', result.statusText, text);
217217 return response.status(500).send(text);
218218 }
219219
220220 fs.rmSync(request.file.path);
221221 const data = await result.json();
222222 console.logdebug('OpenAI transcription response', data);
223223 return response.json(data);
224224 } catch (error) {
225225 console.error('OpenAI transcription failed', error);
@@ -232,7 +232,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
232232 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
233233
234234 if (!key) {
235235 console.logwarn('No OpenAI key found');
236236 return response.sendStatus(400);
237237 }
238238
@@ -253,7 +253,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
253253
254254 if (!result.ok) {
255255 const text = await result.text();
256256 console.logwarn('OpenAI request failed', result.statusText, text);
257257 return response.status(500).send(text);
258258 }
259259
@@ -271,11 +271,11 @@ router.post('/generate-image', jsonParser, async (request, response) => {
271271 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
272272
273273 if (!key) {
274274 console.logwarn('No OpenAI key found');
275275 return response.sendStatus(400);
276276 }
277277
278278 console.logdebug('OpenAI request', request.body);
279279
280280 const result = await fetch('https://api.openai.com/v1/images/generations', {
281281 method: 'POST',
@@ -288,7 +288,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
288288
289289 if (!result.ok) {
290290 const text = await result.text();
291291 console.logwarn('OpenAI request failed', result.statusText, text);
292292 return response.status(500).send(text);
293293 }
294294
@@ -308,7 +308,7 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
308308 const { input, provider_endpoint, response_format, voice, speed, model } = request.body;
309309
310310 if (!provider_endpoint) {
311311 console.logwarn('No OpenAI-compatible TTS provider endpoint provided');
312312 return response.sendStatus(400);
313313 }
314314
@@ -329,7 +329,7 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
329329
330330 if (!result.ok) {
331331 const text = await result.text();
332332 console.logwarn('OpenAI request failed', result.statusText, text);
333333 return response.status(500).send(text);
334334 }
335335
src/endpoints/openrouter.js+25 -0
@@ -4,6 +4,31 @@ import { jsonParser } from '../express-common.js';
44export const router = express.Router();
55const API_OPENROUTER = 'https://openrouter.ai/api/v1';
66
7+router.post('/models/providers', jsonParser, async (req, res) => {
8+ try {
9+ const { model } = req.body;
10+ const response = await fetch(`${API_OPENROUTER}/models/${model}/endpoints`, {
11+ method: 'GET',
12+ headers: {
13+ 'Accept': 'application/json',
14+ },
15+ });
16+
17+ if (!response.ok) {
18+ return res.json([]);
19+ }
20+
21+ const data = await response.json();
22+ const endpoints = data?.data?.endpoints || [];
23+ const providerNames = endpoints.map(e => e.provider_name);
24+
25+ return res.json(providerNames);
26+ } catch (error) {
27+ console.error(error);
28+ return res.sendStatus(500);
29+ }
30+});
31+
732router.post('/models/multimodal', jsonParser, async (_req, res) => {
833 try {
934 // The endpoint is available without authentication
src/endpoints/presets.js+1 -1
@@ -96,7 +96,7 @@ router.post('/restore', jsonParser, function (request, response) {
9696
9797 return response.send(result);
9898 } catch (error) {
9999 console.logerror(error);
100100 return response.sendStatus(500);
101101 }
102102});
src/endpoints/search.js+66 -21
@@ -96,25 +96,26 @@ router.post('/serpapi', jsonParser, async (request, response) => {
9696 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
9797
9898 if (!key) {
9999 console.logerror('No SerpApi key found');
100100 return response.sendStatus(400);
101101 }
102102
103103 const { query } = request.body;
104104 const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
105105
106106 console.logdebug('SerpApi query', query);
107107
108108 if (!result.ok) {
109109 const text = await result.text();
110110 console.logerror('SerpApi request failed', result.statusText, text);
111111 return response.status(500).send(text);
112112 }
113113
114114 const data = await result.json();
115+ console.debug('SerpApi response', data);
115116 return response.json(data);
116117 } catch (error) {
117118 console.logerror(error);
118119 return response.sendStatus(500);
119120 }
120121});
@@ -130,7 +131,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
130131 const json = request.body.json;
131132
132133 if (!id) {
133134 console.logerror('Id is required for /transcript');
134135 return response.sendStatus(400);
135136 }
136137
@@ -155,7 +156,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
155156 throw error;
156157 }
157158 } catch (error) {
158159 console.logerror(error);
159160 return response.sendStatus(500);
160161 }
161162});
@@ -165,17 +166,17 @@ router.post('/searxng', jsonParser, async (request, response) => {
165166 const { baseUrl, query, preferences, categories } = request.body;
166167
167168 if (!baseUrl || !query) {
168169 console.logerror('Missing required parameters for /searxng');
169170 return response.sendStatus(400);
170171 }
171172
172173 console.logdebug('SearXNG query', baseUrl, query);
173174
174175 const mainPageUrl = new URL(baseUrl);
175176 const mainPageRequest = await fetch(mainPageUrl, { headers: visitHeaders });
176177
177178 if (!mainPageRequest.ok) {
178179 console.logerror('SearXNG request failed', mainPageRequest.statusText);
179180 return response.sendStatus(500);
180181 }
181182
@@ -202,14 +203,14 @@ router.post('/searxng', jsonParser, async (request, response) => {
202203
203204 if (!searchResult.ok) {
204205 const text = await searchResult.text();
205206 console.logerror('SearXNG request failed', searchResult.statusText, text);
206207 return response.sendStatus(500);
207208 }
208209
209210 const data = await searchResult.text();
210211 return response.send(data);
211212 } catch (error) {
212213 console.logerror('SearXNG request failed', error);
213214 return response.sendStatus(500);
214215 }
215216});
@@ -219,7 +220,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
219220 const apiKey = readSecret(request.user.directories, SECRET_KEYS.TAVILY);
220221
221222 if (!apiKey) {
222223 console.logerror('No Tavily key found');
223224 return response.sendStatus(400);
224225 }
225226
@@ -246,18 +247,19 @@ router.post('/tavily', jsonParser, async (request, response) => {
246247 body: JSON.stringify(body),
247248 });
248249
249250 console.logdebug('Tavily query', query);
250251
251252 if (!result.ok) {
252253 const text = await result.text();
253254 console.logerror('Tavily request failed', result.statusText, text);
254255 return response.status(500).send(text);
255256 }
256257
257258 const data = await result.json();
259+ console.debug('Tavily response', data);
258260 return response.json(data);
259261 } catch (error) {
260262 console.logerror(error);
261263 return response.sendStatus(500);
262264 }
263265});
@@ -290,6 +292,49 @@ router.post('/koboldcpp', jsonParser, async (request, response) => {
290292 }
291293
292294 const data = await result.json();
295+ console.debug('KoboldCpp search response', data);
296+ return response.json(data);
297+ } catch (error) {
298+ console.error(error);
299+ return response.sendStatus(500);
300+ }
301+});
302+
303+router.post('/serper', jsonParser, async (request, response) => {
304+ try {
305+ const key = readSecret(request.user.directories, SECRET_KEYS.SERPER);
306+
307+ if (!key) {
308+ console.error('No Serper key found');
309+ return response.sendStatus(400);
310+ }
311+
312+ const { query, images } = request.body;
313+
314+ const url = images
315+ ? 'https://google.serper.dev/images'
316+ : 'https://google.serper.dev/search';
317+
318+ const result = await fetch(url, {
319+ method: 'POST',
320+ headers: {
321+ 'X-API-KEY': key,
322+ 'Content-Type': 'application/json',
323+ },
324+ redirect: 'follow',
325+ body: JSON.stringify({ q: query }),
326+ });
327+
328+ console.debug('Serper query', query);
329+
330+ if (!result.ok) {
331+ const text = await result.text();
332+ console.warn('Serper request failed', result.statusText, text);
333+ return response.status(500).send(text);
334+ }
335+
336+ const data = await result.json();
337+ console.debug('Serper response', data);
293338 return response.json(data);
294339 } catch (error) {
295340 console.error(error);
@@ -303,7 +348,7 @@ router.post('/visit', jsonParser, async (request, response) => {
303348 const html = Boolean(request.body.html ?? true);
304349
305350 if (!url) {
306351 console.logerror('No url provided for /visit');
307352 return response.sendStatus(400);
308353 }
309354
@@ -330,16 +375,16 @@ router.post('/visit', jsonParser, async (request, response) => {
330375 throw new Error('Invalid hostname');
331376 }
332377 } catch (error) {
333378 console.logerror('Invalid url provided for /visit', url);
334379 return response.sendStatus(400);
335380 }
336381
337382 console.loginfo('Visiting web URL', url);
338383
339384 const result = await fetch(url, { headers: visitHeaders });
340385
341386 if (!result.ok) {
342387 console.logerror(`Visit failed ${result.status} ${result.statusText}`);
343388 return response.sendStatus(500);
344389 }
345390
@@ -347,7 +392,7 @@ router.post('/visit', jsonParser, async (request, response) => {
347392
348393 if (html) {
349394 if (!contentType.includes('text/html')) {
350395 console.logerror(`Visit failed, content-type is ${contentType}, expected text/html`);
351396 return response.sendStatus(500);
352397 }
353398
@@ -359,7 +404,7 @@ router.post('/visit', jsonParser, async (request, response) => {
359404 const buffer = await result.arrayBuffer();
360405 return response.send(Buffer.from(buffer));
361406 } catch (error) {
362407 console.logerror(error);
363408 return response.sendStatus(500);
364409 }
365410});
src/endpoints/secrets.js+3 -1
@@ -50,8 +50,10 @@ export const SECRET_KEYS = {
5050 TAVILY: 'api_key_tavily',
5151 NANOGPT: 'api_key_nanogpt',
5252 BFL: 'api_key_bfl',
53+ FALAI: 'api_key_falai',
5354 GENERIC: 'api_key_generic',
5455 DEEPSEEK: 'api_key_deepseek',
56+ SERPER: 'api_key_serper',
5557};
5658
5759// These are the keys that are safe to expose, even if allowKeysExposure is false
@@ -151,7 +153,7 @@ export function getAllSecrets(directories) {
151153 const filePath = path.join(directories.root, SECRETS_FILE);
152154
153155 if (!fs.existsSync(filePath)) {
154156 console.logerror('Secrets file does not exist');
155157 return undefined;
156158 }
157159
src/endpoints/settings.js+7 -7
@@ -105,7 +105,7 @@ function readPresetsFromDirectory(directoryPath, options = {}) {
105105 fileNames.push(removeFileExtension ? item.replace(/\.[^/.]+$/, '') : item);
106106 } catch {
107107 // skip
108108 console.logwarn(`${item} is not a valid JSON`);
109109 }
110110 });
111111
@@ -120,7 +120,7 @@ async function backupSettings() {
120120 backupUserSettings(handle, true);
121121 }
122122 } catch (err) {
123123 console.logerror('Could not backup settings file', err);
124124 }
125125}
126126
@@ -202,7 +202,7 @@ router.post('/save', jsonParser, function (request, response) {
202202 triggerAutoSave(request.user.profile.handle);
203203 response.send({ result: 'ok' });
204204 } catch (err) {
205205 console.logerror(err);
206206 response.send(err);
207207 }
208208});
@@ -292,7 +292,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
292292
293293 response.json(result);
294294 } catch (error) {
295295 console.logerror(error);
296296 response.sendStatus(500);
297297 }
298298});
@@ -316,7 +316,7 @@ router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'),
316316
317317 response.send(content);
318318 } catch (error) {
319319 console.logerror(error);
320320 response.sendStatus(500);
321321 }
322322});
@@ -326,7 +326,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
326326 backupUserSettings(request.user.profile.handle, false);
327327 response.sendStatus(204);
328328 } catch (error) {
329329 console.logerror(error);
330330 response.sendStatus(500);
331331 }
332332});
@@ -352,7 +352,7 @@ router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name
352352
353353 response.sendStatus(204);
354354 } catch (error) {
355355 console.logerror(error);
356356 response.sendStatus(500);
357357 }
358358});
src/endpoints/speech.js+3 -3
@@ -43,8 +43,8 @@ router.post('/recognize', jsonParser, async (req, res) => {
4343 const start = performance.now();
4444 const result = await pipe(wav, { language: lang || null, task: 'transcribe' });
4545 const end = performance.now();
4646 console.loginfo(`Execution duration: ${(end - start) / 1000} seconds`);
4747 console.loginfo('Transcribed audio:', result.text);
4848
4949 return res.json({ text: result.text });
5050 } catch (error) {
@@ -64,7 +64,7 @@ router.post('/synthesize', jsonParser, async (req, res) => {
6464 const start = performance.now();
6565 const result = await pipe(text, { speaker_embeddings: speaker_embeddings });
6666 const end = performance.now();
6767 console.logdebug(`Execution duration: ${(end - start) / 1000} seconds`);
6868
6969 const wav = new wavefile.WaveFile();
7070 wav.fromScratch(1, result.sampling_rate, '32f', result.audio);
src/endpoints/sprites.js+3 -3
@@ -82,14 +82,14 @@ export function importRisuSprites(directories, data) {
8282 return;
8383 }
8484
8585 console.loginfo(`RisuAI: Found ${images.length} sprites for ${name}. Writing to disk.`);
8686 const files = fs.readdirSync(spritesPath);
8787
8888 outer: for (const [label, fileBase64] of images) {
8989 // Remove existing sprite with the same label
9090 for (const file of files) {
9191 if (path.parse(file).name === label) {
9292 console.logwarn(`RisuAI: The sprite ${label} for ${name} already exists. Skipping.`);
9393 continue outer;
9494 }
9595 }
@@ -139,7 +139,7 @@ router.get('/get', jsonParser, function (request, response) {
139139 }
140140 }
141141 catch (err) {
142142 console.logerror(err);
143143 }
144144 return response.send(sprites);
145145});
src/endpoints/stable-diffusion.js+201 -74
@@ -45,7 +45,7 @@ router.post('/ping', jsonParser, async (request, response) => {
4545
4646 return response.sendStatus(200);
4747 } catch (error) {
4848 console.logerror(error);
4949 return response.sendStatus(500);
5050 }
5151});
@@ -99,7 +99,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
9999
100100 return response.send(upscalers);
101101 } catch (error) {
102102 console.logerror(error);
103103 return response.sendStatus(500);
104104 }
105105});
@@ -131,7 +131,7 @@ router.post('/vaes', jsonParser, async (request, response) => {
131131 const names = data.map(x => x.model_name);
132132 return response.send(names);
133133 } catch (error) {
134134 console.logerror(error);
135135 return response.sendStatus(500);
136136 }
137137});
@@ -158,7 +158,7 @@ router.post('/samplers', jsonParser, async (request, response) => {
158158 return response.send(names);
159159
160160 } catch (error) {
161161 console.logerror(error);
162162 return response.sendStatus(500);
163163 }
164164});
@@ -184,7 +184,7 @@ router.post('/schedulers', jsonParser, async (request, response) => {
184184 const names = data.map(x => x.name);
185185 return response.send(names);
186186 } catch (error) {
187187 console.logerror(error);
188188 return response.sendStatus(500);
189189 }
190190});
@@ -210,7 +210,7 @@ router.post('/models', jsonParser, async (request, response) => {
210210 const models = data.map(x => ({ value: x.title, text: x.title }));
211211 return response.send(models);
212212 } catch (error) {
213213 console.logerror(error);
214214 return response.sendStatus(500);
215215 }
216216});
@@ -230,7 +230,7 @@ router.post('/get-model', jsonParser, async (request, response) => {
230230 const data = await result.json();
231231 return response.send(data['sd_model_checkpoint']);
232232 } catch (error) {
233233 console.logerror(error);
234234 return response.sendStatus(500);
235235 }
236236});
@@ -283,13 +283,13 @@ router.post('/set-model', jsonParser, async (request, response) => {
283283 break;
284284 }
285285
286286 console.loginfo(`Waiting for SD WebUI to finish model loading... Progress: ${progress}; Job count: ${jobCount}`);
287287 await delay(CHECK_INTERVAL);
288288 }
289289
290290 return response.sendStatus(200);
291291 } catch (error) {
292292 console.logerror(error);
293293 return response.sendStatus(500);
294294 }
295295});
@@ -309,7 +309,7 @@ router.post('/generate', jsonParser, async (request, response) => {
309309 }
310310 }
311311 } catch (error) {
312312 console.logerror('SD WebUI failed to get options:', error);
313313 }
314314
315315 const controller = new AbortController();
@@ -323,7 +323,7 @@ router.post('/generate', jsonParser, async (request, response) => {
323323 controller.abort();
324324 });
325325
326326 console.logdebug('SD WebUI request:', request.body);
327327 const txt2imgUrl = new URL(request.body.url);
328328 txt2imgUrl.pathname = '/sdapi/v1/txt2img';
329329 const result = await fetch(txt2imgUrl, {
@@ -344,7 +344,7 @@ router.post('/generate', jsonParser, async (request, response) => {
344344 const data = await result.json();
345345 return response.send(data);
346346 } catch (error) {
347347 console.logerror(error);
348348 return response.sendStatus(500);
349349 }
350350});
@@ -377,7 +377,7 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => {
377377
378378 return response.send(names);
379379 } catch (error) {
380380 console.logerror(error);
381381 return response.sendStatus(500);
382382 }
383383});
@@ -395,7 +395,7 @@ comfy.post('/ping', jsonParser, async (request, response) => {
395395
396396 return response.sendStatus(200);
397397 } catch (error) {
398398 console.logerror(error);
399399 return response.sendStatus(500);
400400 }
401401});
@@ -413,7 +413,7 @@ comfy.post('/samplers', jsonParser, async (request, response) => {
413413 const data = await result.json();
414414 return response.send(data.KSampler.input.required.sampler_name[0]);
415415 } catch (error) {
416416 console.logerror(error);
417417 return response.sendStatus(500);
418418 }
419419});
@@ -441,7 +441,7 @@ comfy.post('/models', jsonParser, async (request, response) => {
441441
442442 return response.send(models);
443443 } catch (error) {
444444 console.logerror(error);
445445 return response.sendStatus(500);
446446 }
447447});
@@ -459,7 +459,7 @@ comfy.post('/schedulers', jsonParser, async (request, response) => {
459459 const data = await result.json();
460460 return response.send(data.KSampler.input.required.scheduler[0]);
461461 } catch (error) {
462462 console.logerror(error);
463463 return response.sendStatus(500);
464464 }
465465});
@@ -477,7 +477,7 @@ comfy.post('/vaes', jsonParser, async (request, response) => {
477477 const data = await result.json();
478478 return response.send(data.VAELoader.input.required.vae_name[0]);
479479 } catch (error) {
480480 console.logerror(error);
481481 return response.sendStatus(500);
482482 }
483483});
@@ -487,7 +487,7 @@ comfy.post('/workflows', jsonParser, async (request, response) => {
487487 const data = getComfyWorkflows(request.user.directories);
488488 return response.send(data);
489489 } catch (error) {
490490 console.logerror(error);
491491 return response.sendStatus(500);
492492 }
493493});
@@ -501,7 +501,7 @@ comfy.post('/workflow', jsonParser, async (request, response) => {
501501 const data = fs.readFileSync(filePath, { encoding: 'utf-8' });
502502 return response.send(JSON.stringify(data));
503503 } catch (error) {
504504 console.logerror(error);
505505 return response.sendStatus(500);
506506 }
507507});
@@ -513,7 +513,7 @@ comfy.post('/save-workflow', jsonParser, async (request, response) => {
513513 const data = getComfyWorkflows(request.user.directories);
514514 return response.send(data);
515515 } catch (error) {
516516 console.logerror(error);
517517 return response.sendStatus(500);
518518 }
519519});
@@ -526,7 +526,7 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => {
526526 }
527527 return response.sendStatus(200);
528528 } catch (error) {
529529 console.logerror(error);
530530 return response.sendStatus(500);
531531 }
532532});
@@ -591,7 +591,7 @@ comfy.post('/generate', jsonParser, async (request, response) => {
591591 const imgBuffer = await imgResponse.arrayBuffer();
592592 return response.send(Buffer.from(imgBuffer).toString('base64'));
593593 } catch (error) {
594594 console.logerror('ComfyUI error:', error);
595595 response.status(500).send(error.message);
596596 return response;
597597 }
@@ -604,7 +604,7 @@ together.post('/models', jsonParser, async (request, response) => {
604604 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);
605605
606606 if (!key) {
607607 console.logwarn('TogetherAI key not found.');
608608 return response.sendStatus(400);
609609 }
610610
@@ -616,14 +616,14 @@ together.post('/models', jsonParser, async (request, response) => {
616616 });
617617
618618 if (!modelsResponse.ok) {
619619 console.logwarn('TogetherAI returned an error.');
620620 return response.sendStatus(500);
621621 }
622622
623623 const data = await modelsResponse.json();
624624
625625 if (!Array.isArray(data)) {
626626 console.logwarn('TogetherAI returned invalid data.');
627627 return response.sendStatus(500);
628628 }
629629
@@ -633,7 +633,7 @@ together.post('/models', jsonParser, async (request, response) => {
633633
634634 return response.send(models);
635635 } catch (error) {
636636 console.logerror(error);
637637 return response.sendStatus(500);
638638 }
639639});
@@ -643,11 +643,11 @@ together.post('/generate', jsonParser, async (request, response) => {
643643 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);
644644
645645 if (!key) {
646646 console.logwarn('TogetherAI key not found.');
647647 return response.sendStatus(400);
648648 }
649649
650650 console.logdebug('TogetherAI request:', request.body);
651651
652652 const result = await fetch('https://api.together.xyz/v1/images/generations', {
653653 method: 'POST',
@@ -669,13 +669,13 @@ together.post('/generate', jsonParser, async (request, response) => {
669669 });
670670
671671 if (!result.ok) {
672672 console.logwarn('TogetherAI returned an error.', { body: await result.text() });
673673 return response.sendStatus(500);
674674 }
675675
676676 /** @type {any} */
677677 const data = await result.json();
678678 console.logdebug('TogetherAI response:', data);
679679
680680 const choice = data?.data?.[0];
681681 let b64_json = choice.b64_json;
@@ -687,7 +687,7 @@ together.post('/generate', jsonParser, async (request, response) => {
687687
688688 return response.send({ format: 'jpg', data: b64_json });
689689 } catch (error) {
690690 console.logerror(error);
691691 return response.sendStatus(500);
692692 }
693693});
@@ -709,7 +709,7 @@ drawthings.post('/ping', jsonParser, async (request, response) => {
709709
710710 return response.sendStatus(200);
711711 } catch (error) {
712712 console.logerror(error);
713713 return response.sendStatus(500);
714714 }
715715});
@@ -728,7 +728,7 @@ drawthings.post('/get-model', jsonParser, async (request, response) => {
728728
729729 return response.send(data['model']);
730730 } catch (error) {
731731 console.logerror(error);
732732 return response.sendStatus(500);
733733 }
734734});
@@ -747,14 +747,14 @@ drawthings.post('/get-upscaler', jsonParser, async (request, response) => {
747747
748748 return response.send(data['upscaler']);
749749 } catch (error) {
750750 console.logerror(error);
751751 return response.sendStatus(500);
752752 }
753753});
754754
755755drawthings.post('/generate', jsonParser, async (request, response) => {
756756 try {
757757 console.logdebug('SD DrawThings API request:', request.body);
758758
759759 const url = new URL(request.body.url);
760760 url.pathname = '/sdapi/v1/txt2img';
@@ -781,7 +781,7 @@ drawthings.post('/generate', jsonParser, async (request, response) => {
781781 const data = await result.json();
782782 return response.send(data);
783783 } catch (error) {
784784 console.logerror(error);
785785 return response.sendStatus(500);
786786 }
787787});
@@ -794,21 +794,21 @@ pollinations.post('/models', jsonParser, async (_request, response) => {
794794 const result = await fetch(modelsUrl);
795795
796796 if (!result.ok) {
797797 console.logwarn('Pollinations returned an error.', result.status, result.statusText);
798798 throw new Error('Pollinations request failed.');
799799 }
800800
801801 const data = await result.json();
802802
803803 if (!Array.isArray(data)) {
804804 console.logwarn('Pollinations returned invalid data.');
805805 throw new Error('Pollinations request failed.');
806806 }
807807
808808 const models = data.map(x => ({ value: x, text: x }));
809809 return response.send(models);
810810 } catch (error) {
811811 console.logerror(error);
812812 return response.sendStatus(500);
813813 }
814814});
@@ -829,12 +829,12 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
829829 });
830830 promptUrl.search = params.toString();
831831
832832 console.loginfo('Pollinations request URL:', promptUrl.toString());
833833
834834 const result = await fetch(promptUrl);
835835
836836 if (!result.ok) {
837837 console.logwarn('Pollinations returned an error.', result.status, result.statusText);
838838 throw new Error('Pollinations request failed.');
839839 }
840840
@@ -843,7 +843,7 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
843843
844844 return response.send({ image: base64 });
845845 } catch (error) {
846846 console.logerror(error);
847847 return response.sendStatus(500);
848848 }
849849});
@@ -855,13 +855,13 @@ stability.post('/generate', jsonParser, async (request, response) => {
855855 const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY);
856856
857857 if (!key) {
858858 console.logwarn('Stability AI key not found.');
859859 return response.sendStatus(400);
860860 }
861861
862862 const { payload, model } = request.body;
863863
864864 console.logdebug('Stability AI request:', model, payload);
865865
866866 const formData = new FormData();
867867 for (const [key, value] of Object.entries(payload)) {
@@ -896,14 +896,14 @@ stability.post('/generate', jsonParser, async (request, response) => {
896896
897897 if (!result.ok) {
898898 const text = await result.text();
899899 console.logwarn('Stability AI returned an error.', result.status, result.statusText, text);
900900 return response.sendStatus(500);
901901 }
902902
903903 const buffer = await result.arrayBuffer();
904904 return response.send(Buffer.from(buffer).toString('base64'));
905905 } catch (error) {
906906 console.logerror(error);
907907 return response.sendStatus(500);
908908 }
909909});
@@ -915,7 +915,7 @@ blockentropy.post('/models', jsonParser, async (request, response) => {
915915 const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
916916
917917 if (!key) {
918918 console.logwarn('Block Entropy key not found.');
919919 return response.sendStatus(400);
920920 }
921921
@@ -927,21 +927,21 @@ blockentropy.post('/models', jsonParser, async (request, response) => {
927927 });
928928
929929 if (!modelsResponse.ok) {
930930 console.logwarn('Block Entropy returned an error.');
931931 return response.sendStatus(500);
932932 }
933933
934934 const data = await modelsResponse.json();
935935
936936 if (!Array.isArray(data)) {
937937 console.logwarn('Block Entropy returned invalid data.');
938938 return response.sendStatus(500);
939939 }
940940 const models = data.map(x => ({ value: x.name, text: x.name }));
941941 return response.send(models);
942942
943943 } catch (error) {
944944 console.logerror(error);
945945 return response.sendStatus(500);
946946 }
947947});
@@ -951,11 +951,11 @@ blockentropy.post('/generate', jsonParser, async (request, response) => {
951951 const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
952952
953953 if (!key) {
954954 console.logwarn('Block Entropy key not found.');
955955 return response.sendStatus(400);
956956 }
957957
958958 console.logdebug('Block Entropy request:', request.body);
959959
960960 const result = await fetch('https://api.blockentropy.ai/sdapi/v1/txt2img', {
961961 method: 'POST',
@@ -976,16 +976,16 @@ blockentropy.post('/generate', jsonParser, async (request, response) => {
976976 });
977977
978978 if (!result.ok) {
979979 console.logwarn('Block Entropy returned an error.');
980980 return response.sendStatus(500);
981981 }
982982
983983 const data = await result.json();
984984 console.logdebug('Block Entropy response:', data);
985985
986986 return response.send(data);
987987 } catch (error) {
988988 console.logerror(error);
989989 return response.sendStatus(500);
990990 }
991991});
@@ -998,11 +998,11 @@ huggingface.post('/generate', jsonParser, async (request, response) => {
998998 const key = readSecret(request.user.directories, SECRET_KEYS.HUGGINGFACE);
999999
10001000 if (!key) {
10011001 console.logwarn('Hugging Face key not found.');
10021002 return response.sendStatus(400);
10031003 }
10041004
10051005 console.logdebug('Hugging Face request:', request.body);
10061006
10071007 const result = await fetch(`https://api-inference.huggingface.co/models/${request.body.model}`, {
10081008 method: 'POST',
@@ -1016,7 +1016,7 @@ huggingface.post('/generate', jsonParser, async (request, response) => {
10161016 });
10171017
10181018 if (!result.ok) {
10191019 console.logwarn('Hugging Face returned an error.');
10201020 return response.sendStatus(500);
10211021 }
10221022
@@ -1025,7 +1025,7 @@ huggingface.post('/generate', jsonParser, async (request, response) => {
10251025 image: Buffer.from(buffer).toString('base64'),
10261026 });
10271027 } catch (error) {
10281028 console.logerror(error);
10291029 return response.sendStatus(500);
10301030 }
10311031});
@@ -1037,7 +1037,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
10371037 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
10381038
10391039 if (!key) {
10401040 console.logwarn('NanoGPT key not found.');
10411041 return response.sendStatus(400);
10421042 }
10431043
@@ -1050,7 +1050,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
10501050 });
10511051
10521052 if (!modelsResponse.ok) {
10531053 console.logwarn('NanoGPT returned an error.');
10541054 return response.sendStatus(500);
10551055 }
10561056
@@ -1059,7 +1059,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
10591059 const imageModels = data?.models?.image;
10601060
10611061 if (!imageModels || typeof imageModels !== 'object') {
10621062 console.logwarn('NanoGPT returned invalid data.');
10631063 return response.sendStatus(500);
10641064 }
10651065
@@ -1067,7 +1067,7 @@ nanogpt.post('/models', jsonParser, async (request, response) => {
10671067 return response.send(models);
10681068 }
10691069 catch (error) {
10701070 console.logerror(error);
10711071 return response.sendStatus(500);
10721072 }
10731073});
@@ -1077,11 +1077,11 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
10771077 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
10781078
10791079 if (!key) {
10801080 console.logwarn('NanoGPT key not found.');
10811081 return response.sendStatus(400);
10821082 }
10831083
10841084 console.logdebug('NanoGPT request:', request.body);
10851085
10861086 const result = await fetch('https://nano-gpt.com/api/generate-image', {
10871087 method: 'POST',
@@ -1093,7 +1093,7 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
10931093 });
10941094
10951095 if (!result.ok) {
10961096 console.logwarn('NanoGPT returned an error.');
10971097 return response.sendStatus(500);
10981098 }
10991099
@@ -1102,14 +1102,14 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
11021102
11031103 const image = data?.data?.[0]?.b64_json;
11041104 if (!image) {
11051105 console.logwarn('NanoGPT returned invalid data.');
11061106 return response.sendStatus(500);
11071107 }
11081108
11091109 return response.send({ image });
11101110 }
11111111 catch (error) {
11121112 console.logerror(error);
11131113 return response.sendStatus(500);
11141114 }
11151115});
@@ -1121,7 +1121,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
11211121 const key = readSecret(request.user.directories, SECRET_KEYS.BFL);
11221122
11231123 if (!key) {
11241124 console.logwarn('BFL key not found.');
11251125 return response.sendStatus(400);
11261126 }
11271127
@@ -1173,7 +1173,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
11731173 delete requestBody.guidance;
11741174 }
11751175
11761176 console.logdebug('BFL request:', requestBody);
11771177
11781178 const result = await fetch(`https://api.bfl.ml/v1/${request.body.model}`, {
11791179 method: 'POST',
@@ -1185,7 +1185,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
11851185 });
11861186
11871187 if (!result.ok) {
11881188 console.logwarn('BFL returned an error.');
11891189 return response.sendStatus(500);
11901190 }
11911191
@@ -1201,7 +1201,7 @@ bfl.post('/generate', jsonParser, async (request, response) => {
12011201
12021202 if (!statusResult.ok) {
12031203 const text = await statusResult.text();
12041204 console.logwarn('BFL returned an error.', text);
12051205 return response.sendStatus(500);
12061206 }
12071207
@@ -1223,11 +1223,137 @@ bfl.post('/generate', jsonParser, async (request, response) => {
12231223 throw new Error('BFL failed to generate image.', { cause: statusData });
12241224 }
12251225 } catch (error) {
12261226 console.logerror(error);
12271227 return response.sendStatus(500);
12281228 }
12291229});
12301230
1231+const falai = express.Router();
1232+
1233+falai.post('/models', jsonParser, async (_request, response) => {
1234+ try {
1235+ const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image');
1236+ const result = await fetch(modelsUrl);
1237+
1238+ if (!result.ok) {
1239+ console.warn('FAL.AI returned an error.', result.status, result.statusText);
1240+ throw new Error('FAL.AI request failed.');
1241+ }
1242+
1243+ const data = await result.json();
1244+
1245+ if (!Array.isArray(data)) {
1246+ console.warn('FAL.AI returned invalid data.');
1247+ throw new Error('FAL.AI request failed.');
1248+ }
1249+
1250+ const models = data
1251+ .filter(x => !x.title.toLowerCase().includes('inpainting') &&
1252+ !x.title.toLowerCase().includes('control') &&
1253+ !x.title.toLowerCase().includes('upscale'))
1254+ .sort((a, b) => a.title.localeCompare(b.title))
1255+ .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }));
1256+ return response.send(models);
1257+ } catch (error) {
1258+ console.error(error);
1259+ return response.sendStatus(500);
1260+ }
1261+});
1262+
1263+falai.post('/generate', jsonParser, async (request, response) => {
1264+ try {
1265+ const key = readSecret(request.user.directories, SECRET_KEYS.FALAI);
1266+
1267+ if (!key) {
1268+ console.warn('FAL.AI key not found.');
1269+ return response.sendStatus(400);
1270+ }
1271+
1272+ const requestBody = {
1273+ prompt: request.body.prompt,
1274+ image_size: { 'width': request.body.width, 'height': request.body.height },
1275+ num_inference_steps: request.body.steps,
1276+ seed: request.body.seed ?? null,
1277+ guidance_scale: request.body.guidance,
1278+ enable_safety_checker: false, // Disable general safety checks
1279+ safety_tolerance: 6 // Make Flux the least strict
1280+ };
1281+
1282+ console.debug('FAL.AI request:', requestBody);
1283+
1284+ const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, {
1285+ method: 'POST',
1286+ body: JSON.stringify(requestBody),
1287+ headers: {
1288+ 'Content-Type': 'application/json',
1289+ 'Authorization': `Key ${key}`,
1290+ },
1291+ });
1292+
1293+ if (!result.ok) {
1294+ console.warn('FAL.AI returned an error.');
1295+ return response.sendStatus(500);
1296+ }
1297+
1298+ /** @type {any} */
1299+ const taskData = await result.json();
1300+ const { status_url } = taskData;
1301+
1302+ const MAX_ATTEMPTS = 100;
1303+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
1304+ await delay(2500);
1305+
1306+ const statusResult = await fetch(status_url, {
1307+ headers: {
1308+ 'Authorization': `Key ${key}`,
1309+ },
1310+ });
1311+
1312+ if (!statusResult.ok) {
1313+ const text = await statusResult.text();
1314+ console.warn('FAL.AI returned an error.', text);
1315+ return response.sendStatus(500);
1316+ }
1317+
1318+ /** @type {any} */
1319+ const statusData = await statusResult.json();
1320+
1321+ if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') {
1322+ continue;
1323+ }
1324+
1325+ if (statusData?.status === 'COMPLETED') {
1326+ const resultFetch = await fetch(statusData?.response_url, {
1327+ method: 'GET',
1328+ headers: {
1329+ 'Authorization': `Key ${key}`,
1330+ },
1331+ });
1332+ const resultData = await resultFetch.json();
1333+
1334+ if (resultData.detail !== null && resultData.detail !== undefined) {
1335+ throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` });
1336+ }
1337+
1338+ const imageFetch = await fetch(resultData?.images[0].url, {
1339+ headers: {
1340+ 'Authorization': `Key ${key}`,
1341+ },
1342+ });
1343+
1344+ const fetchData = await imageFetch.arrayBuffer();
1345+ const image = Buffer.from(fetchData).toString('base64');
1346+ return response.send({ image: image });
1347+ }
1348+
1349+ throw new Error('FAL.AI failed to generate image.', { cause: statusData });
1350+ }
1351+ } catch (error) {
1352+ console.error(error);
1353+ return response.status(500).send(error.cause || error.message);
1354+ }
1355+});
1356+
12311357router.use('/comfy', comfy);
12321358router.use('/together', together);
12331359router.use('/drawthings', drawthings);
@@ -1237,3 +1363,4 @@ router.use('/blockentropy', blockentropy);
12371363router.use('/huggingface', huggingface);
12381364router.use('/nanogpt', nanogpt);
12391365router.use('/bfl', bfl);
1366+router.use('/falai', falai);
src/endpoints/stats.js+2 -2
@@ -148,7 +148,7 @@ async function collectAndCreateStats(chatsPath, charactersPath) {
148148 * @param {string} charactersPath Path to the directory containing the character files.
149149 */
150150export async function recreateStats(handle, chatsPath, charactersPath) {
151151 console.loginfo('Collecting and creating stats for user:', handle);
152152 const stats = await collectAndCreateStats(chatsPath, charactersPath);
153153 STATS.set(handle, stats);
154154 await saveStatsToFile();
@@ -200,7 +200,7 @@ async function saveStatsToFile() {
200200 await writeFileAtomic(statsFilePath, JSON.stringify(charStats));
201201 TIMESTAMPS.set(handle, Date.now());
202202 } catch (error) {
203203 console.logerror('Failed to save stats to file.', error);
204204 }
205205 }
206206 }
src/endpoints/thumbnails.js+3 -4
@@ -89,7 +89,6 @@ async function generateThumbnail(directories, type, file) {
8989 let thumbnailFolder = getThumbnailFolder(directories, type);
9090 let originalFolder = getOriginalFolder(directories, type);
9191 if (thumbnailFolder === undefined || originalFolder === undefined) throw new Error('Invalid thumbnail type');
92-
9392 const pathToCachedFile = path.join(thumbnailFolder, file);
9493 const pathToOriginalFile = path.join(originalFolder, file);
9594
@@ -104,7 +103,7 @@ async function generateThumbnail(directories, type, file) {
104103 const cachedStat = fs.statSync(pathToCachedFile);
105104
106105 if (originalStat.mtimeMs > cachedStat.ctimeMs) {
107106 //console.logwarn('Original file changed. Regenerating thumbnail...');
108107 shouldRegenerate = true;
109108 }
110109 }
@@ -157,7 +156,7 @@ export async function ensureThumbnailCache() {
157156 return;
158157 }
159158
160159 console.loginfo('Generating thumbnails cache. Please wait...');
161160
162161 const bgFiles = fs.readdirSync(directories.backgrounds);
163162 const tasks = [];
@@ -167,7 +166,7 @@ export async function ensureThumbnailCache() {
167166 }
168167
169168 await Promise.all(tasks);
170169 console.loginfo(`Done! Generated: ${bgFiles.length} preview images`);
171170 }
172171}
173172
src/endpoints/tokenizers.js+17 -17
@@ -96,7 +96,7 @@ async function getPathToTokenizer(model, fallbackModel) {
9696 throw new Error('Downloading tokenizers is disabled, the model is not cached');
9797 }
9898
9999 console.loginfo('Downloading tokenizer model:', model);
100100 const response = await fetch(model);
101101 if (!response.ok) {
102102 throw new Error(`Failed to fetch the model: ${response.status} ${response.statusText}`);
@@ -108,7 +108,7 @@ async function getPathToTokenizer(model, fallbackModel) {
108108 } catch (error) {
109109 const getLastSegment = str => str?.split('/')?.pop() || '';
110110 if (fallbackModel) {
111111 console.logerror(`Could not get a tokenizer from ${getLastSegment(model)}. Reason: ${error.message}. Using a fallback model: ${getLastSegment(fallbackModel)}.`);
112112 return fallbackModel;
113113 }
114114
@@ -156,7 +156,7 @@ class SentencePieceTokenizer {
156156 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);
157157 this.#instance = new SentencePieceProcessor();
158158 await this.#instance.load(pathToModel);
159159 console.loginfo('Instantiated the tokenizer for', path.parse(pathToModel).name);
160160 return this.#instance;
161161 } catch (error) {
162162 console.error('Sentencepiece tokenizer failed to load: ' + this.#model, error);
@@ -205,7 +205,7 @@ class WebTokenizer {
205205 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);
206206 const arrayBuffer = fs.readFileSync(pathToModel).buffer;
207207 this.#instance = await Tokenizer.fromJSON(arrayBuffer);
208208 console.loginfo('Instantiated the tokenizer for', path.parse(pathToModel).name);
209209 return this.#instance;
210210 } catch (error) {
211211 console.error('Web tokenizer failed to load: ' + this.#model, error);
@@ -484,7 +484,7 @@ export function getTiktokenTokenizer(model) {
484484 }
485485
486486 const tokenizer = tiktoken.encoding_for_model(model);
487487 console.loginfo('Instantiated the tokenizer for', model);
488488 tokenizersCache[model] = tokenizer;
489489 return tokenizer;
490490}
@@ -531,7 +531,7 @@ function createSentencepieceEncodingHandler(tokenizer) {
531531 const chunks = instance?.encodePieces(text);
532532 return response.send({ ids, count, chunks });
533533 } catch (error) {
534534 console.logerror(error);
535535 return response.send({ ids: [], count: 0, chunks: [] });
536536 }
537537 };
@@ -562,7 +562,7 @@ function createSentencepieceDecodingHandler(tokenizer) {
562562 const text = chunks.join('');
563563 return response.send({ text, chunks });
564564 } catch (error) {
565565 console.logerror(error);
566566 return response.send({ text: '', chunks: [] });
567567 }
568568 };
@@ -591,7 +591,7 @@ function createTiktokenEncodingHandler(modelId) {
591591 const chunks = await getTiktokenChunks(tokenizer, tokens);
592592 return response.send({ ids: tokens, count: tokens.length, chunks });
593593 } catch (error) {
594594 console.logerror(error);
595595 return response.send({ ids: [], count: 0, chunks: [] });
596596 }
597597 };
@@ -620,7 +620,7 @@ function createTiktokenDecodingHandler(modelId) {
620620 const text = new TextDecoder().decode(textBytes);
621621 return response.send({ text });
622622 } catch (error) {
623623 console.logerror(error);
624624 return response.send({ text: '' });
625625 }
626626 };
@@ -650,7 +650,7 @@ function createWebTokenizerEncodingHandler(tokenizer) {
650650 const chunks = getWebTokenizersChunks(instance, tokens);
651651 return response.send({ ids: tokens, count: tokens.length, chunks });
652652 } catch (error) {
653653 console.logerror(error);
654654 return response.send({ ids: [], count: 0, chunks: [] });
655655 }
656656 };
@@ -681,7 +681,7 @@ function createWebTokenizerDecodingHandler(tokenizer) {
681681 const text = instance.decode(new Int32Array(ids));
682682 return response.send({ text, chunks });
683683 } catch (error) {
684684 console.logerror(error);
685685 return response.send({ text: '', chunks: [] });
686686 }
687687 };
@@ -781,7 +781,7 @@ router.post('/openai/encode', jsonParser, async function (req, res) {
781781 const handler = createTiktokenEncodingHandler(model);
782782 return handler(req, res);
783783 } catch (error) {
784784 console.logerror(error);
785785 return res.send({ ids: [], count: 0, chunks: [] });
786786 }
787787});
@@ -849,7 +849,7 @@ router.post('/openai/decode', jsonParser, async function (req, res) {
849849 const handler = createTiktokenDecodingHandler(model);
850850 return handler(req, res);
851851 } catch (error) {
852852 console.logerror(error);
853853 return res.send({ text: '' });
854854 }
855855});
@@ -988,7 +988,7 @@ router.post('/remote/kobold/count', jsonParser, async function (request, respons
988988 const result = await fetch(url, args);
989989
990990 if (!result.ok) {
991991 console.logwarn(`API returned error: ${result.status} ${result.statusText}`);
992992 return response.send({ error: true });
993993 }
994994
@@ -997,7 +997,7 @@ router.post('/remote/kobold/count', jsonParser, async function (request, respons
997997 const ids = data['ids'] ?? [];
998998 return response.send({ count, ids });
999999 } catch (error) {
10001000 console.logerror(error);
10011001 return response.send({ error: true });
10021002 }
10031003});
@@ -1052,7 +1052,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
10521052 const result = await fetch(url, args);
10531053
10541054 if (!result.ok) {
10551055 console.logwarn(`API returned error: ${result.status} ${result.statusText}`);
10561056 return response.send({ error: true });
10571057 }
10581058
@@ -1062,7 +1062,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
10621062
10631063 return response.send({ count, ids });
10641064 } catch (error) {
10651065 console.logerror(error);
10661066 return response.send({ error: true });
10671067 }
10681068});
src/endpoints/translate.js+39 -39
@@ -35,7 +35,7 @@ function decodeBuffer(buffer) {
3535 try {
3636 return iconv.decode(Buffer.from(buffer), 'utf-8');
3737 } catch (error) {
3838 console.logerror('Failed to decode buffer:', error);
3939 return Buffer.from(buffer).toString('utf-8');
4040 }
4141}
@@ -46,7 +46,7 @@ router.post('/libre', jsonParser, async (request, response) => {
4646 const url = readSecret(request.user.directories, SECRET_KEYS.LIBRE_URL);
4747
4848 if (!url) {
4949 console.logwarn('LibreTranslate URL is not configured.');
5050 return response.sendStatus(400);
5151 }
5252
@@ -69,7 +69,7 @@ router.post('/libre', jsonParser, async (request, response) => {
6969 return response.sendStatus(400);
7070 }
7171
7272 console.logdebug('Input text: ' + text);
7373
7474 const result = await fetch(url, {
7575 method: 'POST',
@@ -85,17 +85,17 @@ router.post('/libre', jsonParser, async (request, response) => {
8585
8686 if (!result.ok) {
8787 const error = await result.text();
8888 console.logwarn('LibreTranslate error: ', result.statusText, error);
8989 return response.sendStatus(500);
9090 }
9191
9292 /** @type {any} */
9393 const json = await result.json();
9494 console.logdebug('Translated text: ' + json.translatedText);
9595
9696 return response.send(json.translatedText);
9797 } catch (error) {
9898 console.logerror('Translation error: ' + error.message);
9999 return response.sendStatus(500);
100100 }
101101});
@@ -109,14 +109,14 @@ router.post('/google', jsonParser, async (request, response) => {
109109 return response.sendStatus(400);
110110 }
111111
112112 console.logdebug('Input text: ' + text);
113113
114114 const { generateRequestUrl, normaliseResponse } = getGoogleTranslateClient();
115115 const requestUrl = generateRequestUrl(text, { to: lang });
116116 const result = await fetch(requestUrl);
117117
118118 if (!result.ok) {
119119 console.logwarn('Google Translate error: ', result.statusText);
120120 return response.sendStatus(500);
121121 }
122122
@@ -125,10 +125,10 @@ router.post('/google', jsonParser, async (request, response) => {
125125 const translatedText = translateResponse.text;
126126
127127 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
128128 console.logdebug('Translated text: ' + translatedText);
129129 return response.send(translatedText);
130130 } catch (error) {
131131 console.logerror('Translation error', error);
132132 return response.sendStatus(500);
133133 }
134134});
@@ -161,7 +161,7 @@ router.post('/yandex', jsonParser, async (request, response) => {
161161 params.append('lang', lang);
162162 const ucid = uuidv4().replaceAll('-', '');
163163
164164 console.logdebug('Input text: ' + inputText);
165165
166166 const result = await fetch(`https://translate.yandex.net/api/v1/tr.json/translate?ucid=${ucid}&srv=android&format=text`, {
167167 method: 'POST',
@@ -173,18 +173,18 @@ router.post('/yandex', jsonParser, async (request, response) => {
173173
174174 if (!result.ok) {
175175 const error = await result.text();
176176 console.logwarn('Yandex error: ', result.statusText, error);
177177 return response.sendStatus(500);
178178 }
179179
180180 /** @type {any} */
181181 const json = await result.json();
182182 const translated = json.text.join();
183183 console.logdebug('Translated text: ' + translated);
184184
185185 return response.send(translated);
186186 } catch (error) {
187187 console.logerror('Translation error: ' + error.message);
188188 return response.sendStatus(500);
189189 }
190190});
@@ -195,7 +195,7 @@ router.post('/lingva', jsonParser, async (request, response) => {
195195 const baseUrl = secretUrl || LINGVA_DEFAULT;
196196
197197 if (!secretUrl && baseUrl === LINGVA_DEFAULT) {
198198 console.logwarn('Lingva URL is using default value.', LINGVA_DEFAULT);
199199 }
200200
201201 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
@@ -213,22 +213,22 @@ router.post('/lingva', jsonParser, async (request, response) => {
213213 return response.sendStatus(400);
214214 }
215215
216216 console.logdebug('Input text: ' + text);
217217
218218 const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text));
219219 const result = await fetch(url);
220220
221221 if (!result.ok) {
222222 const error = await result.text();
223223 console.logwarn('Lingva error: ', result.statusText, error);
224224 }
225225
226226 /** @type {any} */
227227 const data = await result.json();
228228 console.logdebug('Translated text: ' + data.translation);
229229 return response.send(data.translation);
230230 } catch (error) {
231231 console.logerror('Translation error', error);
232232 return response.sendStatus(500);
233233 }
234234});
@@ -238,7 +238,7 @@ router.post('/deepl', jsonParser, async (request, response) => {
238238 const key = readSecret(request.user.directories, SECRET_KEYS.DEEPL);
239239
240240 if (!key) {
241241 console.logwarn('DeepL key is not configured.');
242242 return response.sendStatus(400);
243243 }
244244
@@ -254,7 +254,7 @@ router.post('/deepl', jsonParser, async (request, response) => {
254254 return response.sendStatus(400);
255255 }
256256
257257 console.logdebug('Input text: ' + text);
258258
259259 const params = new URLSearchParams();
260260 params.append('text', text);
@@ -280,17 +280,17 @@ router.post('/deepl', jsonParser, async (request, response) => {
280280
281281 if (!result.ok) {
282282 const error = await result.text();
283283 console.logwarn('DeepL error: ', result.statusText, error);
284284 return response.sendStatus(500);
285285 }
286286
287287 /** @type {any} */
288288 const json = await result.json();
289289 console.logdebug('Translated text: ' + json.translations[0].text);
290290
291291 return response.send(json.translations[0].text);
292292 } catch (error) {
293293 console.logerror('Translation error: ' + error.message);
294294 return response.sendStatus(500);
295295 }
296296});
@@ -301,12 +301,12 @@ router.post('/onering', jsonParser, async (request, response) => {
301301 const url = secretUrl || ONERING_URL_DEFAULT;
302302
303303 if (!url) {
304304 console.logwarn('OneRing URL is not configured.');
305305 return response.sendStatus(400);
306306 }
307307
308308 if (!secretUrl && url === ONERING_URL_DEFAULT) {
309309 console.loginfo('OneRing URL is using default value.', ONERING_URL_DEFAULT);
310310 }
311311
312312 if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') {
@@ -326,7 +326,7 @@ router.post('/onering', jsonParser, async (request, response) => {
326326 params.append('from_lang', from_lang);
327327 params.append('to_lang', to_lang);
328328
329329 console.logdebug('Input text: ' + text);
330330
331331 const fetchUrl = new URL(url);
332332 fetchUrl.search = params.toString();
@@ -337,17 +337,17 @@ router.post('/onering', jsonParser, async (request, response) => {
337337
338338 if (!result.ok) {
339339 const error = await result.text();
340340 console.logwarn('OneRing error: ', result.statusText, error);
341341 return response.sendStatus(500);
342342 }
343343
344344 /** @type {any} */
345345 const data = await result.json();
346346 console.logdebug('Translated text: ' + data.result);
347347
348348 return response.send(data.result);
349349 } catch (error) {
350350 console.logerror('Translation error: ' + error.message);
351351 return response.sendStatus(500);
352352 }
353353});
@@ -358,12 +358,12 @@ router.post('/deeplx', jsonParser, async (request, response) => {
358358 const url = secretUrl || DEEPLX_URL_DEFAULT;
359359
360360 if (!url) {
361361 console.logwarn('DeepLX URL is not configured.');
362362 return response.sendStatus(400);
363363 }
364364
365365 if (!secretUrl && url === DEEPLX_URL_DEFAULT) {
366366 console.loginfo('DeepLX URL is using default value.', DEEPLX_URL_DEFAULT);
367367 }
368368
369369 const text = request.body.text;
@@ -376,7 +376,7 @@ router.post('/deeplx', jsonParser, async (request, response) => {
376376 return response.sendStatus(400);
377377 }
378378
379379 console.logdebug('Input text: ' + text);
380380
381381 const result = await fetch(url, {
382382 method: 'POST',
@@ -393,17 +393,17 @@ router.post('/deeplx', jsonParser, async (request, response) => {
393393
394394 if (!result.ok) {
395395 const error = await result.text();
396396 console.logwarn('DeepLX error: ', result.statusText, error);
397397 return response.sendStatus(500);
398398 }
399399
400400 /** @type {any} */
401401 const json = await result.json();
402402 console.logdebug('Translated text: ' + json.data);
403403
404404 return response.send(json.data);
405405 } catch (error) {
406406 console.logerror('DeepLX translation error: ' + error.message);
407407 return response.sendStatus(500);
408408 }
409409});
@@ -429,14 +429,14 @@ router.post('/bing', jsonParser, async (request, response) => {
429429 return response.sendStatus(400);
430430 }
431431
432432 console.logdebug('Input text: ' + text);
433433
434434 const result = await bingTranslate(text, null, lang);
435435 const translatedText = result?.translation;
436436 console.logdebug('Translated text: ' + translatedText);
437437 return response.send(translatedText);
438438 } catch (error) {
439439 console.logerror('Translation error', error);
440440 return response.sendStatus(500);
441441 }
442442});
src/endpoints/users-admin.js+18 -18
@@ -53,12 +53,12 @@ router.post('/get', requireAdminMiddleware, jsonParser, async (_request, respons
5353router.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
5454 try {
5555 if (!request.body.handle) {
5656 console.logwarn('Disable user failed: Missing required fields');
5757 return response.status(400).json({ error: 'Missing required fields' });
5858 }
5959
6060 if (request.body.handle === request.user.profile.handle) {
6161 console.logwarn('Disable user failed: Cannot disable yourself');
6262 return response.status(400).json({ error: 'Cannot disable yourself' });
6363 }
6464
@@ -66,7 +66,7 @@ router.post('/disable', requireAdminMiddleware, jsonParser, async (request, resp
6666 const user = await storage.getItem(toKey(request.body.handle));
6767
6868 if (!user) {
6969 console.logerror('Disable user failed: User not found');
7070 return response.status(404).json({ error: 'User not found' });
7171 }
7272
@@ -82,7 +82,7 @@ router.post('/disable', requireAdminMiddleware, jsonParser, async (request, resp
8282router.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {
8383 try {
8484 if (!request.body.handle) {
8585 console.logwarn('Enable user failed: Missing required fields');
8686 return response.status(400).json({ error: 'Missing required fields' });
8787 }
8888
@@ -90,7 +90,7 @@ router.post('/enable', requireAdminMiddleware, jsonParser, async (request, respo
9090 const user = await storage.getItem(toKey(request.body.handle));
9191
9292 if (!user) {
9393 console.logerror('Enable user failed: User not found');
9494 return response.status(404).json({ error: 'User not found' });
9595 }
9696
@@ -106,7 +106,7 @@ router.post('/enable', requireAdminMiddleware, jsonParser, async (request, respo
106106router.post('/promote', requireAdminMiddleware, jsonParser, async (request, response) => {
107107 try {
108108 if (!request.body.handle) {
109109 console.logwarn('Promote user failed: Missing required fields');
110110 return response.status(400).json({ error: 'Missing required fields' });
111111 }
112112
@@ -114,7 +114,7 @@ router.post('/promote', requireAdminMiddleware, jsonParser, async (request, resp
114114 const user = await storage.getItem(toKey(request.body.handle));
115115
116116 if (!user) {
117117 console.logerror('Promote user failed: User not found');
118118 return response.status(404).json({ error: 'User not found' });
119119 }
120120
@@ -130,12 +130,12 @@ router.post('/promote', requireAdminMiddleware, jsonParser, async (request, resp
130130router.post('/demote', requireAdminMiddleware, jsonParser, async (request, response) => {
131131 try {
132132 if (!request.body.handle) {
133133 console.logwarn('Demote user failed: Missing required fields');
134134 return response.status(400).json({ error: 'Missing required fields' });
135135 }
136136
137137 if (request.body.handle === request.user.profile.handle) {
138138 console.logwarn('Demote user failed: Cannot demote yourself');
139139 return response.status(400).json({ error: 'Cannot demote yourself' });
140140 }
141141
@@ -143,7 +143,7 @@ router.post('/demote', requireAdminMiddleware, jsonParser, async (request, respo
143143 const user = await storage.getItem(toKey(request.body.handle));
144144
145145 if (!user) {
146146 console.logerror('Demote user failed: User not found');
147147 return response.status(404).json({ error: 'User not found' });
148148 }
149149
@@ -159,7 +159,7 @@ router.post('/demote', requireAdminMiddleware, jsonParser, async (request, respo
159159router.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {
160160 try {
161161 if (!request.body.handle || !request.body.name) {
162162 console.logwarn('Create user failed: Missing required fields');
163163 return response.status(400).json({ error: 'Missing required fields' });
164164 }
165165
@@ -167,12 +167,12 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
167167 const handle = lodash.kebabCase(String(request.body.handle).toLowerCase().trim());
168168
169169 if (!handle) {
170170 console.logwarn('Create user failed: Invalid handle');
171171 return response.status(400).json({ error: 'Invalid handle' });
172172 }
173173
174174 if (handles.some(x => x === handle)) {
175175 console.logwarn('Create user failed: User with that handle already exists');
176176 return response.status(409).json({ error: 'User already exists' });
177177 }
178178
@@ -192,7 +192,7 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
192192 await storage.setItem(toKey(handle), newUser);
193193
194194 // Create user directories
195195 console.loginfo('Creating data directories for', newUser.handle);
196196 await ensurePublicDirectoriesExist();
197197 const directories = getUserDirectories(newUser.handle);
198198 await checkForNewContent([directories]);
@@ -206,17 +206,17 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
206206router.post('/delete', requireAdminMiddleware, jsonParser, async (request, response) => {
207207 try {
208208 if (!request.body.handle) {
209209 console.logwarn('Delete user failed: Missing required fields');
210210 return response.status(400).json({ error: 'Missing required fields' });
211211 }
212212
213213 if (request.body.handle === request.user.profile.handle) {
214214 console.logwarn('Delete user failed: Cannot delete yourself');
215215 return response.status(400).json({ error: 'Cannot delete yourself' });
216216 }
217217
218218 if (request.body.handle === DEFAULT_USER.handle) {
219219 console.logwarn('Delete user failed: Cannot delete default user');
220220 return response.status(400).json({ error: 'Sorry, but the default user cannot be deleted. It is required as a fallback.' });
221221 }
222222
@@ -224,7 +224,7 @@ router.post('/delete', requireAdminMiddleware, jsonParser, async (request, respo
224224
225225 if (request.body.purge) {
226226 const directories = getUserDirectories(request.body.handle);
227227 console.loginfo('Deleting data directories for', request.body.handle);
228228 await fsPromises.rm(directories.root, { recursive: true, force: true });
229229 }
230230
@@ -238,7 +238,7 @@ router.post('/delete', requireAdminMiddleware, jsonParser, async (request, respo
238238router.post('/slugify', requireAdminMiddleware, jsonParser, async (request, response) => {
239239 try {
240240 if (!request.body.text) {
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/cacheBuster.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/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