Merge branch 'staging' into support-multiple-expressions

This commit is contained in:
Wolfsblvt 2025-02-19 20:22:02 +01:00
commit c12f26441e
124 changed files with 3369 additions and 1434 deletions

View File

@ -80,6 +80,8 @@ body:
required: true
- label: I have checked the [docs](https://docs.sillytavern.app/) ![important](https://img.shields.io/badge/Important!-F6094E)
required: true
- 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
required: true
- type: markdown
attributes:

View File

@ -6,7 +6,13 @@ cardsCacheCapacity: 100
# -- SERVER CONFIGURATION --
# Listen for incoming connections
listen: false
# Listen on a specific address, supports IPv4 and IPv6
listenAddress:
ipv4: 0.0.0.0
ipv6: '[::]'
# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
# - Use option "auto" to automatically detect support
# - Use true or false (no qoutes) to enable or disable each protocol
protocol:
ipv4: true
ipv6: false
@ -65,6 +71,8 @@ autheliaAuth: false
# the username and passwords for basic auth are the same as those
# for the individual accounts
perUserBasicAuth: false
# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
minLogLevel: 0
# User session timeout *in seconds* (defaults to 24 hours).
## Set to a positive number to expire session after a certain time of inactivity
@ -179,6 +187,10 @@ ollama:
# * 0: Unload the model immediately after the request
# * N (any positive number): Keep the model loaded for N seconds after the request.
keepAlive: -1
# Controls the "num_batch" (batch size) parameter of the generation request
# * -1: Use the default value of the model
# * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
batchSize: -1
# -- ANTHROPIC CLAUDE API CONFIGURATION --
claude:
# Enables caching of the system prompt (if supported).
@ -198,3 +210,5 @@ claude:
cachingAtDepth: -1
# -- SERVER PLUGIN CONFIGURATION --
enableServerPlugins: false
# Attempt to automatically update server plugins on startup
enableServerPluginsAutoUpdate: true

View File

@ -671,10 +671,6 @@
"filename": "presets/moving-ui/Default.json",
"type": "moving_ui"
},
{
"filename": "presets/moving-ui/Black Magic Time.json",
"type": "moving_ui"
},
{
"filename": "presets/quick-replies/Default.json",
"type": "quick_replies"

View File

@ -1,45 +0,0 @@
{
"name": "Black Magic Time",
"movingUIState": {
"sheld": {
"top": 488,
"left": 1407,
"right": 1,
"bottom": 4,
"margin": "unset",
"width": 471,
"height": 439
},
"floatingPrompt": {
"width": 369,
"height": 441
},
"right-nav-panel": {
"top": 0,
"left": 1400,
"right": 111,
"bottom": 446,
"margin": "unset",
"width": 479,
"height": 487
},
"WorldInfo": {
"top": 41,
"left": 369,
"right": 642,
"bottom": 51,
"margin": "unset",
"width": 1034,
"height": 858
},
"left-nav-panel": {
"top": 442,
"left": 0,
"right": 1546,
"bottom": 25,
"margin": "unset",
"width": 368,
"height": 483
}
}
}

37
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.12.11",
"version": "1.12.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.12.11",
"version": "1.12.12",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -28,7 +28,7 @@
"cors": "^2.8.5",
"csrf-sync": "^4.0.3",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7",
"dompurify": "^3.2.4",
"droll": "^0.2.1",
"express": "^4.21.0",
"form-data": "^4.0.0",
@ -41,6 +41,7 @@
"html-entities": "^2.5.2",
"iconv-lite": "^0.6.3",
"ip-matching": "^2.1.2",
"ip-regex": "^5.0.0",
"ipaddr.js": "^2.0.1",
"jimp": "^0.22.10",
"localforage": "^1.10.0",
@ -1462,6 +1463,13 @@
"@types/jquery": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/write-file-atomic": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
@ -3217,10 +3225,13 @@
}
},
"node_modules/dompurify": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.1.0",
@ -4610,6 +4621,18 @@
"integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
"license": "LGPL-3.0-only"
},
"node_modules/ip-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
"integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ipaddr.js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",

View File

@ -18,7 +18,7 @@
"cors": "^2.8.5",
"csrf-sync": "^4.0.3",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7",
"dompurify": "^3.2.4",
"droll": "^0.2.1",
"express": "^4.21.0",
"form-data": "^4.0.0",
@ -31,6 +31,7 @@
"html-entities": "^2.5.2",
"iconv-lite": "^0.6.3",
"ip-matching": "^2.1.2",
"ip-regex": "^5.0.0",
"ipaddr.js": "^2.0.1",
"jimp": "^0.22.10",
"localforage": "^1.10.0",
@ -86,9 +87,10 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.12.11",
"version": "1.12.12",
"scripts": {
"start": "node server.js",
"debug": "node server.js --inspect",
"start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
"start:bun": "bun server.js",
"start:no-csrf": "node server.js --disableCsrf",

View File

@ -48,6 +48,13 @@ async function updatePlugins() {
console.log(`Updating plugin ${color.green(directory)}...`);
const pluginPath = path.join(pluginsPath, directory);
const pluginRepo = git(pluginPath);
const isRepo = await pluginRepo.checkIsRepo();
if (!isRepo) {
console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
continue;
}
await pluginRepo.fetch();
const commitHash = await pluginRepo.revparse(['HEAD']);
const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);

View File

@ -216,8 +216,6 @@
}
#showRawPrompt,
#copyPromptToClipboard,
#groupCurrentMemberPopoutButton,
#summaryExtensionPopoutButton {
display: none;

View File

@ -72,6 +72,10 @@ dialog {
overflow-x: auto;
}
.popup.left_aligned_dialogue_popup .popup-content {
text-align: start;
}
/* Opening animation */
.popup[opening] {
animation: pop-in var(--popup-animation-speed) ease-in-out;

View File

@ -100,6 +100,13 @@
border: 1px solid var(--SmartThemeBorderColor);
}
.select2-container .select2-results .select2-results__option--disabled {
color: inherit;
background-color: inherit;
cursor: not-allowed;
filter: brightness(0.5);
}
.select2-container .select2-selection--multiple .select2-selection__choice,
.select2-container .select2-selection--single .select2-selection__choice {
border-radius: 5px;

View File

@ -493,3 +493,7 @@ label[for="trim_spaces"]:not(:has(input:checked)) small {
#mistralai_other_models:empty {
display: none;
}
#banned_tokens_block_ooba:not(:has(#send_banned_tokens_textgenerationwebui:checked)) #banned_tokens_controls_ooba {
filter: brightness(0.5);
}

View File

@ -730,7 +730,7 @@
<input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">
</div>
<div class="range-block-counter">
<input type="number" min="0" max="200" step="1" data-for="top_k_openai" id="top_k_counter_openai">
<input type="number" min="0" max="500" step="1" data-for="top_k_openai" id="top_k_counter_openai">
</div>
</div>
</div>
@ -1621,17 +1621,34 @@
</div>
<div data-tg-type-mode="except" data-tg-type="generic" id="banned_tokens_block_ooba" class="wide100p">
<hr class="width100p">
<h4 class="range-block-title justifyCenter">
<span data-i18n="Banned Tokens">Banned Tokens/Strings</span>
<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>
</h4>
<div class="wide100p">
<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>
<div class="range-block-title title_restorable">
<div>
<strong data-i18n="Banned Tokens">Banned Tokens/Strings</strong>
<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>
</div>
<label id="send_banned_tokens_label" for="send_banned_tokens_textgenerationwebui" class="checkbox_label">
<input id="send_banned_tokens_textgenerationwebui" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
</label>
</div>
<div id="banned_tokens_controls_ooba">
<div class="textAlignCenter">
<small data-i18n="Global list">Global list</small>
</div>
<div class="wide100p marginBot10">
<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>
</div>
<div class="textAlignCenter">
<small data-i18n="Preset-specific list">Preset-specific list</small>
</div>
<div class="wide100p">
<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>
</div>
</div>
</div>
<div class="range-block wide100p">
<div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
<span data-i18n="Logit Bias">Logit Bias</span>
<strong data-i18n="Logit Bias">Logit Bias</strong>
<div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
<i class="fa-xs fa-solid fa-plus"></i>
<small data-i18n="Add">Add</small>
@ -1934,7 +1951,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq">
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span>
@ -1957,14 +1974,16 @@
<span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
<label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
Inline Image Quality
</label>
<select id="openai_inline_image_quality">
<option data-i18n="openai_inline_image_quality_auto" value="auto">Auto</option>
<option data-i18n="openai_inline_image_quality_low" value="low">Low</option>
<option data-i18n="openai_inline_image_quality_high" value="high">High</option>
</select>
<div class="flex-container oneline-dropdown">
<label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
Inline Image Quality
</label>
<select id="openai_inline_image_quality">
<option data-i18n="openai_inline_image_quality_auto" value="auto">Auto</option>
<option data-i18n="openai_inline_image_quality_low" value="low">Low</option>
<option data-i18n="openai_inline_image_quality_high" value="high">High</option>
</select>
</div>
</div>
</div>
<div class="range-block" data-source="makersuite">
@ -1981,12 +2000,12 @@
</span>
</div>
</div>
<div class="range-block" data-source="makersuite,deepseek,openrouter">
<div class="range-block" data-source="deepseek,openrouter">
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
<input id="openai_show_thoughts" type="checkbox" />
<span>
<span data-i18n="Request model reasoning">Request model reasoning</span>
<i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
<i class="opacity50p fa-solid fa-circle-info" title="DeepSeek Reasoner"></i>
</span>
</label>
<div class="toggle-description justifyLeft marginBot5">
@ -1995,6 +2014,18 @@
</span>
</div>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
<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.">
<label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
Reasoning Effort
</label>
<select id="openai_reasoning_effort">
<option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
<option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
<option data-i18n="openai_reasoning_effort_high" value="high">High</option>
</select>
</div>
</div>
<div class="range-block" data-source="claude">
<div class="wide100p">
<div class="flex-container alignItemsCenter">
@ -2809,27 +2840,6 @@
<div>
<h4 data-i18n="OpenAI Model">OpenAI Model</h4>
<select id="model_openai_select">
<optgroup label="GPT-3.5 Turbo">
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
<option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
<option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
<option value="gpt-3.5-turbo-0613">gpt-3.5-turbo-0613 (2023)</option>
<option value="gpt-3.5-turbo-0301">gpt-3.5-turbo-0301 (2023)</option>
<option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
<option value="gpt-3.5-turbo-16k-0613">gpt-3.5-turbo-16k-0613 (2023)</option>
</optgroup>
<optgroup label="GPT-3.5 Turbo Instruct">
<option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
<option value="gpt-3.5-turbo-instruct-0914">gpt-3.5-turbo-instruct-0914</option>
</optgroup>
<optgroup label="GPT-4">
<option value="gpt-4">gpt-4</option>
<option value="gpt-4-0613">gpt-4-0613 (2023)</option>
<option value="gpt-4-0314">gpt-4-0314 (2023)</option>
<option value="gpt-4-32k">gpt-4-32k</option>
<option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
<option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
</optgroup>
<optgroup label="GPT-4o">
<option value="gpt-4o">gpt-4o</option>
<option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@ -2837,29 +2847,44 @@
<option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
<option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
</optgroup>
<optgroup label="gpt-4o-mini">
<optgroup label="GPT-4o mini">
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>
<option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
<option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
<option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
<option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
</optgroup>
<optgroup label="GPT-4 Turbo">
<optgroup label="o1 and o1-mini">
<option value="o1">o1</option>
<option value="o1-2024-12-17">o1-2024-12-17</option>
<option value="o1-mini">o1-mini</option>
<option value="o1-mini-2024-09-12">o1-mini-2024-09-12</option>
<option value="o1-preview">o1-preview</option>
<option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
</optgroup>
<optgroup label="o3">
<option value="o3-mini">o3-mini</option>
<option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
</optgroup>
<optgroup label="GPT-4 Turbo and GPT-4">
<option value="gpt-4-turbo">gpt-4-turbo</option>
<option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
<option value="gpt-4-turbo-preview">gpt-4-turbo-preview</option>
<option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option value="gpt-4-0125-preview">gpt-4-0125-preview (2024)</option>
<option value="gpt-4-1106-preview">gpt-4-1106-preview (2023)</option>
<option value="gpt-4">gpt-4</option>
<option value="gpt-4-0613">gpt-4-0613 (2023)</option>
<option value="gpt-4-0314">gpt-4-0314 (2023)</option>
</optgroup>
<optgroup label="o1">
<option value="o1-preview">o1-preview</option>
<option value="o1-mini">o1-mini</option>
<optgroup label="GPT-3.5 Turbo">
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
<option value="gpt-3.5-turbo-0125">gpt-3.5-turbo-0125 (2024)</option>
<option value="gpt-3.5-turbo-1106">gpt-3.5-turbo-1106 (2023)</option>
<option value="gpt-3.5-turbo-instruct">gpt-3.5-turbo-instruct</option>
</optgroup>
<optgroup label="Other">
<option value="text-davinci-003">text-davinci-003</option>
<option value="text-davinci-002">text-davinci-002</option>
<option value="text-curie-001">text-curie-001</option>
<option value="text-babbage-001">text-babbage-001</option>
<option value="text-ada-001">text-ada-001</option>
<option value="code-davinci-002">code-davinci-002</option>
<option value="babbage-002">babbage-002</option>
<option value="davinci-002">davinci-002</option>
</optgroup>
<optgroup id="openai_external_category" label="External">
</optgroup>
@ -3058,6 +3083,7 @@
<h4 data-i18n="Google Model">Google Model</h4>
<select id="model_google_select">
<optgroup label="Primary">
<option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
<option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
<option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
<option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
@ -3066,6 +3092,11 @@
<option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
</optgroup>
<optgroup label="Subversions">
<option value="gemini-2.0-pro-exp">Gemini 2.0 Pro Experimental</option>
<option value="gemini-2.0-pro-exp-02-05">Gemini 2.0 Pro Experimental 2025-02-05</option>
<option value="gemini-2.0-flash-lite-preview">Gemini 2.0 Flash-Lite Preview</option>
<option value="gemini-2.0-flash-lite-preview-02-05">Gemini 2.0 Flash-Lite Preview 2025-02-05</option>
<option value="gemini-2.0-flash-001">Gemini 2.0 Flash [001]</option>
<option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
<option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
<option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
@ -3155,34 +3186,22 @@
</div>
<h4 data-i18n="Groq Model">Groq Model</h4>
<select id="model_groq_select">
<optgroup label="Llama 3.3">
<optgroup label="Production Models">
<option value="gemma2-9b-it">gemma2-9b-it</option>
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
<option value="llama3-70b-8192">llama3-70b-8192</option>
<option value="llama3-8b-8192">llama3-8b-8192</option>
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
</optgroup>
<optgroup label="Llama 3.2">
<optgroup label="Preview Models">
<option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
<option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
<option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
<option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
</optgroup>
<optgroup label="Llama 3.1">
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
<option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
<option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
</optgroup>
<optgroup label="Llama 3">
<option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
<option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
<option value="llama3-8b-8192">llama3-8b-8192</option>
<option value="llama3-70b-8192">llama3-70b-8192</option>
</optgroup>
<optgroup label="Gemma">
<option value="gemma-7b-it">gemma-7b-it</option>
<option value="gemma2-9b-it">gemma2-9b-it</option>
</optgroup>
<optgroup label="Other">
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
</optgroup>
</select>
</div>
<div id="nanogpt_form" data-source="nanogpt">
@ -3515,7 +3534,7 @@
</label>
<label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
<input id="instruct_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
<small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
</label>
</div>
</h4>
@ -3693,7 +3712,7 @@
<div class="flex-container">
<label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
<input id="sysprompt_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
<small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
</label>
</div>
</h4>
@ -3747,8 +3766,8 @@
</div>
<label class="checkbox_label" for="custom_stopping_strings_macro">
<input id="custom_stopping_strings_macro" type="checkbox" checked>
<small data-i18n="Replace Macro in Custom Stopping Strings">
Replace Macro in Custom Stopping Strings
<small data-i18n="Replace Macro in Stop Strings">
Replace Macro in Stop Strings
</small>
</label>
</div>
@ -3795,38 +3814,59 @@
<span data-i18n="Reasoning">Reasoning</span>
</h4>
<div>
<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">
<input id="reasoning_auto_parse" type="checkbox" />
<small data-i18n="Auto-Parse Reasoning">
Auto-Parse Reasoning
</small>
</label>
<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">
<input id="reasoning_add_to_prompts" type="checkbox" />
<small data-i18n="Add Reasoning to Prompts">
Add Reasoning to Prompts
</small>
</label>
<div class="flex-container">
<div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
<small data-i18n="Prefix">Prefix</small>
<textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
</div>
<div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
<small data-i18n="Suffix">Suffix</small>
<textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
<div class="flex-container alignItemsBaseline">
<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">
<input id="reasoning_auto_parse" type="checkbox" />
<small data-i18n="Auto-Parse">
Auto-Parse
</small>
</label>
<label class="checkbox_label flex1" for="reasoning_auto_expand" title="Automatically expand reasoning blocks." data-i18n="[title]reasoning_auto_expand">
<input id="reasoning_auto_expand" type="checkbox" />
<small data-i18n="Auto-Expand">
Auto-Expand
</small>
</label>
<label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
<input id="reasoning_show_hidden" type="checkbox" />
<small data-i18n="Show Hidden">
Show Hidden
</small>
</label>
</div>
<div class="flex-container alignItemsBaseline">
<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">
<input id="reasoning_add_to_prompts" type="checkbox" />
<small data-i18n="Add to Prompts">
Add to Prompts
</small>
</label>
<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">
<input id="reasoning_max_additions" class="text_pole textarea_compact widthUnset" type="number" min="0" max="999"></textarea>
<small data-i18n="Max">Max</small>
</div>
</div>
<div class="flex-container">
<div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
<small data-i18n="Separator">Separator</small>
<textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
<details>
<summary data-i18n="Reasoning Formatting">
Reasoning Formatting
</summary>
<div class="flex-container">
<div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
<small data-i18n="Prefix">Prefix</small>
<textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
</div>
<div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
<small data-i18n="Suffix">Suffix</small>
<textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
</div>
</div>
<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">
<small data-i18n="Max Additions">Max Additions</small>
<input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
<div class="flex-container">
<div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
<small data-i18n="Separator">Separator</small>
<textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
</div>
</div>
</div>
</details>
</div>
</div>
<div>
@ -3961,7 +4001,7 @@
<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">
<small>
<span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
<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>
<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" 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>
</small>
<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">
<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 @@
</div>
<div id="extensions_settings" class="flex1 wide50p">
<div id="assets_container" class="extension_container"></div>
<div id="typing_indicator_container" class="extension_container"></div>
<div id="expressions_container" class="extension_container"></div>
<div id="sd_container" class="extension_container"></div>
<div id="tts_container" class="extension_container"></div>
@ -5837,7 +5878,7 @@
<div class="inline-drawer-content flex-container paddingBottom5px wide100p">
<div class="flex-container wide100p alignitemscenter">
<div name="keywordsAndLogicBlock" class="flex-container wide100p alignitemscenter">
<div class="world_entry_form_control flex1">
<div class="world_entry_form_control keyprimary flex1">
<small class="displayNone">
<span data-i18n="Comma separated (required)">
Comma separated (required)
@ -6259,14 +6300,19 @@
</div>
</div>
<details class="mes_reasoning_details">
<summary class="mes_reasoning_summary">
<span data-i18n="Reasoning">Reasoning</span>
<div class="mes_reasoning_actions">
<div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
<div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
<div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
<summary class="mes_reasoning_summary flex-container">
<div class="mes_reasoning_header_block flex-container">
<div class="mes_reasoning_header flex-container">
<span class="mes_reasoning_header_title" data-i18n="Thought for some time">Thought for some time</span>
<div class="mes_reasoning_arrow fa-solid fa-chevron-up"></div>
</div>
</div>
<div class="mes_reasoning_actions flex-container">
<div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
<div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
<div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
<div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
<div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
<div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
</div>
</summary>
<div class="mes_reasoning"></div>
@ -6485,9 +6531,6 @@
</div>
<!-- chat and input bar -->
<div id="typing_indicator_template" class="template_element">
<div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
</div>
<div id="message_file_template" class="template_element">
<div class="mes_file_container">
<div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
@ -6847,8 +6890,8 @@
</div>
<div id="form_sheld">
<div id="dialogue_del_mes">
<div id="dialogue_del_mes_ok" class="menu_button">Delete</div>
<div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>
<div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
<div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
</div>
<div id="send_form" class="no-connection">
<form id="file_form" class="wide100p displayNone">

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "فصل بفواصل دون مسافة بينها",
"Custom Stopping Strings": "سلاسل توقف مخصصة",
"JSON serialized array of strings": "مصفوفة سلسلة JSON متسلسلة",
"Replace Macro in Custom Stopping Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",
"Replace Macro in Stop Strings": "استبدال الماكرو في سلاسل التوقف المخصصة",
"Auto-Continue": "المتابعة التلقائية",
"Allow for Chat Completion APIs": "السماح بواجهات برمجة التطبيقات لإكمال الدردشة",
"Target length (tokens)": "الطول المستهدف (رموز)",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "getrennt durch Kommas ohne Leerzeichen dazwischen",
"Custom Stopping Strings": "Benutzerdefinierte Stoppzeichenfolgen",
"JSON serialized array of strings": "JSON serialisierte Reihe von Zeichenfolgen",
"Replace Macro in Custom Stopping Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",
"Replace Macro in Stop Strings": "Makro in benutzerdefinierten Stoppzeichenfolgen ersetzen",
"Auto-Continue": "Automatisch fortsetzen",
"Allow for Chat Completion APIs": "Erlaube Chat-Vervollständigungs-APIs",
"Target length (tokens)": "Ziel-Länge (Tokens)",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "separe con comas sin espacio entre ellas",
"Custom Stopping Strings": "Cadenas de Detención Personalizadas",
"JSON serialized array of strings": "Arreglo de cadenas serializado en JSON",
"Replace Macro in Custom Stopping Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",
"Replace Macro in Stop Strings": "Reemplazar macro en Cadenas de Detención Personalizadas",
"Auto-Continue": "Autocontinuar",
"Allow for Chat Completion APIs": "Permitir para APIs de Completado de Chat",
"Target length (tokens)": "Longitud objetivo (tokens)",

View File

@ -434,7 +434,7 @@
"Non-markdown strings": "Chaînes non Markdown",
"Custom Stopping Strings": "Chaînes d'arrêt personnalisées",
"JSON serialized array of strings": "Tableau de chaînes sérialisé JSON",
"Replace Macro in Custom Stopping Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",
"Replace Macro in Stop Strings": "Remplacer les macro dans les chaînes d'arrêt personnalisées",
"Auto-Continue": "Auto-Continue",
"Allow for Chat Completion APIs": "Autoriser les APIs de complétion de chat",
"Target length (tokens)": "Longueur cible (tokens)",
@ -1485,7 +1485,7 @@
"(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
"Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
"Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
"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)",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "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)",
"Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
"Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
"Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "aðskilið með kommum án bila milli",
"Custom Stopping Strings": "Eigin stopp-strengir",
"JSON serialized array of strings": "JSON raðað fylki af strengjum",
"Replace Macro in Custom Stopping Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",
"Replace Macro in Stop Strings": "Skiptu út í macro í sérsniðnum stoppa strengjum",
"Auto-Continue": "Sjálfvirk Forná",
"Allow for Chat Completion APIs": "Leyfa fyrir spjall Loka APIs",
"Target length (tokens)": "Markaðarlengd (texti)",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "separati con virgole senza spazio tra loro",
"Custom Stopping Strings": "Stringhe di Stop Personalizzate",
"JSON serialized array of strings": "Matrice serializzata JSON di stringhe",
"Replace Macro in Custom Stopping Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",
"Replace Macro in Stop Strings": "Sostituisci Macro in Stringhe di Arresto Personalizzate",
"Auto-Continue": "Auto-continua",
"Allow for Chat Completion APIs": "Consenti per API di completamento chat",
"Target length (tokens)": "Lunghezza obiettivo (token)",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "間にスペースのないカンマで区切ります",
"Custom Stopping Strings": "カスタム停止文字列",
"JSON serialized array of strings": "文字列のJSONシリアル化配列",
"Replace Macro in Custom Stopping Strings": "カスタム停止文字列内のマクロを置換する",
"Replace Macro in Stop Strings": "カスタム停止文字列内のマクロを置換する",
"Auto-Continue": "自動継続",
"Allow for Chat Completion APIs": "チャット補完APIを許可",
"Target length (tokens)": "ターゲット長さ(トークン)",

View File

@ -211,7 +211,7 @@
"Sampler Priority": "샘플러 우선 순위",
"Ooba only. Determines the order of samplers.": "Ooba 전용. 샘플러의 순서를 결정합니다.",
"Character Names Behavior": "캐릭터 이름 동작",
"[title]character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",
"character_names_none": "캐릭터 이름 접두사를 추가하지 않습니다. 그룹 채팅에서는 좋지 않을 수 있으므로, 이 설정을 선택할 때는 주의해야 합니다.",
"Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
"None": "없음",
"None (not injected)": "없음 (삽입되지 않음)",
@ -404,7 +404,7 @@
"Custom API Key": "커스텀 API 키",
"Available Models": "사용 가능한 모델",
"Prompt Post-Processing": "신속한 후처리",
"[title]API Connections;[no_connection_text]api_no_connection": "연결이 되지 않았습니다...",
"api_no_connection": "연결이 되지 않았습니다...",
"Applies additional processing to the prompt before sending it to the API.": "API로 보내기 전에 프롬프트에 추가 처리를 적용합니다.",
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 테스트 메시지를 보내어 API 연결을 확인합니다. 이에 대해 유료 크레딧이 지불될 수 있음을 인식하세요!",
"Test Message": "테스트 메시지",
@ -492,7 +492,7 @@
"separate with commas w/o space between": "쉼표로 구분 (공백 없이)",
"Custom Stopping Strings": "사용자 정의 중지 문자열",
"JSON serialized array of strings": "문자열의 JSON 직렬화된 배열",
"Replace Macro in Custom Stopping Strings": "사용자 정의 중단 문자열에서 매크로 교체",
"Replace Macro in Stop Strings": "사용자 정의 중단 문자열에서 매크로 교체",
"Auto-Continue": "자동 계속하기",
"Allow for Chat Completion APIs": "채팅 완성 API 허용",
"Target length (tokens)": "대상 길이 (토큰)",
@ -625,7 +625,7 @@
"Single-row message input area. Mobile only, no effect on PC": "한 줄짜리 메시지 입력 영역. 모바일 전용, PC에는 영향 없음",
"Compact Input Area (Mobile)": "조그마한 입력 영역 (모바일)",
"Swipe # for All Messages": "모든 스와이프 메시지에 대해 번호 매기기",
"[title]Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",
"Display swipe numbers for all messages, not just the last.": "마지막 메시지만이 아니라 모든 메시지에 대한 스와이프 번호를 표시합니다.",
"In the Character Management panel, show quick selection buttons for favorited characters": "캐릭터 관리 패널에서 즐겨찾는 캐릭터에 대한 빠른 선택 버튼을 표시합니다",
"Characters Hotswap": "캐릭터 핫스왑",
"Enable magnification for zoomed avatar display.": "마우스 포인터를 아바타 위에 올려두면 아바타가 확대 됩니다.",
@ -1537,7 +1537,7 @@
"Only apply color as accent": "색상은 오직 강조로써만 적용됩니다",
"qr--colorClear": "색상 지우기",
"Color": "색상",
"[title]world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
"world_button_title": "캐릭터 로어. 클릭하여 로드하세요. Shift를 클릭하면 '월드 인포 링크' 팝업이 열립니다.",
"Select TTS Provider": "TTS 공급자 선택",
"tts_enabled": "활성화",
"Narrate user messages": "사용자 메시지 나레이션",
@ -1582,15 +1582,15 @@
"Prompt Content": "프롬프트 내용",
"Instruct Sequences": "지시 시퀀스",
"Prefer Character Card Instructions": "캐릭터 카드의 지시사항을 선호",
"[title]If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
"If checked and the character card contains a Post-History Instructions override, use that instead": "활성화 된 경우, 캐릭터 카드에 Post-History 지시 무시 항목이 포함되어 있으면, 카드 지시사항의 내용으로 대신 사용합니다.",
"Auto-select Input Text": "입력 텍스트 자동 선택",
"[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.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
"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.": "일부 텍스트 필드를 클릭하거나 선택할 때 자동으로 입력된 텍스트가 선택되도록 설정합니다. 팝업 입력창과 기타 커스텀 입력 필드에 적용됩니다.",
"Markdown Hotkeys": "마크다운 입력 단축키",
"[title]markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
"markdown_hotkeys_desc": "특정 텍스트 입력창에서 마크다운 형식 문자를 입력하기 위한 단축키를 활성화합니다. '/help hotkeys'를 참고하세요.",
"Show group chat queue": "그룹 채팅 대기열 표시",
"[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
"In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "그룹 채팅에서 응답을 생성하기 위해 현재 대기 중인 캐릭터와 응답할 순서를 강조 표시합니다.",
"Quick 'Impersonate' button": "빠른 '사칭' 버튼",
"[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
"Show a button in the input area to ask the AI to impersonate your character for a single message": "입력 영역에 AI에게 한 메시지 동안 당신의 캐릭터 연기를 사칭하도록 요청하는 버튼을 표시합니다.",
"Injection Template": "삽입 템플릿",
"Query messages": "쿼리 메시지 수",
"Score threshold": "점수 임계값",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "gescheiden met komma's zonder spatie ertussen",
"Custom Stopping Strings": "Aangepaste Stopreeksen",
"JSON serialized array of strings": "JSON geserialiseerde reeks van strings",
"Replace Macro in Custom Stopping Strings": "Macro vervangen in aangepaste stopreeksen",
"Replace Macro in Stop Strings": "Macro vervangen in aangepaste stopreeksen",
"Auto-Continue": "Automatisch doorgaan",
"Allow for Chat Completion APIs": "Chatvervolledigings-API's toestaan",
"Target length (tokens)": "Doellengte (tokens)",

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "separe com vírgulas sem espaço entre",
"Custom Stopping Strings": "Cadeias de parada personalizadas",
"JSON serialized array of strings": "Matriz de strings serializada em JSON",
"Replace Macro in Custom Stopping Strings": "Substituir Macro em Strings de Parada Personalizadas",
"Replace Macro in Stop Strings": "Substituir Macro em Strings de Parada Personalizadas",
"Auto-Continue": "Auto-Continuar",
"Allow for Chat Completion APIs": "Permitir APIs de Completar Chat",
"Target length (tokens)": "Comprimento alvo (tokens)",

View File

@ -161,7 +161,7 @@
"View hidden API keys": "Посмотреть скрытые API-ключи",
"Advanced Formatting": "Расширенное форматирование",
"Context Template": "Шаблон контекста",
"Replace Macro in Custom Stopping Strings": "Заменять макросы в пользовательских стоп-строках",
"Replace Macro in Stop Strings": "Заменять макросы в пользовательских стоп-строках",
"Story String": "Строка истории",
"Example Separator": "Разделитель примеров сообщений",
"Chat Start": "Начало чата",
@ -195,7 +195,7 @@
"Yes": "Да",
"No": "Нет",
"Context %": "Процент контекста",
"Budget Cap": "Бюджетный лимит",
"Budget Cap": "Лимит бюджета",
"(0 = disabled)": "(0 = отключено)",
"None": "Отсутствует",
"User Settings": "Настройки пользователя",
@ -575,10 +575,10 @@
"Characters sorting order": "Порядок сортировки персонажей",
"Remove": "Убрать",
"Select a World Info file for": "Выбрать файл с миром для",
"Primary Lorebook": "Основного лорбука",
"A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук",
"When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира",
"Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON",
"Primary Lorebook": "Основной лорбук",
"A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
"When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
"Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
"Additional Lorebooks": "Вспомогательные лорбуки",
"Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
"NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@ -593,7 +593,7 @@
"Prompt": "Промпт",
"Copy": "Скопировать",
"Confirm": "Подтвердить",
"Copy this message": "Скопировать сообщение",
"Copy this message": "Продублировать сообщение",
"Delete this message": "Удалить сообщение",
"Move message up": "Переместить сообщение вверх",
"Move message down": "Переместить сообщение вниз",
@ -612,7 +612,7 @@
"Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
"Continue the last message": "Продолжить текущее сообщение",
"Bind user name to that avatar": "Закрепить имя за этим аватаром",
"Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.",
"Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
"Change persona image": "Сменить аватар персоны",
"Delete persona": "Удалить персону",
"Reduced Motion": "Сокращение анимаций",
@ -640,7 +640,7 @@
"Token Probabilities": "Вероятности токенов",
"Close chat": "Закрыть чат",
"Manage chat files": "Все чаты",
"Import Extension From Git Repo": "Импортировать расширение из Git Repository",
"Import Extension From Git Repo": "Импортировать расширение из Git-репозитория.",
"Install extension": "Установить расширение",
"Manage extensions": "Управление расширениями",
"Tokens persona description": "Токенов",
@ -1122,7 +1122,7 @@
"help_hotkeys_0": "Горячие клавиши",
"You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
"Download Extensions & Assets": "Загрузить расширения и ресурсы",
"menu within": нутри этих кубиков",
"menu within": меню",
"Assets URL": "URL с описанием ресурсов",
"Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
"Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@ -1943,7 +1943,7 @@
"and connect to an": "и подключитесь к",
"You can add more": "Можете добавить больше",
"from other websites": "с других сайтов.",
"Go to the": "Загляните в",
"Go to the": "Заходите в",
"to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
"or_welcome": "; также доступен",
"Claude API Key": "Ключ от API Claude",
@ -1958,7 +1958,7 @@
"Save": "Сохранить",
"Chat Lorebook": "Лорбук для чата",
"chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
"world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + клик, чтобы открыть диалог привязки мира",
"world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",
"No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
"ext_regex_user_input_desc": "Отправленные вами сообщения.",
"ext_regex_ai_input_desc": "Полученные от API ответы.",
@ -2144,5 +2144,65 @@
"Not connected to the API!": "Нет соединения с API!",
"ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
"Update all": "Обновить все",
"Close": "Закрыть"
}
"Close": "Закрыть",
"Optional modules:": "Необязательные модули:",
"Sort: Display Name": "Сортировать: по названию",
"Sort: Loading Order": "Сортировать: в порядке загрузки",
"Click to toggle": "Нажмите, чтобы включить или выключить",
"Loading Asset List": "Загрузить список ресурсов",
"Don't ask again for this URL": "Запомнить выбор для этого адреса",
"Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
"All": "Всё",
"Characters": "Персонажи",
"Ambient sounds": "Звуковой эмбиент",
"Blip sounds": "Звуки уведомлений",
"Background music": "Фоновая музыка",
"Search": "Поиск",
"extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
"extension_install_2": ".",
"extension_install_3": "Нажмите на иконку ",
"extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
"Extension repo/guide:": "Репозиторий расширения:",
"Preview in browser": "Предпросмотр",
"Adds a function tool": "Частично или полностью работает через вызов функций",
"Tool": "Функции",
"Move extension": "Переместить расширение",
"ext_type_local": "Это локальное расширение, доступно только вам",
"ext_type_global": "Это глобальное расширение, доступно всем пользователям",
"Move": "Переместить",
"Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
"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.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
"Disclaimer:": "Внимание:",
"Example:": "Пример:",
"context_derived": "Считывать из метаданных модели (по возможности)",
"instruct_derived": "Считывать из метаданных модели (по возможности)",
"Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
"Reasoning Effort": "Рассуждения",
"Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
"openai_reasoning_effort_low": "Поверхностные",
"openai_reasoning_effort_medium": "Обычные",
"openai_reasoning_effort_high": "Подробные",
"Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
"Persona Lorebook for": "Лорбук для персоны",
"persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
"Global list": "Глобальный список",
"Preset-specific list": "Список для данного пресета",
"Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
"Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
"Add a reasoning block": "Добавить блок рассуждений",
"Create a copy of this message?": "Продублировать это сообщение?",
"Max Recursion Steps": "Макс. глубина рекурсии",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
"(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
"Enter a valid API URL": "Введите корректный адрес API",
"No Ollama model selected.": "Не выбрана модель Ollama",
"Background Fitting": "Способ подгонки фона под разрешение",
"Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
"Token Counter": "Подсчитать токены",
"Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
"Selected tokenizer:": "Выбранный токенайзер:",
"Input:": "Входные данные:",
"Tokenized text:": "Токенизированный текст:",
"Token IDs:": "Идентификаторы токенов:",
"Tokens:": "Токенов:"
}

View File

@ -482,7 +482,7 @@
"separate with commas w/o space between": "розділяйте комами без пропусків між ними",
"Custom Stopping Strings": "Власні рядки зупинки",
"JSON serialized array of strings": "JSON-серіалізований масив рядків",
"Replace Macro in Custom Stopping Strings": "Замінювати макроси у власних рядках зупинки",
"Replace Macro in Stop Strings": "Замінювати макроси у власних рядках зупинки",
"Auto-Continue": "Автоматичне продовження",
"Allow for Chat Completion APIs": "Дозволити для Chat Completion API",
"Target length (tokens)": "Цільова довжина (токени)",

View File

@ -482,7 +482,7 @@
"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",
"Custom Stopping Strings": "Chuỗi dừng tùy chỉnh",
"JSON serialized array of strings": "Mảng chuỗi được tuần tự hóa JSON",
"Replace Macro in Custom Stopping Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",
"Replace Macro in Stop Strings": "Thay thế Macro trong Chuỗi Dừng Tùy chỉnh",
"Auto-Continue": "Tự động Tiếp tục",
"Allow for Chat Completion APIs": "Cho phép các API hoàn thành Trò chuyện",
"Target length (tokens)": "Độ dài mục tiêu (token)",

View File

@ -215,7 +215,7 @@
"Classifier Free Guidance. More helpful tip coming soon": "无分类器指导CFG。更多有用的提示敬请期待。",
"Scale": "缩放比例",
"Negative Prompt": "负面提示词",
"Used if CFG Scale is unset globally, per chat or character": "如果无分类器指导CFG缩放比例未在全局设置它将作用于每个聊天或每个角色",
"Used if CFG Scale is unset globally, per chat or character": "如果CFG缩放比例未被全局设置它将作用于所有聊天或角色",
"Add text here that would make the AI generate things you don't want in your outputs.": "请在此处添加文本,以避免生成您不希望出现在输出中的内容。",
"Grammar String": "语法字符串",
"GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF取决于使用的后端。如果您使用这个您应该知道该用哪一个。",
@ -559,7 +559,7 @@
"Prompt Content": "提示词内容",
"Custom Stopping Strings": "自定义停止字符串",
"JSON serialized array of strings": "JSON序列化的字符串数组",
"Replace Macro in Custom Stopping Strings": "替换自定义停止字符串中的宏",
"Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",
"Token Padding": "词符填充",
"Miscellaneous": "杂项",
"Non-markdown strings": "非 Markdown 字符串",
@ -584,7 +584,7 @@
"(0 = unlimited, use budget)": "“0”为无限制使用预算",
"Cap the number of entry activation recursions": "限制条目激活递归的次数",
"Max Recursion Steps": "最大递归深度",
"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当使用最小激活次数时此功能被禁用",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "“0”为无限制“1”为扫描一次且不递归“2”为扫描一次且递归一次依此类推\n当使用最小激活次数时此功能被禁用",
"Insertion Strategy": "插入策略",
"Sorted Evenly": "均匀排序",
"Character Lore First": "角色世界书优先",
@ -1208,7 +1208,7 @@
"View contents": "查看内容",
"Remove the file": "删除文件",
"Author's Note": "作者注释",
"Unique to this chat": "此聊天独有",
"Unique to this chat": "仅对此聊天生效",
"Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",
"Include in World Info Scanning": "纳入世界信息扫描",
"Before Main Prompt / Story String": "主提示词/故事线之前",
@ -1224,13 +1224,13 @@
"Replace Author's Note": "替换作者注",
"Default Author's Note": "默认作者注",
"Will be automatically added as the Author's Note for all new chats.": "将自动添加为所有新聊天的作者注释。",
"Chat CFG": "聊天CFG",
"1 = disabled": "“1”为禁用",
"Chat CFG": "聊天CFG缩放",
"1 = disabled": "“1”为禁用",
"write short replies, write replies using past tense": "写简短的回复,用过去时写回复",
"Positive Prompt": "正面提示词",
"Use character CFG scales": "单独为各个角色设置CFG缩放",
"Character CFG": "角色CFG配置",
"Will be automatically added as the CFG for this character.": "将自动添加为该角色的 CFG。",
"Will be automatically added as the CFG for this character.": "将自动添加到该角色的CFG设置中。",
"Global CFG": "全局CFG",
"Will be used as the default CFG options for every chat unless overridden.": "除非被覆盖,否则将用作每次聊天的默认 CFG 选项。",
"CFG Prompt Cascading": "CFG 提示词级联",
@ -1485,7 +1485,7 @@
"ext_regex_replace_string_placeholder": "使用 {{match}} 包含来自“查找正则表达式”或“$1”、“$2”等的匹配文本作为捕获组。",
"Trim Out": "修剪掉",
"ext_regex_trim_placeholder": "在替换之前全局修剪正则表达式匹配中任何不需要的部分。用回车键分隔每个元素。",
"ext_regex_affects": "影响",
"ext_regex_affects": "作用范围",
"ext_regex_user_input_desc": "用户发送的消息",
"ext_regex_user_input": "用户输入",
"ext_regex_ai_input_desc": "从生成式API中获取的信息。",
@ -1719,9 +1719,9 @@
"Chat Lorebook for": "聊天知识书",
"chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
"chat_rename_1": "输入聊天的新名称:",
"chat_rename_2": "注意!!使用已有文件名会导致错误!!",
"chat_rename_3": "此举会将聊天与标记为“检查点”的聊天解绑。",
"chat_rename_4": "不需要在结尾添加 '.JSONL'",
"chat_rename_2": "注意!!与其他文件重名会导致错误!!",
"chat_rename_3": "此举会将聊天与标记为“检查点”的聊天解绑。",
"chat_rename_4": "不需要在结尾添加 '.JSONL' 后缀)",
"Enter Checkpoint Name:": "输入检查点名称:",
"(Leave empty to auto-generate)": "(留空以自动生成)",
"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 @@
"Enter your password below to confirm:": "输入您的密码以确认:",
"Chat Scenario Override": "聊天场景覆盖",
"Remove": "移除",
"Unique to this chat.": "Unique to this chat.",
"Unique to this chat.": "仅对此聊天生效。",
"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.",
"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.",
"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.",

View File

@ -483,7 +483,7 @@
"separate with commas w/o space between": "用逗號分隔,之間無空格",
"Custom Stopping Strings": "自訂停止字串",
"JSON serialized array of strings": "JSON 序列化字串數組",
"Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集",
"Replace Macro in Stop Strings": "取代自訂停止字串中的巨集",
"Auto-Continue": "自動繼續",
"Allow for Chat Completion APIs": "允許聊天補全 API",
"Target length (tokens)": "目標長度(符元)",
@ -1458,7 +1458,7 @@
"Example: http://localhost:1234/v1": "例如http://localhost:1234/v1",
"popup-button-crop": "裁剪",
"(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
"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使用最小啟動設定時將停用",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制1 = 掃描一次且不遞歸2 = 掃描一次並遞歸一次,以此類推\n使用最小啟動設定時將停用",
"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。",
"A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
"Abort current image generation task": "終止目前的圖片生成任務",
@ -1805,7 +1805,7 @@
"context_derived": "若可能,根據模型元數據推導。",
"instruct_derived": "若可能,根據模型元數據推導。",
"Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
"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啟用最小啟動次數時無效",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制1 = 掃描一次不遞歸2 = 掃描一次後遞歸一次 ⋯以此類推\n啟用最小啟動次數時無效",
"Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
"Manual": "手動",
"Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示Post-History Instructions。\nv2 格式specpost_history_instructions",

View File

@ -269,7 +269,8 @@ import { initSettingsSearch } from './scripts/setting-search.js';
import { initBulkEdit } from './scripts/bulk-edit.js';
import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
import { getContext } from './scripts/st-context.js';
import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
import { accountStorage } from './scripts/util/AccountStorage.js';
// API OBJECT FOR EXTERNAL WIRING
globalThis.SillyTavern = {
@ -365,6 +366,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
return;
}
if (!(node instanceof Element)) {
return;
}
let mediaBlocked = false;
switch (node.tagName) {
@ -419,7 +424,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
const entityId = getCurrentEntityId();
const warningShownKey = `mediaWarningShown:${entityId}`;
if (localStorage.getItem(warningShownKey) === null) {
if (accountStorage.getItem(warningShownKey) === null) {
const warningToast = toastr.warning(
t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
t`External media has been blocked`,
@ -430,7 +435,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
},
);
localStorage.setItem(warningShownKey, 'true');
accountStorage.setItem(warningShownKey, 'true');
}
}
});
@ -492,9 +497,11 @@ export const event_types = {
// TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted',
CHARACTER_DUPLICATED: 'character_duplicated',
CHARACTER_RENAMED: 'character_renamed',
/** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
STREAM_TOKEN_RECEIVED: 'stream_token_received',
STREAM_REASONING_DONE: 'stream_reasoning_done',
FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
OPEN_CHARACTER_LIBRARY: 'open_character_library',
@ -1025,12 +1032,22 @@ export function setAnimationDuration(ms = null) {
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
}
/**
* Sets the currently active character
* @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`.
*/
export function setActiveCharacter(entityOrKey) {
active_character = getTagKeyForEntity(entityOrKey);
active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
if (active_character) active_group = null;
}
/**
* Sets the currently active group.
* @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`.
*/
export function setActiveGroup(entityOrKey) {
active_group = getTagKeyForEntity(entityOrKey);
active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
if (active_group) active_character = null;
}
/**
@ -1489,7 +1506,7 @@ export async function printCharacters(fullRefresh = false) {
$('#rm_print_characters_pagination').pagination({
dataSource: entities,
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default,
sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
pageRange: 1,
pageNumber: saveCharactersPage || 1,
@ -1533,7 +1550,7 @@ export async function printCharacters(fullRefresh = false) {
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
accountStorage.setItem(storageKey, e.target.value);
},
afterPaging: function (e) {
saveCharactersPage = e;
@ -2189,26 +2206,29 @@ function insertSVGIcon(mes, extra) {
modelName = extra.api;
}
const image = new Image();
// Add classes for styling and identification
image.classList.add('icon-svg', 'timestamp-icon');
image.src = `/img/${modelName}.svg`;
image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
image.onload = async function () {
// Check if an SVG already exists adjacent to the timestamp
let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
if (existingSVG.length) {
// Replace existing SVG
existingSVG.replaceWith(image);
} else {
// Append the new SVG if none exists
mes.find('.timestamp').after(image);
}
await SVGInject(image);
const insertOrReplaceSVG = (image, className, targetSelector, insertBefore) => {
image.onload = async function () {
let existingSVG = insertBefore ? mes.find(targetSelector).prev(`.${className}`) : mes.find(targetSelector).next(`.${className}`);
if (existingSVG.length) {
existingSVG.replaceWith(image);
} else {
if (insertBefore) mes.find(targetSelector).before(image);
else mes.find(targetSelector).after(image);
}
await SVGInject(image);
};
};
const createModelImage = (className, targetSelector, insertBefore) => {
const image = new Image();
image.classList.add('icon-svg', className);
image.src = `/img/${modelName}.svg`;
image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
insertOrReplaceSVG(image, className, targetSelector, insertBefore);
};
createModelImage('timestamp-icon', '.timestamp');
createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
}
@ -2219,7 +2239,6 @@ function getMessageFromTemplate({
isUser,
avatarImg,
bias,
reasoning,
isSystem,
title,
timerValue,
@ -2244,7 +2263,6 @@ function getMessageFromTemplate({
mes.find('.avatar img').attr('src', avatarImg);
mes.find('.ch_name .name_text').text(characterName);
mes.find('.mes_bias').html(bias);
mes.find('.mes_reasoning').html(reasoning);
mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
mes.find('.mesIDDisplay').text(`#${mesId}`);
tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@ -2252,6 +2270,8 @@ function getMessageFromTemplate({
timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
bookmarkLink && updateBookmarkDisplay(mes);
updateReasoningUI(mes);
if (power_user.timestamp_model_icon && extra?.api) {
insertSVGIcon(mes, extra);
}
@ -2263,12 +2283,18 @@ function getMessageFromTemplate({
* Re-renders a message block with updated content.
* @param {number} messageId Message ID
* @param {object} message Message object
* @param {object} [options={}] Optional arguments
* @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
*/
export function updateMessageBlock(messageId, message) {
export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
const messageElement = $(`#chat [mesid="${messageId}"]`);
const text = message?.extra?.display_text ?? message.mes;
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
if (rerenderMessage) {
const text = message?.extra?.display_text ?? message.mes;
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
}
updateReasoningUI(messageElement);
addCopyToCodeBlocks(messageElement);
appendMediaToMessage(message, messageElement);
}
@ -2428,7 +2454,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
false,
);
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
let params = {
@ -2438,7 +2463,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
isUser: mes.is_user,
avatarImg: avatarImg,
bias: bias,
reasoning: reasoning,
isSystem: isSystem,
title: title,
bookmarkLink: bookmarkLink,
@ -2446,7 +2470,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
timestamp: timestamp,
extra: mes.extra,
tokenCount: mes.extra?.token_count ?? 0,
...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),
...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
};
const renderedMessage = getMessageFromTemplate(params);
@ -2498,8 +2522,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
swipeMessage.attr('swipeid', params.swipeId);
swipeMessage.find('.mes_text').html(messageText).attr('title', title);
swipeMessage.find('.mes_reasoning').html(reasoning);
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
updateReasoningUI(swipeMessage);
appendMediaToMessage(mes, swipeMessage);
if (power_user.timestamp_model_icon && params.extra?.api) {
insertSVGIcon(swipeMessage, params.extra);
@ -2566,13 +2590,14 @@ export function formatCharacterAvatar(characterAvatar) {
* @param {Date} gen_started Date when generation was started
* @param {Date} gen_finished Date when generation was finished
* @param {number} tokenCount Number of tokens generated (0 if not available)
* @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
* @returns {Object} Object containing the formatted timer value and title
* @example
* const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
* console.log(timerValue); // 1.2s
* 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
*/
function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
if (!gen_started || !gen_finished) {
return {};
}
@ -2586,8 +2611,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
`Generation queued: ${start.format(dateFormat)}`,
`Reply received: ${finish.format(dateFormat)}`,
`Time to generate: ${seconds} seconds`,
reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
].join('\n');
].filter(x => x).join('\n').trim();
if (isNaN(seconds) || seconds < 0) {
return { timerValue: '', timerTitle };
@ -2775,7 +2801,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
TempResponseLength.save(main_api, responseLength);
eventHook = TempResponseLength.setupEventHook(main_api);
}
return await Generate('quiet', options);
const result = await Generate('quiet', options);
return removeReasoningFromString(result);
} finally {
if (responseLengthCustomized && TempResponseLength.isCustomized()) {
TempResponseLength.restore(main_api);
@ -3075,8 +3102,8 @@ export function isStreamingEnabled() {
(main_api == 'openai' &&
oai_settings.stream_openai &&
!noStreamSources.includes(oai_settings.chat_completion_source) &&
!(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) &&
!(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
!(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))
)
|| (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
|| (main_api == 'novel' && nai_settings.streaming_novel)
|| (main_api == 'textgenerationwebui' && textgen_settings.streaming));
@ -3105,11 +3132,14 @@ class StreamingProcessor {
constructor(type, forceName2, timeStarted, continueMessage) {
this.result = '';
this.messageId = -1;
/** @type {HTMLElement} */
this.messageDom = null;
/** @type {HTMLElement} */
this.messageTextDom = null;
/** @type {HTMLElement} */
this.messageTimerDom = null;
/** @type {HTMLElement} */
this.messageTokenCounterDom = null;
this.messageReasoningDom = null;
/** @type {HTMLTextAreaElement} */
this.sendTextarea = document.querySelector('#send_textarea');
this.type = type;
@ -3125,7 +3155,8 @@ class StreamingProcessor {
/** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
this.messageLogprobs = [];
this.toolCalls = [];
this.reasoning = '';
// Initialize reasoning in its own handler
this.reasoningHandler = new ReasoningHandler(timeStarted);
}
#checkDomElements(messageId) {
@ -3134,8 +3165,8 @@ class StreamingProcessor {
this.messageTextDom = this.messageDom?.querySelector('.mes_text');
this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
}
this.reasoningHandler.updateDom(messageId);
}
#updateMessageBlockVisibility() {
@ -3145,22 +3176,12 @@ class StreamingProcessor {
}
}
showMessageButtons(messageId) {
if (messageId == -1) {
return;
}
showStopButton();
$(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
markUIGenStarted() {
deactivateSendButtons();
}
hideMessageButtons(messageId) {
if (messageId == -1) {
return;
}
hideStopButton();
$(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });
markUIGenStopped() {
activateSendButtons();
}
async onStartStreaming(text) {
@ -3169,20 +3190,18 @@ class StreamingProcessor {
if (this.type == 'impersonate') {
this.sendTextarea.value = '';
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
}
else {
} else {
await saveReply(this.type, text, true, '', [], '');
messageId = chat.length - 1;
this.#checkDomElements(messageId);
this.showMessageButtons(messageId);
this.markUIGenStarted();
}
hideSwipeButtons();
scrollChatToBottom();
return messageId;
}
onProgressStreaming(messageId, text, isFinal) {
async onProgressStreaming(messageId, text, isFinal) {
const isImpersonate = this.type == 'impersonate';
const isContinue = this.type == 'continue';
@ -3194,11 +3213,9 @@ class StreamingProcessor {
let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
// Predict unbalanced asterisks / quotes during streaming
const charsToBalance = ['*', '"', '```'];
for (const char of charsToBalance) {
if (!isFinal && isOdd(countOccurrences(processedText, char))) {
// Add character at the end to balance it
const separator = char.length > 1 ? '\n' : '';
processedText = processedText.trimEnd() + separator + char;
}
@ -3207,31 +3224,25 @@ class StreamingProcessor {
if (isImpersonate) {
this.sendTextarea.value = processedText;
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
}
else {
} else {
const mesChanged = chat[messageId]['mes'] !== processedText;
this.#checkDomElements(messageId);
this.#updateMessageBlockVisibility();
const currentTime = new Date();
chat[messageId]['mes'] = processedText;
chat[messageId]['gen_started'] = this.timeStarted;
chat[messageId]['gen_finished'] = currentTime;
if (!chat[messageId]['extra']) {
chat[messageId]['extra'] = {};
}
if (this.reasoning) {
chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
if (this.messageReasoningDom instanceof HTMLElement) {
const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
this.messageReasoningDom.innerHTML = formattedReasoning;
}
}
// Update reasoning
await this.reasoningHandler.process(messageId, mesChanged);
processedText = chat[messageId]['mes'];
// Don't waste time calculating token count for streaming
const tokenCountText = (this.reasoning || '') + processedText;
// Token count update.
const tokenCountText = this.reasoningHandler.reasoning + processedText;
const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
if (currentTokenCount) {
chat[messageId]['extra']['token_count'] = currentTokenCount;
if (this.messageTokenCounterDom instanceof HTMLElement) {
@ -3257,7 +3268,7 @@ class StreamingProcessor {
this.messageTextDom.innerHTML = formattedText;
}
const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
if (this.messageTimerDom instanceof HTMLElement) {
this.messageTimerDom.textContent = timePassed.timerValue;
this.messageTimerDom.title = timePassed.timerTitle;
@ -3272,10 +3283,12 @@ class StreamingProcessor {
}
async onFinishStreaming(messageId, text) {
this.hideMessageButtons(this.messageId);
this.onProgressStreaming(messageId, text, true);
this.markUIGenStopped();
await this.onProgressStreaming(messageId, text, true);
addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
await this.reasoningHandler.finish(messageId);
if (Array.isArray(this.swipes) && this.swipes.length > 0) {
const message = chat[messageId];
const swipeInfo = {
@ -3315,7 +3328,7 @@ class StreamingProcessor {
this.abortController.abort();
this.isStopped = true;
this.hideMessageButtons(this.messageId);
this.markUIGenStopped();
generatedPromptCache = '';
unblockGeneration();
@ -3366,7 +3379,7 @@ class StreamingProcessor {
for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
timestamps.push(Date.now());
if (this.isStopped || this.abortController.signal.aborted) {
return;
return this.result;
}
this.toolCalls = toolCalls;
@ -3375,9 +3388,10 @@ class StreamingProcessor {
if (logprobs) {
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
}
this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
// Get the updated reasoning string into the handler
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
}
const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
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
break;
}
case 'textgenerationwebui':
generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
generateData = await getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
TempResponseLength.restore(api);
break;
case 'openai': {
@ -4440,7 +4454,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
// For prompt bit itemization
let mesSendString = '';
function getCombinedPrompt(isNegative) {
async function getCombinedPrompt(isNegative) {
// Only return if the guidance scale doesn't exist or the value is 1
// Also don't return if constructing the neutral prompt
if (isNegative && !useCfgPrompt) {
@ -4467,7 +4481,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
// TODO: Make all extension prompts use an array/splice method
const lengthDiff = mesSend.length - cfgPrompt.depth;
const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
const cfgMessage = finalMesSend[cfgDepth];
if (cfgMessage) {
if (!Array.isArray(finalMesSend[cfgDepth].extensionPrompts)) {
finalMesSend[cfgDepth].extensionPrompts = [];
}
finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
}
}
}
}
@ -4543,13 +4563,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
};
// Before returning the combined prompt, give available context related information to all subscribers.
eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
await eventSource.emit(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
// If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
return !data.combinedPrompt ? combine() : data.combinedPrompt;
}
let finalPrompt = getCombinedPrompt(false);
let finalPrompt = await getCombinedPrompt(false);
const eventData = { prompt: finalPrompt, dryRun: dryRun };
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
}
break;
case 'textgenerationwebui': {
const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: getCombinedPrompt(true) } : null;
generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: await getCombinedPrompt(true) } : null;
generate_data = await getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
break;
}
case 'novel': {
@ -5510,7 +5530,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
toastr.info(t`Copied!`);
});
popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () {
popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
//console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
console.log(PromptArrayItemForRawPromptDisplay);
console.log(itemizedPrompts);
@ -5518,6 +5538,17 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
// Mobile needs special handholding. The side-view on the popup wouldn't work,
// so we just show an additional popup for this.
if (isMobile()) {
const content = document.createElement('div');
content.classList.add('tokenItemizingMaintext');
content.innerText = rawPrompt;
const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
await popup.show();
return;
}
//let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
const rawPromptWrapper = document.getElementById('rawPromptWrapper');
rawPromptWrapper.innerText = rawPrompt;
@ -5700,37 +5731,6 @@ function extractMessageFromData(data) {
}
}
/**
* Extracts the reasoning from the response data.
* @param {object} data Response data
* @returns {string} Extracted reasoning
*/
function extractReasoningFromData(data) {
switch (main_api) {
case 'textgenerationwebui':
switch (textgen_settings.type) {
case textgen_types.OPENROUTER:
return data?.choices?.[0]?.reasoning ?? '';
}
break;
case 'openai':
if (!oai_settings.show_thoughts) break;
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.DEEPSEEK:
return data?.choices?.[0]?.message?.reasoning_content ?? '';
case chat_completion_sources.OPENROUTER:
return data?.choices?.[0]?.message?.reasoning ?? '';
case chat_completion_sources.MAKERSUITE:
return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
}
break;
}
return '';
}
/**
* Extracts multiswipe swipes from the response data.
* @param {Object} data Response data
@ -5921,6 +5921,15 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra'] = {};
}
// Coerce null/undefined to empty string
if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {
chat[chat.length - 1]['extra']['reasoning'] = '';
}
if (!reasoning) {
reasoning = '';
}
let oldMessage = '';
const generationFinished = new Date();
const img = extractImageFromMessage(getMessage);
@ -5937,6 +5946,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] = reasoning;
chat[chat.length - 1]['extra']['reasoning_duration'] = null;
if (power_user.message_token_count_enabled) {
const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@ -5958,7 +5968,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] += reasoning;
chat[chat.length - 1]['extra']['reasoning'] = reasoning;
chat[chat.length - 1]['extra']['reasoning_duration'] = null;
if (power_user.message_token_count_enabled) {
const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@ -5978,6 +5989,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] += reasoning;
// We don't know if the reasoning duration extended, so we don't update it here on purpose.
if (power_user.message_token_count_enabled) {
const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
@ -5997,6 +6009,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
chat[chat.length - 1]['extra']['reasoning'] = reasoning;
chat[chat.length - 1]['extra']['reasoning_duration'] = null;
if (power_user.trim_spaces) {
getMessage = getMessage.trim();
}
@ -6137,20 +6150,21 @@ function extractImageFromMessage(getMessage) {
return { getMessage, image, title };
}
/**
* A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
*/
export function activateSendButtons() {
is_send_press = false;
$('#send_but').removeClass('displayNone');
$('#mes_continue').removeClass('displayNone');
$('#mes_impersonate').removeClass('displayNone');
$('.mes_buttons:last').show();
hideStopButton();
delete document.body.dataset.generating;
}
/**
* A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
*/
export function deactivateSendButtons() {
$('#send_but').addClass('displayNone');
$('#mes_continue').addClass('displayNone');
$('#mes_impersonate').addClass('displayNone');
showStopButton();
document.body.dataset.generating = 'true';
}
export function resetChatState() {
@ -6243,9 +6257,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats
const data = await response.json();
const newAvatar = data.avatar;
// Replace tags list
const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar });
const newName = getCharaFilename(null, { manualAvatarKey: newAvatar });
// Replace other auxillery fields where was referenced by avatar key
// Tag List
renameTagKey(oldAvatar, newAvatar);
// Addtional lore books
const charLore = world_info.charLore?.find(x => x.name == oldName);
if (charLore) {
charLore.name = newName;
saveSettingsDebounced();
}
// Char-bound Author's Notes
const charNote = extension_settings.note.chara?.find(x => x.name == oldName);
if (charNote) {
charNote.name = newName;
saveSettingsDebounced();
}
// Update active character, if the current one was the currently active one
if (active_character === oldAvatar) {
active_character = newAvatar;
saveSettingsDebounced();
}
await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
// Reload characters list
await getCharacters();
@ -6846,10 +6886,11 @@ export async function getSettings() {
$('#your_name').val(name1);
}
accountStorage.init(settings?.accountStorage);
await setUserControls(data.enable_accounts);
// Allow subscribers to mutate settings
eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
//Load KoboldAI settings
koboldai_setting_names = data.koboldai_setting_names;
@ -6946,7 +6987,7 @@ export async function getSettings() {
loadProxyPresets(settings);
// Allow subscribers to mutate settings
eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
await eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);
// Set context size after loading power user (may override the max value)
$('#max_context').val(max_context);
@ -7006,7 +7047,7 @@ export async function getSettings() {
}
await validateDisabledSamplers();
settingsReady = true;
eventSource.emit(event_types.SETTINGS_LOADED);
await eventSource.emit(event_types.SETTINGS_LOADED);
}
function selectKoboldGuiPreset() {
@ -7017,7 +7058,8 @@ function selectKoboldGuiPreset() {
export async function saveSettings(loopCounter = 0) {
if (!settingsReady) {
console.warn('Settings not ready, aborting save');
console.warn('Settings not ready, scheduling another save');
saveSettingsDebounced();
return;
}
@ -7038,6 +7080,7 @@ export async function saveSettings(loopCounter = 0) {
url: '/api/settings/save',
data: JSON.stringify({
firstRun: firstRun,
accountStorage: accountStorage.getState(),
currentVersion: currentVersion,
username: name1,
active_character: active_character,
@ -7103,8 +7146,10 @@ export function setGenerationParamsFromPreset(preset) {
// Common code for message editor done and auto-save
function updateMessage(div) {
const mesBlock = div.closest('.mes_block');
let text = mesBlock.find('.edit_textarea').val();
const mes = chat[this_edit_mes_id];
let text = mesBlock.find('.edit_textarea').val()
?? mesBlock.find('.mes_text').text();
const mesElement = div.closest('.mes');
const mes = chat[mesElement.attr('mesid')];
let regexPlacement;
if (mes.is_user) {
@ -7223,6 +7268,11 @@ async function messageEditDone(div) {
appendMediaToMessage(mes, div.closest('.mes'));
addCopyToCodeBlocks(div.closest('.mes'));
const reasoningEditDone = mesBlock.find('.mes_reasoning_edit_done:visible');
if (reasoningEditDone.length > 0) {
reasoningEditDone.trigger('click');
}
await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
this_edit_mes_id = undefined;
await saveChatConditional();
@ -7486,7 +7536,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
}
try {
const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;
const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
const page = Math.floor(charIndex / perPage) + 1;
const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
$('#rm_print_characters_pagination').pagination('go', page);
@ -7518,7 +7568,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
return;
}
const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;
const perPage = Number(accountStorage.getItem('Characters_PerPage')) || per_page_default;
const page = Math.floor(charIndex / perPage) + 1;
$('#rm_print_characters_pagination').pagination('go', page);
const selector = `#rm_print_characters_block [grid="${charId}"]`;
@ -8723,11 +8773,6 @@ const swipe_right = () => {
easing: animation_easing,
queue: false,
complete: async function () {
/*if (!selected_group) {
var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
} */
/* $("#chat").append(typingIndicator); */
const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10);
//console.log(parseInt(chat[chat.length-1]['swipe_id']));
//console.log(chat[chat.length-1]['swipes'].length);
@ -8738,7 +8783,7 @@ const swipe_right = () => {
// resets the timer
swipeMessage.find('.mes_timer').html('');
swipeMessage.find('.tokenCounterDisplay').text('');
swipeMessage.find('.mes_reasoning').html('');
updateReasoningUI(swipeMessage, { reset: true });
} else {
//console.log('showing previously generated swipe candidate, or "..."');
//console.log('onclick right swipe calling addOneMessage');
@ -8788,7 +8833,6 @@ const swipe_right = () => {
if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
console.debug('caught here 2');
is_send_press = true;
$('.mes_buttons:last').hide();
await Generate('swipe');
} else {
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 } = {})
continue;
}
accountStorage.removeItem(`AlertWI_${character.avatar}`);
accountStorage.removeItem(`AlertRegex_${character.avatar}`);
accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
delete tag_map[character.avatar];
select_rm_info('char_delete', character.name);
@ -9592,8 +9639,8 @@ function addDebugFunctions() {
});
registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
localStorage.setItem('RegenerateWithCtrlEnter', localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
toastr.info('Regenerate warning is now ' + (localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
accountStorage.setItem('RegenerateWithCtrlEnter', accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
toastr.info('Regenerate warning is now ' + (accountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
});
registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
@ -10735,6 +10782,12 @@ jQuery(async function () {
var edit_mes_id = $(this).closest('.mes').attr('mesid');
this_edit_mes_id = edit_mes_id;
// Also edit reasoning, if it exists
const reasoningEdit = $(this).closest('.mes_block').find('.mes_reasoning_edit:visible');
if (reasoningEdit.length > 0) {
reasoningEdit.trigger('click');
}
var text = chat[edit_mes_id]['mes'];
if (chat[edit_mes_id]['is_user']) {
this_edit_mes_chname = name1;
@ -10868,6 +10921,11 @@ jQuery(async function () {
appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
addCopyToCodeBlocks($(this).closest('.mes'));
const reasoningEditDone = $(this).closest('.mes_block').find('.mes_reasoning_edit_cancel:visible');
if (reasoningEditDone.length > 0) {
reasoningEditDone.trigger('click');
}
await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
this_edit_mes_id = undefined;
});
@ -10931,7 +10989,7 @@ jQuery(async function () {
});
$(document).on('click', '.mes_edit_copy', async function () {
const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM);
const confirmation = await callGenericPopup(t`Create a copy of this message?`, POPUP_TYPE.CONFIRM);
if (!confirmation) {
return;
}
@ -11410,7 +11468,7 @@ jQuery(async function () {
);
break;*/
default:
eventSource.emit('charManagementDropdown', target);
await eventSource.emit('charManagementDropdown', target);
}
$('#char-management-dropdown').prop('selectedIndex', 0);
});
@ -11572,7 +11630,7 @@ jQuery(async function () {
$(document).on('click', '.open_characters_library', async function () {
await getCharacters();
eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
await eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
});
// Added here to prevent execution before script.js is loaded and get rid of quirky timeouts

View File

@ -27,7 +27,6 @@ import {
send_on_enter_options,
} from './power-user.js';
import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
import {
@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
import { debounce_timeout } from './constants.js';
import { Popup } from './popup.js';
import { accountStorage } from './util/AccountStorage.js';
import { getCurrentUserHandle } from './user.js';
var RPanelPin = document.getElementById('rm_button_panel_pin');
var LPanelPin = document.getElementById('lm_button_panel_pin');
@ -279,17 +280,32 @@ async function RA_autoloadchat() {
// active character is the name, we should look it up in the character list and get the id
if (active_character !== null && active_character !== undefined) {
const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
if (active_character_id !== null) {
if (active_character_id !== -1) {
await selectCharacterById(String(active_character_id));
// Do a little tomfoolery to spoof the tag selector
const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
applyTagsOnCharacterSelect.call(selectedCharElement);
} else {
setActiveCharacter(null);
saveSettingsDebounced();
console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
}
}
if (active_group !== null && active_group !== undefined) {
await openGroupById(String(active_group));
if (active_character) {
console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
setActiveGroup(null);
saveSettingsDebounced();
} else {
const result = await openGroupById(String(active_group));
if (!result) {
setActiveGroup(null);
saveSettingsDebounced();
console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
}
}
}
// if the character list hadn't been loaded yet, try again.
@ -409,32 +425,34 @@ function RA_autoconnect(PrevApi) {
function OpenNavPanels() {
if (!isMobile()) {
//auto-open R nav if locked and previously open
if (LoadLocalBool('NavLockOn') == true && LoadLocalBool('NavOpened') == true) {
if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
//console.log("RA -- clicking right nav to open");
$('#rightNavDrawerIcon').click();
}
//auto-open L nav if locked and previously open
if (LoadLocalBool('LNavLockOn') == true && LoadLocalBool('LNavOpened') == true) {
if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
console.debug('RA -- clicking left nav to open');
$('#leftNavDrawerIcon').click();
}
//auto-open WI if locked and previously open
if (LoadLocalBool('WINavLockOn') == true && LoadLocalBool('WINavOpened') == true) {
if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
console.debug('RA -- clicking WI to open');
$('#WIDrawerIcon').click();
}
}
}
const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
function restoreUserInput() {
if (!power_user.restore_user_input) {
console.debug('restoreUserInput disabled');
return;
}
const userInput = LoadLocal('userInput');
const userInput = localStorage.getItem(getUserInputKey());
if (userInput) {
$('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
}
@ -442,7 +460,8 @@ function restoreUserInput() {
function saveUserInput() {
const userInput = String($('#send_textarea').val());
SaveLocal('userInput', userInput);
localStorage.setItem(getUserInputKey(), userInput);
console.debug('User Input -- ', userInput);
}
const saveUserInputDebounced = debounce(saveUserInput);
@ -739,7 +758,7 @@ export function initRossMods() {
//toggle pin class when lock toggle clicked
$(RPanelPin).on('click', function () {
SaveLocal('NavLockOn', $(RPanelPin).prop('checked'));
accountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
if ($(RPanelPin).prop('checked') == true) {
//console.log('adding pin class to right nav');
$(RightNavPanel).addClass('pinnedOpen');
@ -757,7 +776,7 @@ export function initRossMods() {
}
});
$(LPanelPin).on('click', function () {
SaveLocal('LNavLockOn', $(LPanelPin).prop('checked'));
accountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
if ($(LPanelPin).prop('checked') == true) {
//console.log('adding pin class to Left nav');
$(LeftNavPanel).addClass('pinnedOpen');
@ -776,7 +795,7 @@ export function initRossMods() {
});
$(WIPanelPin).on('click', function () {
SaveLocal('WINavLockOn', $(WIPanelPin).prop('checked'));
accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
if ($(WIPanelPin).prop('checked') == true) {
console.debug('adding pin class to WI');
$(WorldInfo).addClass('pinnedOpen');
@ -796,8 +815,8 @@ export function initRossMods() {
});
// read the state of right Nav Lock and apply to rightnav classlist
$(RPanelPin).prop('checked', LoadLocalBool('NavLockOn'));
if (LoadLocalBool('NavLockOn') == true) {
$(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true');
if (accountStorage.getItem('NavLockOn') == 'true') {
//console.log('setting pin class via local var');
$(RightNavPanel).addClass('pinnedOpen');
$(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@ -808,8 +827,8 @@ export function initRossMods() {
$(RightNavDrawerIcon).addClass('drawerPinnedOpen');
}
// read the state of left Nav Lock and apply to leftnav classlist
$(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));
if (LoadLocalBool('LNavLockOn') == true) {
$(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true');
if (accountStorage.getItem('LNavLockOn') == 'true') {
//console.log('setting pin class via local var');
$(LeftNavPanel).addClass('pinnedOpen');
$(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@ -821,8 +840,8 @@ export function initRossMods() {
}
// read the state of left Nav Lock and apply to leftnav classlist
$(WIPanelPin).prop('checked', LoadLocalBool('WINavLockOn'));
if (LoadLocalBool('WINavLockOn') == true) {
$(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true');
if (accountStorage.getItem('WINavLockOn') == 'true') {
//console.log('setting pin class via local var');
$(WorldInfo).addClass('pinnedOpen');
$(WIDrawerIcon).addClass('drawerPinnedOpen');
@ -837,22 +856,22 @@ export function initRossMods() {
//save state of Right nav being open or closed
$('#rightNavDrawerIcon').on('click', function () {
if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
SaveLocal('NavOpened', 'true');
} else { SaveLocal('NavOpened', 'false'); }
accountStorage.setItem('NavOpened', 'true');
} else { accountStorage.setItem('NavOpened', 'false'); }
});
//save state of Left nav being open or closed
$('#leftNavDrawerIcon').on('click', function () {
if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
SaveLocal('LNavOpened', 'true');
} else { SaveLocal('LNavOpened', 'false'); }
accountStorage.setItem('LNavOpened', 'true');
} else { accountStorage.setItem('LNavOpened', 'false'); }
});
//save state of Left nav being open or closed
$('#WorldInfo').on('click', function () {
if (!$('#WorldInfo').hasClass('openIcon')) {
SaveLocal('WINavOpened', 'true');
} else { SaveLocal('WINavOpened', 'false'); }
accountStorage.setItem('WINavOpened', 'true');
} else { accountStorage.setItem('WINavOpened', 'false'); }
});
var chatbarInFocus = false;
@ -868,8 +887,8 @@ export function initRossMods() {
OpenNavPanels();
}, 300);
$(SelectedCharacterTab).click(function () { SaveLocal('SelectedNavTab', 'rm_button_selected_ch'); });
$('#rm_button_characters').click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });
$(SelectedCharacterTab).click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
$('#rm_button_characters').click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
// when a char is selected from the list, save them as the auto-load character for next page load
@ -1063,14 +1082,21 @@ export function initRossMods() {
// Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
if (event.ctrlKey && event.key == 'Enter') {
const editMesDone = $('.mes_edit_done:visible');
const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
if (editMesDone.length > 0) {
console.debug('Accepting edits with Ctrl+Enter');
$('#send_textarea').focus();
$('#send_textarea').trigger('focus');
editMesDone.trigger('click');
return;
} else if (is_send_press == false) {
} else if (reasoningMesDone.length > 0) {
console.debug('Accepting edits with Ctrl+Enter');
$('#send_textarea').trigger('focus');
reasoningMesDone.trigger('click');
return;
}
else if (is_send_press == false) {
const skipConfirmKey = 'RegenerateWithCtrlEnter';
const skipConfirm = LoadLocalBool(skipConfirmKey);
const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';
function doRegenerate() {
console.debug('Regenerating with Ctrl+Enter');
$('#option_regenerate').trigger('click');
@ -1082,13 +1108,15 @@ export function initRossMods() {
let regenerateWithCtrlEnter = false;
const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
onClose: (popup) => regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false,
onClose: (popup) => {
regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
},
});
if (!result) {
return;
}
SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);
accountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
doRegenerate();
}
return;

View File

@ -69,6 +69,7 @@ const hash_derivations = {
// DeepSeek R1
'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
'DeepSeek-V2.5'
,
};
const substr_derivations = {
@ -97,6 +98,6 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
}
}
console.log(`Unknown chat template hash: ${hash} for [${chat_template}]`);
console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
return null;
}

View File

@ -45,6 +45,7 @@ import { DragAndDropHandler } from './dragdrop.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
import { humanizedDateTime } from './RossAscends-mods.js';
import { accountStorage } from './util/AccountStorage.js';
/**
* @typedef {Object} FileAttachment
@ -621,21 +622,56 @@ async function enlargeMessageImage() {
}
async function deleteMessageImage() {
const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);
const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.TEXT, '', {
okButton: t`Delete one`,
customButtons: [
{
text: t`Delete all`,
appendAtEnd: true,
result: POPUP_RESULT.CUSTOM1,
},
{
text: t`Cancel`,
appendAtEnd: true,
result: POPUP_RESULT.CANCELLED,
},
],
});
if (value !== POPUP_RESULT.AFFIRMATIVE) {
if (!value) {
return;
}
const mesBlock = $(this).closest('.mes');
const mesId = mesBlock.attr('mesid');
const message = chat[mesId];
delete message.extra.image;
delete message.extra.inline_image;
delete message.extra.title;
delete message.extra.append_title;
mesBlock.find('.mes_img_container').removeClass('img_extra');
mesBlock.find('.mes_img').attr('src', '');
let isLastImage = true;
if (Array.isArray(message.extra.image_swipes)) {
const indexOf = message.extra.image_swipes.indexOf(message.extra.image);
if (indexOf > -1) {
message.extra.image_swipes.splice(indexOf, 1);
isLastImage = message.extra.image_swipes.length === 0;
if (!isLastImage) {
const newIndex = Math.min(indexOf, message.extra.image_swipes.length - 1);
message.extra.image = message.extra.image_swipes[newIndex];
}
}
}
if (isLastImage || value === POPUP_RESULT.CUSTOM1) {
delete message.extra.image;
delete message.extra.inline_image;
delete message.extra.title;
delete message.extra.append_title;
delete message.extra.image_swipes;
mesBlock.find('.mes_img_container').removeClass('img_extra');
mesBlock.find('.mes_img').attr('src', '');
} else {
appendMediaToMessage(message, mesBlock);
}
await saveChatConditional();
}
@ -1043,8 +1079,8 @@ async function openAttachmentManager() {
renderAttachments();
});
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
let sortField = accountStorage.getItem('DataBank_sortField') || 'created';
let sortOrder = accountStorage.getItem('DataBank_sortOrder') || 'desc';
let filterString = '';
const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@ -1060,8 +1096,8 @@ async function openAttachmentManager() {
sortField = this.selectedOptions[0].dataset.sortField;
sortOrder = this.selectedOptions[0].dataset.sortOrder;
localStorage.setItem('DataBank_sortField', sortField);
localStorage.setItem('DataBank_sortOrder', sortOrder);
accountStorage.setItem('DataBank_sortField', sortField);
accountStorage.setItem('DataBank_sortOrder', sortOrder);
renderAttachments();
});
function handleBulkAction(action) {

View File

@ -9,6 +9,7 @@ import { getContext } from './st-context.js';
import { isAdmin } from './user.js';
import { t } from './i18n.js';
import { debounce_timeout } from './constants.js';
import { accountStorage } from './util/AccountStorage.js';
export {
getContext,
@ -612,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
}
let toggleElement = isActive || isDisabled ?
`<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
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>` : '';
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>` : '';
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
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>` : '';
let modulesInfo = '';
if (isActive && Array.isArray(manifest.optional)) {
@ -625,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
modules.forEach(x => optional.delete(x));
if (optional.size > 0) {
const optionalString = DOMPurify.sanitize([...optional].join(', '));
modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;
modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
}
} else if (!isDisabled) { // Neither active nor disabled
const requirements = new Set(manifest.requires);
@ -724,7 +725,7 @@ async function showExtensionsDetails() {
htmlExternal.append(htmlLoading);
const sortOrderKey = 'extensions_sortByName';
const sortByName = localStorage.getItem(sortOrderKey) === 'true';
const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
@ -755,7 +756,7 @@ async function showExtensionsDetails() {
text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
action: async () => {
abortController.abort();
localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
accountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
await showExtensionsDetails();
},
};
@ -1163,11 +1164,11 @@ async function checkForExtensionUpdates(force) {
const currentDate = new Date().toDateString();
// Don't nag more than once a day
if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
if (accountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
return;
}
localStorage.setItem(STORAGE_NAG_KEY, currentDate);
accountStorage.setItem(STORAGE_NAG_KEY, currentDate);
}
const isCurrentUserAdmin = isAdmin();

View File

@ -8,7 +8,9 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { executeSlashCommands } from '../../slash-commands.js';
import { accountStorage } from '../../util/AccountStorage.js';
import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
import { t } from '../../i18n.js';
export { MODULE_NAME };
const MODULE_NAME = 'assets';
@ -58,11 +60,11 @@ const KNOWN_TYPES = {
'blip': 'Blip sounds',
};
function downloadAssetsList(url) {
updateCurrentAssets().then(function () {
async function downloadAssetsList(url) {
updateCurrentAssets().then(async function () {
fetch(url, { cache: 'no-cache' })
.then(response => response.json())
.then(json => {
.then(async function(json) {
availableAssets = {};
$('#assets_menu').empty();
@ -83,10 +85,10 @@ function downloadAssetsList(url) {
$('#assets_type_select').empty();
$('#assets_search').val('');
$('#assets_type_select').append($('<option />', { value: '', text: 'All' }));
$('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
for (const type of assetTypes) {
const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type });
const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
$('#assets_type_select').append(option);
}
@ -103,11 +105,7 @@ function downloadAssetsList(url) {
assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
if (assetType == 'extension') {
assetTypeMenu.append(`
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
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.
</div>`);
assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
}
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) {
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
const description = DOMPurify.sanitize(asset['description'] || '');
const url = isValidUrl(asset['url']) ? asset['url'] : '';
const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const toolTag = assetType === 'extension' && asset['tool'];
@ -194,9 +192,10 @@ function downloadAssetsList(url) {
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}
</span>
</a>` +
(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
t`Tool` + '</span>' : '') +
`</span>
<small class="asset-description">
${description}
</small>
@ -432,14 +431,14 @@ jQuery(async () => {
connectButton.on('click', async function () {
const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = localStorage.getItem(rememberKey) === 'true';
const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {
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>`, {
customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
onClose: popup => {
if (popup.result) {
const rememberValue = popup.inputResults.get('assets-remember');
localStorage.setItem(rememberKey, String(rememberValue));
accountStorage.setItem(rememberKey, String(rememberValue));
}
},
});

View File

@ -0,0 +1,4 @@
<div class="assets-list-git">
<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>
<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>
</div>

View File

@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
<div id="assets_filters" class="flex-container">
<select id="assets_type_select" class="text_pole flex1">
</select>
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
<input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
<div id="assets-characters-button" class="menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Characters">Characters</span>

View File

@ -10,7 +10,7 @@
<select id="caption_source" class="text_pole">
<option value="local" data-i18n="Local">Local</option>
<option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>
<option value="extras" data-i18n="Extras">Extras</option>
<option value="extras" data-i18n="Extras">Extras (deprecated)</option>
<option value="horde" data-i18n="Horde">Horde</option>
</select>
<div id="caption_multimodal_block" class="flex-container wide100p">
@ -53,6 +53,12 @@
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>
<option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>
<option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
<option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
<option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
<option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
<option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>

View File

@ -2190,7 +2190,6 @@ function migrateSettings() {
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)',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
forceEnum: true,
}),
],
helpString: 'Returns the last set expression for the named character.',

View File

@ -23,7 +23,7 @@
<small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
<select id="expression_api" class="flex1 margin0">
<option value="0" data-i18n="Local">Local</option>
<option value="1" data-i18n="Extras">Extras</option>
<option value="1" data-i18n="Extras">Extras (deprecated)</option>
<option value="2" data-i18n="Main API">Main API</option>
<option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>
</select>

View File

@ -441,7 +441,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
description: 'character name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'group',

View File

@ -12,7 +12,7 @@
<label for="summary_source" data-i18n="ext_sum_with">Summarize with:</label>
<select id="summary_source">
<option value="main" data-i18n="ext_sum_main_api">Main API</option>
<option value="extras">Extras API</option>
<option value="extras">Extras API (deprecated)</option>
<option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
</select><br>

View File

@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { accountStorage } from '../../../util/AccountStorage.js';
import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
import { log, quickReplyApi, warn } from '../index.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@ -544,9 +545,9 @@ export class QuickReply {
this.editorSyntax = messageSyntaxInner;
/**@type {HTMLInputElement}*/
const wrap = dom.querySelector('#qr--modal-wrap');
wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false');
wrap.addEventListener('click', () => {
localStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
updateWrap();
});
const updateWrap = () => {
@ -594,27 +595,27 @@ export class QuickReply {
};
/**@type {HTMLInputElement}*/
const tabSize = dom.querySelector('#qr--modal-tabSize');
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4');
const updateTabSize = () => {
message.style.tabSize = tabSize.value;
messageSyntaxInner.style.tabSize = tabSize.value;
updateScrollDebounced();
};
tabSize.addEventListener('change', () => {
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
updateTabSize();
});
/**@type {HTMLInputElement}*/
const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
executeShortcut.checked = JSON.parse(localStorage.getItem('qr--executeShortcut') ?? 'true');
executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true');
executeShortcut.addEventListener('click', () => {
localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
});
/**@type {HTMLInputElement}*/
const syntax = dom.querySelector('#qr--modal-syntax');
syntax.checked = JSON.parse(localStorage.getItem('qr--syntax') ?? 'true');
syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true');
syntax.addEventListener('click', () => {
localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
updateSyntaxEnabled();
});
if (navigator.keyboard) {

View File

@ -1,15 +1,14 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounceAsync, log, warn } from '../index.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { debounceAsync, warn } from '../index.js';
import { QuickReply } from './QuickReply.js';
export class QuickReplySet {
/**@type {QuickReplySet[]}*/ static list = [];
static from(props) {
props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
const instance = Object.assign(new this(), props);
@ -24,9 +23,6 @@ export class QuickReplySet {
return this.list.find(it=>it.name == name);
}
/**@type {string}*/ name;
/**@type {boolean}*/ disableSend = false;
/**@type {boolean}*/ placeBeforeInput = false;
@ -34,19 +30,12 @@ export class QuickReplySet {
/**@type {string}*/ color = 'transparent';
/**@type {boolean}*/ onlyBorderColor = false;
/**@type {QuickReply[]}*/ qrList = [];
/**@type {number}*/ idIndex = 0;
/**@type {boolean}*/ isDeleted = false;
/**@type {function}*/ save;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ settingsDom;
constructor() {
this.save = debounceAsync(()=>this.performSave(), 200);
}
@ -55,9 +44,6 @@ export class QuickReplySet {
this.qrList.forEach(qr=>this.hookQuickReply(qr));
}
unrender() {
this.dom?.remove();
this.dom = null;
@ -100,9 +86,6 @@ export class QuickReplySet {
}
}
renderSettings() {
if (!this.settingsDom) {
this.settingsDom = document.createElement('div'); {
@ -123,9 +106,6 @@ export class QuickReplySet {
this.settingsDom.append(qr.renderSettings(idx));
}
/**
*
* @param {QuickReply} qr
@ -138,6 +118,7 @@ export class QuickReplySet {
closure.scope.setMacro('arg::*', '');
return (await closure.execute())?.pipe;
}
/**
*
* @param {QuickReply} qr The QR to execute.
@ -207,6 +188,7 @@ export class QuickReplySet {
document.querySelector('#send_but').click();
}
}
/**
* @param {QuickReply} qr
* @param {string} [message] - optional altered message to be used
@ -220,9 +202,6 @@ export class QuickReplySet {
});
}
addQuickReply(data = {}) {
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
data.id =
@ -239,6 +218,7 @@ export class QuickReplySet {
this.save();
return qr;
}
addQuickReplyFromText(qrJson) {
let data;
if (qrJson) {
@ -371,7 +351,6 @@ export class QuickReplySet {
this.save();
}
toJSON() {
return {
version: 2,
@ -386,7 +365,6 @@ export class QuickReplySet {
};
}
async performSave() {
const response = await fetch('/api/quick-replies/save', {
method: 'POST',

View File

@ -346,7 +346,7 @@ export class SettingsUi {
}
async addQrSet() {
const name = await Popup.show.input('Create a new World Info', 'Enter a name for the new Quick Reply Set:');
const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
if (name && name.length > 0) {
const oldQrs = QuickReplySet.get(name);
if (oldQrs) {

View File

@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
import { t } from '../../i18n.js';
import { accountStorage } from '../../util/AccountStorage.js';
/**
* @typedef {object} RegexScript
@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() {
if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
const checkKey = `AlertRegex_${characters[chid].avatar}`;
if (!localStorage.getItem(checkKey)) {
localStorage.setItem(checkKey, 'true');
if (!accountStorage.getItem(checkKey)) {
accountStorage.setItem(checkKey, 'true');
const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });

View File

@ -81,6 +81,7 @@ const sources = {
huggingface: 'huggingface',
nanogpt: 'nanogpt',
bfl: 'bfl',
falai: 'falai',
};
const initiators = {
@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
}
async function onFalaiKeyClick() {
return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
}
function onBflUpsamplingInput() {
extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
saveSettingsDebounced();
@ -1299,6 +1304,7 @@ async function onModelChange() {
sources.huggingface,
sources.nanogpt,
sources.bfl,
sources.falai,
];
if (cloudSources.includes(extension_settings.sd.source)) {
@ -1707,6 +1713,9 @@ async function loadModels() {
case sources.bfl:
models = await loadBflModels();
break;
case sources.falai:
models = await loadFalaiModels();
break;
}
for (const model of models) {
@ -1744,6 +1753,21 @@ async function loadBflModels() {
];
}
async function loadFalaiModels() {
$('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
const result = await fetch('/api/sd/falai/models', {
method: 'POST',
headers: getRequestHeaders(),
});
if (result.ok) {
return await result.json();
}
return [];
}
async function loadPollinationsModels() {
const result = await fetch('/api/sd/pollinations/models', {
method: 'POST',
@ -2081,6 +2105,9 @@ async function loadSchedulers() {
case sources.bfl:
schedulers = ['N/A'];
break;
case sources.falai:
schedulers = ['N/A'];
break;
}
for (const scheduler of schedulers) {
@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
case sources.bfl:
result = await generateBflImage(prefixedPrompt, signal);
break;
case sources.falai:
result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
break;
}
if (!result.data) {
@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
}
}
/**
* Generates an image using the FAL.AI API.
* @param {string} prompt - The main instruction used to guide the image generation.
* @param {string} negativePrompt - The negative prompt used to guide the image generation.
* @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
*/
async function generateFalaiImage(prompt, negativePrompt, signal) {
const result = await fetch('/api/sd/falai/generate', {
method: 'POST',
headers: getRequestHeaders(),
signal: signal,
body: JSON.stringify({
prompt: prompt,
negative_prompt: negativePrompt,
model: extension_settings.sd.model,
steps: clamp(extension_settings.sd.steps, 1, 50),
guidance: clamp(extension_settings.sd.scale, 1.5, 5),
width: clamp(extension_settings.sd.width, 256, 1440),
height: clamp(extension_settings.sd.height, 256, 1440),
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
}),
});
if (result.ok) {
const data = await result.json();
return { format: 'jpg', data: data.image };
} else {
const text = await result.text();
console.log(text);
throw new Error(text);
}
}
async function onComfyOpenWorkflowEditorClick() {
let workflow = await (await fetch('/api/sd/comfy/workflow', {
method: 'POST',
@ -3782,6 +3846,8 @@ function isValidState() {
return secret_state[SECRET_KEYS.NANOGPT];
case sources.bfl:
return secret_state[SECRET_KEYS.BFL];
case sources.falai:
return secret_state[SECRET_KEYS.FALAI];
}
}
@ -4443,6 +4509,7 @@ jQuery(async () => {
$('#sd_function_tool').on('input', onFunctionToolInput);
$('#sd_bfl_key').on('click', onBflKeyClick);
$('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
$('#sd_falai_key').on('click', onFalaiKeyClick);
if (!CSS.supports('field-sizing', 'content')) {
$('.sd_settings .inline-drawer-toggle').on('click', function () {

View File

@ -41,7 +41,8 @@
<option value="blockentropy">Block Entropy</option>
<option value="comfy">ComfyUI</option>
<option value="drawthings">DrawThings HTTP API</option>
<option value="extras">Extras API (local / remote)</option>
<option value="extras">Extras API (deprecated)</option>
<option value="falai">FAL.AI</option>
<option value="huggingface">HuggingFace Inference API (serverless)</option>
<option value="nanogpt">NanoGPT</option>
<option value="novel">NovelAI Diffusion</option>
@ -256,6 +257,20 @@
</label>
</div>
<div data-sd-source="falai">
<div class="flex-container flexnowrap alignItemsBaseline marginBot5">
<a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
<strong data-i18n="API Key">API Key</strong>
<i class="fa-solid fa-share-from-square"></i>
</a>
<span class="expander"></span>
<div id="sd_falai_key" class="menu_button menu_button_icon">
<i class="fa-fw fa-solid fa-key"></i>
<span data-i18n="Click to set">Click to set</span>
</div>
</div>
</div>
<div class="flex-container">
<div class="flex1">
<label for="sd_model" data-i18n="Model">Model</label>

View File

@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
import { resetScrollHeight, debounce } from '../../utils.js';
import { debounce_timeout } from '../../constants.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { renderExtensionTemplateAsync } from '../../extensions.js';
import { t } from '../../i18n.js';
function rgb2hex(rgb) {
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@ -22,23 +24,7 @@ $('button').click(function () {
async function doTokenCounter() {
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
const html = `
<div class="wide100p">
<h3>Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4>Type / paste in the box below to see the number of tokens in the text.</h4>
<p>Selected tokenizer: ${tokenizerName}</p>
<div>Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div>Tokens: <span id="token_counter_result">0</span></div>
<hr>
<div>Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div>Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>`;
const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
const dialog = $(html);
const countDebounced = debounce(async () => {
@ -131,9 +117,9 @@ async function doCount() {
jQuery(() => {
const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>
Token Counter
</div>`;
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
t`Token Counter` +
'</div>';
$('#token_counter_wand_container').append(buttonHtml);
$('#token_counter').on('click', doTokenCounter);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({

View File

@ -0,0 +1,16 @@
<div class="wide100p">
<h3 data-i18n="Token Counter">Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<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>
<p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
<div data-i18n="Input:">Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
<hr>
<div data-i18n="Tokenized text:">Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div data-i18n="Token IDs:">Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>

View File

@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
<label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
<div class="flex-container alignItemsCenter">
<div class="flex1">
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
</div>
<div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
<i class="fa-solid fa-key"></i>
@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
</div>
</div>
<label for="openai_compatible_model">Model:</label>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
<label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
<label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
<input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
return html;

View File

@ -745,6 +745,44 @@ async function getQueryText(chat, initiator) {
return collapseNewlines(queryText).trim();
}
/**
* Gets common body parameters for vector requests.
* @returns {object}
*/
function getVectorsRequestBody() {
const body = {};
switch (settings.source) {
case 'extras':
body.extrasUrl = extension_settings.apiUrl;
body.extrasKey = extension_settings.apiKey;
break;
case 'togetherai':
body.model = extension_settings.vectors.togetherai_model;
break;
case 'openai':
body.model = extension_settings.vectors.openai_model;
break;
case 'cohere':
body.model = extension_settings.vectors.cohere_model;
break;
case 'ollama':
body.model = extension_settings.vectors.ollama_model;
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
body.keep = !!extension_settings.vectors.ollama_keep;
break;
case 'llamacpp':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
break;
case 'vllm':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
body.model = extension_settings.vectors.vllm_model;
break;
default:
break;
}
return body;
}
/**
* Gets the saved hashes for a collection
* @param {string} collectionId
@ -753,8 +791,9 @@ async function getQueryText(chat, initiator) {
async function getSavedHashes(collectionId) {
const response = await fetch('/api/vector/list', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
source: settings.source,
}),
@ -768,54 +807,6 @@ async function getSavedHashes(collectionId) {
return hashes;
}
function getVectorHeaders() {
const headers = getRequestHeaders();
switch (settings.source) {
case 'extras':
Object.assign(headers, {
'X-Extras-Url': extension_settings.apiUrl,
'X-Extras-Key': extension_settings.apiKey,
});
break;
case 'togetherai':
Object.assign(headers, {
'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
});
break;
case 'openai':
Object.assign(headers, {
'X-OpenAI-Model': extension_settings.vectors.openai_model,
});
break;
case 'cohere':
Object.assign(headers, {
'X-Cohere-Model': extension_settings.vectors.cohere_model,
});
break;
case 'ollama':
Object.assign(headers, {
'X-Ollama-Model': extension_settings.vectors.ollama_model,
'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
});
break;
case 'llamacpp':
Object.assign(headers, {
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
});
break;
case 'vllm':
Object.assign(headers, {
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
'X-Vllm-Model': extension_settings.vectors.vllm_model,
});
break;
default:
break;
}
return headers;
}
/**
* Inserts vector items into a collection
* @param {string} collectionId - The collection to insert into
@ -825,12 +816,11 @@ function getVectorHeaders() {
async function insertVectorItems(collectionId, items) {
throwIfSourceInvalid();
const headers = getVectorHeaders();
const response = await fetch('/api/vector/insert', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
items: items,
source: settings.source,
@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
async function deleteVectorItems(collectionId, hashes) {
const response = await fetch('/api/vector/delete', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
hashes: hashes,
source: settings.source,
@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
*/
async function queryCollection(collectionId, searchText, topK) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
searchText: searchText,
topK: topK,
@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
*/
async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query-multi', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionIds: collectionIds,
searchText: searchText,
topK: topK,
@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
const response = await fetch('/api/vector/purge', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
}),
});
@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
const response = await fetch('/api/vector/purge', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
}),
});
@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
try {
const response = await fetch('/api/vector/purge-all', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
}),
});
if (!response.ok) {
@ -1621,14 +1615,14 @@ jQuery(async () => {
const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
const collectionIds = await ingestDataBankAttachments(String(source));
const queryResults = await queryMultipleCollections(collectionIds, String(query), count, threshold);
// Get URLs
const urls = Object
.keys(queryResults)
.map(x => attachments.find(y => getFileCollectionId(y.url) === x))
.filter(x => x)
.map(x => x.url);
// Gets the actual text content of chunks
const getChunksText = () => {
let textResult = '';
@ -1638,14 +1632,12 @@ jQuery(async () => {
}
return textResult;
};
if (args.return === 'chunks') {
return getChunksText();
}
// @ts-ignore
return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
},
aliases: ['databank-search', 'data-bank-search'],
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 () => {
defaultValue: 'object',
enumList: [
new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
...slashCommandReturnHelper.enumList({ allowObject: true })
...slashCommandReturnHelper.enumList({ allowObject: true }),
],
forceEnum: true,
})
}),
],
unnamedArgumentList: [
new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),

View File

@ -11,7 +11,7 @@
</label>
<select id="vectors_source" class="text_pole">
<option value="cohere">Cohere</option>
<option value="extras">Extras</option>
<option value="extras">Extras (deprecated)</option>
<option value="palm">Google AI Studio</option>
<option value="llamacpp">llama.cpp</option>
<option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>

View File

@ -1,18 +1,30 @@
////////////////// LOCAL STORAGE HANDLING /////////////////////
/**
* @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
*/
export function SaveLocal(target, val) {
localStorage.setItem(target, val);
console.debug('SaveLocal -- ' + target + ' : ' + val);
}
/**
* @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
*/
export function LoadLocal(target) {
console.debug('LoadLocal -- ' + target);
return localStorage.getItem(target);
}
/**
* @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
*/
export function LoadLocalBool(target) {
let result = localStorage.getItem(target) === 'true';
return result;
}
/**
* @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
*/
export function CheckLocal() {
console.log('----------local storage---------');
var i;
@ -22,6 +34,9 @@ export function CheckLocal() {
console.log('------------------------------');
}
/**
* @deprecated THIS FUNCTION IS OBSOLETE. DO NOT USE
*/
export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }
/////////////////////////////////////////////////////////////////////////

View File

@ -78,6 +78,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
import { isExternalMediaAllowed } from './chats.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
import { accountStorage } from './util/AccountStorage.js';
export {
selected_group,
@ -292,10 +293,11 @@ export function getGroupNames() {
/**
* Finds the character ID for a group member.
* @param {string} arg 0-based member index or character name
* @returns {number} 0-based character ID
* @param {number|string} arg 0-based member index or character name
* @param {Boolean} full Whether to return a key-value object containing extra data
* @returns {number|Object} 0-based character ID or key-value object if full is true
*/
export function findGroupMemberId(arg) {
export function findGroupMemberId(arg, full = false) {
arg = arg?.trim();
if (!arg) {
@ -311,15 +313,19 @@ export function findGroupMemberId(arg) {
}
const index = parseInt(arg);
const searchByName = isNaN(index);
const searchByString = isNaN(index);
if (searchByName) {
const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) }));
const fuse = new Fuse(memberNames, { keys: ['name'] });
if (searchByString) {
const memberNames = group.members.map(x => ({
avatar: x,
name: characters.find(y => y.avatar === x)?.name,
index: characters.findIndex(y => y.avatar === x),
}));
const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] });
const result = fuse.search(arg);
if (!result.length) {
console.warn(`WARN: No group member found with name ${arg}`);
console.warn(`WARN: No group member found using string ${arg}`);
return;
}
@ -330,9 +336,11 @@ export function findGroupMemberId(arg) {
return;
}
console.log(`Triggering group member ${chid} (${arg}) from search result`, result[0]);
return chid;
} else {
console.log(`Targeting group member ${chid} (${arg}) from search result`, result[0]);
return !full ? chid : { ...{ id: chid }, ...result[0].item };
}
else {
const memberAvatar = group.members[index];
if (memberAvatar === undefined) {
@ -347,8 +355,14 @@ export function findGroupMemberId(arg) {
return;
}
console.log(`Triggering group member ${memberAvatar} at index ${index}`);
return chid;
console.log(`Targeting group member ${memberAvatar} at index ${index}`);
return !full ? chid : {
id: chid,
avatar: memberAvatar,
name: characters.find(y => y.avatar === memberAvatar)?.name,
index: index,
};
}
}
@ -805,7 +819,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
/** @type {any} Caution: JS war crimes ahead */
let textResult = '';
let typingIndicator = $('#chat .typing_indicator');
const group = groups.find((x) => x.id === selected_group);
if (!group || !Array.isArray(group.members) || !group.members.length) {
@ -821,14 +834,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
setCharacterId(undefined);
const userInput = String($('#send_textarea').val());
if (typingIndicator.length === 0 && !isStreamingEnabled()) {
typingIndicator = $(
'#typing_indicator_template .typing_indicator',
).clone();
typingIndicator.hide();
$('#chat').append(typingIndicator);
}
// id of this specific batch for regeneration purposes
group_generation_id = Date.now();
const lastMessage = chat[chat.length - 1];
@ -906,14 +911,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
}
await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId);
if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
// update indicator and scroll down
typingIndicator
.find('.typing_indicator_name')
.text(characters[chId].name);
typingIndicator.show();
}
// Wait for generation to finish
textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
let messageChunk = textResult?.messageChunk;
@ -930,8 +927,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
}
}
} finally {
typingIndicator.hide();
is_group_generating = false;
setSendButtonState(false);
setCharacterId(undefined);
@ -1315,10 +1310,10 @@ function printGroupCandidates() {
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
showSizeChanger: true,
pageSize: Number(localStorage.getItem(storageKey)) || 5,
pageSize: Number(accountStorage.getItem(storageKey)) || 5,
sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
accountStorage.setItem(storageKey, e.target.value);
},
callback: function (data) {
$('#rm_group_add_members').empty();
@ -1342,10 +1337,10 @@ function printGroupMembers() {
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
showSizeChanger: true,
pageSize: Number(localStorage.getItem(storageKey)) || 5,
pageSize: Number(accountStorage.getItem(storageKey)) || 5,
sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
accountStorage.setItem(storageKey, e.target.value);
},
callback: function (data) {
$('.rm_group_members').empty();
@ -1669,12 +1664,12 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) {
if (isChatSaving) {
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
return false;
}
if (!groups.find(x => x.id === groupId)) {
console.log('Group not found', groupId);
return;
return false;
}
if (!is_send_press && !is_group_generating) {
@ -1691,8 +1686,11 @@ export async function openGroupById(groupId) {
updateChatMetadata({}, true);
chat.length = 0;
await getGroupChat(groupId);
return true;
}
}
return false;
}
function openCharacterDefinition(characterSelect) {

View File

@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js
import { Popup, POPUP_RESULT } from './popup.js';
import { t } from './i18n.js';
import { ToolManager } from './tool-calling.js';
import { accountStorage } from './util/AccountStorage.js';
export {
openai_messages_count,
@ -82,7 +83,6 @@ export {
setOpenAIMessageExamples,
setupChatCompletionPromptManager,
sendOpenAIRequest,
getChatCompletionModel,
TokenHandler,
IdentifierNotFoundError,
Message,
@ -259,7 +259,7 @@ const default_settings = {
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
perplexity_model: 'sonar-pro',
groq_model: 'llama-3.1-70b-versatile',
groq_model: 'llama-3.3-70b-versatile',
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
blockentropy_model: 'be-70b-base-llama3.1',
@ -299,6 +299,7 @@ const default_settings = {
continue_postfix: continue_postfix_types.SPACE,
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
show_thoughts: true,
reasoning_effort: 'medium',
seed: -1,
n: 1,
};
@ -378,6 +379,7 @@ const oai_settings = {
continue_postfix: continue_postfix_types.SPACE,
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
show_thoughts: true,
reasoning_effort: 'medium',
seed: -1,
n: 1,
};
@ -412,7 +414,7 @@ async function validateReverseProxy() {
throw err;
}
const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
const skipConfirm = localStorage.getItem(rememberKey) === 'true';
const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
@ -423,7 +425,7 @@ async function validateReverseProxy() {
throw new Error('Proxy connection denied.');
}
localStorage.setItem(rememberKey, String(true));
accountStorage.setItem(rememberKey, String(true));
}
/**
@ -1443,9 +1445,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
}
const onStreamResult = (res, err) => {
if (err) {
return;
}
if (err) return;
const thisContent = res?.message?.content;
@ -1497,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
}
}
function getChatCompletionModel() {
export function getChatCompletionModel() {
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.CLAUDE:
return oai_settings.claude_model;
@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3')));
const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
const useLogprobs = !!power_user.request_token_probabilities;
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
@ -1914,8 +1914,13 @@ async function sendOpenAIRequest(type, messages, signal) {
'char_name': name2,
'group_names': getGroupNames(),
'include_reasoning': Boolean(oai_settings.show_thoughts),
'reasoning_effort': String(oai_settings.reasoning_effort),
};
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
// Empty array will produce a validation error
if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
delete generate_data.stop;
@ -2039,6 +2044,8 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data.top_logprobs;
delete generate_data.logprobs;
delete generate_data.logit_bias;
delete generate_data.tools;
delete generate_data.tool_choice;
}
}
@ -2046,10 +2053,6 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['seed'] = oai_settings.seed;
}
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
generate_data.messages.forEach((msg) => {
if (msg.role === 'system') {
@ -2058,7 +2061,6 @@ async function sendOpenAIRequest(type, messages, signal) {
});
generate_data.max_completion_tokens = generate_data.max_tokens;
delete generate_data.max_tokens;
delete generate_data.stream;
delete generate_data.logprobs;
delete generate_data.top_logprobs;
delete generate_data.n;
@ -2069,8 +2071,7 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data.tools;
delete generate_data.tool_choice;
delete generate_data.stop;
// It does support logit_bias, but the tokenizer used and its effect is yet unknown.
// delete generate_data.logit_bias;
delete generate_data.logit_bias;
}
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@ -3124,6 +3125,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
oai_settings.seed = settings.seed ?? default_settings.seed;
oai_settings.n = settings.n ?? default_settings.n;
@ -3253,6 +3255,9 @@ function loadOpenAISettings(data, settings) {
$('#n_openai').val(oai_settings.n);
$('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
$('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
$(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
$('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
@ -3513,6 +3518,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
continue_postfix: settings.continue_postfix,
function_calling: settings.function_calling,
show_thoughts: settings.show_thoughts,
reasoning_effort: settings.reasoning_effort,
seed: settings.seed,
n: settings.n,
};
@ -3971,6 +3977,7 @@ function onSettingsPresetChange() {
continue_postfix: ['#continue_postfix', 'continue_postfix', false],
function_calling: ['#openai_function_calling', 'function_calling', true],
show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
seed: ['#seed_openai', 'seed', false],
n: ['#n_openai', 'n', false],
};
@ -4232,9 +4239,9 @@ async function onModelChange() {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
$('#openai_max_context').attr('max', max_32k);
} else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
} else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash')) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
$('#openai_max_context').attr('max', max_32k);
@ -4403,24 +4410,30 @@ async function onModelChange() {
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
} else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
$('#openai_max_context').attr('max', max_8k);
}
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')) {
} else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
$('#openai_max_context').attr('max', max_128k);
}
else if (oai_settings.groq_model.includes('llama3-groq')) {
} else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
$('#openai_max_context').attr('max', max_8k);
}
else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
} else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
$('#openai_max_context').attr('max', max_8k);
}
else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
} else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
$('#openai_max_context').attr('max', max_32k);
}
else {
$('#openai_max_context').attr('max', max_4k);
} else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
$('#openai_max_context').attr('max', max_128k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@ -4921,6 +4934,12 @@ export function isImageInliningSupported() {
// gultra just isn't being offered as multimodal, thanks google.
const visionSupportedModels = [
'gpt-4-vision',
'gemini-2.0-pro-exp',
'gemini-2.0-pro-exp-02-05',
'gemini-2.0-flash-lite-preview',
'gemini-2.0-flash-lite-preview-02-05',
'gemini-2.0-flash',
'gemini-2.0-flash-001',
'gemini-2.0-flash-thinking-exp-1219',
'gemini-2.0-flash-thinking-exp-01-21',
'gemini-2.0-flash-thinking-exp',
@ -4948,6 +4967,8 @@ export function isImageInliningSupported() {
'gpt-4-turbo',
'gpt-4o',
'gpt-4o-mini',
'o1',
'o1-2024-12-17',
'chatgpt-4o-latest',
'yi-vision',
'pixtral-latest',
@ -5506,6 +5527,11 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#openai_reasoning_effort').on('input', function () {
oai_settings.reasoning_effort = String($(this).val());
saveSettingsDebounced();
});
if (!CSS.supports('field-sizing', 'content')) {
$(document).on('input', '#openai_settings .autoSetHeight', function () {
resetScrollHeight($(this));

View File

@ -25,6 +25,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
import { openWorldInfoEditor, world_names } from './world-info.js';
import { renderTemplateAsync } from './templates.js';
import { accountStorage } from './util/AccountStorage.js';
let savePersonasPage = 0;
const GRID_STORAGE_KEY = 'Personas_GridView';
@ -34,7 +35,7 @@ export let user_avatar = '';
export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
function switchPersonaGridView() {
const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';
const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
$('#user_avatar_block').toggleClass('gridView', state);
}
@ -182,7 +183,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
const storageKey = 'Personas_PerPage';
const listId = '#user_avatar_block';
const perPage = Number(localStorage.getItem(storageKey)) || 5;
const perPage = Number(accountStorage.getItem(storageKey)) || 5;
$('#persona_pagination_container').pagination({
dataSource: entities,
@ -205,7 +206,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
highlightSelectedAvatar();
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
accountStorage.setItem(storageKey, e.target.value);
},
afterPaging: function (e) {
savePersonasPage = e;
@ -1132,8 +1133,8 @@ export function initPersonas() {
saveSettingsDebounced();
});
$('#persona_grid_toggle').on('click', () => {
const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';
localStorage.setItem(GRID_STORAGE_KEY, String(!state));
const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
accountStorage.setItem(GRID_STORAGE_KEY, String(!state));
switchPersonaGridView();
});

View File

@ -24,6 +24,15 @@ export const POPUP_RESULT = {
AFFIRMATIVE: 1,
NEGATIVE: 0,
CANCELLED: null,
CUSTOM1: 1001,
CUSTOM2: 1002,
CUSTOM3: 1003,
CUSTOM4: 1004,
CUSTOM5: 1005,
CUSTOM6: 1006,
CUSTOM7: 1007,
CUSTOM8: 1008,
CUSTOM9: 1009,
};
/**
@ -37,6 +46,7 @@ export const POPUP_RESULT = {
* @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
* @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
* @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
* @property {boolean?} [leftAlign=false] - Whether the popup content should be left-aligned by default
* @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
* @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`.
* @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 {
* @param {string} [inputValue=''] - The initial value of the input field
* @param {PopupOptions} [options={}] - Additional options for the popup
*/
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
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 } = {}) {
Popup.util.popups.push(this);
// Make this popup uniquely identifiable
@ -209,6 +219,7 @@ export class Popup {
if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup');
if (animation) this.dlg.classList.add('popup--animation-' + animation);
// If custom button captions are provided, we set them beforehand

View File

@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { loadSystemPrompts } from './sysprompt.js';
import { fuzzySearchCategories } from './filters.js';
import { accountStorage } from './util/AccountStorage.js';
export {
loadPowerUserSettings,
@ -256,6 +257,8 @@ let power_user = {
reasoning: {
auto_parse: false,
add_to_prompts: false,
auto_expand: false,
show_hidden: false,
prefix: '<think>\n',
suffix: '\n</think>',
separator: '\n\n',
@ -2019,7 +2022,7 @@ export function renderStoryString(params) {
*/
function validateStoryString(storyString, params) {
/** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
const cache = JSON.parse(accountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
const hash = getStringHash(storyString);
@ -2056,7 +2059,7 @@ function validateStoryString(storyString, params) {
toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
}
localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
accountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
}
@ -2451,7 +2454,7 @@ async function resetMovablePanels(type) {
}
saveSettingsDebounced();
eventSource.emit(event_types.MOVABLE_PANELS_RESET);
await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
eventSource.once(event_types.SETTINGS_UPDATED, () => {
$('.resizing').removeClass('resizing');

View File

@ -587,6 +587,8 @@ class PresetManager {
'derived',
'generic_model',
'include_reasoning',
'global_banned_tokens',
'send_banned_tokens',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));

View File

@ -1,14 +1,32 @@
import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import {
moment,
} from '../lib.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { t } from './i18n.js';
import { getCurrentLocale, t, translate } from './i18n.js';
import { MacrosParser } from './macros.js';
import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
import { Popup } from './popup.js';
import { power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
/**
* Enum representing the type of the reasoning for a message (where it came from)
* @enum {string}
* @readonly
*/
export const ReasoningType = {
Model: 'model',
Parsed: 'parsed',
Manual: 'manual',
Edited: 'edited',
};
/**
* Gets a message from a jQuery element.
@ -22,13 +40,468 @@ function getMessageFromJquery(element) {
return { messageId: messageId, message, messageBlock };
}
/**
* Toggles the auto-expand state of reasoning blocks.
*/
function toggleReasoningAutoExpand() {
const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details');
reasoningBlocks.forEach((block) => {
if (block instanceof HTMLDetailsElement) {
block.open = power_user.reasoning.auto_expand;
}
});
}
/**
* Extracts the reasoning from the response data.
* @param {object} data Response data
* @returns {string} Extracted reasoning
*/
export function extractReasoningFromData(data) {
switch (main_api) {
case 'textgenerationwebui':
switch (textgenerationwebui_settings.type) {
case textgen_types.OPENROUTER:
return data?.choices?.[0]?.reasoning ?? '';
}
break;
case 'openai':
if (!oai_settings.show_thoughts) break;
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.DEEPSEEK:
return data?.choices?.[0]?.message?.reasoning_content ?? '';
case chat_completion_sources.OPENROUTER:
return data?.choices?.[0]?.message?.reasoning ?? '';
case chat_completion_sources.MAKERSUITE:
return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
}
break;
}
return '';
}
/**
* Check if the model supports reasoning, but does not send back the reasoning
* @returns {boolean} True if the model supports reasoning
*/
export function isHiddenReasoningModel() {
if (main_api !== 'openai') {
return false;
}
/** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
/** @type {Record.<string, MatchingFunc>} */
const FUNCS = {
equals: (currentModel, supportedModel) => currentModel === supportedModel,
startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
};
/** @type {{ name: string; func: MatchingFunc; }[]} */
const hiddenReasoningModels = [
{ name: 'o1', func: FUNCS.startsWith },
{ name: 'o3', func: FUNCS.startsWith },
{ name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
{ name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
];
const model = getChatCompletionModel() || '';
const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
return isHidden;
}
/**
* Updates the Reasoning UI for a specific message
* @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
* @param {Object} [options={}] - Optional arguments
* @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
*/
export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
const handler = new ReasoningHandler();
handler.initHandleMessage(messageIdOrElement, { reset });
}
/**
* Enum for representing the state of reasoning
* @enum {string}
* @readonly
*/
export const ReasoningState = {
None: 'none',
Thinking: 'thinking',
Done: 'done',
Hidden: 'hidden',
};
/**
* Handles reasoning-specific logic and DOM updates for messages.
* This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
*/
export class ReasoningHandler {
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
#isHiddenReasoningModel;
/** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
#isParsingReasoning = false;
/** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
#parsingReasoningMesStartIndex = null;
/**
* @param {Date?} [timeStarted=null] - When the generation started
*/
constructor(timeStarted = null) {
/** @type {ReasoningState} The current state of the reasoning process */
this.state = ReasoningState.None;
/** @type {ReasoningType?} The type of the reasoning (where it came from) */
this.type = null;
/** @type {string} The reasoning output */
this.reasoning = '';
/** @type {Date} When the reasoning started */
this.startTime = null;
/** @type {Date} When the reasoning ended */
this.endTime = null;
/** @type {Date} Initial starting time of the generation */
this.initialTime = timeStarted ?? new Date();
this.#isHiddenReasoningModel = isHiddenReasoningModel();
// Cached DOM elements for reasoning
/** @type {HTMLElement} Main message DOM element `.mes` */
this.messageDom = null;
/** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */
this.messageReasoningDetailsDom = null;
/** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
this.messageReasoningContentDom = null;
/** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
this.messageReasoningHeaderDom = null;
}
/**
* Initializes the reasoning handler for a specific message.
*
* Can be used to update the DOM elements or read other reasoning states.
* It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
* The state will always be either done/hidden or none.
*
* @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
* @param {Object} [options={}] - Optional arguments
* @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
*/
initHandleMessage(messageIdOrElement, { reset = false } = {}) {
/** @type {HTMLElement} */
const messageElement = typeof messageIdOrElement === 'number'
? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
: messageIdOrElement instanceof HTMLElement
? messageIdOrElement
: $(messageIdOrElement)[0];
const messageId = Number(messageElement.getAttribute('mesid'));
if (isNaN(messageId) || !chat[messageId]) return;
if (!chat[messageId].extra) {
chat[messageId].extra = {};
}
const extra = chat[messageId].extra;
if (extra.reasoning) {
this.state = ReasoningState.Done;
} else if (extra.reasoning_duration) {
this.state = ReasoningState.Hidden;
}
this.type = extra?.reasoning_type;
this.reasoning = extra?.reasoning ?? '';
if (this.state !== ReasoningState.None) {
this.initialTime = new Date(chat[messageId].gen_started);
this.startTime = this.initialTime;
this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
}
// Prefill main dom element, as message might not have been rendered yet
this.messageDom = messageElement;
// Make sure reset correctly clears all relevant states
if (reset) {
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
this.type = null;
this.reasoning = '';
this.initialTime = new Date();
this.startTime = null;
this.endTime = null;
}
this.updateDom(messageId);
if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
this.messageReasoningDetailsDom.open = true;
}
}
/**
* Gets the duration of the reasoning in milliseconds.
*
* @returns {number?} The duration in milliseconds, or null if the start or end time is not set
*/
getDuration() {
if (this.startTime && this.endTime) {
return this.endTime.getTime() - this.startTime.getTime();
}
return null;
}
/**
* Updates the reasoning text/string for a message.
*
* @param {number} messageId - The ID of the message to update
* @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
* @param {Object} [options={}] - Optional arguments
* @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
* @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
* @returns {boolean} - Returns true if the reasoning was changed, otherwise false
*/
updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
if (messageId == -1 || !chat[messageId]) {
return false;
}
reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
reasoning = trimSpaces(reasoning);
// Ensure the chat extra exists
if (!chat[messageId].extra) {
chat[messageId].extra = {};
}
const extra = chat[messageId].extra;
const reasoningChanged = extra.reasoning !== reasoning;
this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
if (persist) {
// Build and save the reasoning data to message extras
extra.reasoning = this.reasoning;
extra.reasoning_duration = this.getDuration();
extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
}
return reasoningChanged;
}
/**
* Handles processing of reasoning for a message.
*
* This is usually called by the message processor when a message is changed.
*
* @param {number} messageId - The ID of the message to process
* @param {boolean} mesChanged - Whether the message has changed
* @returns {Promise<void>}
*/
async process(messageId, mesChanged) {
mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
if (!this.reasoning && !this.#isHiddenReasoningModel)
return;
// Ensure reasoning string is updated and regexes are applied correctly
const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
this.state = ReasoningState.Thinking;
this.startTime = this.initialTime;
}
if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
this.endTime = new Date();
await this.finish(messageId);
}
}
#autoParseReasoningFromMessage(messageId, mesChanged) {
if (!power_user.reasoning.auto_parse)
return;
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
return mesChanged;
/** @type {{ mes: string, [key: string]: any}} */
const message = chat[messageId];
if (!message) return mesChanged;
// If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
if (this.#parsingReasoningMesStartIndex) {
message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
return mesChanged;
}
if (this.state === ReasoningState.None) {
// If streamed message starts with the opening, cut it out and put all inside reasoning
if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
this.#isParsingReasoning = true;
// Manually set starting state here, as we might already have received the ending suffix
this.state = ReasoningState.Thinking;
this.startTime = this.initialTime;
}
}
if (!this.#isParsingReasoning)
return mesChanged;
// If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
const originalMes = message.mes;
this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
message.mes = '';
// If the reasoning contains the ending suffix, we cut that off and continue as message streaming
if (this.reasoning.includes(power_user.reasoning.suffix)) {
this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
this.#isParsingReasoning = false;
}
// Only return the original mesChanged value if we haven't cut off the complete message
return message.mes.length ? mesChanged : false;
}
/**
* Completes the reasoning process for a message.
*
* Records the finish time if it was not set during streaming and updates the reasoning state.
* Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
*
* @param {number} messageId - The ID of the message to complete reasoning for
* @returns {Promise<void>}
*/
async finish(messageId) {
if (this.state === ReasoningState.None) return;
// Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
if (this.startTime !== null && this.endTime === null) {
this.endTime = new Date();
}
if (this.state === ReasoningState.Thinking) {
this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
this.updateReasoning(messageId, null, { persist: true });
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
}
this.updateDom(messageId);
}
/**
* Updates the reasoning UI elements for a message.
*
* Toggles the CSS class, updates states, reasoning message, and duration.
*
* @param {number} messageId - The ID of the message to update
*/
updateDom(messageId) {
this.#checkDomElements(messageId);
// Main CSS class to show this message includes reasoning
this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
// Update states to the relevant DOM elements
setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
// Update the reasoning message
const reasoning = trimSpaces(this.reasoning);
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
this.messageReasoningContentDom.innerHTML = displayReasoning;
// Update tooltip for hidden reasoning edit
/** @type {HTMLElement} */
const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
// Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button
if (this.state === ReasoningState.Hidden) {
this.messageReasoningDetailsDom.open = false;
}
// Update the reasoning duration in the UI
this.#updateReasoningTimeUI();
}
/**
* Finds and caches reasoning-related DOM elements for the given message.
*
* @param {number} messageId - The ID of the message to cache the DOM elements for
*/
#checkDomElements(messageId) {
// Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
this.messageDom = null;
}
// Cache the DOM elements once
if (this.messageDom === null) {
this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
if (this.messageDom === null) throw new Error('message dom does not exist');
}
if (this.messageReasoningDetailsDom === null) {
this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
}
if (this.messageReasoningContentDom === null) {
this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
}
if (this.messageReasoningHeaderDom === null) {
this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
}
}
/**
* Updates the reasoning time display in the UI.
*
* Shows the duration in a human-readable format with a tooltip for exact seconds.
* Displays "Thinking..." if still processing, or a generic message otherwise.
*/
#updateReasoningTimeUI() {
const element = this.messageReasoningHeaderDom;
const duration = this.getDuration();
let data = null;
let title = '';
if (duration) {
const seconds = moment.duration(duration).asSeconds();
const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
element.textContent = t`Thought for ${durationStr}`;
data = String(seconds);
title = `${seconds} seconds`;
} else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
element.textContent = t`Thought for some time`;
data = 'unknown';
} else {
element.textContent = t`Thinking...`;
data = null;
}
if (this.type !== ReasoningType.Model) {
title += ` [${translate(this.type)}]`;
title = title.trim();
}
element.title = title;
setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
setDatasetProperty(element, 'duration', data);
}
}
/**
* Helper class for adding reasoning to messages.
* Keeps track of the number of reasoning additions.
*/
export class PromptReasoning {
static REASONING_PLACEHOLDER = '\u200B';
static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
constructor() {
this.counter = 0;
@ -59,7 +532,7 @@ export class PromptReasoning {
return content;
}
// No reasoning provided or a placeholder
// No reasoning provided or a legacy placeholder
if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
return content;
}
@ -118,6 +591,22 @@ function loadReasoningSettings() {
power_user.reasoning.auto_parse = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
$('#reasoning_auto_expand').on('change', function () {
power_user.reasoning.auto_expand = !!$(this).prop('checked');
toggleReasoningAutoExpand();
saveSettingsDebounced();
});
toggleReasoningAutoExpand();
$('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
$('#reasoning_show_hidden').on('change', function () {
power_user.reasoning.show_hidden = !!$(this).prop('checked');
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
saveSettingsDebounced();
});
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
}
function registerReasoningSlashCommands() {
@ -134,10 +623,10 @@ function registerReasoningSlashCommands() {
}),
],
callback: (_args, value) => {
const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
const message = chat[messageId];
const reasoning = String(message?.extra?.reasoning ?? '');
return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
return reasoning;
},
}));
@ -163,11 +652,16 @@ function registerReasoningSlashCommands() {
callback: async (args, value) => {
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
const message = chat[messageId];
if (!message?.extra) {
if (!message) {
return '';
}
// Make sure the message has an extra object
if (!message.extra || typeof message.extra !== 'object') {
message.extra = {};
}
message.extra.reasoning = String(value ?? '');
message.extra.reasoning_type = ReasoningType.Manual;
await saveChatConditional();
closeMessageEditor('reasoning');
@ -188,7 +682,26 @@ function registerReasoningSlashCommands() {
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'Whether to return the parsed reasoning or the content without reasoning',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'reasoning',
isRequired: false,
enumList: [
new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'strict',
description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
@ -198,19 +711,27 @@ function registerReasoningSlashCommands() {
}),
],
callback: (args, value) => {
if (!value) {
if (!value || typeof value !== 'string') {
return '';
}
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
return String(value);
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
return value;
}
if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
}
const parsedReasoning = parseReasoningFromString(String(value));
const returnMessage = args.return === 'content';
const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
if (!parsedReasoning) {
return '';
return returnMessage ? value : '';
}
if (returnMessage) {
return parsedReasoning.content;
}
const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@ -228,6 +749,31 @@ function registerReasoningMacros() {
}
function setReasoningEventHandlers() {
$(document).on('click', '.mes_reasoning_details', function (e) {
if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
e.preventDefault();
}
});
$(document).on('click', '.mes_reasoning_header', function (e) {
const details = $(this).closest('.mes_reasoning_details');
// Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited
if (details.find('.mes_reasoning').is(':empty')) {
e.preventDefault();
return;
}
// If we are in message edit mode and reasoning area is closed, a click opens and edits it
const mes = $(this).closest('.mes');
const mesEditArea = mes.find('#curEditTextarea');
if (mesEditArea.length) {
const summary = $(mes).find('.mes_reasoning_summary');
if (!summary.attr('open')) {
summary.find('.mes_reasoning_edit').trigger('click');
}
}
});
$(document).on('click', '.mes_reasoning_copy', (e) => {
e.stopPropagation();
e.preventDefault();
@ -246,7 +792,7 @@ function setReasoningEventHandlers() {
const textarea = document.createElement('textarea');
const reasoningBlock = messageBlock.find('.mes_reasoning');
textarea.classList.add('reasoning_edit_textarea');
textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
textarea.value = reasoning;
$(textarea).insertBefore(reasoningBlock);
if (!CSS.supports('field-sizing', 'content')) {
@ -285,9 +831,12 @@ function setReasoningEventHandlers() {
const textarea = messageBlock.find('.reasoning_edit_textarea');
const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning;
message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
await saveChatConditional();
updateMessageBlock(messageId, message);
textarea.remove();
messageBlock.find('.mes_edit_done:visible').trigger('click');
});
$(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@ -297,10 +846,14 @@ function setReasoningEventHandlers() {
const { messageBlock } = getMessageFromJquery(this);
const textarea = messageBlock.find('.reasoning_edit_textarea');
textarea.remove();
messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
updateReasoningUI(messageBlock);
});
$(document).on('click', '.mes_edit_add_reasoning', async function () {
const { message, messageId } = getMessageFromJquery(this);
const { message, messageBlock } = getMessageFromJquery(this);
if (!message?.extra) {
return;
}
@ -310,34 +863,46 @@ function setReasoningEventHandlers() {
return;
}
message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
messageBlock.addClass('reasoning');
// To make hidden reasoning blocks editable, we just set them to "Done" here already.
// They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
messageBlock.attr('data-reasoning-state', ReasoningState.Done);
}
// Open the reasoning area so we can actually edit it
messageBlock.find('.mes_reasoning_details').attr('open', '');
messageBlock.find('.mes_reasoning_edit').trigger('click');
await saveChatConditional();
closeMessageEditor();
updateMessageBlock(messageId, message);
});
$(document).on('click', '.mes_reasoning_delete', async function (e) {
e.stopPropagation();
e.preventDefault();
const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`);
if (!confirm) {
return;
}
const { message, messageId } = getMessageFromJquery(this);
const { message, messageId, messageBlock } = getMessageFromJquery(this);
if (!message?.extra) {
return;
}
message.extra.reasoning = '';
delete message.extra.reasoning_type;
delete message.extra.reasoning_duration;
await saveChatConditional();
updateMessageBlock(messageId, message);
const textarea = messageBlock.find('.reasoning_edit_textarea');
textarea.remove();
});
$(document).on('pointerup', '.mes_reasoning_copy', async function () {
const { message } = getMessageFromJquery(this);
const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
const reasoning = String(message?.extra?.reasoning ?? '');
if (!reasoning) {
return;
@ -348,22 +913,38 @@ function setReasoningEventHandlers() {
});
}
/**
* Removes reasoning from a string if auto-parsing is enabled.
* @param {string} str Input string
* @returns {string} Output string
*/
export function removeReasoningFromString(str) {
if (!power_user.reasoning.auto_parse) {
return str;
}
const parsedReasoning = parseReasoningFromString(str);
return parsedReasoning?.content ?? str;
}
/**
* Parses reasoning from a string using the power user reasoning settings.
* @typedef {Object} ParsedReasoning
* @property {string} reasoning Reasoning block
* @property {string} content Message content
* @param {string} str Content of the message
* @param {Object} options Optional arguments
* @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
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
*/
function parseReasoningFromString(str) {
function parseReasoningFromString(str, { strict = true } = {}) {
// Both prefix and suffix must be defined
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
return null;
}
try {
const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
let didReplace = false;
let reasoning = '';
@ -373,9 +954,9 @@ function parseReasoningFromString(str) {
return '';
});
if (didReplace && power_user.trim_spaces) {
reasoning = reasoning.trim();
content = content.trim();
if (didReplace) {
reasoning = trimSpaces(reasoning);
content = trimSpaces(content);
}
return { reasoning, content };
@ -404,6 +985,11 @@ function registerReasoningAppEvents() {
return null;
}
if (message.extra?.reasoning) {
console.debug('[Reasoning] Message already has reasoning', idx);
return null;
}
const parsedReasoning = parseReasoningFromString(message.mes);
// No reasoning block found
@ -421,6 +1007,7 @@ function registerReasoningAppEvents() {
// If reasoning was found, add it to the message
if (parsedReasoning.reasoning) {
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
message.extra.reasoning_type = ReasoningType.Parsed;
}
// Update the message text if it was changed

View File

@ -40,6 +40,8 @@ export const SECRET_KEYS = {
BFL: 'api_key_bfl',
GENERIC: 'api_key_generic',
DEEPSEEK: 'api_key_deepseek',
SERPER: 'api_key_serper',
FALAI: 'api_key_falai',
};
const INPUT_MAP = {

View File

@ -75,6 +75,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
import { accountStorage } from './util/AccountStorage.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@ -235,7 +236,6 @@ export function initDefaultSlashCommands() {
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
forceEnum: false,
}),
],
helpString: `
@ -274,7 +274,6 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
forceEnum: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'avatar',
@ -518,7 +517,6 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('all'),
forceEnum: true,
}),
],
helpString: 'Opens up a chat with the character or group by its name',
@ -733,6 +731,57 @@ export function initDefaultSlashCommands() {
],
helpString: 'Unhides a message from the prompt.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'member-get',
aliases: ['getmember', 'memberget'],
callback: (async ({ field = 'name' }, arg) => {
if (!selected_group) {
toastr.warning('Cannot run /member-get command outside of a group chat.');
return '';
}
if (field === '') {
toastr.warning('\'/member-get field=\' argument required!');
return '';
}
field = field.toString();
arg = arg.toString();
if (!['name', 'index', 'id', 'avatar'].includes(field)) {
toastr.warning('\'/member-get field=\' argument required!');
return '';
}
const isId = !isNaN(parseInt(arg));
const groupMember = findGroupMemberId(arg, true);
if (!groupMember) {
toastr.warn(`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
return '';
}
return groupMember[field];
}),
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'field',
description: 'Whether to retrieve the name, index, id, or avatar.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
defaultValue: 'name',
enumList: [
new SlashCommandEnumValue('name', 'Character name'),
new SlashCommandEnumValue('index', 'Group member index'),
new SlashCommandEnumValue('avatar', 'Character avatar'),
new SlashCommandEnumValue('id', 'Character index'),
],
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'member index (starts with 0), name, or avatar',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.groupMembers(),
}),
],
helpString: 'Retrieves a group member\'s name, index, id, or avatar.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'member-disable',
callback: disableGroupMemberCallback,
@ -843,7 +892,8 @@ export function initDefaultSlashCommands() {
helpString: 'Moves a group member down in the group chat list.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'peek',
name: 'member-peek',
aliases: ['peek', 'memberpeek', 'peekmember'],
callback: peekCallback,
unnamedArgumentList: [
SlashCommandArgument.fromProps({
@ -1009,7 +1059,6 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'System',
enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
forceEnum: false,
}),
new SlashCommandNamedArgument(
'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
@ -3047,7 +3096,7 @@ function performGroupMemberAction(chid, action) {
async function disableGroupMemberCallback(_, arg) {
if (!selected_group) {
toastr.warning('Cannot run /disable command outside of a group chat.');
toastr.warning('Cannot run /member-disable command outside of a group chat.');
return '';
}
@ -3064,7 +3113,7 @@ async function disableGroupMemberCallback(_, arg) {
async function enableGroupMemberCallback(_, arg) {
if (!selected_group) {
toastr.warning('Cannot run /enable command outside of a group chat.');
toastr.warning('Cannot run /member-enable command outside of a group chat.');
return '';
}
@ -3081,7 +3130,7 @@ async function enableGroupMemberCallback(_, arg) {
async function moveGroupMemberUpCallback(_, arg) {
if (!selected_group) {
toastr.warning('Cannot run /memberup command outside of a group chat.');
toastr.warning('Cannot run /member-up command outside of a group chat.');
return '';
}
@ -3098,7 +3147,7 @@ async function moveGroupMemberUpCallback(_, arg) {
async function moveGroupMemberDownCallback(_, arg) {
if (!selected_group) {
toastr.warning('Cannot run /memberdown command outside of a group chat.');
toastr.warning('Cannot run /member-down command outside of a group chat.');
return '';
}
@ -3115,12 +3164,12 @@ async function moveGroupMemberDownCallback(_, arg) {
async function peekCallback(_, arg) {
if (!selected_group) {
toastr.warning('Cannot run /peek command outside of a group chat.');
toastr.warning('Cannot run /member-peek command outside of a group chat.');
return '';
}
if (is_group_generating) {
toastr.warning('Cannot run /peek command while the group reply is generating.');
toastr.warning('Cannot run /member-peek command while the group reply is generating.');
return '';
}
@ -3137,7 +3186,7 @@ async function peekCallback(_, arg) {
async function removeGroupMemberCallback(_, arg) {
if (!selected_group) {
toastr.warning('Cannot run /memberremove command outside of a group chat.');
toastr.warning('Cannot run /member-remove command outside of a group chat.');
return '';
}
@ -3558,9 +3607,9 @@ export async function sendMessageAs(args, text) {
if (!name) {
const namelessWarningKey = 'sendAsNamelessWarningShown';
if (localStorage.getItem(namelessWarningKey) !== 'true') {
if (accountStorage.getItem(namelessWarningKey) !== 'true') {
toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
localStorage.setItem(namelessWarningKey, 'true');
accountStorage.setItem(namelessWarningKey, 'true');
}
name = name2;
}

View File

@ -34,6 +34,7 @@ export const enumIcons = {
preset: '⚙️',
file: '📄',
message: '💬',
reasoning: '💡',
voice: '🎤',
server: '🖥️',
popup: '🗔',

View File

@ -68,11 +68,14 @@ import { tag_map, tags } from './tags.js';
import { textgenerationwebui_settings } from './textgen-settings.js';
import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
import { ToolManager } from './tool-calling.js';
import { accountStorage } from './util/AccountStorage.js';
import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
export function getContext() {
return {
accountStorage,
chat,
characters,
groups,
@ -186,6 +189,10 @@ export function getContext() {
set: setGlobalVariable,
},
},
loadWorldInfo,
saveWorldInfo,
updateWorldInfoList,
convertCharacterBook,
};
}

View File

@ -146,5 +146,5 @@
</div>
<hr>
<div id="rawPromptPopup" class="list-group">
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
<div id="rawPromptWrapper" class="tokenItemizingMaintext"></div>
</div>

View File

@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js';
import { renderTemplateAsync } from './templates.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
import { accountStorage } from './util/AccountStorage.js';
let mancerModels = [];
let togetherModels = [];
@ -57,9 +58,14 @@ const OPENROUTER_PROVIDERS = [
'Minimax',
'Nineteen',
'Liquid',
'InferenceNet',
'Friendli',
'AionLabs',
'Alibaba',
'Nebius',
'Chutes',
'Kluster',
'Targon',
'01.AI',
'HuggingFace',
'Mancer',
@ -336,7 +342,7 @@ export async function loadFeatherlessModels(data) {
populateClassSelection(data);
// Retrieve the stored number of items per page or default to 10
const perPage = Number(localStorage.getItem(storageKey)) || 10;
const perPage = Number(accountStorage.getItem(storageKey)) || 10;
// Initialize pagination
applyFiltersAndSort();
@ -412,7 +418,7 @@ export async function loadFeatherlessModels(data) {
},
afterSizeSelectorChange: function (e) {
const newPerPage = e.target.value;
localStorage.setItem('Models_PerPage', newPerPage);
accountStorage.setItem(storageKey, newPerPage);
setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
},
});
@ -513,7 +519,7 @@ export async function loadFeatherlessModels(data) {
const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
setupPagination(filteredModels, Number(accountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
}
// Required to keep the /model command function

View File

@ -10,6 +10,7 @@ import {
setOnlineStatus,
substituteParams,
} from '../script.js';
import { t } from './i18n.js';
import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
import { power_user, registerDebugFunction } from './power-user.js';
@ -182,6 +183,8 @@ const settings = {
grammar_string: '',
json_schema: {},
banned_tokens: '',
global_banned_tokens: '',
send_banned_tokens: true,
sampler_priority: OOBA_DEFAULT_ORDER,
samplers: LLAMACPP_DEFAULT_ORDER,
samplers_priorities: APHRODITE_DEFAULT_ORDER,
@ -274,6 +277,8 @@ export const setting_names = [
'grammar_string',
'json_schema',
'banned_tokens',
'global_banned_tokens',
'send_banned_tokens',
'ignore_eos_token',
'spaces_between_special_tokens',
'speculative_ngram',
@ -306,7 +311,7 @@ export function validateTextGenUrl() {
const formattedUrl = formatTextGenURL(url);
if (!formattedUrl) {
toastr.error('Enter a valid API URL', 'Text Completion API');
toastr.error(t`Enter a valid API URL`, 'Text Completion API');
return;
}
@ -394,7 +399,7 @@ function getTokenizerForTokenIds() {
* @returns {TokenBanResult} String with comma-separated banned token IDs
*/
function getCustomTokenBans() {
if (!settings.banned_tokens && !textgenerationwebui_banned_in_macros.length) {
if (!settings.send_banned_tokens || (!settings.banned_tokens && !settings.global_banned_tokens && !textgenerationwebui_banned_in_macros.length)) {
return {
banned_tokens: '',
banned_strings: [],
@ -404,8 +409,9 @@ function getCustomTokenBans() {
const tokenizer = getTokenizerForTokenIds();
const banned_tokens = [];
const banned_strings = [];
const sequences = settings.banned_tokens
.split('\n')
const sequences = []
.concat(settings.banned_tokens.split('\n'))
.concat(settings.global_banned_tokens.split('\n'))
.concat(textgenerationwebui_banned_in_macros)
.filter(x => x.length > 0)
.filter(onlyUnique);
@ -452,6 +458,18 @@ function getCustomTokenBans() {
};
}
/**
* Sets the banned strings kill switch toggle.
* @param {boolean} isEnabled Kill switch state
* @param {string} title Label title
*/
function toggleBannedStringsKillSwitch(isEnabled, title) {
$('#send_banned_tokens_textgenerationwebui').prop('checked', isEnabled);
$('#send_banned_tokens_label').find('.menu_button').toggleClass('toggleEnabled', isEnabled).prop('title', title);
settings.send_banned_tokens = isEnabled;
saveSettingsDebounced();
}
/**
* Calculates logit bias object from the logit bias list.
* @returns {object} Logit bias object
@ -594,6 +612,14 @@ function sortAphroditeItemsByOrder(orderArray) {
}
jQuery(function () {
$('#send_banned_tokens_textgenerationwebui').on('change', function () {
const checked = !!$(this).prop('checked');
toggleBannedStringsKillSwitch(checked,
checked
? t`Banned tokens/strings are being sent in the request.`
: t`Banned tokens/strings are NOT being sent in the request.`);
});
$('#koboldcpp_order').sortable({
delay: getSortableDelay(),
stop: function () {
@ -932,6 +958,10 @@ function setSettingByName(setting, value, trigger) {
if (isCheckbox) {
const val = Boolean(value);
$(`#${setting}_textgenerationwebui`).prop('checked', val);
if ('send_banned_tokens' === setting) {
$(`#${setting}_textgenerationwebui`).trigger('change');
}
}
else if (isText) {
$(`#${setting}_textgenerationwebui`).val(value);
@ -1157,7 +1187,7 @@ export function getTextGenModel() {
return settings.aphrodite_model;
case OLLAMA:
if (!settings.ollama_model) {
toastr.error('No Ollama model selected.', 'Text Completion API');
toastr.error(t`No Ollama model selected.`, 'Text Completion API');
throw new Error('No Ollama model selected');
}
return settings.ollama_model;
@ -1221,7 +1251,7 @@ function replaceMacrosInList(str) {
}
}
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
export async function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
const dynatemp = isDynamicTemperatureSupported();
const { banned_tokens, banned_strings } = getCustomTokenBans();
@ -1449,7 +1479,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
}
}
eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
await eventSource.emit(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
// Grammar conflicts with with json_schema
if (settings.type === LLAMACPP) {

View File

@ -563,6 +563,7 @@ export class ToolManager {
chat_completion_sources.OPENROUTER,
chat_completion_sources.GROQ,
chat_completion_sources.COHERE,
chat_completion_sources.DEEPSEEK,
];
return supportedSources.includes(oai_settings.chat_completion_source);
}

View File

@ -43,6 +43,14 @@ export function isAdmin() {
return Boolean(currentUser.admin);
}
/**
* Gets the handle string of the current user.
* @returns {string} User handle
*/
export function getCurrentUserHandle() {
return currentUser?.handle || 'default-user';
}
/**
* Get the current user.
* @returns {Promise<void>}

View File

@ -0,0 +1,139 @@
import { saveSettingsDebounced } from '../../script.js';
const MIGRATED_MARKER = '__migrated';
const MIGRATABLE_KEYS = [
/^AlertRegex_/,
/^AlertWI_/,
/^Assets_SkipConfirm_/,
/^Characters_PerPage$/,
/^DataBank_sortField$/,
/^DataBank_sortOrder$/,
/^extension_update_nag$/,
/^extensions_sortByName$/,
/^FeatherlessModels_PerPage$/,
/^GroupMembers_PerPage$/,
/^GroupCandidates_PerPage$/,
/^LNavLockOn$/,
/^LNavOpened$/,
/^mediaWarningShown:/,
/^NavLockOn$/,
/^NavOpened$/,
/^Personas_PerPage$/,
/^Personas_GridView$/,
/^Proxy_SkipConfirm_/,
/^qr--executeShortcut$/,
/^qr--syntax$/,
/^qr--tabSize$/,
/^qr--wrap$/,
/^RegenerateWithCtrlEnter$/,
/^SelectedNavTab$/,
/^sendAsNamelessWarningShown$/,
/^StoryStringValidationCache$/,
/^WINavOpened$/,
/^WI_PerPage$/,
/^world_info_sort_order$/,
];
/**
* Provides access to account storage of arbitrary key-value pairs.
*/
class AccountStorage {
/**
* @type {Record<string, string>} Storage state
*/
#state = {};
/**
* @type {boolean} If the storage was initialized
*/
#ready = false;
#migrateLocalStorage() {
const localStorageKeys = [];
for (let i = 0; i < globalThis.localStorage.length; i++) {
localStorageKeys.push(globalThis.localStorage.key(i));
}
for (const key of localStorageKeys) {
if (MIGRATABLE_KEYS.some(k => k.test(key))) {
const value = globalThis.localStorage.getItem(key);
this.#state[key] = value;
globalThis.localStorage.removeItem(key);
}
}
}
/**
* Initialize the account storage.
* @param {Object} state Initial state
*/
init(state) {
if (state && typeof state === 'object') {
this.#state = Object.assign(this.#state, state);
}
if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
this.#migrateLocalStorage();
this.#state[MIGRATED_MARKER] = '1';
saveSettingsDebounced();
}
this.#ready = true;
}
/**
* Get the value of a key in account storage.
* @param {string} key Key to get
* @returns {string|null} Value of the key
*/
getItem(key) {
if (!this.#ready) {
console.warn(`AccountStorage not ready (trying to read from ${key})`);
}
return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
}
/**
* Set a key in account storage.
* @param {string} key Key to set
* @param {string} value Value to set
*/
setItem(key, value) {
if (!this.#ready) {
console.warn(`AccountStorage not ready (trying to write to ${key})`);
}
this.#state[key] = String(value);
saveSettingsDebounced();
}
/**
* Remove a key from account storage.
* @param {string} key Key to remove
*/
removeItem(key) {
if (!this.#ready) {
console.warn(`AccountStorage not ready (trying to remove ${key})`);
}
if (!Object.hasOwn(this.#state, key)) {
return;
}
delete this.#state[key];
saveSettingsDebounced();
}
/**
* Gets a snapshot of the storage state.
* @returns {Record<string, string>} A deep clone of the storage state
*/
getState() {
return structuredClone(this.#state);
}
}
/**
* Account storage instance.
*/
export const accountStorage = new AccountStorage();

View File

@ -8,7 +8,7 @@ import {
import { getContext } from './extensions.js';
import { characters, getRequestHeaders, this_chid } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { collapseNewlines, power_user } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@ -676,6 +676,19 @@ export function sortByCssOrder(a, b) {
return _a - _b;
}
/**
* Trims leading and trailing whitespace from the input string based on a configuration setting.
* @param {string} input - The string to be trimmed
* @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
*/
export function trimSpaces(input) {
if (!input || typeof input !== 'string') {
return input;
}
return power_user.trim_spaces ? input.trim() : input;
}
/**
* Trims a string to the end of a nearest sentence.
* @param {string} input The string to trim.
@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) {
});
}
export function getCharaFilename(chid) {
/**
* Gets the filename of the character avatar without extension
* @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
* @param {object} [options={}] - Options arguments
* @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
* @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
*/
export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
const context = getContext();
const fileName = context.characters[chid ?? context.characterId]?.avatar;
const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, '');
}
return fileName?.replace(/\.[^/.]+$/, '') ?? null;
}
/**
@ -2059,6 +2077,23 @@ export function toggleDrawer(drawer, expand = true) {
}
}
/**
* Sets or removes a dataset property on an HTMLElement
*
* Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
*
* @param {HTMLElement} element - The element to modify
* @param {string} name - The name of the dataset property
* @param {string|null} value - The value to set - If null, the dataset property will be removed
*/
export function setDatasetProperty(element, name, value) {
if (value === null) {
delete element.dataset[name];
} else {
element.dataset[name] = value;
}
}
export async function fetchFaFile(name) {
const style = document.createElement('style');
style.innerHTML = await (await fetch(`/css/${name}`)).text();

View File

@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
import { StructuredCloneMap } from './util/StructuredCloneMap.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
import { accountStorage } from './util/AccountStorage.js';
export const world_info_insertion_strategy = {
evenly: 0,
@ -399,6 +400,12 @@ class WorldInfoTimedEffects {
*/
#entries = [];
/**
* Is this a dry run?
* @type {boolean}
*/
#isDryRun = false;
/**
* Buffer for active timed effects.
* @type {Record<TimedEffectType, WIScanEntry[]>}
@ -448,10 +455,12 @@ class WorldInfoTimedEffects {
* Initialize the timed effects with the given messages.
* @param {string[]} chat Array of chat messages
* @param {WIScanEntry[]} entries Array of entries
* @param {boolean} isDryRun Whether the operation is a dry run
*/
constructor(chat, entries) {
constructor(chat, entries, isDryRun = false) {
this.#chat = chat;
this.#entries = entries;
this.#isDryRun = isDryRun;
this.#ensureChatMetadata();
}
@ -583,8 +592,10 @@ class WorldInfoTimedEffects {
* Checks for timed effects on chat messages.
*/
checkTimedEffects() {
this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
if (!this.#isDryRun) {
this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
}
this.#checkDelayEffect(this.#buffer.delay);
}
@ -629,6 +640,7 @@ class WorldInfoTimedEffects {
* @param {WIScanEntry[]} activatedEntries Entries that were activated
*/
setTimedEffects(activatedEntries) {
if (this.#isDryRun) return;
for (const entry of activatedEntries) {
this.#setTimedEffectOfType('sticky', entry);
this.#setTimedEffectOfType('cooldown', entry);
@ -645,6 +657,9 @@ class WorldInfoTimedEffects {
if (!this.isValidEffectType(type)) {
return;
}
if (this.#isDryRun && type !== 'delay') {
return;
}
const key = this.#getEntryKey(entry);
delete chat_metadata.timedWorldInfo[type][key];
@ -858,7 +873,7 @@ export function setWorldInfoSettings(settings, data) {
$('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
});
$('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');
$('#world_info_sort_order').val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
$('#world_info').trigger('change');
$('#world_editor_select').trigger('change');
@ -1708,7 +1723,7 @@ export async function loadWorldInfo(name) {
return null;
}
async function updateWorldInfoList() {
export async function updateWorldInfoList() {
const result = await fetch('/api/settings/get', {
method: 'POST',
headers: getRequestHeaders(),
@ -1933,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
if (typeof navigation === 'number' && Number(navigation) >= 0) {
const data = getDataArray();
const uidIndex = data.findIndex(x => x.uid === navigation);
const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;
const perPage = Number(accountStorage.getItem(storageKey)) || perPageDefault;
startPage = Math.floor(uidIndex / perPage) + 1;
}
$('#world_info_pagination').pagination({
dataSource: getDataArray,
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
pageSize: Number(accountStorage.getItem(storageKey)) || perPageDefault,
sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
showSizeChanger: true,
pageRange: 1,
@ -1969,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
worldEntriesList.append(blocks);
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
accountStorage.setItem(storageKey, e.target.value);
},
afterPaging: function () {
$('#world_popup_entries_list textarea[name="comment"]').each(function () {
@ -2174,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() {
// If search got cleared, we make sure to hide the option and go back to the one before
if (!searchTerm && !isHidden) {
searchOption.attr('hidden', '');
selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');
selector.val(accountStorage.getItem(SORT_ORDER_KEY) || '0');
}
}
@ -2423,7 +2438,9 @@ export async function getWorldEntry(name, data, entry) {
setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
await saveWorldInfo(name, data);
}
$(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
});
input.toggleClass('empty', !entry[entryPropName].length);
input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
@ -2458,6 +2475,7 @@ export async function getWorldEntry(name, data, entry) {
data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
await saveWorldInfo(name, data);
$(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
}
});
input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@ -3435,7 +3453,7 @@ async function _save(name, data) {
headers: getRequestHeaders(),
body: JSON.stringify({ name: name, data: data }),
});
eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
await eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
}
@ -3847,7 +3865,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
const context = getContext();
const buffer = new WorldInfoBuffer(chat);
console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages) ---`);
console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
// Combine the chat
@ -3879,9 +3897,9 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
const sortedEntries = await getSortedEntries();
const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);
const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries, isDryRun);
!isDryRun && timedEffects.checkTimedEffects();
timedEffects.checkTimedEffects();
if (sortedEntries.length === 0) {
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
@ -4324,12 +4342,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
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]);
}
!isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
buffer.resetExternalEffects();
timedEffects.cleanUp();
console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
console.debug('[WI] --- DONE ---');
console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
}
@ -4658,7 +4676,7 @@ function convertNovelLorebook(inputObj) {
return outputObj;
}
function convertCharacterBook(characterBook) {
export function convertCharacterBook(characterBook) {
const result = { entries: {}, originalData: characterBook };
characterBook.entries.forEach((entry, index) => {
@ -4736,8 +4754,8 @@ export function checkEmbeddedWorld(chid) {
// Only show the alert once per character
const checkKey = `AlertWI_${characters[chid].avatar}`;
const worldName = characters[chid]?.data?.extensions?.world;
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
localStorage.setItem(checkKey, 'true');
if (!accountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
accountStorage.setItem(checkKey, 'true');
if (power_user.world_import_dialog) {
const html = `<h3>This character has an embedded World/Lorebook.</h3>
@ -5181,7 +5199,7 @@ jQuery(() => {
$('#world_info_sort_order').on('change', function () {
const value = String($(this).find(':selected').val());
// Save sort order, but do not save search sorting, as this is a temporary sorting option
if (value !== 'search') localStorage.setItem(SORT_ORDER_KEY, value);
if (value !== 'search') accountStorage.setItem(SORT_ORDER_KEY, value);
updateEditor(navigation_option.none);
});

View File

@ -106,6 +106,8 @@
--tool-cool-color-picker-btn-bg: transparent;
--tool-cool-color-picker-btn-border-color: transparent;
--mes-right-spacing: 30px;
--avatar-base-height: 50px;
--avatar-base-width: 50px;
--avatar-base-border-radius: 2px;
@ -260,6 +262,10 @@ input[type='checkbox']:focus-visible {
color: var(--SmartThemeEmColor);
}
.tokenItemizingMaintext {
font-size: calc(var(--mainFontSize) * 0.8);
}
.tokenGraph {
border-radius: 10px;
border: 1px solid var(--SmartThemeBorderColor);
@ -342,18 +348,68 @@ input[type='checkbox']:focus-visible {
.mes_reasoning {
display: block;
border: 1px solid var(--SmartThemeBorderColor);
background-color: var(--black30a);
border-radius: 5px;
border-left: 2px solid var(--SmartThemeEmColor);
border-radius: 2px;
padding: 5px;
margin: 5px 0;
padding-left: 14px;
margin-bottom: 0.5em;
overflow-y: auto;
color: var(--SmartThemeEmColor);
}
.mes_reasoning_summary {
.mes_reasoning_details {
margin-right: var(--mes-right-spacing);
}
.mes_reasoning_details .mes_reasoning_summary {
list-style: none;
margin-right: calc(var(--mes-right-spacing) * -1);
}
.mes_reasoning_details summary::-webkit-details-marker {
display: none;
}
.mes_reasoning *:last-child {
margin-bottom: 0;
}
.mes_reasoning em,
.mes_reasoning i,
.mes_reasoning u,
.mes_reasoning q,
.mes_reasoning blockquote {
filter: saturate(0.5);
}
.mes_reasoning_details .mes_reasoning em {
color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
}
.mes_reasoning_header_block {
flex-grow: 1;
}
.mes_reasoning_header {
cursor: pointer;
position: relative;
margin: 2px;
user-select: none;
margin: 0.5em 2px;
padding: 7px 14px;
padding-right: calc(0.7em + 14px);
border-radius: 5px;
background-color: var(--grey30);
font-size: calc(var(--mainFontSize) * 0.9);
align-items: baseline;
}
.mes:has(.mes_reasoning:empty) .mes_reasoning_header {
cursor: default;
}
/* TWIMC: Remove with custom CSS to show the icon */
.mes_reasoning_header>.icon-svg {
display: none;
}
@supports not selector(:has(*)) {
@ -363,29 +419,41 @@ input[type='checkbox']:focus-visible {
}
.mes_bias:empty,
.mes_reasoning:empty,
.mes_reasoning_details:has(.mes_reasoning:empty),
.mes_block:has(.edit_textarea) .mes_reasoning_details,
.mes:not(.reasoning) .mes_reasoning_details,
.mes_reasoning_details:not([open]) .mes_reasoning_actions,
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
.mes:has(.mes_reasoning:empty) .mes_reasoning,
.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
display: none;
}
.mes_reasoning_actions {
position: absolute;
right: 0;
top: 0;
.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
}
display: flex;
gap: 4px;
flex-wrap: nowrap;
justify-content: flex-end;
transition: all 200ms;
overflow-x: hidden;
padding: 1px;
/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
#chat:not([data-show-hidden-reasoning="true"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
display: none;
}
.mes_reasoning_details .mes_reasoning_arrow {
position: absolute;
top: 50%;
right: 7px;
transform: translateY(-50%);
font-size: calc(var(--mainFontSize) * 0.7);
width: calc(var(--mainFontSize) * 0.7);
height: calc(var(--mainFontSize) * 0.7);
}
.mes_reasoning_details:not([open]) .mes_reasoning_arrow {
transform: translateY(-50%) rotate(180deg);
}
.mes_reasoning_summary>span {
@ -1100,13 +1168,8 @@ body .panelControlBar {
/*only affects bubblechat to make it sit nicely at the bottom*/
}
.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
margin-bottom: 30px;
}
.last_mes .mes_reasoning,
.last_mes .mes_text {
padding-right: 30px;
.last_mes:has(.mes_text:empty):has(.mes_reasoning_details) .mes_reasoning:not(:empty) {
margin-bottom: var(--mes-right-spacing);
}
/* SWIPE RELATED STYLES*/
@ -1330,6 +1393,7 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
padding-left: 0;
padding-top: 5px;
padding-bottom: 5px;
padding-right: var(--mes-right-spacing);
}
br {
@ -2815,9 +2879,8 @@ select option:not(:checked) {
color: var(--active) !important;
}
#instruct_enabled_label .menu_button:not(.toggleEnabled),
#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
color: Red;
.menu_button.togglable:not(.toggleEnabled) {
color: red;
}
.displayBlock {
@ -3000,6 +3063,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
.mes_block .ch_name {
max-width: 100%;
min-height: 22px;
align-items: flex-start;
}
/*applies to both groups and solos chars in the char list*/
@ -4224,7 +4288,13 @@ input[type="range"]::-webkit-slider-thumb {
transition: 0.3s ease-in-out;
}
.mes_edit_buttons .menu_button {
.mes_reasoning_actions {
margin: 0;
margin-top: 0.5em;
}
.mes_edit_buttons .menu_button,
.mes_reasoning_actions .edit_button {
opacity: 0.5;
padding: 0px;
font-size: 1rem;
@ -4237,6 +4307,12 @@ input[type="range"]::-webkit-slider-thumb {
align-items: center;
}
.mes_reasoning_actions .edit_button {
margin-bottom: 0.5em;
opacity: 1;
filter: brightness(0.7);
}
.mes_reasoning_edit_cancel,
.mes_edit_cancel.menu_button {
background-color: var(--crimson70a);
@ -4263,6 +4339,14 @@ input[type="range"]::-webkit-slider-thumb {
field-sizing: content;
}
body[data-generating="true"] #send_but,
body[data-generating="true"] #mes_continue,
body[data-generating="true"] #mes_impersonate,
body[data-generating="true"] #chat .last_mes .mes_buttons,
body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
display: none;
}
#anchor_order {
margin-bottom: 15px;
}
@ -4602,23 +4686,6 @@ body .ui-widget-content li:hover {
opacity: 1;
}
.typing_indicator {
position: sticky;
bottom: 10px;
margin: 10px;
opacity: 0.85;
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
order: 9999;
}
.typing_indicator:after {
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1500ms infinite;
content: "";
width: 0px;
}
#group_avatar_preview .missing-avatar {
display: inline;
vertical-align: middle;
@ -5707,11 +5774,13 @@ body:not(.movingUI) .drawer-content.maximized {
overflow-wrap: anywhere;
}
#SystemPromptColumn summary,
#InstructSequencesColumn summary {
font-size: 0.95em;
cursor: pointer;
}
#SystemPromptColumn details,
#InstructSequencesColumn details:not(:last-of-type) {
margin-bottom: 5px;
}
@ -5875,3 +5944,15 @@ body:not(.movingUI) .drawer-content.maximized {
.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
flex: 1;
}
.oneline-dropdown label {
margin-top: 3px;
margin-bottom: 5px;
flex-grow: 1;
text-align: left;
}
.oneline-dropdown select {
min-width: fit-content;
width: 40%;
}

275
server.js
View File

@ -4,6 +4,7 @@
import fs from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import os from 'os';
import path from 'node:path';
import util from 'node:util';
import net from 'node:net';
@ -29,6 +30,7 @@ import bodyParser from 'body-parser';
// net related library imports
import fetch from 'node-fetch';
import ipRegex from 'ip-regex';
// Unrestrict console logs display limit
util.inspect.defaultOptions.maxArrayLength = null;
@ -55,9 +57,10 @@ import {
import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
import basicAuthMiddleware from './src/middleware/basicAuth.js';
import whitelistMiddleware from './src/middleware/whitelist.js';
import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js';
import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
import initRequestProxy from './src/request-proxy.js';
import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
import {
getVersion,
getConfigValue,
@ -65,7 +68,11 @@ import {
forwardFetchResponse,
removeColorFormatting,
getSeparator,
stringToBool,
urlHostnameToIPv6,
canResolve,
safeReadFileSync,
setupLogLevel,
} from './src/util.js';
import { UPLOADS_DIRECTORY } from './src/constants.js';
import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@ -125,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
const DEFAULT_PORT = 8000;
const DEFAULT_AUTORUN = false;
const DEFAULT_LISTEN = false;
const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
const DEFAULT_CORS_PROXY = false;
const DEFAULT_WHITELIST = true;
const DEFAULT_ACCOUNTS = false;
@ -149,11 +158,11 @@ const DEFAULT_PROXY_BYPASS = [];
const cliArguments = yargs(hideBin(process.argv))
.usage('Usage: <your-start-script> <command> [options]')
.option('enableIPv6', {
type: 'boolean',
type: 'string',
default: null,
describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
}).option('enableIPv4', {
type: 'boolean',
type: 'string',
default: null,
describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
}).option('port', {
@ -180,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
type: 'boolean',
default: null,
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}]`,
}).option('listenAddressIPv6', {
type: 'string',
default: null,
describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
}).option('listenAddressIPv4', {
type: 'string',
default: null,
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 ]',
}).option('corsProxy', {
type: 'boolean',
default: null,
@ -242,27 +259,46 @@ app.use(helmet({
app.use(compression());
app.use(responseTime());
/** @type {number} */
const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
/** @type {boolean} */
const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
/** @type {boolean} */
const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
/** @type {string} */
const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
/** @type {string} */
const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
/** @type {boolean} */
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
/** @type {string} */
const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
/** @type {boolean} */
const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
/** @type {boolean} */
const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
/** @type {boolean | "auto"} */
let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
/** @type {boolean | "auto"} */
let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
/** @type {string} */
const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
/** @type {number} */
const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
/** @type {boolean} */
const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
/** @type {boolean} */
const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@ -279,7 +315,19 @@ if (dnsPreferIPv6) {
console.log('Preferring IPv4 for DNS resolution');
}
if (!enableIPv6 && !enableIPv4) {
const ipOptions = [true, 'auto', false];
if (!ipOptions.includes(enableIPv6)) {
console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6);
enableIPv6 = DEFAULT_ENABLE_IPV6;
}
if (!ipOptions.includes(enableIPv4)) {
console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4);
enableIPv4 = DEFAULT_ENABLE_IPV4;
}
if (enableIPv6 === false && enableIPv4 === false) {
console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
process.exit(1);
}
@ -364,6 +412,55 @@ function getSessionCookieAge() {
return undefined;
}
/**
* Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses.
*
* @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing:
* - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`).
* - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`).
* - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`).
* - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`).
*/
async function getHasIP() {
let hasIPv6 = false;
let hasIPv6Local = false;
let hasIPv4 = false;
let hasIPv4Local = false;
const interfaces = os.networkInterfaces();
for (const iface of Object.values(interfaces)) {
if (iface === undefined) {
continue;
}
for (const info of iface) {
if (info.family === 'IPv6') {
hasIPv6 = true;
if (info.address === '::1') {
hasIPv6Local = true;
}
}
if (info.family === 'IPv4') {
hasIPv4 = true;
if (info.address === '127.0.0.1') {
hasIPv4Local = true;
}
}
if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
}
if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
}
return [
hasIPv6,
hasIPv4,
hasIPv6Local,
hasIPv4Local,
];
}
app.use(cookieSession({
name: getCookieSessionName(),
sameSite: 'strict',
@ -419,7 +516,7 @@ if (!disableCsrf) {
// Static files
// Host index page
app.get('/', (request, response) => {
app.get('/', getCacheBusterMiddleware(), (request, response) => {
if (shouldRedirectToLogin(request)) {
const query = request.url.split('?')[1];
const redirectUrl = query ? `/login?${query}` : '/login';
@ -627,13 +724,13 @@ app.use('/api/azure', azureRouter);
const tavernUrlV6 = new URL(
(cliArguments.ssl ? 'https://' : 'http://') +
(listen ? '[::]' : '[::1]') +
(listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
(':' + server_port),
);
const tavernUrl = new URL(
(cliArguments.ssl ? 'https://' : 'http://') +
(listen ? '0.0.0.0' : '127.0.0.1') +
(listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
(':' + server_port),
);
@ -657,6 +754,7 @@ const preSetupTasks = async function () {
await checkForNewContent(directories);
await ensureThumbnailCache();
cleanUploads();
migrateAccessLog();
await settingsInit();
await statsInit();
@ -693,20 +791,23 @@ const preSetupTasks = async function () {
/**
* Gets the hostname to use for autorun in the browser.
* @returns {string} The hostname to use for autorun
* @param {boolean} useIPv6 If use IPv6
* @param {boolean} useIPv4 If use IPv4
* @returns Promise<string> The hostname to use for autorun
*/
function getAutorunHostname() {
async function getAutorunHostname(useIPv6, useIPv4) {
if (autorunHostname === 'auto') {
if (enableIPv6 && enableIPv4) {
if (avoidLocalhost) return '[::1]';
return 'localhost';
let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
if (useIPv6 && useIPv4) {
return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
}
if (enableIPv6) {
if (useIPv6) {
return '[::1]';
}
if (enableIPv4) {
if (useIPv4) {
return '127.0.0.1';
}
}
@ -718,11 +819,13 @@ function getAutorunHostname() {
* Tasks that need to be run after the server starts listening.
* @param {boolean} v6Failed If the server failed to start on IPv6
* @param {boolean} v4Failed If the server failed to start on IPv4
* @param {boolean} useIPv6 If the server is using IPv6
* @param {boolean} useIPv4 If the server is using IPv4
*/
const postSetupTasks = async function (v6Failed, v4Failed) {
const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
const autorunUrl = new URL(
(cliArguments.ssl ? 'https://' : 'http://') +
(getAutorunHostname()) +
(await getAutorunHostname(useIPv6, useIPv4)) +
(':') +
((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
);
@ -735,36 +838,48 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
let logListen = 'SillyTavern is listening on';
if (enableIPv6 && !v6Failed) {
logListen += color.green(' IPv6: ' + tavernUrlV6.host);
if (useIPv6 && !v6Failed) {
logListen += color.green(
' IPv6: ' + tavernUrlV6.host,
);
}
if (enableIPv4 && !v4Failed) {
logListen += color.green(' IPv4: ' + tavernUrl.host);
if (useIPv4 && !v4Failed) {
logListen += color.green(
' IPv4: ' + tavernUrl.host,
);
}
const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
const plainGoToLog = removeColorFormatting(goToLog);
console.log(logListen);
if (listen) {
console.log();
console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
}
console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
console.log(goToLog);
console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
if (listen) {
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');
}
if (basicAuthMode) {
if (perUserBasicAuth && !enableAccounts) {
console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));
console.error(color.red(
'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
));
} else if (!perUserBasicAuth) {
const basicAuthUser = getConfigValue('basicAuthUser', {});
if (!basicAuthUser?.username || !basicAuthUser?.password) {
console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
console.warn(color.yellow(
'Basic Authentication is enabled, but username or password is not set or empty!',
));
}
}
}
setupLogLevel();
};
/**
@ -814,14 +929,16 @@ function logSecurityAlert(message) {
* Handles the case where the server failed to start on one or both protocols.
* @param {boolean} v6Failed If the server failed to start on IPv6
* @param {boolean} v4Failed If the server failed to start on IPv4
* @param {boolean} useIPv6 If use IPv6
* @param {boolean} useIPv4 If use IPv4
*/
function handleServerListenFail(v6Failed, v4Failed) {
if (v6Failed && !enableIPv4) {
function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
if (v6Failed && !useIPv4) {
console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
process.exit(1);
}
if (v4Failed && !enableIPv6) {
if (v4Failed && !useIPv6) {
console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
process.exit(1);
}
@ -835,10 +952,11 @@ function handleServerListenFail(v6Failed, v4Failed) {
/**
* Creates an HTTPS server.
* @param {URL} url The URL to listen on
* @param {number} ipVersion the ip version to use
* @returns {Promise<void>} A promise that resolves when the server is listening
* @throws {Error} If the server fails to start
*/
function createHttpsServer(url) {
function createHttpsServer(url, ipVersion) {
return new Promise((resolve, reject) => {
const server = https.createServer(
{
@ -847,34 +965,56 @@ function createHttpsServer(url) {
}, app);
server.on('error', reject);
server.on('listening', resolve);
server.listen(Number(url.port || 443), url.hostname);
let host = url.hostname;
if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
server.listen({
host: host,
port: Number(url.port || 443),
// see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
ipv6Only: true,
});
});
}
/**
* Creates an HTTP server.
* @param {URL} url The URL to listen on
* @param {number} ipVersion the ip version to use
* @returns {Promise<void>} A promise that resolves when the server is listening
* @throws {Error} If the server fails to start
*/
function createHttpServer(url) {
function createHttpServer(url, ipVersion) {
return new Promise((resolve, reject) => {
const server = http.createServer(app);
server.on('error', reject);
server.on('listening', resolve);
server.listen(Number(url.port || 80), url.hostname);
let host = url.hostname;
if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
server.listen({
host: host,
port: Number(url.port || 80),
// see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
ipv6Only: true,
});
});
}
async function startHTTPorHTTPS() {
/**
* Starts the server using http or https depending on config
* @param {boolean} useIPv6 If use IPv6
* @param {boolean} useIPv4 If use IPv4
*/
async function startHTTPorHTTPS(useIPv6, useIPv4) {
let v6Failed = false;
let v4Failed = false;
const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
if (enableIPv6) {
if (useIPv6) {
try {
await createFunc(tavernUrlV6);
await createFunc(tavernUrlV6, 6);
} catch (error) {
console.error('non-fatal error: failed to start server on IPv6');
console.error(error);
@ -883,9 +1023,9 @@ async function startHTTPorHTTPS() {
}
}
if (enableIPv4) {
if (useIPv4) {
try {
await createFunc(tavernUrl);
await createFunc(tavernUrl, 4);
} catch (error) {
console.error('non-fatal error: failed to start server on IPv4');
console.error(error);
@ -898,10 +1038,59 @@ async function startHTTPorHTTPS() {
}
async function startServer() {
const [v6Failed, v4Failed] = await startHTTPorHTTPS();
let useIPv6 = (enableIPv6 === true);
let useIPv4 = (enableIPv4 === true);
handleServerListenFail(v6Failed, v4Failed);
postSetupTasks(v6Failed, v4Failed);
let hasIPv6 = false,
hasIPv4 = false,
hasIPv6Local = false,
hasIPv4Local = false,
hasIPv6Any = false,
hasIPv4Any = false;
if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
[hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
if (enableIPv6 === 'auto') {
useIPv6 = hasIPv6;
}
if (hasIPv6) {
if (useIPv6) {
console.log(color.green('IPv6 support detected'));
} else {
console.log('IPv6 support detected (but disabled)');
}
}
hasIPv4 = listen ? hasIPv4Any : hasIPv4Local;
if (enableIPv4 === 'auto') {
useIPv4 = hasIPv4;
}
if (hasIPv4) {
if (useIPv4) {
console.log(color.green('IPv4 support detected'));
} else {
console.log('IPv4 support detected (but disabled)');
}
}
if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
if (!hasIPv6 && !hasIPv4) {
console.error('Both IPv6 and IPv4 are not detected');
process.exit(1);
}
}
}
if (!useIPv6 && !useIPv4) {
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');
process.exit(1);
}
const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
}
async function verifySecuritySettings() {
@ -911,7 +1100,7 @@ async function verifySecuritySettings() {
}
if (!enableAccounts) {
logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');
logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
}
const users = await getAllEnabledUsers();

View File

@ -139,19 +139,19 @@ export const UNSAFE_EXTENSIONS = [
export const GEMINI_SAFETY = [
{
category: 'HARM_CATEGORY_HARASSMENT',
threshold: 'BLOCK_NONE',
threshold: 'OFF',
},
{
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: 'BLOCK_NONE',
threshold: 'OFF',
},
{
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: 'BLOCK_NONE',
threshold: 'OFF',
},
{
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
threshold: 'BLOCK_NONE',
threshold: 'OFF',
},
{
category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [
export const OLLAMA_KEYS = [
'num_predict',
'num_ctx',
'num_batch',
'stop',
'temperature',
'repeat_penalty',
@ -414,3 +415,10 @@ export const VLLM_KEYS = [
'guided_decoding_backend',
'guided_whitespace_pattern',
];
export const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};

View File

@ -32,7 +32,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
max_tokens: 4096,
};
console.log('Multimodal captioning request', body);
console.debug('Multimodal captioning request', body);
const result = await fetch(url, {
body: JSON.stringify(body),
@ -46,14 +46,14 @@ router.post('/caption-image', jsonParser, async (request, response) => {
if (!result.ok) {
const text = await result.text();
console.log(`Claude API returned error: ${result.status} ${result.statusText}`, text);
console.warn(`Claude API returned error: ${result.status} ${result.statusText}`, text);
return response.status(result.status).send({ error: true });
}
/** @type {any} */
const generateResponseJson = await result.json();
const caption = generateResponseJson.content[0].text;
console.log('Claude response:', generateResponseJson);
console.debug('Claude response:', generateResponseJson);
if (!caption) {
return response.status(500).send('No caption found');

View File

@ -176,7 +176,7 @@ router.post('/get', jsonParser, async (request, response) => {
}
}
catch (err) {
console.log(err);
console.error(err);
}
return response.send(output);
});
@ -200,7 +200,7 @@ router.post('/download', jsonParser, async (request, response) => {
category = i;
if (category === null) {
console.debug('Bad request: unsupported asset category.');
console.error('Bad request: unsupported asset category.');
return response.sendStatus(400);
}
@ -212,7 +212,7 @@ router.post('/download', jsonParser, async (request, response) => {
const temp_path = path.join(request.user.directories.assets, 'temp', request.body.filename);
const file_path = path.join(request.user.directories.assets, category, request.body.filename);
console.debug('Request received to download', url, 'to', file_path);
console.info('Request received to download', url, 'to', file_path);
try {
// Download to temp
@ -241,13 +241,13 @@ router.post('/download', jsonParser, async (request, response) => {
}
// Move into asset place
console.debug('Download finished, moving file from', temp_path, 'to', file_path);
console.info('Download finished, moving file from', temp_path, 'to', file_path);
fs.copyFileSync(temp_path, file_path);
fs.rmSync(temp_path);
response.sendStatus(200);
}
catch (error) {
console.log(error);
console.error(error);
response.sendStatus(500);
}
});
@ -270,7 +270,7 @@ router.post('/delete', jsonParser, async (request, response) => {
category = i;
if (category === null) {
console.debug('Bad request: unsupported asset category.');
console.error('Bad request: unsupported asset category.');
return response.sendStatus(400);
}
@ -280,7 +280,7 @@ router.post('/delete', jsonParser, async (request, response) => {
return response.status(400).send(validation.message);
const file_path = path.join(request.user.directories.assets, category, request.body.filename);
console.debug('Request received to delete', category, file_path);
console.info('Request received to delete', category, file_path);
try {
// Delete if previous download failed
@ -288,17 +288,17 @@ router.post('/delete', jsonParser, async (request, response) => {
fs.unlink(file_path, (err) => {
if (err) throw err;
});
console.debug('Asset deleted.');
console.info('Asset deleted.');
}
else {
console.debug('Asset not found.');
console.error('Asset not found.');
response.sendStatus(400);
}
// Move into asset place
response.sendStatus(200);
}
catch (error) {
console.log(error);
console.error(error);
response.sendStatus(500);
}
});
@ -314,6 +314,7 @@ router.post('/delete', jsonParser, async (request, response) => {
*/
router.post('/character', jsonParser, async (request, response) => {
if (request.query.name === undefined) return response.sendStatus(400);
// For backwards compatibility, don't reject invalid character names, just sanitize them
const name = sanitize(request.query.name.toString());
const inputCategory = request.query.category;
@ -325,7 +326,7 @@ router.post('/character', jsonParser, async (request, response) => {
category = i;
if (category === null) {
console.debug('Bad request: unsupported asset category.');
console.error('Bad request: unsupported asset category.');
return response.sendStatus(400);
}
@ -364,7 +365,7 @@ router.post('/character', jsonParser, async (request, response) => {
return response.send(output);
}
catch (err) {
console.log(err);
console.error(err);
return response.sendStatus(500);
}
});

View File

@ -11,14 +11,14 @@ router.post('/list', jsonParser, async (req, res) => {
const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
if (!key) {
console.error('Azure TTS API Key not set');
console.warn('Azure TTS API Key not set');
return res.sendStatus(403);
}
const region = req.body.region;
if (!region) {
console.error('Azure TTS region not set');
console.warn('Azure TTS region not set');
return res.sendStatus(400);
}
@ -32,7 +32,7 @@ router.post('/list', jsonParser, async (req, res) => {
});
if (!response.ok) {
console.error('Azure Request failed', response.status, response.statusText);
console.warn('Azure Request failed', response.status, response.statusText);
return res.sendStatus(500);
}
@ -49,13 +49,13 @@ router.post('/generate', jsonParser, async (req, res) => {
const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
if (!key) {
console.error('Azure TTS API Key not set');
console.warn('Azure TTS API Key not set');
return res.sendStatus(403);
}
const { text, voice, region } = req.body;
if (!text || !voice || !region) {
console.error('Missing required parameters');
console.warn('Missing required parameters');
return res.sendStatus(400);
}
@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async (req, res) => {
});
if (!response.ok) {
console.error('Azure Request failed', response.status, response.statusText);
console.warn('Azure Request failed', response.status, response.statusText);
return res.sendStatus(500);
}

View File

@ -114,7 +114,7 @@ async function sendClaudeRequest(request, response) {
}
if (!apiKey) {
console.log(color.red(`Claude API key is missing.\n${divider}`));
console.warn(color.red(`Claude API key is missing.\n${divider}`));
return response.status(400).send({ error: true });
}
@ -179,7 +179,7 @@ async function sendClaudeRequest(request, response) {
additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
}
console.log('Claude request:', requestBody);
console.debug('Claude request:', requestBody);
const generateResponse = await fetch(apiUrl + '/messages', {
method: 'POST',
@ -199,21 +199,21 @@ async function sendClaudeRequest(request, response) {
} else {
if (!generateResponse.ok) {
const generateResponseText = await generateResponse.text();
console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
return response.status(generateResponse.status).send({ error: true });
}
/** @type {any} */
const generateResponseJson = await generateResponse.json();
const responseText = generateResponseJson?.content?.[0]?.text || '';
console.log('Claude response:', generateResponseJson);
console.debug('Claude response:', generateResponseJson);
// Wrap it back to OAI format + save the original content
const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };
return response.send(reply);
}
} catch (error) {
console.log(color.red(`Error communicating with Claude: ${error}\n${divider}`));
console.error(color.red(`Error communicating with Claude: ${error}\n${divider}`));
if (!response.headersSent) {
return response.status(500).send({ error: true });
}
@ -230,12 +230,12 @@ async function sendScaleRequest(request, response) {
const apiKey = readSecret(request.user.directories, SECRET_KEYS.SCALE);
if (!apiKey) {
console.log('Scale API key is missing.');
console.warn('Scale API key is missing.');
return response.status(400).send({ error: true });
}
const requestPrompt = convertTextCompletionPrompt(request.body.messages);
console.log('Scale request:', requestPrompt);
console.debug('Scale request:', requestPrompt);
try {
const controller = new AbortController();
@ -254,18 +254,18 @@ async function sendScaleRequest(request, response) {
});
if (!generateResponse.ok) {
console.log(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
console.warn(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
return response.status(500).send({ error: true });
}
/** @type {any} */
const generateResponseJson = await generateResponse.json();
console.log('Scale response:', generateResponseJson);
console.debug('Scale response:', generateResponseJson);
const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };
return response.send(reply);
} catch (error) {
console.log(error);
console.error(error);
if (!response.headersSent) {
return response.status(500).send({ error: true });
}
@ -282,13 +282,12 @@ async function sendMakerSuiteRequest(request, response) {
const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
if (!request.body.reverse_proxy && !apiKey) {
console.log('Google AI Studio API key is missing.');
console.warn('Google AI Studio API key is missing.');
return response.status(400).send({ error: true });
}
const model = String(request.body.model);
const stream = Boolean(request.body.stream);
const showThoughts = Boolean(request.body.include_reasoning);
const isThinking = model.includes('thinking');
const generationConfig = {
@ -306,8 +305,9 @@ async function sendMakerSuiteRequest(request, response) {
}
const should_use_system_prompt = (
model.includes('gemini-2.0-pro') ||
model.includes('gemini-2.0-flash') ||
model.includes('gemini-2.0-flash-thinking-exp') ||
model.includes('gemini-2.0-flash-exp') ||
model.includes('gemini-1.5-flash') ||
model.includes('gemini-1.5-pro') ||
model.startsWith('gemini-exp')
@ -316,9 +316,15 @@ async function sendMakerSuiteRequest(request, response) {
const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
let safetySettings = GEMINI_SAFETY;
if (model.includes('gemini-2.0-flash-exp')) {
// These old models do not support setting the threshold to OFF at all.
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)) {
safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));
}
// Interestingly, Gemini 2.0 Flash does support setting the threshold for HARM_CATEGORY_CIVIC_INTEGRITY to OFF.
else if (['gemini-2.0-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash-exp'].includes(model)) {
safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));
}
// Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
let body = {
contents: prompt.contents,
@ -330,17 +336,11 @@ async function sendMakerSuiteRequest(request, response) {
body.systemInstruction = prompt.system_instruction;
}
if (isThinking && showThoughts) {
generationConfig.thinkingConfig = {
includeThoughts: true,
};
}
return body;
}
const body = getGeminiBody();
console.log('Google AI Studio request:', body);
console.debug('Google AI Studio request:', body);
try {
const controller = new AbortController();
@ -366,14 +366,14 @@ async function sendMakerSuiteRequest(request, response) {
// Pipe remote SSE stream to Express response
forwardFetchResponse(generateResponse, response);
} catch (error) {
console.log('Error forwarding streaming response:', error);
console.error('Error forwarding streaming response:', error);
if (!response.headersSent) {
return response.status(500).send({ error: true });
}
}
} else {
if (!generateResponse.ok) {
console.log(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
console.warn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
return response.status(generateResponse.status).send({ error: true });
}
@ -383,7 +383,7 @@ async function sendMakerSuiteRequest(request, response) {
const candidates = generateResponseJson?.candidates;
if (!candidates || candidates.length === 0) {
let message = 'Google AI Studio API returned no candidate';
console.log(message, generateResponseJson);
console.warn(message, generateResponseJson);
if (generateResponseJson?.promptFeedback?.blockReason) {
message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
}
@ -391,12 +391,12 @@ async function sendMakerSuiteRequest(request, response) {
}
const responseContent = candidates[0].content ?? candidates[0].output;
console.log('Google AI Studio response:', responseContent);
console.warn('Google AI Studio response:', responseContent);
const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
if (!responseText) {
let message = 'Google AI Studio Candidate text empty';
console.log(message, generateResponseJson);
console.warn(message, generateResponseJson);
return response.send({ error: { message } });
}
@ -405,7 +405,7 @@ async function sendMakerSuiteRequest(request, response) {
return response.send(reply);
}
} catch (error) {
console.log('Error communicating with Google AI Studio API: ', error);
console.error('Error communicating with Google AI Studio API: ', error);
if (!response.headersSent) {
return response.status(500).send({ error: true });
}
@ -419,8 +419,9 @@ async function sendMakerSuiteRequest(request, response) {
*/
async function sendAI21Request(request, response) {
if (!request.body) return response.sendStatus(400);
const controller = new AbortController();
console.log(request.body.messages);
console.debug(request.body.messages);
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
controller.abort();
@ -446,7 +447,7 @@ async function sendAI21Request(request, response) {
signal: controller.signal,
};
console.log('AI21 request:', body);
console.debug('AI21 request:', body);
try {
const generateResponse = await fetch(API_AI21 + '/chat/completions', options);
@ -455,16 +456,16 @@ async function sendAI21Request(request, response) {
} else {
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.log(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
console.warn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.log('AI21 response:', generateResponseJson);
console.debug('AI21 response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.log('Error communicating with AI21 API: ', error);
console.error('Error communicating with AI21 API: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
@ -483,7 +484,7 @@ async function sendMistralAIRequest(request, response) {
const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
if (!apiKey) {
console.log('MistralAI API key is missing.');
console.warn('MistralAI API key is missing.');
return response.status(400).send({ error: true });
}
@ -524,7 +525,7 @@ async function sendMistralAIRequest(request, response) {
timeout: 0,
};
console.log('MisralAI request:', requestBody);
console.debug('MisralAI request:', requestBody);
const generateResponse = await fetch(apiUrl + '/chat/completions', config);
if (request.body.stream) {
@ -532,16 +533,16 @@ async function sendMistralAIRequest(request, response) {
} else {
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.log(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
console.warn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.log('MistralAI response:', generateResponseJson);
console.debug('MistralAI response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.log('Error communicating with MistralAI API: ', error);
console.error('Error communicating with MistralAI API: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
@ -564,7 +565,7 @@ async function sendCohereRequest(request, response) {
});
if (!apiKey) {
console.log('Cohere API key is missing.');
console.warn('Cohere API key is missing.');
return response.status(400).send({ error: true });
}
@ -603,7 +604,7 @@ async function sendCohereRequest(request, response) {
requestBody.safety_mode = 'OFF';
}
console.log('Cohere request:', requestBody);
console.debug('Cohere request:', requestBody);
const config = {
method: 'POST',
@ -625,16 +626,16 @@ async function sendCohereRequest(request, response) {
const generateResponse = await fetch(apiUrl, config);
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
console.warn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.log('Cohere response:', generateResponseJson);
console.debug('Cohere response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.log('Error communicating with Cohere API: ', error);
console.error('Error communicating with Cohere API: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
@ -653,7 +654,7 @@ async function sendDeepSeekRequest(request, response) {
const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
if (!apiKey && !request.body.reverse_proxy) {
console.log('DeepSeek API key is missing.');
console.warn('DeepSeek API key is missing.');
return response.status(400).send({ error: true });
}
@ -671,6 +672,11 @@ async function sendDeepSeekRequest(request, response) {
bodyParams['logprobs'] = true;
}
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
bodyParams['tools'] = request.body.tools;
bodyParams['tool_choice'] = request.body.tool_choice;
}
const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
@ -698,7 +704,7 @@ async function sendDeepSeekRequest(request, response) {
signal: controller.signal,
};
console.log('DeepSeek request:', requestBody);
console.debug('DeepSeek request:', requestBody);
const generateResponse = await fetch(apiUrl + '/chat/completions', config);
@ -707,16 +713,16 @@ async function sendDeepSeekRequest(request, response) {
} else {
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
console.warn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.log('DeepSeek response:', generateResponseJson);
console.debug('DeepSeek response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.log('Error communicating with DeepSeek API: ', error);
console.error('Error communicating with DeepSeek API: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
@ -774,12 +780,12 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
headers = {};
} else {
console.log('This chat completion source is not supported yet.');
console.warn('This chat completion source is not supported yet.');
return response_getstatus_openai.status(400).send({ error: true });
}
if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
console.log('Chat Completion API key is missing.');
console.warn('Chat Completion API key is missing.');
return response_getstatus_openai.status(400).send({ error: true });
}
@ -814,23 +820,23 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
};
});
console.log('Available OpenRouter models:', models);
console.info('Available OpenRouter models:', models);
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
const models = data?.data;
console.log(models);
console.info(models);
} else {
const models = data?.data;
if (Array.isArray(models)) {
const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
console.log('Available models:', modelIds);
console.info('Available models:', modelIds);
} else {
console.log('Chat Completion endpoint did not return a list of models.');
console.warn('Chat Completion endpoint did not return a list of models.');
}
}
}
else {
console.log('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');
console.error('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');
response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });
}
} catch (e) {
@ -863,7 +869,7 @@ router.post('/bias', jsonParser, async function (request, response) {
const tokenizer = getSentencepiceTokenizer(model);
const instance = await tokenizer?.get();
if (!instance) {
console.warn('Tokenizer not initialized:', model);
console.error('Tokenizer not initialized:', model);
return response.send({});
}
encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));
@ -1025,7 +1031,7 @@ router.post('/generate', jsonParser, function (request, response) {
mergeObjectWithYaml(headers, request.body.custom_include_headers);
if (request.body.custom_prompt_post_processing) {
console.log('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
console.info('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
request.body.messages = postProcessPrompt(
request.body.messages,
request.body.custom_prompt_post_processing,
@ -1058,12 +1064,19 @@ router.post('/generate', jsonParser, function (request, response) {
headers = {};
bodyParams = {};
} else {
console.log('This chat completion source is not supported yet.');
console.warn('This chat completion source is not supported yet.');
return response.status(400).send({ error: true });
}
// A few of OpenAIs reasoning models support reasoning effort
if ([CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
if (['o1', 'o3-mini', 'o3-mini-2025-01-31'].includes(request.body.model)) {
bodyParams['reasoning_effort'] = request.body.reasoning_effort;
}
}
if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
console.log('OpenAI API key is missing.');
console.warn('OpenAI API key is missing.');
return response.status(400).send({ error: true });
}
@ -1123,7 +1136,7 @@ router.post('/generate', jsonParser, function (request, response) {
signal: controller.signal,
};
console.log(requestBody);
console.debug(requestBody);
makeRequest(config, response, request);
@ -1140,7 +1153,7 @@ router.post('/generate', jsonParser, function (request, response) {
const fetchResponse = await fetch(endpointUrl, config);
if (request.body.stream) {
console.log('Streaming request in progress');
console.info('Streaming request in progress');
forwardFetchResponse(fetchResponse, response);
return;
}
@ -1149,10 +1162,10 @@ router.post('/generate', jsonParser, function (request, response) {
/** @type {any} */
let json = await fetchResponse.json();
response.send(json);
console.log(json);
console.log(json?.choices?.[0]?.message);
console.debug(json);
console.debug(json?.choices?.[0]?.message);
} else if (fetchResponse.status === 429 && retries > 0) {
console.log(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
console.warn(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
setTimeout(() => {
timeout *= 2;
makeRequest(config, response, request, retries - 1, timeout);
@ -1161,7 +1174,7 @@ router.post('/generate', jsonParser, function (request, response) {
await handleErrorResponse(fetchResponse);
}
} catch (error) {
console.log('Generation failed', error);
console.error('Generation failed', error);
const message = error.code === 'ECONNREFUSED'
? `Connection refused: ${error.message}`
: error.message || 'Unknown error occurred';
@ -1183,7 +1196,7 @@ router.post('/generate', jsonParser, function (request, response) {
const message = errorResponse.statusText || 'Unknown error occurred';
const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';
console.log('Chat completion request error: ', message, responseText);
console.error('Chat completion request error: ', message, responseText);
if (!response.headersSent) {
response.send({ error: { message }, quota_error: quota_error });

View File

@ -22,17 +22,17 @@ router.post('/generate', jsonParser, async function (request, response_generate)
request.socket.on('close', async function () {
if (request.body.can_abort && !response_generate.writableEnded) {
try {
console.log('Aborting Kobold generation...');
console.info('Aborting Kobold generation...');
// send abort signal to koboldcpp
const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {
method: 'POST',
});
if (!abortResponse.ok) {
console.log('Error sending abort request to Kobold:', abortResponse.status);
console.error('Error sending abort request to Kobold:', abortResponse.status);
}
} catch (error) {
console.log(error);
console.error(error);
}
}
controller.abort();
@ -81,7 +81,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
}
}
console.log(this_settings);
console.debug(this_settings);
const args = {
body: JSON.stringify(this_settings),
headers: Object.assign(
@ -105,7 +105,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
} else {
if (!response.ok) {
const errorText = await response.text();
console.log(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);
console.warn(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);
try {
const errorJson = JSON.parse(errorText);
@ -117,7 +117,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
}
const data = await response.json();
console.log('Endpoint response:', data);
console.debug('Endpoint response:', data);
return response_generate.send(data);
}
} catch (error) {
@ -125,19 +125,19 @@ router.post('/generate', jsonParser, async function (request, response_generate)
switch (error?.status) {
case 403:
case 503: // retry in case of temporary service issue, possibly caused by a queue failure?
console.debug(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);
console.warn(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);
await delay(delayAmount);
break;
default:
if ('status' in error) {
console.log('Status Code from Kobold:', error.status);
console.error('Status Code from Kobold:', error.status);
}
return response_generate.send({ error: true });
}
}
}
console.log('Max retries exceeded. Giving up.');
console.error('Max retries exceeded. Giving up.');
return response_generate.send({ error: true });
});
@ -193,16 +193,16 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
const server = request.body.server;
if (!server) {
console.log('Server is not set');
console.error('Server is not set');
return response.sendStatus(400);
}
if (!request.file) {
console.log('No audio file found');
console.error('No audio file found');
return response.sendStatus(400);
}
console.log('Transcribing audio with KoboldCpp', server);
console.debug('Transcribing audio with KoboldCpp', server);
const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
fs.rmSync(request.file.path);
@ -226,12 +226,12 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
if (!result.ok) {
const text = await result.text();
console.log('KoboldCpp request failed', result.statusText, text);
console.error('KoboldCpp request failed', result.statusText, text);
return response.status(500).send(text);
}
const data = await result.json();
console.log('KoboldCpp transcription response', data);
console.debug('KoboldCpp transcription response', data);
return response.json(data);
} catch (error) {
console.error('KoboldCpp transcription failed', error);

View File

@ -13,7 +13,7 @@ router.post('/generate', jsonParser, async function (request, response) {
const cookie = readSecret(request.user.directories, SECRET_KEYS.SCALE_COOKIE);
if (!cookie) {
console.log('No Scale cookie found');
console.error('No Scale cookie found');
return response.sendStatus(400);
}
@ -62,7 +62,7 @@ router.post('/generate', jsonParser, async function (request, response) {
},
};
console.log('Scale request:', body);
console.debug('Scale request:', body);
const result = await fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {
method: 'POST',
@ -75,7 +75,7 @@ router.post('/generate', jsonParser, async function (request, response) {
if (!result.ok) {
const text = await result.text();
console.log('Scale request failed', result.statusText, text);
console.error('Scale request failed', result.statusText, text);
return response.status(500).send({ error: { message: result.statusText } });
}
@ -83,7 +83,7 @@ router.post('/generate', jsonParser, async function (request, response) {
const data = await result.json();
const output = data?.result?.data?.json?.outputs?.[0] || '';
console.log('Scale response:', data);
console.debug('Scale response:', data);
if (!output) {
console.warn('Scale response is empty');
@ -92,7 +92,7 @@ router.post('/generate', jsonParser, async function (request, response) {
return response.json({ output });
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});

View File

@ -58,12 +58,12 @@ async function parseOllamaStream(jsonStream, request, response) {
});
jsonStream.body.on('end', () => {
console.log('Streaming request finished');
console.info('Streaming request finished');
response.write('data: [DONE]\n\n');
response.end();
});
} catch (error) {
console.log('Error forwarding streaming response:', error);
console.error('Error forwarding streaming response:', error);
if (!response.headersSent) {
return response.status(500).send({ error: true });
} else {
@ -79,16 +79,16 @@ async function parseOllamaStream(jsonStream, request, response) {
*/
async function abortKoboldCppRequest(url) {
try {
console.log('Aborting Kobold generation...');
console.info('Aborting Kobold generation...');
const abortResponse = await fetch(`${url}/api/extra/abort`, {
method: 'POST',
});
if (!abortResponse.ok) {
console.log('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
console.error('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
}
} catch (error) {
console.log(error);
console.error(error);
}
}
@ -101,7 +101,7 @@ router.post('/status', jsonParser, async function (request, response) {
request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
}
console.log('Trying to connect to API:', request.body);
console.debug('Trying to connect to API', request.body);
const baseUrl = trimV1(request.body.api_server);
const args = {
@ -123,6 +123,7 @@ router.post('/status', jsonParser, async function (request, response) {
case TEXTGEN_TYPES.LLAMACPP:
case TEXTGEN_TYPES.INFERMATICAI:
case TEXTGEN_TYPES.OPENROUTER:
case TEXTGEN_TYPES.FEATHERLESS:
url += '/v1/models';
break;
case TEXTGEN_TYPES.DREAMGEN:
@ -140,9 +141,6 @@ router.post('/status', jsonParser, async function (request, response) {
case TEXTGEN_TYPES.OLLAMA:
url += '/api/tags';
break;
case TEXTGEN_TYPES.FEATHERLESS:
url += '/v1/models';
break;
case TEXTGEN_TYPES.HUGGINGFACE:
url += '/info';
break;
@ -152,7 +150,7 @@ router.post('/status', jsonParser, async function (request, response) {
const isPossiblyLmStudio = modelsReply.headers.get('x-powered-by') === 'Express';
if (!modelsReply.ok) {
console.log('Models endpoint is offline.');
console.error('Models endpoint is offline.');
return response.sendStatus(400);
}
@ -173,12 +171,12 @@ router.post('/status', jsonParser, async function (request, response) {
}
if (!Array.isArray(data.data)) {
console.log('Models response is not an array.');
console.error('Models response is not an array.');
return response.sendStatus(400);
}
const modelIds = data.data.map(x => x.id);
console.log('Models available:', modelIds);
console.info('Models available:', modelIds);
// Set result to the first model ID
result = modelIds[0] || 'Valid';
@ -191,7 +189,7 @@ router.post('/status', jsonParser, async function (request, response) {
if (modelInfoReply.ok) {
/** @type {any} */
const modelInfo = await modelInfoReply.json();
console.log('Ooba model info:', modelInfo);
console.debug('Ooba model info:', modelInfo);
const modelName = modelInfo?.model_name;
result = modelName || result;
@ -208,7 +206,7 @@ router.post('/status', jsonParser, async function (request, response) {
if (modelInfoReply.ok) {
/** @type {any} */
const modelInfo = await modelInfoReply.json();
console.log('Tabby model info:', modelInfo);
console.debug('Tabby model info:', modelInfo);
const modelName = modelInfo?.id;
result = modelName || result;
@ -255,7 +253,7 @@ router.post('/props', jsonParser, async function (request, response) {
props['chat_template'] = props['chat_template'].slice(0, -1) + '\n';
}
props['chat_template_hash'] = createHash('sha256').update(props['chat_template']).digest('hex');
console.log(`Model properties: ${JSON.stringify(props)}`);
console.debug(`Model properties: ${JSON.stringify(props)}`);
return response.send(props);
} catch (error) {
console.error(error);
@ -273,7 +271,7 @@ router.post('/generate', jsonParser, async function (request, response) {
const apiType = request.body.api_type;
const baseUrl = request.body.api_server;
console.log(request.body);
console.debug(request.body);
const controller = new AbortController();
request.socket.removeAllListeners('close');
@ -375,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) {
if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
const keepAlive = getConfigValue('ollama.keepAlive', -1);
const numBatch = getConfigValue('ollama.batchSize', -1);
if (numBatch > 0) {
request.body['num_batch'] = numBatch;
}
args.body = JSON.stringify({
model: request.body.model,
prompt: request.body.prompt,
@ -399,7 +401,7 @@ router.post('/generate', jsonParser, async function (request, response) {
if (completionsReply.ok) {
/** @type {any} */
const data = await completionsReply.json();
console.log('Endpoint response:', data);
console.debug('Endpoint response:', data);
// Map InfermaticAI response to OAI completions format
if (apiType === TEXTGEN_TYPES.INFERMATICAI) {
@ -411,24 +413,20 @@ router.post('/generate', jsonParser, async function (request, response) {
const text = await completionsReply.text();
const errorBody = { error: true, status: completionsReply.status, response: text };
if (!response.headersSent) {
return response.send(errorBody);
}
return response.end();
return !response.headersSent
? response.send(errorBody)
: response.end();
}
}
} catch (error) {
const status = error?.status ?? error?.code ?? 'UNKNOWN';
const text = error?.error ?? error?.statusText ?? error?.message ?? 'Unknown error on /generate endpoint';
let value = { error: true, status: status, response: text };
console.log('Endpoint error:', error);
console.error('Endpoint error:', error);
if (!response.headersSent) {
return response.send(value);
}
return response.end();
return !response.headersSent
? response.send(value)
: response.end();
}
});
@ -451,7 +449,7 @@ ollama.post('/download', jsonParser, async function (request, response) {
});
if (!fetchResponse.ok) {
console.log('Download error:', fetchResponse.status, fetchResponse.statusText);
console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
return response.status(fetchResponse.status).send({ error: true });
}
@ -468,7 +466,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
return response.sendStatus(400);
}
console.log('Ollama caption request:', request.body);
console.debug('Ollama caption request:', request.body);
const baseUrl = trimV1(request.body.server_url);
const fetchResponse = await fetch(`${baseUrl}/api/generate`, {
@ -483,18 +481,18 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
});
if (!fetchResponse.ok) {
console.log('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);
console.error('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);
return response.status(500).send({ error: true });
}
/** @type {any} */
const data = await fetchResponse.json();
console.log('Ollama caption response:', data);
console.debug('Ollama caption response:', data);
const caption = data?.response || '';
if (!caption) {
console.log('Ollama caption is empty.');
console.error('Ollama caption is empty.');
return response.status(500).send({ error: true });
}
@ -513,7 +511,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
return response.sendStatus(400);
}
console.log('LlamaCpp caption request:', request.body);
console.debug('LlamaCpp caption request:', request.body);
const baseUrl = trimV1(request.body.server_url);
const fetchResponse = await fetch(`${baseUrl}/completion`, {
@ -529,18 +527,18 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
});
if (!fetchResponse.ok) {
console.log('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);
console.error('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);
return response.status(500).send({ error: true });
}
/** @type {any} */
const data = await fetchResponse.json();
console.log('LlamaCpp caption response:', data);
console.debug('LlamaCpp caption response:', data);
const caption = data?.content || '';
if (!caption) {
console.log('LlamaCpp caption is empty.');
console.error('LlamaCpp caption is empty.');
return response.status(500).send({ error: true });
}
@ -558,7 +556,7 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
return response.sendStatus(400);
}
console.log('LlamaCpp props request:', request.body);
console.debug('LlamaCpp props request:', request.body);
const baseUrl = trimV1(request.body.server_url);
const fetchResponse = await fetch(`${baseUrl}/props`, {
@ -566,12 +564,12 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
});
if (!fetchResponse.ok) {
console.log('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);
console.error('LlamaCpp props error:', fetchResponse.status, fetchResponse.statusText);
return response.status(500).send({ error: true });
}
const data = await fetchResponse.json();
console.log('LlamaCpp props response:', data);
console.debug('LlamaCpp props response:', data);
return response.send(data);
@ -590,7 +588,7 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
return response.sendStatus(400);
}
console.log('LlamaCpp slots request:', request.body);
console.debug('LlamaCpp slots request:', request.body);
const baseUrl = trimV1(request.body.server_url);
let fetchResponse;
@ -616,12 +614,12 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
}
if (!fetchResponse.ok) {
console.log('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);
console.error('LlamaCpp slots error:', fetchResponse.status, fetchResponse.statusText);
return response.status(500).send({ error: true });
}
const data = await fetchResponse.json();
console.log('LlamaCpp slots response:', data);
console.debug('LlamaCpp slots response:', data);
return response.send(data);
@ -659,14 +657,14 @@ tabby.post('/download', jsonParser, async function (request, response) {
return response.status(403).send({ error: true });
}
} else {
console.log('API Permission error:', permissionResponse.status, permissionResponse.statusText);
console.error('API Permission error:', permissionResponse.status, permissionResponse.statusText);
return response.status(permissionResponse.status).send({ error: true });
}
const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
if (!fetchResponse.ok) {
console.log('Download error:', fetchResponse.status, fetchResponse.statusText);
console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
return response.status(fetchResponse.status).send({ error: true });
}

View File

@ -27,7 +27,7 @@ router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function
const fileName = path.join(request.user.directories.backgrounds, sanitize(request.body.bg));
if (!fs.existsSync(fileName)) {
console.log('BG file not found');
console.error('BG file not found');
return response.sendStatus(400);
}
@ -43,12 +43,12 @@ router.post('/rename', jsonParser, function (request, response) {
const newFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.new_bg));
if (!fs.existsSync(oldFileName)) {
console.log('BG file not found');
console.error('BG file not found');
return response.sendStatus(400);
}
if (fs.existsSync(newFileName)) {
console.log('New BG file already exists');
console.error('New BG file already exists');
return response.sendStatus(400);
}

View File

@ -2,10 +2,10 @@ import express from 'express';
import { jsonParser } from '../express-common.js';
import { getPipeline, getRawImage } from '../transformers.js';
const TASK = 'image-to-text';
export const router = express.Router();
const TASK = 'image-to-text';
router.post('/', jsonParser, async (req, res) => {
try {
const { image } = req.body;
@ -13,14 +13,14 @@ router.post('/', jsonParser, async (req, res) => {
const rawImage = await getRawImage(image);
if (!rawImage) {
console.log('Failed to parse captioned image');
console.warn('Failed to parse captioned image');
return res.sendStatus(400);
}
const pipe = await getPipeline(TASK);
const result = await pipe(rawImage);
const text = result[0].generated_text;
console.log('Image caption:', text);
console.info('Image caption:', text);
return res.json({ caption: text });
} catch (error) {

View File

@ -97,7 +97,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
writeFileAtomicSync(outputImagePath, outputImage);
return true;
} catch (err) {
console.log(err);
console.error(err);
return false;
}
}
@ -166,7 +166,7 @@ async function tryReadImage(imgPath, crop) {
}
// If it's an unsupported type of image (APNG) - just read the file as buffer
catch (error) {
console.log(`Failed to read image: ${imgPath}`, error);
console.error(`Failed to read image: ${imgPath}`, error);
return fs.readFileSync(imgPath);
}
}
@ -229,12 +229,12 @@ const processCharacter = async (item, directories) => {
return character;
}
catch (err) {
console.log(`Could not process character: ${item}`);
console.error(`Could not process character: ${item}`);
if (err instanceof SyntaxError) {
console.log(`${item} does not contain a valid JSON object.`);
console.error(`${item} does not contain a valid JSON object.`);
} else {
console.log('An unexpected error occurred: ', err);
console.error('An unexpected error occurred: ', err);
}
return {
@ -322,7 +322,7 @@ function readFromV2(char) {
};
_.forEach(fieldMappings, (v2Path, charField) => {
//console.log(`Migrating field: ${charField} from ${v2Path}`);
//console.info(`Migrating field: ${charField} from ${v2Path}`);
const v2Value = _.get(char.data, v2Path);
if (_.isUndefined(v2Value)) {
let defaultValue = undefined;
@ -337,15 +337,15 @@ function readFromV2(char) {
}
if (!_.isUndefined(defaultValue)) {
//console.debug(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
//console.warn(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
char[charField] = defaultValue;
} else {
console.debug(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
console.warn(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
return;
}
}
if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {
console.debug(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
console.warn(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
}
char[charField] = v2Value;
});
@ -442,7 +442,7 @@ function charaFormatData(data, directories) {
}
} catch {
console.debug(`Failed to read world info file: ${data.world}. Character book will not be available.`);
console.warn(`Failed to read world info file: ${data.world}. Character book will not be available.`);
}
}
@ -452,7 +452,7 @@ function charaFormatData(data, directories) {
// Deep merge the extensions object
_.set(char, 'data.extensions', deepMerge(char.data.extensions, extensions));
} catch {
console.debug(`Failed to parse extensions JSON: ${data.extensions}`);
console.warn(`Failed to parse extensions JSON: ${data.extensions}`);
}
}
@ -526,7 +526,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
const fileText = fs.readFileSync(uploadPath, 'utf8');
fs.rmSync(uploadPath);
const yamlData = yaml.parse(fileText);
console.log('Importing from YAML');
console.info('Importing from YAML');
yamlData.name = sanitize(yamlData.name);
const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
let char = convertToV2({
@ -559,7 +559,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
async function importFromCharX(uploadPath, { request }, preservedFileName) {
const data = fs.readFileSync(uploadPath).buffer;
fs.rmSync(uploadPath);
console.log('Importing from CharX');
console.info('Importing from CharX');
const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
if (!cardBuffer) {
@ -608,7 +608,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
let jsonData = JSON.parse(data);
if (jsonData.spec !== undefined) {
console.log(`Importing from ${jsonData.spec} json`);
console.info(`Importing from ${jsonData.spec} json`);
importRisuSprites(request.user.directories, jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
@ -618,7 +618,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
return result ? pngName : '';
} else if (jsonData.name !== undefined) {
console.log('Importing from v1 json');
console.info('Importing from v1 json');
jsonData.name = sanitize(jsonData.name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@ -644,7 +644,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
return result ? pngName : '';
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
console.log('Importing from gradio json');
console.info('Importing from gradio json');
jsonData.char_name = sanitize(jsonData.char_name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@ -691,7 +691,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
if (jsonData.spec !== undefined) {
console.log(`Found a ${jsonData.spec} character file.`);
console.info(`Found a ${jsonData.spec} character file.`);
importRisuSprites(request.user.directories, jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
@ -701,7 +701,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
fs.unlinkSync(uploadPath);
return result ? pngName : '';
} else if (jsonData.name !== undefined) {
console.log('Found a v1 character file.');
console.info('Found a v1 character file.');
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
@ -812,13 +812,13 @@ router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (
router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
if (!request.body) {
console.error('Error: no response body detected');
console.warn('Error: no response body detected');
response.status(400).send('Error: no response body detected');
return;
}
if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
console.error('Error: invalid name.');
console.warn('Error: invalid name.');
response.status(400).send('Error: invalid name.');
return;
}
@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
fs.unlinkSync(newAvatarPath);
// Bust cache to reload the new avatar
response.setHeader('Clear-Site-Data', '"cache"');
}
return response.sendStatus(200);
@ -860,14 +863,14 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
* @returns {void}
*/
router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
console.log(request.body);
console.debug(request.body);
if (!request.body) {
console.error('Error: no response body detected');
console.warn('Error: no response body detected');
return response.status(400).send('Error: no response body detected');
}
if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
console.error('Error: invalid name.');
console.warn('Error: invalid name.');
return response.status(400).send('Error: invalid name.');
}
@ -879,7 +882,7 @@ router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async fu
const char = JSON.parse(charJSON);
//check if the field exists
if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
console.error('Error: invalid field.');
console.warn('Error: invalid field.');
response.status(400).send('Error: invalid field.');
return;
}
@ -928,7 +931,7 @@ router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avat
await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);
response.sendStatus(200);
} else {
console.log(validator.lastValidationError);
console.warn(validator.lastValidationError);
response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });
}
} catch (exception) {
@ -1050,7 +1053,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
if (stats.size === 0) {
console.log(`Found an empty chat file: ${pathToFile}`);
console.warn(`Found an empty chat file: ${pathToFile}`);
res({});
return;
}
@ -1082,7 +1085,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
res(chatData);
} else {
console.log('Found an invalid or corrupted chat file:', pathToFile);
console.warn('Found an invalid or corrupted chat file:', pathToFile);
res({});
}
}
@ -1095,7 +1098,7 @@ router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (r
return response.send(validFiles);
} catch (error) {
console.log(error);
console.error(error);
return response.send({ error: true });
}
});
@ -1152,7 +1155,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);
if (!fileName) {
console.error('Failed to import character');
console.warn('Failed to import character');
return response.sendStatus(400);
}
@ -1162,7 +1165,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
response.send({ file_name: fileName });
} catch (err) {
console.log(err);
console.error(err);
response.send({ error: true });
}
});
@ -1170,14 +1173,13 @@ router.post('/import', urlencodedParser, async function (request, response) {
router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
try {
if (!request.body.avatar_url) {
console.log('avatar URL not found in request body');
console.log(request.body);
console.warn('avatar URL not found in request body');
console.debug(request.body);
return response.sendStatus(400);
}
let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
if (!fs.existsSync(filename)) {
console.log('file for dupe not found');
console.log(filename);
console.error('file for dupe not found', filename);
return response.sendStatus(404);
}
let suffix = 1;
@ -1205,7 +1207,7 @@ router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async functio
}
fs.copyFileSync(filename, newFilename);
console.log(`${filename} was copied to ${newFilename}`);
console.info(`${filename} was copied to ${newFilename}`);
response.send({ path: path.parse(newFilename).base });
}
catch (error) {

View File

@ -50,7 +50,7 @@ function backupChat(directory, name, chat) {
removeOldBackups(directory, 'chat_', maxTotalChatBackups);
} catch (err) {
console.log(`Could not backup chat for ${name}`, err);
console.error(`Could not backup chat for ${name}`, err);
}
}
@ -306,8 +306,8 @@ router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request,
getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);
return response.send({ result: 'ok' });
} catch (error) {
response.send(error);
return console.log(error);
console.error(error);
return response.send(error);
}
});
@ -359,17 +359,17 @@ router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (
const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file));
const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file));
const sanitizedFileName = path.parse(pathToRenamedFile).name;
console.log('Old chat name', pathToOriginalFile);
console.log('New chat name', pathToRenamedFile);
console.info('Old chat name', pathToOriginalFile);
console.info('New chat name', pathToRenamedFile);
if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {
console.log('Either Source or Destination files are not available');
console.error('Either Source or Destination files are not available');
return response.status(400).send({ error: true });
}
fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
fs.rmSync(pathToOriginalFile);
console.log('Successfully renamed.');
console.info('Successfully renamed.');
return response.send({ ok: true, sanitizedFileName });
});
@ -380,12 +380,12 @@ router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (reques
const chatFileExists = fs.existsSync(filePath);
if (!chatFileExists) {
console.log(`Chat file not found '${filePath}'`);
console.error(`Chat file not found '${filePath}'`);
return response.sendStatus(400);
}
fs.rmSync(filePath);
console.log('Deleted chat file: ' + filePath);
console.info(`Deleted chat file: ${filePath}`);
return response.send('ok');
});
@ -402,7 +402,7 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
const errorMessage = {
message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
};
console.log(errorMessage.message);
console.error(errorMessage.message);
return response.status(404).json(errorMessage);
}
try {
@ -415,14 +415,14 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
result: rawFile,
};
console.log(`Chat exported as ${exportfilename}`);
console.info(`Chat exported as ${exportfilename}`);
return response.status(200).json(successMessage);
} catch (err) {
console.error(err);
const errorMessage = {
message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
};
console.log(errorMessage.message);
console.error(errorMessage.message);
return response.status(500).json(errorMessage);
}
}
@ -449,12 +449,11 @@ router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (
message: `Chat saved to ${exportfilename}`,
result: buffer,
};
console.log(`Chat exported as ${exportfilename}`);
console.info(`Chat exported as ${exportfilename}`);
return response.status(200).json(successMessage);
});
} catch (err) {
console.log('chat export failed.');
console.log(err);
console.error('chat export failed.', err);
return response.sendStatus(400);
}
});
@ -513,7 +512,7 @@ router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (
} else if (jsonData.type === 'risuChat') { // RisuAI format
importFunc = importRisuChat;
} else { // Unknown format
console.log('Incorrect chat format .json');
console.error('Incorrect chat format .json');
return response.send({ error: true });
}
@ -541,7 +540,7 @@ router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (
const jsonData = JSON.parse(header);
if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {
console.log('Incorrect chat format .jsonl');
console.error('Incorrect chat format .jsonl');
return response.send({ error: true });
}
@ -647,7 +646,7 @@ router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (reques
break;
}
} catch (error) {
console.error(groupFile, 'group file is corrupted:', error);
console.warn(groupFile, 'group file is corrupted:', error);
}
}

View File

@ -44,9 +44,9 @@ router.post('/', jsonParser, async (req, res) => {
}
}
console.log('Classify input:', text);
console.debug('Classify input:', text);
const result = await getResult(text);
console.log('Classify output:', result);
console.debug('Classify output:', result);
return res.json({ classification: result });
} catch (error) {

View File

@ -71,7 +71,7 @@ export function getDefaultPresets(directories) {
return presets;
} catch (err) {
console.log('Failed to get default presets', err);
console.warn('Failed to get default presets', err);
return [];
}
}
@ -92,7 +92,7 @@ export function getDefaultPresetFile(filename) {
const fileContent = fs.readFileSync(contentPath, 'utf8');
return JSON.parse(fileContent);
} catch (err) {
console.log(`Failed to get default file ${filename}`, err);
console.warn(`Failed to get default file ${filename}`, err);
return null;
}
}
@ -121,21 +121,21 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
}
if (!contentItem.folder) {
console.log(`Content file ${contentItem.filename} has no parent folder`);
console.warn(`Content file ${contentItem.filename} has no parent folder`);
continue;
}
const contentPath = path.join(contentItem.folder, contentItem.filename);
if (!fs.existsSync(contentPath)) {
console.log(`Content file ${contentItem.filename} is missing`);
console.warn(`Content file ${contentItem.filename} is missing`);
continue;
}
const contentTarget = getTargetByType(contentItem.type, directories);
if (!contentTarget) {
console.log(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);
console.warn(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);
continue;
}
@ -144,12 +144,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
contentLog.push(contentItem.filename);
if (fs.existsSync(targetPath)) {
console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
console.warn(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
continue;
}
fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
console.log(`Content file ${contentItem.filename} copied to ${contentTarget}`);
console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);
anyContentAdded = true;
}
@ -182,12 +182,12 @@ export async function checkForNewContent(directoriesList, forceCategories = [])
}
if (anyContentAdded && !contentCheckSkip && forceCategories?.length === 0) {
console.log();
console.log(`${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.')}`);
console.log();
console.info();
console.info(`${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.')}`);
console.info();
}
} catch (err) {
console.log('Content check failed', err);
console.error('Content check failed', err);
}
}
@ -331,7 +331,7 @@ async function downloadChubLorebook(id) {
if (!result.ok) {
const text = await result.text();
console.log('Chub returned error', result.statusText, text);
console.error('Chub returned error', result.statusText, text);
throw new Error('Failed to download lorebook');
}
@ -355,7 +355,7 @@ async function downloadChubCharacter(id) {
if (!result.ok) {
const text = await result.text();
console.log('Chub returned error', result.statusText, text);
console.error('Chub returned error', result.statusText, text);
throw new Error('Failed to download character');
}
@ -376,7 +376,7 @@ async function downloadPygmalionCharacter(id) {
if (!result.ok) {
const text = await result.text();
console.log('Pygsite returned error', result.status, text);
console.error('Pygsite returned error', result.status, text);
throw new Error('Failed to download character');
}
@ -485,7 +485,7 @@ async function downloadJannyCharacter(uuid) {
}
}
console.log('Janny returned error', result.statusText, await result.text());
console.error('Janny returned error', result.statusText, await result.text());
throw new Error('Failed to download character');
}
@ -577,7 +577,7 @@ async function downloadRisuCharacter(uuid) {
if (!result.ok) {
const text = await result.text();
console.log('RisuAI returned error', result.statusText, text);
console.error('RisuAI returned error', result.statusText, text);
throw new Error('Failed to download character');
}
@ -673,11 +673,11 @@ router.post('/importURL', jsonParser, async (request, response) => {
type = chubParsed?.type;
if (chubParsed?.type === 'character') {
console.log('Downloading chub character:', chubParsed.id);
console.info('Downloading chub character:', chubParsed.id);
result = await downloadChubCharacter(chubParsed.id);
}
else if (chubParsed?.type === 'lorebook') {
console.log('Downloading chub lorebook:', chubParsed.id);
console.info('Downloading chub lorebook:', chubParsed.id);
result = await downloadChubLorebook(chubParsed.id);
}
else {
@ -692,7 +692,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
type = 'character';
result = await downloadRisuCharacter(uuid);
} else if (isGeneric) {
console.log('Downloading from generic url.');
console.info('Downloading from generic url.');
type = 'character';
result = await downloadGenericPng(url);
} else {
@ -708,7 +708,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
response.set('X-Custom-Content-Type', type);
return response.send(result.buffer);
} catch (error) {
console.log('Importing custom content failed', error);
console.error('Importing custom content failed', error);
return response.sendStatus(500);
}
});
@ -728,22 +728,22 @@ router.post('/importUUID', jsonParser, async (request, response) => {
const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
if (isPygmalion) {
console.log('Downloading Pygmalion character:', uuid);
console.info('Downloading Pygmalion character:', uuid);
result = await downloadPygmalionCharacter(uuid);
} else if (isJannny) {
console.log('Downloading Janitor character:', uuid.split('_')[0]);
console.info('Downloading Janitor character:', uuid.split('_')[0]);
result = await downloadJannyCharacter(uuid.split('_')[0]);
} else if (isAICC) {
const [, author, card] = uuid.split('/');
console.log('Downloading AICC character:', `${author}/${card}`);
console.info('Downloading AICC character:', `${author}/${card}`);
result = await downloadAICCCharacter(`${author}/${card}`);
} else {
if (uuidType === 'character') {
console.log('Downloading chub character:', uuid);
console.info('Downloading chub character:', uuid);
result = await downloadChubCharacter(uuid);
}
else if (uuidType === 'lorebook') {
console.log('Downloading chub lorebook:', uuid);
console.info('Downloading chub lorebook:', uuid);
result = await downloadChubLorebook(uuid);
}
else {
@ -756,7 +756,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
response.set('X-Custom-Content-Type', uuidType);
return response.send(result.buffer);
} catch (error) {
console.log('Importing custom content failed', error);
console.error('Importing custom content failed', error);
return response.sendStatus(500);
}
});

View File

@ -80,7 +80,7 @@ router.post('/install', jsonParser, async (request, response) => {
const { url, global } = request.body;
if (global && !request.user.profile.admin) {
console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
return response.status(403).send('Forbidden: No permission to install global extensions.');
}
@ -92,13 +92,13 @@ router.post('/install', jsonParser, async (request, response) => {
}
await git.clone(url, extensionPath, { '--depth': 1 });
console.log(`Extension has been cloned at ${extensionPath}`);
console.info(`Extension has been cloned at ${extensionPath}`);
const { version, author, display_name } = await getManifest(extensionPath);
return response.send({ version, author, display_name, extensionPath });
} catch (error) {
console.log('Importing custom content failed', error);
console.error('Importing custom content failed', error);
return response.status(500).send(`Server Error: ${error.message}`);
}
});
@ -124,7 +124,7 @@ router.post('/update', jsonParser, async (request, response) => {
const { extensionName, global } = request.body;
if (global && !request.user.profile.admin) {
console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
return response.status(403).send('Forbidden: No permission to update global extensions.');
}
@ -139,9 +139,9 @@ router.post('/update', jsonParser, async (request, response) => {
const currentBranch = await git.cwd(extensionPath).branch();
if (!isUpToDate) {
await git.cwd(extensionPath).pull('origin', currentBranch.current);
console.log(`Extension has been updated at ${extensionPath}`);
console.info(`Extension has been updated at ${extensionPath}`);
} else {
console.log(`Extension is up to date at ${extensionPath}`);
console.info(`Extension is up to date at ${extensionPath}`);
}
await git.cwd(extensionPath).fetch('origin');
const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
@ -150,7 +150,7 @@ router.post('/update', jsonParser, async (request, response) => {
return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
} catch (error) {
console.log('Updating custom content failed', error);
console.error('Updating custom content failed', error);
return response.status(500).send(`Server Error: ${error.message}`);
}
});
@ -164,7 +164,7 @@ router.post('/move', jsonParser, async (request, response) => {
}
if (!request.user.profile.admin) {
console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`);
console.error(`User ${request.user.profile.handle} does not have permission to move extensions.`);
return response.status(403).send('Forbidden: No permission to move extensions.');
}
@ -190,11 +190,11 @@ router.post('/move', jsonParser, async (request, response) => {
fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
fs.rmSync(sourcePath, { recursive: true, force: true });
console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
console.info(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
return response.sendStatus(204);
} catch (error) {
console.log('Moving extension failed', error);
console.error('Moving extension failed', error);
return response.status(500).send('Internal Server Error. Try again later.');
}
});
@ -237,13 +237,13 @@ router.post('/version', jsonParser, async (request, response) => {
// get only the working branch
const currentBranchName = currentBranch.current;
await git.cwd(extensionPath).fetch('origin');
console.log(extensionName, currentBranchName, currentCommitHash);
console.debug(extensionName, currentBranchName, currentCommitHash);
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
} catch (error) {
console.log('Getting extension version failed', error);
console.error('Getting extension version failed', error);
return response.status(500).send(`Server Error: ${error.message}`);
}
});
@ -265,7 +265,7 @@ router.post('/delete', jsonParser, async (request, response) => {
const { extensionName, global } = request.body;
if (global && !request.user.profile.admin) {
console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
console.error(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
return response.status(403).send('Forbidden: No permission to delete global extensions.');
}
@ -277,12 +277,12 @@ router.post('/delete', jsonParser, async (request, response) => {
}
await fs.promises.rm(extensionPath, { recursive: true });
console.log(`Extension has been deleted at ${extensionPath}`);
console.info(`Extension has been deleted at ${extensionPath}`);
return response.send(`Extension has been deleted at ${extensionPath}`);
} catch (error) {
console.log('Deleting custom content failed', error);
console.error('Deleting custom content failed', error);
return response.status(500).send(`Server Error: ${error.message}`);
}
});
@ -323,7 +323,7 @@ router.get('/discover', jsonParser, function (request, response) {
// Combine all extensions
const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];
console.log('Extensions available for', request.user.profile.handle, allExtensions);
console.info('Extensions available for', request.user.profile.handle, allExtensions);
return response.send(allExtensions);
});

View File

@ -21,7 +21,7 @@ router.post('/sanitize-filename', jsonParser, async (request, response) => {
const sanitizedFilename = sanitize(fileName);
return response.send({ fileName: sanitizedFilename });
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -44,10 +44,10 @@ router.post('/upload', jsonParser, async (request, response) => {
const pathToUpload = path.join(request.user.directories.files, request.body.name);
writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
const url = clientRelativePath(request.user.directories.root, pathToUpload);
console.log(`Uploaded file: ${url} from ${request.user.profile.handle}`);
console.info(`Uploaded file: ${url} from ${request.user.profile.handle}`);
return response.send({ path: url });
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -68,10 +68,10 @@ router.post('/delete', jsonParser, async (request, response) => {
}
fs.rmSync(pathToDelete);
console.log(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
console.info(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
return response.sendStatus(200);
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -87,7 +87,7 @@ router.post('/verify', jsonParser, async (request, response) => {
for (const url of request.body.urls) {
const pathToVerify = path.join(request.user.directories.root, url);
if (!pathToVerify.startsWith(request.user.directories.files)) {
console.debug(`File verification: Invalid path: ${pathToVerify}`);
console.warn(`File verification: Invalid path: ${pathToVerify}`);
continue;
}
const fileExists = fs.existsSync(pathToVerify);
@ -96,7 +96,7 @@ router.post('/verify', jsonParser, async (request, response) => {
return response.send(verified);
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});

View File

@ -34,7 +34,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
generationConfig: { maxOutputTokens: 1000 },
};
console.log('Multimodal captioning request', model, body);
console.debug('Multimodal captioning request', model, body);
const result = await fetch(url, {
body: JSON.stringify(body),
@ -46,13 +46,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
if (!result.ok) {
const error = await result.json();
console.log(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
console.error(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
return response.status(result.status).send({ error: true });
}
/** @type {any} */
const data = await result.json();
console.log('Multimodal captioning response', data);
console.info('Multimodal captioning response', data);
const candidates = data?.candidates;
if (!candidates) {

View File

@ -114,7 +114,7 @@ router.post('/delete', jsonParser, async (request, response) => {
if (group && Array.isArray(group.chats)) {
for (const chat of group.chats) {
console.log('Deleting group chat', chat);
console.info('Deleting group chat', chat);
const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
if (fs.existsSync(pathToFile)) {

View File

@ -155,7 +155,7 @@ router.post('/cancel-task', jsonParser, async (request, response) => {
});
const data = await fetchResult.json();
console.log(`Cancelled Horde task ${taskId}`);
console.info(`Cancelled Horde task ${taskId}`);
return response.send(data);
} catch (error) {
console.error(error);
@ -174,7 +174,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
});
const data = await fetchResult.json();
console.log(`Horde task ${taskId} status:`, data);
console.info(`Horde task ${taskId} status:`, data);
return response.send(data);
} catch (error) {
console.error(error);
@ -187,7 +187,7 @@ router.post('/generate-text', jsonParser, async (request, response) => {
const url = 'https://aihorde.net/api/v2/generate/text/async';
const agent = await getClientAgent();
console.log(request.body);
console.debug(request.body);
try {
const result = await fetch(url, {
method: 'POST',
@ -201,14 +201,14 @@ router.post('/generate-text', jsonParser, async (request, response) => {
if (!result.ok) {
const message = await result.text();
console.log('Horde returned an error:', message);
console.error('Horde returned an error:', message);
return response.send({ error: { message } });
}
const data = await result.json();
return response.send(data);
} catch (error) {
console.log(error);
console.error(error);
return response.send({ error: true });
}
});
@ -254,7 +254,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
await delay(CHECK_INTERVAL);
const status = await ai_horde.getInterrogationStatus(result.id);
console.log(status);
console.info(status);
if (status.state === HordeAsyncRequestStates.done) {
@ -263,7 +263,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
return response.sendStatus(500);
}
console.log('Image interrogation result:', status);
console.debug('Image interrogation result:', status);
const caption = status?.forms[0]?.result?.caption || '';
if (!caption) {
@ -275,7 +275,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
}
if (status.state === HordeAsyncRequestStates.faulted || status.state === HordeAsyncRequestStates.cancelled) {
console.log('Image interrogation request is not successful.');
console.error('Image interrogation request is not successful.');
return response.sendStatus(503);
}
}
@ -315,7 +315,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
try {
const maxLength = PROMPT_THRESHOLD - String(request.body.negative_prompt).length - 5;
if (String(request.body.prompt).length > maxLength) {
console.log('Stable Horde prompt is too long, truncating...');
console.warn('Stable Horde prompt is too long, truncating...');
request.body.prompt = String(request.body.prompt).substring(0, maxLength);
}
@ -324,14 +324,14 @@ router.post('/generate-image', jsonParser, async (request, response) => {
const sanitized = sanitizeHordeImagePrompt(request.body.prompt);
if (request.body.prompt !== sanitized) {
console.log('Stable Horde prompt was sanitized.');
console.info('Stable Horde prompt was sanitized.');
}
request.body.prompt = sanitized;
}
const api_key_horde = readSecret(request.user.directories, SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
console.log('Stable Horde request:', request.body);
console.debug('Stable Horde request:', request.body);
const ai_horde = await getHordeClient();
// noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan
@ -360,16 +360,16 @@ router.post('/generate-image', jsonParser, async (request, response) => {
{ token: api_key_horde });
if (!generation.id) {
console.error('Image generation request is not satisfyable:', generation.message || 'unknown error');
console.warn('Image generation request is not satisfyable:', generation.message || 'unknown error');
return response.sendStatus(400);
}
console.log('Horde image generation request:', generation);
console.info('Horde image generation request:', generation);
const controller = new AbortController();
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
console.log('Horde image generation request aborted.');
console.warn('Horde image generation request aborted.');
controller.abort();
if (generation.id) ai_horde.deleteImageGenerationRequest(generation.id);
});
@ -378,7 +378,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
controller.signal.throwIfAborted();
await delay(CHECK_INTERVAL);
const check = await ai_horde.getImageGenerationCheck(generation.id);
console.log(check);
console.info(check);
if (check.done) {
const result = await ai_horde.getImageGenerationStatus(generation.id);

View File

@ -71,7 +71,7 @@ router.post('/upload', jsonParser, async (request, response) => {
await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer));
response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });
} catch (error) {
console.log(error);
console.error(error);
response.status(500).send({ error: 'Failed to save the image' });
}
});

View File

@ -120,7 +120,7 @@ router.post('/status', jsonParser, async function (req, res) {
const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);
if (!api_key_novel) {
console.log('NovelAI Access Token is missing.');
console.warn('NovelAI Access Token is missing.');
return res.sendStatus(400);
}
@ -137,15 +137,15 @@ router.post('/status', jsonParser, async function (req, res) {
const data = await response.json();
return res.send(data);
} else if (response.status == 401) {
console.log('NovelAI Access Token is incorrect.');
console.error('NovelAI Access Token is incorrect.');
return res.send({ error: true });
}
else {
console.log('NovelAI returned an error:', response.statusText);
console.warn('NovelAI returned an error:', response.statusText);
return res.send({ error: true });
}
} catch (error) {
console.log(error);
console.error(error);
return res.send({ error: true });
}
});
@ -156,7 +156,7 @@ router.post('/generate', jsonParser, async function (req, res) {
const api_key_novel = readSecret(req.user.directories, SECRET_KEYS.NOVEL);
if (!api_key_novel) {
console.log('NovelAI Access Token is missing.');
console.warn('NovelAI Access Token is missing.');
return res.sendStatus(400);
}
@ -241,7 +241,7 @@ router.post('/generate', jsonParser, async function (req, res) {
}
}
console.log(util.inspect(data, { depth: 4 }));
console.debug(util.inspect(data, { depth: 4 }));
const args = {
body: JSON.stringify(data),
@ -261,7 +261,7 @@ router.post('/generate', jsonParser, async function (req, res) {
if (!response.ok) {
const text = await response.text();
let message = text;
console.log(`Novel API returned error: ${response.status} ${response.statusText} ${text}`);
console.warn(`Novel API returned error: ${response.status} ${response.statusText} ${text}`);
try {
const data = JSON.parse(text);
@ -276,7 +276,7 @@ router.post('/generate', jsonParser, async function (req, res) {
/** @type {any} */
const data = await response.json();
console.log('NovelAI Output', data?.output);
console.info('NovelAI Output', data?.output);
return res.send(data);
}
} catch (error) {
@ -292,12 +292,12 @@ router.post('/generate-image', jsonParser, async (request, response) => {
const key = readSecret(request.user.directories, SECRET_KEYS.NOVEL);
if (!key) {
console.log('NovelAI Access Token is missing.');
console.warn('NovelAI Access Token is missing.');
return response.sendStatus(400);
}
try {
console.log('NAI Diffusion request:', request.body);
console.debug('NAI Diffusion request:', request.body);
const generateUrl = `${IMAGE_NOVELAI}/ai/generate-image`;
const generateResult = await fetch(generateUrl, {
method: 'POST',
@ -358,7 +358,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
if (!generateResult.ok) {
const text = await generateResult.text();
console.log('NovelAI returned an error.', generateResult.statusText, text);
console.warn('NovelAI returned an error.', generateResult.statusText, text);
return response.sendStatus(500);
}
@ -366,7 +366,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
const imageBuffer = await extractFileFromZipBuffer(archiveBuffer, '.png');
if (!imageBuffer) {
console.warn('NovelAI generated an image, but the PNG file was not found.');
console.error('NovelAI generated an image, but the PNG file was not found.');
return response.sendStatus(500);
}
@ -378,7 +378,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
}
try {
console.debug('Upscaling image...');
console.info('Upscaling image...');
const upscaleUrl = `${API_NOVELAI}/ai/upscale`;
const upscaleResult = await fetch(upscaleUrl, {
method: 'POST',
@ -413,7 +413,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
return response.send(originalBase64);
}
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -422,7 +422,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
const token = readSecret(request.user.directories, SECRET_KEYS.NOVEL);
if (!token) {
console.log('NovelAI Access Token is missing.');
console.error('NovelAI Access Token is missing.');
return response.sendStatus(400);
}
@ -445,7 +445,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
if (!result.ok) {
const errorText = await result.text();
console.log('NovelAI returned an error.', result.statusText, errorText);
console.error('NovelAI returned an error.', result.statusText, errorText);
return response.sendStatus(500);
}

View File

@ -63,7 +63,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
}
if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
console.log('No key found for API', request.body.api);
console.warn('No key found for API', request.body.api);
return response.sendStatus(400);
}
@ -93,7 +93,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
excludeKeysByYaml(body, request.body.custom_exclude_body);
}
console.log('Multimodal captioning request', body);
console.debug('Multimodal captioning request', body);
let apiUrl = '';
@ -158,13 +158,13 @@ router.post('/caption-image', jsonParser, async (request, response) => {
if (!result.ok) {
const text = await result.text();
console.log('Multimodal captioning request failed', result.statusText, text);
console.warn('Multimodal captioning request failed', result.statusText, text);
return response.status(500).send(text);
}
/** @type {any} */
const data = await result.json();
console.log('Multimodal captioning response', data);
console.info('Multimodal captioning response', data);
const caption = data?.choices[0]?.message?.content;
if (!caption) {
@ -184,17 +184,17 @@ router.post('/transcribe-audio', urlencodedParser, async (request, response) =>
const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
if (!key) {
console.log('No OpenAI key found');
console.warn('No OpenAI key found');
return response.sendStatus(400);
}
if (!request.file) {
console.log('No audio file found');
console.warn('No audio file found');
return response.sendStatus(400);
}
const formData = new FormData();
console.log('Processing audio file', request.file.path);
console.info('Processing audio file', request.file.path);
formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });
formData.append('model', request.body.model);
@ -213,13 +213,13 @@ router.post('/transcribe-audio', urlencodedParser, async (request, response) =>
if (!result.ok) {
const text = await result.text();
console.log('OpenAI request failed', result.statusText, text);
console.warn('OpenAI request failed', result.statusText, text);
return response.status(500).send(text);
}
fs.rmSync(request.file.path);
const data = await result.json();
console.log('OpenAI transcription response', data);
console.debug('OpenAI transcription response', data);
return response.json(data);
} catch (error) {
console.error('OpenAI transcription failed', error);
@ -232,7 +232,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
if (!key) {
console.log('No OpenAI key found');
console.warn('No OpenAI key found');
return response.sendStatus(400);
}
@ -253,7 +253,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
if (!result.ok) {
const text = await result.text();
console.log('OpenAI request failed', result.statusText, text);
console.warn('OpenAI request failed', result.statusText, text);
return response.status(500).send(text);
}
@ -271,11 +271,11 @@ router.post('/generate-image', jsonParser, async (request, response) => {
const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
if (!key) {
console.log('No OpenAI key found');
console.warn('No OpenAI key found');
return response.sendStatus(400);
}
console.log('OpenAI request', request.body);
console.debug('OpenAI request', request.body);
const result = await fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
@ -288,7 +288,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
if (!result.ok) {
const text = await result.text();
console.log('OpenAI request failed', result.statusText, text);
console.warn('OpenAI request failed', result.statusText, text);
return response.status(500).send(text);
}
@ -308,7 +308,7 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
const { input, provider_endpoint, response_format, voice, speed, model } = request.body;
if (!provider_endpoint) {
console.log('No OpenAI-compatible TTS provider endpoint provided');
console.warn('No OpenAI-compatible TTS provider endpoint provided');
return response.sendStatus(400);
}
@ -329,7 +329,7 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
if (!result.ok) {
const text = await result.text();
console.log('OpenAI request failed', result.statusText, text);
console.warn('OpenAI request failed', result.statusText, text);
return response.status(500).send(text);
}

View File

@ -4,6 +4,31 @@ import { jsonParser } from '../express-common.js';
export const router = express.Router();
const API_OPENROUTER = 'https://openrouter.ai/api/v1';
router.post('/models/providers', jsonParser, async (req, res) => {
try {
const { model } = req.body;
const response = await fetch(`${API_OPENROUTER}/models/${model}/endpoints`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
return res.json([]);
}
const data = await response.json();
const endpoints = data?.data?.endpoints || [];
const providerNames = endpoints.map(e => e.provider_name);
return res.json(providerNames);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
router.post('/models/multimodal', jsonParser, async (_req, res) => {
try {
// The endpoint is available without authentication

View File

@ -96,7 +96,7 @@ router.post('/restore', jsonParser, function (request, response) {
return response.send(result);
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});

View File

@ -96,25 +96,26 @@ router.post('/serpapi', jsonParser, async (request, response) => {
const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
if (!key) {
console.log('No SerpApi key found');
console.error('No SerpApi key found');
return response.sendStatus(400);
}
const { query } = request.body;
const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
console.log('SerpApi query', query);
console.debug('SerpApi query', query);
if (!result.ok) {
const text = await result.text();
console.log('SerpApi request failed', result.statusText, text);
console.error('SerpApi request failed', result.statusText, text);
return response.status(500).send(text);
}
const data = await result.json();
console.debug('SerpApi response', data);
return response.json(data);
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -130,7 +131,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
const json = request.body.json;
if (!id) {
console.log('Id is required for /transcript');
console.error('Id is required for /transcript');
return response.sendStatus(400);
}
@ -155,7 +156,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
throw error;
}
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -165,17 +166,17 @@ router.post('/searxng', jsonParser, async (request, response) => {
const { baseUrl, query, preferences, categories } = request.body;
if (!baseUrl || !query) {
console.log('Missing required parameters for /searxng');
console.error('Missing required parameters for /searxng');
return response.sendStatus(400);
}
console.log('SearXNG query', baseUrl, query);
console.debug('SearXNG query', baseUrl, query);
const mainPageUrl = new URL(baseUrl);
const mainPageRequest = await fetch(mainPageUrl, { headers: visitHeaders });
if (!mainPageRequest.ok) {
console.log('SearXNG request failed', mainPageRequest.statusText);
console.error('SearXNG request failed', mainPageRequest.statusText);
return response.sendStatus(500);
}
@ -202,14 +203,14 @@ router.post('/searxng', jsonParser, async (request, response) => {
if (!searchResult.ok) {
const text = await searchResult.text();
console.log('SearXNG request failed', searchResult.statusText, text);
console.error('SearXNG request failed', searchResult.statusText, text);
return response.sendStatus(500);
}
const data = await searchResult.text();
return response.send(data);
} catch (error) {
console.log('SearXNG request failed', error);
console.error('SearXNG request failed', error);
return response.sendStatus(500);
}
});
@ -219,7 +220,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
const apiKey = readSecret(request.user.directories, SECRET_KEYS.TAVILY);
if (!apiKey) {
console.log('No Tavily key found');
console.error('No Tavily key found');
return response.sendStatus(400);
}
@ -246,18 +247,19 @@ router.post('/tavily', jsonParser, async (request, response) => {
body: JSON.stringify(body),
});
console.log('Tavily query', query);
console.debug('Tavily query', query);
if (!result.ok) {
const text = await result.text();
console.log('Tavily request failed', result.statusText, text);
console.error('Tavily request failed', result.statusText, text);
return response.status(500).send(text);
}
const data = await result.json();
console.debug('Tavily response', data);
return response.json(data);
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});
@ -290,6 +292,49 @@ router.post('/koboldcpp', jsonParser, async (request, response) => {
}
const data = await result.json();
console.debug('KoboldCpp search response', data);
return response.json(data);
} catch (error) {
console.error(error);
return response.sendStatus(500);
}
});
router.post('/serper', jsonParser, async (request, response) => {
try {
const key = readSecret(request.user.directories, SECRET_KEYS.SERPER);
if (!key) {
console.error('No Serper key found');
return response.sendStatus(400);
}
const { query, images } = request.body;
const url = images
? 'https://google.serper.dev/images'
: 'https://google.serper.dev/search';
const result = await fetch(url, {
method: 'POST',
headers: {
'X-API-KEY': key,
'Content-Type': 'application/json',
},
redirect: 'follow',
body: JSON.stringify({ q: query }),
});
console.debug('Serper query', query);
if (!result.ok) {
const text = await result.text();
console.warn('Serper request failed', result.statusText, text);
return response.status(500).send(text);
}
const data = await result.json();
console.debug('Serper response', data);
return response.json(data);
} catch (error) {
console.error(error);
@ -303,7 +348,7 @@ router.post('/visit', jsonParser, async (request, response) => {
const html = Boolean(request.body.html ?? true);
if (!url) {
console.log('No url provided for /visit');
console.error('No url provided for /visit');
return response.sendStatus(400);
}
@ -330,16 +375,16 @@ router.post('/visit', jsonParser, async (request, response) => {
throw new Error('Invalid hostname');
}
} catch (error) {
console.log('Invalid url provided for /visit', url);
console.error('Invalid url provided for /visit', url);
return response.sendStatus(400);
}
console.log('Visiting web URL', url);
console.info('Visiting web URL', url);
const result = await fetch(url, { headers: visitHeaders });
if (!result.ok) {
console.log(`Visit failed ${result.status} ${result.statusText}`);
console.error(`Visit failed ${result.status} ${result.statusText}`);
return response.sendStatus(500);
}
@ -347,7 +392,7 @@ router.post('/visit', jsonParser, async (request, response) => {
if (html) {
if (!contentType.includes('text/html')) {
console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
console.error(`Visit failed, content-type is ${contentType}, expected text/html`);
return response.sendStatus(500);
}
@ -359,7 +404,7 @@ router.post('/visit', jsonParser, async (request, response) => {
const buffer = await result.arrayBuffer();
return response.send(Buffer.from(buffer));
} catch (error) {
console.log(error);
console.error(error);
return response.sendStatus(500);
}
});

View File

@ -50,8 +50,10 @@ export const SECRET_KEYS = {
TAVILY: 'api_key_tavily',
NANOGPT: 'api_key_nanogpt',
BFL: 'api_key_bfl',
FALAI: 'api_key_falai',
GENERIC: 'api_key_generic',
DEEPSEEK: 'api_key_deepseek',
SERPER: 'api_key_serper',
};
// These are the keys that are safe to expose, even if allowKeysExposure is false
@ -151,7 +153,7 @@ export function getAllSecrets(directories) {
const filePath = path.join(directories.root, SECRETS_FILE);
if (!fs.existsSync(filePath)) {
console.log('Secrets file does not exist');
console.error('Secrets file does not exist');
return undefined;
}

View File

@ -105,7 +105,7 @@ function readPresetsFromDirectory(directoryPath, options = {}) {
fileNames.push(removeFileExtension ? item.replace(/\.[^/.]+$/, '') : item);
} catch {
// skip
console.log(`${item} is not a valid JSON`);
console.warn(`${item} is not a valid JSON`);
}
});
@ -120,7 +120,7 @@ async function backupSettings() {
backupUserSettings(handle, true);
}
} catch (err) {
console.log('Could not backup settings file', err);
console.error('Could not backup settings file', err);
}
}
@ -202,7 +202,7 @@ router.post('/save', jsonParser, function (request, response) {
triggerAutoSave(request.user.profile.handle);
response.send({ result: 'ok' });
} catch (err) {
console.log(err);
console.error(err);
response.send(err);
}
});
@ -292,7 +292,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
response.json(result);
} catch (error) {
console.log(error);
console.error(error);
response.sendStatus(500);
}
});
@ -316,7 +316,7 @@ router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'),
response.send(content);
} catch (error) {
console.log(error);
console.error(error);
response.sendStatus(500);
}
});
@ -326,7 +326,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
backupUserSettings(request.user.profile.handle, false);
response.sendStatus(204);
} catch (error) {
console.log(error);
console.error(error);
response.sendStatus(500);
}
});
@ -352,7 +352,7 @@ router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name
response.sendStatus(204);
} catch (error) {
console.log(error);
console.error(error);
response.sendStatus(500);
}
});

Some files were not shown because too many files have changed in this diff Show More