Merge branch 'staging' into ffmpeg-videobg

573ada296ecae9b59da6cfb69b406227ee3950ff

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

167 files changed, +3251 -1779Ignore whitespace
.dockerignore+1 -0
@@ -13,3 +13,4 @@ access.log
1313/cache
1414.DS_Store
1515/public/scripts/extensions/third-party
16+/colab
.github/readme.md+1 -0
@@ -350,6 +350,7 @@ Start.bat --port 8000 --listen false
350350| Option | Description | Type |
351351|-------------------------|----------------------------------------------------------------------|----------|
352352| `--version` | Show version number | boolean |
353+| `--configPath` | Override the path to the config.yaml file | string |
353354| `--dataRoot` | Root directory for data storage | string |
354355| `--port` | Sets the port under which SillyTavern will run | number |
355356| `--listen` | SillyTavern will listen on all network interfaces | boolean |
.github/workflows/pr-auto-manager.yml+3 -2
@@ -30,7 +30,8 @@ jobs:
3030 # https://github.com/marketplace/actions/checkout
3131 uses: actions/checkout@v4.2.2
3232 with:
3333 ref: ${{ github.head_refevent.pull_request.head.sha }}
34+ repository: ${{ github.event.pull_request.head.repo.full_name }}
3435
3536 - name: Setup Node.js
3637 # Setup Node.js environment
@@ -49,7 +50,7 @@ jobs:
4950 with:
5051 token: ${{ secrets.GITHUB_TOKEN }}
5152 eslint-args: '--ignore-path=.gitignore --quiet'
5253 extensions: 'js,ts'
5354 annotations: true
5455 ignore-patterns: |
5556 dist/
.npmignore+1 -0
@@ -12,3 +12,4 @@ access.log
1212.vscode
1313.git
1414/public/scripts/extensions/third-party
15+/colab
Dockerfile+3 -5
@@ -12,15 +12,13 @@ WORKDIR ${APP_HOME}
1212# Set NODE_ENV to production
1313ENV NODE_ENV=production
1414
1515# InstallBundle app dependenciessource
1616COPY package*.json post-install.js ./
17+
1718RUN \
1819 echo "*** Install npm packages ***" && \
1920 npm i --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force
2021
21-# Bundle app source
22-COPY . ./
23-
2422# Copy default chats, characters and user avatars to <folder>.default folder
2523RUN \
2624 rm -f "config.yaml" || true && \
default/config.yaml+5 -0
@@ -155,6 +155,7 @@ whitelistImportDomains:
155155 - cdn.discordapp.com
156156 - files.catbox.moe
157157 - raw.githubusercontent.com
158+ - char-archive.evulid.cc
158159# API request overrides (for KoboldAI and Text Completion APIs)
159160## Note: host includes the port number if it's not the default (80 or 443)
160161## Format is an array of objects:
@@ -233,6 +234,10 @@ claude:
233234 # should be ideal for most use cases.
234235 # Any value other than a non-negative integer will be ignored and caching at depth will not be enabled.
235236 cachingAtDepth: -1
237+# -- GOOGLE GEMINI API CONFIGURATION --
238+gemini:
239+ # API endpoint version ("v1beta" or "v1alpha")
240+ apiVersion: 'v1beta'
236241# -- SERVER PLUGIN CONFIGURATION --
237242enableServerPlugins: false
238243# Attempt to automatically update server plugins on startup
default/content/index.json+21 -5
@@ -540,7 +540,7 @@
540540 "type": "context"
541541 },
542542 {
543543 "filename": "presets/context/PygmalionMetharme.json",
544544 "type": "context"
545545 },
546546 {
@@ -564,6 +564,10 @@
564564 "type": "context"
565565 },
566566 {
567+ "filename": "presets/context/Llama 4 Instruct.json",
568+ "type": "context"
569+ },
570+ {
567571 "filename": "presets/context/Phi.json",
568572 "type": "context"
569573 },
@@ -616,10 +620,6 @@
616620 "type": "instruct"
617621 },
618622 {
619- "filename": "presets/instruct/Pygmalion.json",
620- "type": "instruct"
621- },
622- {
623623 "filename": "presets/instruct/Story.json",
624624 "type": "instruct"
625625 },
@@ -664,6 +664,10 @@
664664 "type": "instruct"
665665 },
666666 {
667+ "filename": "presets/instruct/Llama 4 Instruct.json",
668+ "type": "instruct"
669+ },
670+ {
667671 "filename": "presets/instruct/Phi.json",
668672 "type": "instruct"
669673 },
@@ -748,6 +752,10 @@
748752 "type": "sysprompt"
749753 },
750754 {
755+ "filename": "presets/sysprompt/Lightning 1.1.json",
756+ "type": "sysprompt"
757+ },
758+ {
751759 "filename": "presets/instruct/Mistral V1.json",
752760 "type": "instruct"
753761 },
@@ -788,6 +796,14 @@
788796 "type": "context"
789797 },
790798 {
799+ "filename": "presets/instruct/GLM-4.json",
800+ "type": "instruct"
801+ },
802+ {
803+ "filename": "presets/context/GLM-4.json",
804+ "type": "context"
805+ },
806+ {
791807 "filename": "presets/reasoning/DeepSeek.json",
792808 "type": "reasoning"
793809 },
default/content/presets/context/Adventure.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": false,
87 "trim_sentences": false,
98 "single_line": true,
default/content/presets/context/Alpaca-Single-Turn.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": false,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Alpaca.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/ChatML-Names.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/ChatML.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Command R.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>New Roleplay:<|END_OF_TURN_TOKEN|>",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/DeepSeek-V2.5.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Default.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "***",
44 "chat_start": "***",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/DreamGen Role-Play V1 ChatML.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": false,
87 "trim_sentences": true,
98 "single_line": false,
default/content/presets/context/DreamGen Role-Play V1 Llama3.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nWrite an example narrative / conversation that is not part of the main story.",
44 "chat_start": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nStart the role-play between {{char}} and {{user}}.",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": false,
87 "trim_sentences": true,
98 "single_line": false,
default/content/presets/context/GLM-4.json+10 -0
@@ -0,0 +1,10 @@
1+{
2+ "story_string": "[gMASK]<sop>{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}\n",
3+ "example_separator": "",
4+ "chat_start": "",
5+ "use_stop_strings": false,
6+ "always_force_name2": true,
7+ "trim_sentences": false,
8+ "single_line": false,
9+ "name": "GLM-4"
10+}
default/content/presets/context/Gemma 2.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Libra-32B.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "### Example:",
44 "chat_start": "### START ROLEPLAY:",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Lightning 1.1.json+3 -4
@@ -1,9 +1,8 @@
11{
22 "story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{{system}}\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{char}}'s description:{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality:{{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{user}}'s persona: {{persona}}\n{{/if}}\n\n",
33 "example_separator": "Example of an interaction:\n",
44 "chat_start": "This is the history of the roleplay:\n",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Llama 2 Chat.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Llama 3 Instruct.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Llama 4 Instruct.json+10 -0
@@ -0,0 +1,10 @@
1+{
2+ "story_string": "<|begin_of_text|><|header_start|>system<|header_end|>\n\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|eot|>",
3+ "example_separator": "",
4+ "chat_start": "",
5+ "use_stop_strings": false,
6+ "always_force_name2": true,
7+ "trim_sentences": false,
8+ "single_line": false,
9+ "name": "Llama 4 Instruct"
10+}
default/content/presets/context/Llama-3-Instruct-Names.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Pygmalion.json → default/content/presets/context/Metharme.json+1 -2
@@ -3,9 +3,8 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
109 "name": "PygmalionMetharme"
1110}
default/content/presets/context/Minimalist.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Mistral V1.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Mistral V2 & V3.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Mistral V3-Tekken.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Mistral V7.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/NovelAI.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "***",
44 "chat_start": "***",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/OldDefault.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "This is how {{char}} should talk",
44 "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Phi.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Story.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Synthia.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/Tulu.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/context/simple-proxy-for-tavern.json+0 -1
@@ -3,7 +3,6 @@
33 "example_separator": "### New Roleplay:",
44 "chat_start": "### New Roleplay:",
55 "use_stop_strings": false,
6- "allow_jailbreak": false,
76 "always_force_name2": true,
87 "trim_sentences": false,
98 "single_line": false,
default/content/presets/instruct/ChatML.json+1 -1
@@ -6,7 +6,7 @@
66 "stop_sequence": "<|im_end|>",
77 "wrap": true,
88 "macro": true,
99 "names_behavior": "alwaysforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
default/content/presets/instruct/Command R.json+1 -1
@@ -8,7 +8,7 @@
88 "stop_sequence": "<|END_OF_TURN_TOKEN|>",
99 "wrap": false,
1010 "macro": true,
1111 "names_behavior": "alwaysforce",
1212 "activation_regex": "",
1313 "skip_examples": false,
1414 "output_suffix": "<|END_OF_TURN_TOKEN|>",
default/content/presets/instruct/Pygmalion.json → default/content/presets/instruct/GLM-4.json+10 -10
@@ -1,22 +1,22 @@
11{
22 "input_sequence": "<|user|>\n",
33 "output_sequence": "<|modelassistant|>\n",
4+ "first_output_sequence": "",
45 "last_output_sequence": "",
56 "system_sequencesystem_sequence_prefix": "<|system|>\n",
67 "stop_sequencesystem_sequence_suffix": "<|user|>",
8+ "stop_sequence": "",
79 "wrap": false,
810 "macro": true,
911 "names_behavior": "alwaysforce",
1012 "activation_regex": "",
11- "system_sequence_prefix": "<|system|>",
12- "system_sequence_suffix": "",
13- "first_output_sequence": "",
1413 "skip_examples": false,
1514 "output_suffix": "",
1615 "input_suffix": "",
16+ "system_sequence": "",
1717 "system_suffix": "",
1818 "user_alignment_message": "",
19- "system_same_as_user": true,
2019 "last_system_sequence": "",
2120 "namesystem_same_as_user": "Pygmalion"true,
21+ "name": "GLM-4"
2222}
default/content/presets/instruct/Gemma 2.json+1 -1
@@ -6,7 +6,7 @@
66 "stop_sequence": "<end_of_turn>",
77 "wrap": true,
88 "macro": true,
99 "names_behavior": "noneforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
default/content/presets/instruct/Lightning 1.1.json+4 -4
@@ -1,7 +1,7 @@
11{
22 "input_sequence": "### Instruction:",
33 "output_sequence": "### Response: (length = unlimited)",
44 "last_output_sequence": "### Response: (length = unlimited)",
55 "system_sequence": "",
66 "stop_sequence": "",
77 "wrap": true,
@@ -12,8 +12,8 @@
1212 "system_sequence_suffix": "",
1313 "first_output_sequence": "",
1414 "skip_examples": false,
1515 "output_suffix": "\n\n",
1616 "input_suffix": "\n\n",
1717 "system_suffix": "",
1818 "user_alignment_message": "",
1919 "system_same_as_user": true,
default/content/presets/instruct/Llama 3 Instruct.json+2 -2
@@ -6,7 +6,7 @@
66 "stop_sequence": "<|eot_id|>",
77 "wrap": false,
88 "macro": true,
99 "names_behavior": "alwaysforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
@@ -16,7 +16,7 @@
1616 "input_suffix": "<|eot_id|>",
1717 "system_suffix": "<|eot_id|>",
1818 "user_alignment_message": "",
1919 "system_same_as_user": truefalse,
2020 "last_system_sequence": "",
2121 "name": "Llama 3 Instruct"
2222}
default/content/presets/instruct/Llama 4 Instruct.json+22 -0
@@ -0,0 +1,22 @@
1+{
2+ "input_sequence": "<|header_start|>user<|header_end|>\n\n",
3+ "output_sequence": "<|header_start|>assistant<|header_end|>\n\n",
4+ "last_output_sequence": "",
5+ "system_sequence": "<|header_start|>system<|header_end|>\n\n",
6+ "stop_sequence": "<|eot|>",
7+ "wrap": false,
8+ "macro": true,
9+ "names_behavior": "force",
10+ "activation_regex": "",
11+ "system_sequence_prefix": "",
12+ "system_sequence_suffix": "",
13+ "first_output_sequence": "",
14+ "skip_examples": false,
15+ "output_suffix": "<|eot|>",
16+ "input_suffix": "<|eot|>",
17+ "system_suffix": "<|eot|>",
18+ "user_alignment_message": "",
19+ "system_same_as_user": false,
20+ "last_system_sequence": "",
21+ "name": "Llama 4 Instruct"
22+}
default/content/presets/instruct/Mistral V1.json+1 -1
@@ -6,7 +6,7 @@
66 "stop_sequence": "",
77 "wrap": false,
88 "macro": true,
99 "names_behavior": "alwaysforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
default/content/presets/instruct/Mistral V2 & V3.json+1 -1
@@ -6,7 +6,7 @@
66 "stop_sequence": "",
77 "wrap": false,
88 "macro": true,
99 "names_behavior": "alwaysforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
default/content/presets/instruct/Mistral V3-Tekken.json+1 -1
@@ -6,7 +6,7 @@
66 "stop_sequence": "",
77 "wrap": false,
88 "macro": true,
99 "names_behavior": "alwaysforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
default/content/presets/instruct/Mistral V7.json+1 -1
@@ -6,7 +6,7 @@
66 "stop_sequence": "</s>",
77 "wrap": false,
88 "macro": true,
99 "names_behavior": "alwaysforce",
1010 "activation_regex": "",
1111 "system_sequence_prefix": "",
1212 "system_sequence_suffix": "",
default/content/presets/instruct/Phi.json+1 -1
@@ -8,7 +8,7 @@
88 "stop_sequence": "<|end|>",
99 "wrap": false,
1010 "macro": true,
1111 "names_behavior": "alwaysforce",
1212 "activation_regex": "",
1313 "skip_examples": false,
1414 "output_suffix": "<|end|>\n",
default/content/presets/instruct/Tulu.json+1 -1
@@ -8,7 +8,7 @@
88 "stop_sequence": "<|end_of_text|>",
99 "wrap": false,
1010 "macro": true,
1111 "names_behavior": "alwaysforce",
1212 "activation_regex": "",
1313 "skip_examples": false,
1414 "output_suffix": "<|end_of_text|>\n",
default/content/presets/sysprompt/Actor.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Actor",
33 "content": "You are an expert actor that can fully immerse yourself into any role given. You do not break character for any reason, even if someone tries addressing you as an AI or language model. Currently your role is {{char}}, which is described in detail below. As {{char}}, continue the exchange with {{user}}.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Assistant - Expert.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Assistant - Expert",
33 "content": "You are a helpful assistant. Please answer truthfully and write out your thinking step by step to be sure you get the right answer. If you make a mistake or encounter an error in your thinking, say so out loud and attempt to correct it. If you don't know or aren't sure about something, say so clearly. You will act as a professional logician, mathematician, and physicist. You will also act as the most appropriate type of expert to answer any particular question or solve the relevant problem; state which expert type your are, if so. Also think of any particular named expert that would be ideal to answer the relevant question or solve the relevant problem; name and act as them, if appropriate.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Assistant - Simple.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Assistant - Simple",
33 "content": "A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Blank.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Blank",
33 "content": "",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Chain of Thought.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Chain of Thought",
33 "content": "Elaborate on the topic using a Tree of Thoughts and backtrack when necessary to construct a clear, cohesive Chain of Thought reasoning. Always answer without hesitation.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Lightning 1.1.json+5 -0
@@ -0,0 +1,5 @@
1+{
2+ "name": "Lightning 1.1",
3+ "content": "Take the role of {{char}} in a play that leaves a lasting impression on {{user}}. Write {{char}}'s next reply.\nNever skip or gloss over {{char}}’s actions. Progress the scene at a naturally slow pace.",
4+ "post_history": ""
5+}
default/content/presets/sysprompt/Neutral - Chat.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Neutral - Chat",
33 "content": "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Roleplay - Detailed.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Roleplay - Detailed",
33 "content": "Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions. Keep the story immersive and engaging.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Roleplay - Immersive.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Roleplay - Immersive",
33 "content": "[System note: Write one reply only. Do not decide what {{user}} says or does. Write at least one paragraph, up to four. Be descriptive and immersive, providing vivid details about {{char}}'s actions, emotions, and the environment. Write with a high degree of complexity and burstiness. Do not repeat this message.]",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Roleplay - Simple.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Roleplay - Simple",
33 "content": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Text Adventure.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Text Adventure",
33 "content": "[Enter Adventure Mode. Narrate the story based on {{user}}'s dialogue and actions after \">\". Describe the surroundings in vivid detail. Be detailed, creative, verbose, and proactive. Move the story forward by introducing fantasy elements and interesting characters.]",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Writer - Creative.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Writer - Creative",
33 "content": "You are an intelligent, skilled, versatile writer.\n\nYour task is to write a role-play based on the information below.",
4+ "post_history": ""
45}
default/content/presets/sysprompt/Writer - Realistic.json+2 -1
@@ -1,4 +1,5 @@
11{
22 "name": "Writer - Realistic",
33 "content": "Continue writing this story and portray characters realistically.",
4+ "post_history": ""
45}
package-lock.json+2 -2
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.12.1314",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.12.1314",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
package.json+1 -1
@@ -109,7 +109,7 @@
109109 "type": "git",
110110 "url": "https://github.com/SillyTavern/SillyTavern.git"
111111 },
112112 "version": "1.12.1314",
113113 "scripts": {
114114 "start": "node server.js",
115115 "debug": "node --inspect server.js",
post-install.js+3 -229
@@ -3,133 +3,17 @@
33 */
44import fs from 'node:fs';
55import path from 'node:path';
6-import crypto from 'node:crypto';
76import process from 'node:process';
87import yaml from 'yaml';
9-import _ from 'lodash';
108import chalk from 'chalk';
119import { createRequire } from 'node:module';
10+import { addMissingConfigValues } from './src/config-init.js';
1211
1312/**
1413 * Colorizes console output.
1514 */
1615const color = chalk;
1716
18-const keyMigrationMap = [
19- {
20- oldKey: 'disableThumbnails',
21- newKey: 'thumbnails.enabled',
22- migrate: (value) => !value,
23- },
24- {
25- oldKey: 'thumbnailsQuality',
26- newKey: 'thumbnails.quality',
27- migrate: (value) => value,
28- },
29- {
30- oldKey: 'avatarThumbnailsPng',
31- newKey: 'thumbnails.format',
32- migrate: (value) => (value ? 'png' : 'jpg'),
33- },
34- {
35- oldKey: 'disableChatBackup',
36- newKey: 'backups.chat.enabled',
37- migrate: (value) => !value,
38- },
39- {
40- oldKey: 'numberOfBackups',
41- newKey: 'backups.common.numberOfBackups',
42- migrate: (value) => value,
43- },
44- {
45- oldKey: 'maxTotalChatBackups',
46- newKey: 'backups.chat.maxTotalBackups',
47- migrate: (value) => value,
48- },
49- {
50- oldKey: 'chatBackupThrottleInterval',
51- newKey: 'backups.chat.throttleInterval',
52- migrate: (value) => value,
53- },
54- {
55- oldKey: 'enableExtensions',
56- newKey: 'extensions.enabled',
57- migrate: (value) => value,
58- },
59- {
60- oldKey: 'enableExtensionsAutoUpdate',
61- newKey: 'extensions.autoUpdate',
62- migrate: (value) => value,
63- },
64- {
65- oldKey: 'extras.disableAutoDownload',
66- newKey: 'extensions.models.autoDownload',
67- migrate: (value) => !value,
68- },
69- {
70- oldKey: 'extras.classificationModel',
71- newKey: 'extensions.models.classification',
72- migrate: (value) => value,
73- },
74- {
75- oldKey: 'extras.captioningModel',
76- newKey: 'extensions.models.captioning',
77- migrate: (value) => value,
78- },
79- {
80- oldKey: 'extras.embeddingModel',
81- newKey: 'extensions.models.embedding',
82- migrate: (value) => value,
83- },
84- {
85- oldKey: 'extras.speechToTextModel',
86- newKey: 'extensions.models.speechToText',
87- migrate: (value) => value,
88- },
89- {
90- oldKey: 'extras.textToSpeechModel',
91- newKey: 'extensions.models.textToSpeech',
92- migrate: (value) => value,
93- },
94- {
95- oldKey: 'minLogLevel',
96- newKey: 'logging.minLogLevel',
97- migrate: (value) => value,
98- },
99- {
100- oldKey: 'cardsCacheCapacity',
101- newKey: 'performance.memoryCacheCapacity',
102- migrate: (value) => `${value}mb`,
103- },
104- {
105- oldKey: 'cookieSecret',
106- newKey: 'cookieSecret',
107- migrate: () => void 0,
108- remove: true,
109- },
110-];
111-
112-/**
113- * Gets all keys from an object recursively.
114- * @param {object} obj Object to get all keys from
115- * @param {string} prefix Prefix to prepend to all keys
116- * @returns {string[]} Array of all keys in the object
117- */
118-function getAllKeys(obj, prefix = '') {
119- if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
120- return [];
121- }
122-
123- return _.flatMap(Object.keys(obj), key => {
124- const newPrefix = prefix ? `${prefix}.${key}` : key;
125- if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
126- return getAllKeys(obj[key], newPrefix);
127- } else {
128- return [newPrefix];
129- }
130- });
131-}
132-
13317/**
13418 * Converts the old config.conf file to the new config.yaml format.
13519 */
@@ -157,71 +41,6 @@ function convertConfig() {
15741}
15842
15943/**
160- * Compares the current config.yaml with the default config.yaml and adds any missing values.
161- */
162-function addMissingConfigValues() {
163- try {
164- const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8'));
165- let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));
166-
167- // Migrate old keys to new keys
168- const migratedKeys = [];
169- for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) {
170- if (_.has(config, oldKey)) {
171- if (remove) {
172- _.unset(config, oldKey);
173- migratedKeys.push({
174- oldKey,
175- newValue: void 0,
176- });
177- continue;
178- }
179-
180- const oldValue = _.get(config, oldKey);
181- const newValue = migrate(oldValue);
182- _.set(config, newKey, newValue);
183- _.unset(config, oldKey);
184-
185- migratedKeys.push({
186- oldKey,
187- newKey,
188- oldValue,
189- newValue,
190- });
191- }
192- }
193-
194- // Get all keys from the original config
195- const originalKeys = getAllKeys(config);
196-
197- // Use lodash's defaultsDeep function to recursively apply default properties
198- config = _.defaultsDeep(config, defaultConfig);
199-
200- // Get all keys from the updated config
201- const updatedKeys = getAllKeys(config);
202-
203- // Find the keys that were added
204- const addedKeys = _.difference(updatedKeys, originalKeys);
205-
206- if (addedKeys.length === 0 && migratedKeys.length === 0) {
207- return;
208- }
209-
210- if (addedKeys.length > 0) {
211- console.log('Adding missing config values to config.yaml:', addedKeys);
212- }
213-
214- if (migratedKeys.length > 0) {
215- console.log('Migrating config values in config.yaml:', migratedKeys);
216- }
217-
218- fs.writeFileSync('./config.yaml', yaml.stringify(config));
219- } catch (error) {
220- console.error(color.red('FATAL: Could not add missing config values to config.yaml'), error);
221- }
222-}
223-
224-/**
22544 * Creates the default config files if they don't exist yet.
22645 */
22746function createDefaultFiles() {
@@ -283,58 +102,13 @@ function createDefaultFiles() {
283102 }
284103}
285104
286-/**
287- * Returns the MD5 hash of the given data.
288- * @param {Buffer} data Input data
289- * @returns {string} MD5 hash of the input data
290- */
291-function getMd5Hash(data) {
292- return crypto
293- .createHash('md5')
294- .update(new Uint8Array(data))
295- .digest('hex');
296-}
297-
298-/**
299- * Copies the WASM binaries from the sillytavern-transformers package to the dist folder.
300- */
301-function copyWasmFiles() {
302- if (!fs.existsSync('./dist')) {
303- fs.mkdirSync('./dist');
304- }
305-
306- const listDir = fs.readdirSync('./node_modules/sillytavern-transformers/dist');
307-
308- for (const file of listDir) {
309- if (file.endsWith('.wasm')) {
310- const sourcePath = `./node_modules/sillytavern-transformers/dist/${file}`;
311- const targetPath = `./dist/${file}`;
312-
313- // Don't copy if the file already exists and is the same checksum
314- if (fs.existsSync(targetPath)) {
315- const sourceChecksum = getMd5Hash(fs.readFileSync(sourcePath));
316- const targetChecksum = getMd5Hash(fs.readFileSync(targetPath));
317-
318- if (sourceChecksum === targetChecksum) {
319- continue;
320- }
321- }
322-
323- fs.copyFileSync(sourcePath, targetPath);
324- console.log(`${file} successfully copied to ./dist/${file}`);
325- }
326- }
327-}
328-
329105try {
330106 // 0. Convert config.conf to config.yaml
331107 convertConfig();
332108 // 1. Create default config files
333109 createDefaultFiles();
334110 // 2. Copy transformers WASMAdd binariesmissing fromconfig node_modulesvalues
335- copyWasmFiles();
111+ addMissingConfigValues(path.join(process.cwd(), './config.yaml'));
336- // 3. Add missing config values
337- addMissingConfigValues();
338112} catch (error) {
339113 console.error(error);
340114}
public/css/rm-groups.css+2 -2
@@ -87,7 +87,7 @@
8787}
8888
8989#rm_group_members:empty::before {
90- content: 'Group is empty';
90+ content: attr(group_empty_text);
9191
9292 font-weight: bolder;
9393 width: 100%;
@@ -115,7 +115,7 @@
115115}
116116
117117#rm_group_add_members:empty::before {
118- content: 'No characters available';
118+ content: attr(no_characters_text);
119119
120120 font-weight: bolder;
121121 width: 100%;
public/css/world-info.css+4 -0
@@ -124,6 +124,10 @@
124124 cursor: initial;
125125}
126126
127+.world_entry .inline-drawer-header-pointer {
128+ cursor: pointer;
129+}
130+
127131.world_entry .killSwitch {
128132 cursor: pointer;
129133}
public/img/xai.svg+46 -0
@@ -0,0 +1,46 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3+
4+<svg
5+ version="1.1"
6+ id="katman_1"
7+ x="0px"
8+ y="0px"
9+ viewBox="0 0 438.67001 481.44999"
10+ xml:space="preserve"
11+ sodipodi:docname="XAI_Logo.svg"
12+ width="438.67001"
13+ height="481.45001"
14+ inkscape:version="1.3 (0e150ed, 2023-07-21)"
15+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
16+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
17+ xmlns="http://www.w3.org/2000/svg"
18+ xmlns:svg="http://www.w3.org/2000/svg"><defs
19+ id="defs4" /><sodipodi:namedview
20+ id="namedview4"
21+ pagecolor="#ffffff"
22+ bordercolor="#000000"
23+ borderopacity="0.25"
24+ inkscape:showpageshadow="2"
25+ inkscape:pageopacity="0.0"
26+ inkscape:pagecheckerboard="0"
27+ inkscape:deskcolor="#d1d1d1"
28+ inkscape:zoom="0.39645207"
29+ inkscape:cx="219.44645"
30+ inkscape:cy="238.36425"
31+ inkscape:window-width="1512"
32+ inkscape:window-height="856"
33+ inkscape:window-x="0"
34+ inkscape:window-y="38"
35+ inkscape:window-maximized="1"
36+ inkscape:current-layer="katman_1" />&#10;<g
37+ id="g4"
38+ transform="translate(-201.61,-56.91)">&#10; <polygon
39+ points="631.96,538.36 640.28,93.18 557.09,211.99 565.4,538.36 "
40+ id="polygon1" />&#10; <polygon
41+ points="379.35,284.53 430.13,357.05 640.28,56.91 538.72,56.91 "
42+ id="polygon2" />&#10; <polygon
43+ points="353.96,465.84 303.17,393.31 201.61,538.36 303.17,538.36 "
44+ id="polygon3" />&#10; <polygon
45+ points="531.69,538.36 303.17,211.99 201.61,211.99 430.13,538.36 "
46+ id="polygon4" />&#10;</g>&#10;</svg>
public/index.html+182 -92
@@ -646,7 +646,7 @@
646646 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
647647 </div>
648648 </div>
649649 <div class="range-block" data-source="openai,custom,xai">
650650 <div class="range-block-title" data-i18n="Multiple swipes per generation">
651651 Multiple swipes per generation
652652 </div>
@@ -685,7 +685,7 @@
685685 </span>
686686 </div>
687687 </div>
688688 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai">
689689 <div class="range-block-title" data-i18n="Temperature">
690690 Temperature
691691 </div>
@@ -698,7 +698,7 @@
698698 </div>
699699 </div>
700700 </div>
701701 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai">
702702 <div class="range-block-title" data-i18n="Frequency Penalty">
703703 Frequency Penalty
704704 </div>
@@ -711,7 +711,7 @@
711711 </div>
712712 </div>
713713 </div>
714714 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai">
715715 <div class="range-block-title" data-i18n="Presence Penalty">
716716 Presence Penalty
717717 </div>
@@ -737,7 +737,7 @@
737737 </div>
738738 </div>
739739 </div>
740740 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai">
741741 <div class="range-block-title" data-i18n="Top P">
742742 Top P
743743 </div>
@@ -974,7 +974,7 @@
974974 </div>
975975 </div>
976976 </div>
977977 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai">
978978 <div class="range-block-title justifyLeft" data-i18n="Seed">
979979 Seed
980980 </div>
@@ -1419,7 +1419,7 @@
14191419 </div>
14201420 </div>
14211421
14221422 <div data-tg-type="aphrodite, ooba, koboldcpp, tabby, llamacpp, dreamgen" id="dryBlock" class="wide100p">
14231423 <h4 class="wide100p textAlignCenter" title="DRY penalizes tokens that would extend the end of the input into a sequence that has previously occurred in the input. Set multiplier to 0 to disable." data-i18n="[title]DRY_Repetition_Penalty_desc">
14241424 <label data-i18n="DRY Repetition Penalty">DRY Repetition Penalty</label>
14251425 <a href="https://github.com/oobabooga/text-generation-webui/pull/5677" target="_blank">
@@ -1574,7 +1574,7 @@
15741574 <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative" title="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative."></div>
15751575 </label>
15761576 </label>
15771577 <label data-tg-type="ooba, llamacpp, tabby, koboldcpp, dreamgen" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
15781578 <input type="checkbox" id="ban_eos_token_textgenerationwebui" />
15791579 <label>
15801580 <small data-i18n="Ban EOS Token">Ban EOS Token</small>
@@ -1963,9 +1963,12 @@
19631963 <span data-i18n="Use search capabilities provided by the backend.">
19641964 Use search capabilities provided by the backend.
19651965 </span>
1966+ <b data-source="openrouter" data-i18n="openrouter_web_search_fee">
1967+ Not free, adds a $0.02 fee to each prompt.
1968+ </b>
19661969 </div>
19671970 </div>
19681971 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,ai21,xai">
19691972 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19701973 <input id="openai_function_calling" type="checkbox" />
19711974 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1975,7 +1978,7 @@
19751978 <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
19761979 </div>
19771980 </div>
19781981 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,claude,custom,01ai,xai">
19791982 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
19801983 <input id="openai_image_inlining" type="checkbox" />
19811984 <span data-i18n="Send inline images">Send inline images</span>
@@ -1987,7 +1990,7 @@
19871990 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
19881991 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
19891992 </div>
19901993 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai">
19911994 <div class="flex-container oneline-dropdown">
19921995 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
19931996 Inline Image Quality
@@ -2022,7 +2025,7 @@
20222025 <input id="use_makersuite_sysprompt" type="checkbox" />
20232026 <span>
20242027 <span data-i18n="Use system prompt">Use system prompt</span>
20252028 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 1.5/2.0+, Pro/FlashLearnLM"></i>
20262029 </span>
20272030 </label>
20282031 <div class="toggle-description justifyLeft marginBot5">
@@ -2031,7 +2034,7 @@
20312034 </span>
20322035 </div>
20332036 </div>
20342037 <div class="range-block" data-source="deepseek,openrouter,custom,claude,xai">
20352038 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20362039 <input id="openai_show_thoughts" type="checkbox" />
20372040 <span>
@@ -2045,16 +2048,20 @@
20452048 </span>
20462049 </div>
20472050 </div>
20482051 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,openrouter">
20492052 <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.">
20502053 <label for="openai_reasoning_effort">
20512054 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
20522055 <i data-source="claudeopenai,custom,xai,openrouter" class="opacity50p fa-solid fa-circle-info" title="AllocatesOpenAI-style aoptions: portionlow, ofmedium, thehigh. responseMinimum lengthand formaximum thinkingare (aliased to low and high. Auto does not send an effort level." data-i18n="[title]OpenAI-style options: 10%low, medium: 25%, high:. 50%),Minimum butand minimummaximum 1024are tokensaliased to low and high. Auto does not send an effort level."></i>
2056+ <i data-source="claude,makersuite" class="opacity50p fa-solid fa-circle-info" title="Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%). Other options are model-dependent." data-i18n="[title]Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%). Other options are model-dependent."></i>
20532057 </label>
20542058 <select id="openai_reasoning_effort">
2059+ <option data-i18n="openai_reasoning_effort_auto" value="auto">Auto</option>
2060+ <option data-i18n="openai_reasoning_effort_minimum" value="min">Mininum</option>
20552061 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
20562062 <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
20572063 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2064+ <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
20582065 </select>
20592066 </div>
20602067 </div>
@@ -2491,8 +2498,8 @@
24912498 <option value="search" data-i18n="Search" hidden>A-Z</option>
24922499 <option value="asc">A-Z</option>
24932500 <option value="desc">Z-A</option>
24942501 <option data-i18n="Date Asc" value="date_asc">Date Asc</option>
24952502 <option data-i18n="Date Desc" value="date_desc">Date Desc</option>
24962503 </select>
24972504 <select id="featherless_category_selection" class="text_pole">
24982505 <option value="" disabled selected data-i18n="category">category</option>
@@ -2502,7 +2509,7 @@
25022509 <option value="All" data-i18n="All">All</option>
25032510 </select>
25042511 <select id="featherless_class_selection" class="text_pole">
25052512 <option value="" selected data-i18n="classAll Classes">All Classes</option>
25062513 </select>
25072514 <div id="featherless_model_pagination_container" class="flex1"></div>
25082515 <i id="featherless_model_grid_toggle" class="fa-solid fa-table-cells-large menu_button" data-i18n="[title]Toggle grid view" title="Toggle grid view"></i>
@@ -2615,8 +2622,8 @@
26152622 </div>
26162623 <div data-tg-type="ollama">
26172624 <div class="flex-container flexFlowColumn">
26182625 <a href="https://github.com/jmorgancaollama/ollama" target="_blank">
26192626 jmorgancaollama/ollama
26202627 </a>
26212628 </div>
26222629 <div class="flex1">
@@ -2756,9 +2763,10 @@
27562763 <option value="perplexity">Perplexity</option>
27572764 <option value="scale">Scale</option>
27582765 <option value="windowai">Window AI</option>
2766+ <option value="xai">xAI (Grok)</option>
27592767 </optgroup>
27602768 </select>
27612769 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
27622770 <div class="inline-drawer-toggle inline-drawer-header">
27632771 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
27642772 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2821,7 +2829,7 @@
28212829 </div>
28222830 </div>
28232831 </div>
28242832 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
28252833 <div class="reverse_proxy_warning">
28262834 <b>
28272835 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -2880,12 +2888,17 @@
28802888 </optgroup>
28812889 <optgroup label="GPT-4o mini">
28822890 <option value="gpt-4o-mini">gpt-4o-mini</option>
28832891 <option value="gpt-4o-mini-2024-1107-2018">gpt-4o-mini-2024-1107-2018</option>
2884- <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2892+ </optgroup>
2885- <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2893+ <optgroup label="GPT-4.1">
28862894 <option value="chatgpt-4ogpt-latest4.1">chatgpt-4ogpt-latest4.1</option>
2895+ <option value="gpt-4.1-2025-04-14">gpt-4.1-2025-04-14</option>
2896+ <option value="gpt-4.1-mini">gpt-4.1-mini</option>
2897+ <option value="gpt-4.1-mini-2025-04-14">gpt-4.1-mini-2025-04-14</option>
2898+ <option value="gpt-4.1-nano">gpt-4.1-nano</option>
2899+ <option value="gpt-4.1-nano-2025-04-14">gpt-4.1-nano-2025-04-14</option>
28872900 </optgroup>
28882901 <optgroup label="o1 and o1-mini">
28892902 <option value="o1">o1</option>
28902903 <option value="o1-2024-12-17">o1-2024-12-17</option>
28912904 <option value="o1-mini">o1-mini</option>
@@ -2894,9 +2907,15 @@
28942907 <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
28952908 </optgroup>
28962909 <optgroup label="o3">
2910+ <option value="o3">o3</option>
2911+ <option value="o3-2025-04-16">o3-2025-04-16</option>
28972912 <option value="o3-mini">o3-mini</option>
28982913 <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
28992914 </optgroup>
2915+ <optgroup label="o4">
2916+ <option value="o4-mini">o4-mini</option>
2917+ <option value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
2918+ </optgroup>
29002919 <optgroup label="GPT-4.5">
29012920 <option value="gpt-4.5-preview">gpt-4.5-preview</option>
29022921 <option value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
@@ -3127,48 +3146,49 @@
31273146 <div>
31283147 <h4 data-i18n="Google Model">Google Model</h4>
31293148 <select id="model_google_select">
31303149 <optgroup label="PrimaryGemini 2.5">
31313150 <option value="gemini-2.05-flashpro-preview-03-25">Gemini gemini-2.0 Flash5-pro-preview-03-25</option>
31323151 <option value="gemini-12.5-pro-exp-03-25">Gemini 1gemini-2.5 Pro-pro-exp-03-25</option>
31333152 <option value="gemini-12.5-flash-preview-04-17">Gemini 1gemini-2.5 Flash-flash-preview-04-17</option>
3134- <option value="gemini-1.0-pro">Gemini 1.0 Pro (Deprecated)</option>
3153+ </optgroup>
3135- <option value="gemini-pro">Gemini Pro (1.0) (Deprecated)</option>
3154+ <optgroup label="Gemini 2.0">
3136- <option value="gemini-ultra">Gemini Ultra (1.0)</option>
3155+ <option value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option>
31373156 <option value="gemini-12.0-ultrapro-latestexp">Gemini 1gemini-2.0-pro-exp Ultra→ 2.5-pro-exp-03-25</option>
3157+ <option value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option>
3158+ <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
3159+ <option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
3160+ <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
3161+ <option value="gemini-2.0-flash">gemini-2.0-flash</option>
3162+ <option value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-04-17</option>
3163+ <option value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option>
3164+ <option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option>
3165+ <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
3166+ <option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
3167+ <option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
3168+ </optgroup>
3169+ <optgroup label="Gemini 1.5">
3170+ <option value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
3171+ <option value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
3172+ <option value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
3173+ <option value="gemini-1.5-pro">gemini-1.5-pro</option>
3174+ <option value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
3175+ <option value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
3176+ <option value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
3177+ <option value="gemini-1.5-flash">gemini-1.5-flash</option>
3178+ <option value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
3179+ <option value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
3180+ <option value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
3181+ <option value="gemini-1.5-flash-8b">gemini-1.5-flash-8b</option>
31383182 </optgroup>
31393183 <optgroup label="Gemma">
31403184 <option value="gemma-3-27b-it">Gemma gemma-3 27B-27b-it</option>
3185+ <option value="gemma-3-12b-it">gemma-3-12b-it</option>
3186+ <option value="gemma-3-4b-it">gemma-3-4b-it</option>
3187+ <option value="gemma-3-1b-it">gemma-3-1b-it</option>
31413188 </optgroup>
31423189 <optgroup label="SubversionsLearnLM">
31433190 <option value="geminilearnlm-2.5-pro-preview0-03flash-25experimental">Gemini learnlm-2.5 Pro Preview 20250-03flash-25experimental</option>
31443191 <option value="geminilearnlm-21.5-pro-exp-03-25experimental">Gemini 2learnlm-1.5 Pro Experimental 2025-03pro-25experimental</option>
3145- <option value="gemini-2.0-pro-exp">Gemini 2.0 Pro Experimental</option>
3146- <option value="gemini-2.0-pro-exp-02-05">Gemini 2.0 Pro Experimental 2025-02-05</option>
3147- <option value="gemini-2.0-flash-lite-preview">Gemini 2.0 Flash-Lite Preview</option>
3148- <option value="gemini-2.0-flash-lite-preview-02-05">Gemini 2.0 Flash-Lite Preview 2025-02-05</option>
3149- <option value="gemini-2.0-flash-001">Gemini 2.0 Flash [001]</option>
3150- <option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking Experimental</option>
3151- <option value="gemini-2.0-flash-thinking-exp-01-21">Gemini 2.0 Flash Thinking Experimental 2025-01-21</option>
3152- <option value="gemini-2.0-flash-thinking-exp-1219">Gemini 2.0 Flash Thinking Experimental 2024-12-19</option>
3153- <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash Experimental</option>
3154- <option value="gemini-2.0-flash-exp-image-generation">Gemini 2.0 Flash (Image Generation) Experimental</option>
3155- <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
3156- <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
3157- <option value="gemini-exp-1206">Gemini Experimental 2024-12-06</option>
3158- <option value="gemini-1.5-pro-exp-0801">Gemini 1.5 Pro Experimental 2024-08-01</option>
3159- <option value="gemini-1.5-pro-exp-0827">Gemini 1.5 Pro Experimental 2024-08-27</option>
3160- <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
3161- <option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>
3162- <option value="gemini-1.5-pro-002">Gemini 1.5 Pro [002]</option>
3163- <option value="gemini-1.5-flash-8b">Gemini 1.5 Flash 8B</option>
3164- <option value="gemini-1.5-flash-exp-0827">Gemini 1.5 Flash Experimental 2024-08-27</option>
3165- <option value="gemini-1.5-flash-8b-exp-0827">Gemini 1.5 Flash 8B Experimental 2024-08-27</option>
3166- <option value="gemini-1.5-flash-8b-exp-0924">Gemini 1.5 Flash 8B Experimental 2024-09-24</option>
3167- <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash [latest]</option>
3168- <option value="gemini-1.5-flash-001">Gemini 1.5 Flash [001]</option>
3169- <option value="gemini-1.5-flash-002">Gemini 1.5 Flash [002]</option>
3170- <option value="gemini-1.0-pro-latest">Gemini 1.0 Pro [latest] (Deprecated)</option>
3171- <option value="gemini-1.0-pro-001">Gemini 1.0 Pro (Tuning) [001] (Deprecated)</option>
31723192 </optgroup>
31733193 </select>
31743194 </div>
@@ -3424,6 +3444,31 @@
34243444 <select id="model_01ai_select">
34253445 </select>
34263446 </div>
3447+ <div id="xai_form" data-source="xai">
3448+ <h4>
3449+ <a data-i18n="xAI API Key" href="https://console.x.ai/" target="_blank" rel="noopener noreferrer">
3450+ xAI API Key
3451+ </a>
3452+ </h4>
3453+ <div class="flex-container">
3454+ <input id="api_key_xai" name="api_key_xai" class="text_pole flex1" value="" type="text" autocomplete="off">
3455+ <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_xai"></div>
3456+ </div>
3457+ <div data-for="api_key_xai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
3458+ For privacy reasons, your API key will be hidden after you reload the page.
3459+ </div>
3460+ <h4 data-i18n="xAI Model">xAI Model</h4>
3461+ <select id="model_xai_select">
3462+ <option value="grok-3-beta">grok-3-beta</option>
3463+ <option value="grok-3-fast-beta">grok-3-fast-beta</option>
3464+ <option value="grok-3-mini-beta">grok-3-mini-beta</option>
3465+ <option value="grok-3-mini-fast-beta">grok-3-mini-fast-beta</option>
3466+ <option value="grok-2-vision-1212">grok-2-vision-1212</option>
3467+ <option value="grok-2-1212">grok-2-1212</option>
3468+ <option value="grok-vision-beta">grok-vision-beta</option>
3469+ <option value="grok-beta">grok-beta</option>
3470+ </select>
3471+ </div>
34273472 <div id="prompt_post_porcessing_form" data-source="custom,openrouter">
34283473 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
34293474 <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
@@ -3458,7 +3503,7 @@
34583503 <div class="drawer-toggle">
34593504 <div class="drawer-icon fa-solid fa-font fa-fw closedIcon" title="AI Response Formatting" data-i18n="[title]AI Response Formatting"></div>
34603505 </div>
34613506 <div id="AdvancedFormatting" class="drawer-content closedDrawer">
34623507 <div class="flex-container alignItemsBaseline">
34633508 <h3 class="margin0 flex1 flex-container alignItemsBaseline">
34643509 <span data-i18n="Advanced Formatting">
@@ -3577,11 +3622,6 @@
35773622 <small data-i18n="Names as Stop Strings">Names as Stop Strings</small>
35783623 </label>
35793624 </div>
3580-
3581- <label class="checkbox_label" title="Includes Post-History Instructions at the end of the prompt, if defined in the character card AND ''Prefer Char. Instructions'' is enabled.&#10;THIS IS NOT RECOMMENDED FOR TEXT COMPLETION MODELS, CAN LEAD TO BAD OUTPUT." data-i18n="[title]context_allow_post_history_instructions">
3582- <input id="context_allow_jailbreak" type="checkbox" />
3583- <small data-i18n="Allow Post-History Instructions">Allow Post-History Instructions</small>
3584- </label>
35853625 </div>
35863626 </div>
35873627 </div>
@@ -3783,9 +3823,7 @@
37833823 </label>
37843824 </div>
37853825 </h4>
37863826 <div id="SystemPromptBlock" class="marginBot10">
3787-
3788-
37893827 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
37903828 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>
37913829 <div class="flex-container margin0 justifyCenter gap3px">
@@ -3807,10 +3845,14 @@
38073845 </label>
38083846 <textarea id="sysprompt_content" class="text_pole textarea_compact autoSetHeight"></textarea>
38093847 </div>
3810- </div>
38113848
38123849 <div>
3813- &nbsp;
3850+ <label for="sysprompt_post_history" class="flex-container">
3851+ <small data-i18n="Post-History Instructions">Post-History Instructions</small>
3852+ <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="sysprompt_post_history" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
3853+ </label>
3854+ <textarea id="sysprompt_post_history" class="text_pole textarea_compact autoSetHeight"></textarea>
3855+ </div>
38143856 </div>
38153857
38163858 <div>
@@ -5113,7 +5155,7 @@
51135155 <div id="persona_depth_position_settings" class="flex-container">
51145156 <div class="flex1">
51155157 <label for="persona_depth_value" data-i18n="Depth:">Depth:</label>
51165158 <input id="persona_depth_value" class="text_pole" type="number" min="0" max="9999999" step="1">
51175159 </div>
51185160 <div class="flex1">
51195161 <label for="persona_depth_role" data-i18n="Role:">Role:</label>
@@ -5489,7 +5531,7 @@
54895531 <div class="inline-drawer-content">
54905532 <div id="currentGroupMembers" name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">
54915533 <div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div>
54925534 <div id="rm_group_members" class="rm_group_members overflowYAuto flex-container" group_empty_text="Group is empty." data-i18n="[group_empty_text]Group is empty."></div>
54935535 </div>
54945536 </div>
54955537 </div>
@@ -5507,7 +5549,7 @@
55075549 <div class="tags rm_tag_filter"></div>
55085550 </div>
55095551 <div id="rm_group_add_members_pagination" class="group_pagination"></div>
55105552 <div id="rm_group_add_members" class="overflowYAuto flex-container" no_characters_text="No characters available" data-i18n="[no_characters_text]No characters available"></div>
55115553 </div>
55125554 </div>
55135555 </div>
@@ -5766,7 +5808,7 @@
57665808 @ Depth
57675809 </span>
57685810 </h4>
57695811 <input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole textarea_compact m-t-0" type="number" min="0" max="9999999" value="4" form="form_create" />
57705812 <h4>
57715813 <span data-i18n="Role">
57725814 Role
@@ -5927,7 +5969,7 @@
59275969 <div class="tag_view_color_picker" data-value="color"></div>
59285970 <div class="tag_view_color_picker" data-value="color2"></div>
59295971 <div class="tag_view_name" contenteditable="true"></div>
59305972 <div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;<span data-i18n="tag_entries">entries</span></div>
59315973 <div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>
59325974 </div>
59335975 </div>
@@ -5987,11 +6029,11 @@
59876029 </div>
59886030 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">
59896031 <label for="depth" class="WIEntryHeaderTitleMobile" data-i18n="Depth:">Depth:</label>
59906032 <input title="Depth" class="text_pole wideMax100px margin0" type="number" name="depth" data-i18n="[title]Depth" placeholder="" min="0" max="9999999" />
59916033 </div>
59926034 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">
59936035 <label for="order" class="WIEntryHeaderTitleMobile" data-i18n="Order:">Order:</label>
59946036 <input title="Order" data-i18n="[title]Order" class="text_pole wideMax100px margin0" type="number" name="order" placeholder="" min="0" max="9999999" />
59956037 </div>
59966038 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap probabilityContainer">
59976039 <label for="order" class="WIEntryHeaderTitleMobile" data-i18n="Trigger %:">Trigger %:</label>
@@ -6247,6 +6289,54 @@
62476289 </label>
62486290 </div>
62496291 </div>
6292+ <div class="inline-drawer wide100p flexFlowColumn">
6293+ <div class="inline-drawer-toggle inline-drawer-header inline-drawer-header-pointer userSettingsInnerExpandable">
6294+ <strong data-i18n="Additional Matching Sources">Additional Matching Sources</strong>
6295+ <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
6296+ </div>
6297+ <div class="inline-drawer-content flex-container flexFlowRow flexGap10 paddingBottom5px">
6298+ <small class="flex-container flex1 flexFlowColumn">
6299+ <label class="checkbox flex-container alignItemsCenter flexNoGap">
6300+ <input type="checkbox" name="matchCharacterDescription" />
6301+ <span data-i18n="Character Description">
6302+ Character Description
6303+ </span>
6304+ </label>
6305+ <label class="checkbox flex-container alignItemsCenter flexNoGap">
6306+ <input type="checkbox" name="matchCharacterPersonality" />
6307+ <span data-i18n="Character Personality">
6308+ Character Personality
6309+ </span>
6310+ </label>
6311+ <label class="checkbox flex-container alignItemsCenter flexNoGap">
6312+ <input type="checkbox" name="matchScenario" />
6313+ <span data-i18n="Scenario">
6314+ Scenario
6315+ </span>
6316+ </label>
6317+ </small>
6318+ <small class="flex-container flex1 flexFlowColumn">
6319+ <label class="checkbox flex-container alignItemsCenter flexNoGap">
6320+ <input type="checkbox" name="matchPersonaDescription" />
6321+ <span data-i18n="Persona Description">
6322+ Persona Description
6323+ </span>
6324+ </label>
6325+ <label class="checkbox flex-container alignItemsCenter flexNoGap">
6326+ <input type="checkbox" name="matchCharacterDepthPrompt" />
6327+ <span data-i18n="Character's Note">
6328+ Character's Note
6329+ </span>
6330+ </label>
6331+ <label class="checkbox flex-container alignItemsCenter flexNoGap">
6332+ <input type="checkbox" name="matchCreatorNotes" />
6333+ <span data-i18n="Creator's Notes">
6334+ Creator's Notes
6335+ </span>
6336+ </label>
6337+ </small>
6338+ </div>
6339+ </div>
62506340 </div>
62516341 </div>
62526342 </form>
@@ -6353,7 +6443,7 @@
63536443 <span data-i18n="prompt_manager_depth">Depth</span>
63546444 </label>
63556445 <div class="text_muted" data-i18n="Injection depth. 0 = after the last message, 1 = before the last message, etc.">Injection depth. 0 = after the last message, 1 = before the last message, etc.</div>
63566446 <input id="completion_prompt_manager_popup_entry_form_injection_depth" class="text_pole" type="number" name="injection_depth" min="0" max="9999999" value="4" />
63576447 </div>
63586448 </div>
63596449 <div class="completion_prompt_manager_popup_entry_form_control">
@@ -6614,7 +6704,7 @@
66146704 </div>
66156705 <div class="flex-container wide100pLess70px character_select_container height100p alignitemscenter">
66166706 <div class="wide100p character_name_block">
66176707 <span class="ch_name" data-i18n="Go back">Go back</span>
66186708 </div>
66196709 </div>
66206710 </div>
@@ -6713,7 +6803,7 @@
67136803 <label class="checkbox_label alignItemsCenter" for="extension_floating_position_depth">
67146804 <input type="radio" id="extension_floating_position_depth" name="extension_floating_position" value="1" />
67156805 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
67166806 <input id="extension_floating_depth" class="text_pole textarea_compact widthNatural" type="number" min="0" max="9999999" />
67176807 <span data-i18n="as">as</span>
67186808 <select id="extension_floating_role" class="text_pole widthNatural">
67196809 <option data-i18n="System" value="0">System</option>
@@ -6728,7 +6818,7 @@
67286818 <span data-i18n="Insertion Frequency">Insertion Frequency</span>
67296819 <small data-i18n="(0 = Disable, 1 = Always)">(0 = Disable, 1 = Always)</small>
67306820 </label>
67316821 <input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="9999999" />
67326822 </div>
67336823 <br>
67346824 <span><span data-i18n="User inputs until next insertion:">User inputs until next insertion:</span> <span id="extension_floating_counter">(disabled)</span></span>
@@ -6798,7 +6888,7 @@
67986888 <label class="checkbox_label alignItemsCenter" for="extension_default_position_depth">
67996889 <input type="radio" id="extension_default_position_depth" name="extension_default_position" value="1" />
68006890 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
68016891 <input id="extension_default_depth" class="text_pole textarea_compact widthNatural" type="number" min="0" max="9999999" />
68026892 <span data-i18n="as">as</span>
68036893 <select id="extension_default_role" class="text_pole widthNatural">
68046894 <option data-i18n="System" value="0">System</option>
@@ -6812,7 +6902,7 @@
68126902 <span data-i18n="Insertion Frequency">Insertion Frequency</span>
68136903 <small data-i18n="(0 = Disable, 1 = Always)">(0 = Disable, 1 = Always)</small>
68146904 </label>
68156905 <input id="extension_default_interval" class="text_pole widthUnset" type="number" min="0" max="9999999" />
68166906 </div>
68176907 </div>
68186908 </div>
public/locales/ar-sa.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "بداية الدردشة",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "أضف بداية الدردشة وفاصل الأمثلة إلى قائمة سلاسل التوقف.",
413413 "Use as Stop Strings": "استخدم كسلاسل التوقف",
414- "context_allow_jailbreak": "يتضمن كسر الحماية في نهاية المطالبة، إذا تم تحديده في بطاقة الشخصية و''Prefer Char. تم تمكين الهروب من السجن.\nلا يُنصح بهذا بالنسبة لنماذج إكمال النص، فقد يؤدي إلى نتائج سيئة.",
415414 "Allow Jailbreak": "السماح بالجيلبريك",
416415 "Context Order": "ترتيب السياق",
417416 "Summary": "ملخص",
public/locales/de-de.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Chat-Start",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Fügen Sie einer Liste von Stoppzeichenfolgen „Chat-Start“ und „Beispieltrennzeichen“ hinzu.",
413413 "Use as Stop Strings": "Verwende als Stoppzeichenfolgen",
414- "context_allow_jailbreak": "Schließt Jailbreak am Ende der Eingabeaufforderung ein, wenn dies in der Charakterkarte definiert ist UND „Charakter-Jailbreak bevorzugen“ aktiviert ist.\nDIES WIRD FÜR TEXTVERVOLLSTÄNDIGUNGSMODELLE NICHT EMPFOHLEN, KANN ZU SCHLECHTEN AUSGABEN FÜHREN.",
415414 "Allow Jailbreak": "Jailbreak zulassen",
416415 "Context Order": "Kontextreihenfolge",
417416 "Summary": "Zusammenfassung",
public/locales/es-es.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Inicio de chat",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Agregue Inicio de chat y Separador de ejemplo a una lista de cadenas de parada.",
413413 "Use as Stop Strings": "Usar como Cadenas de Parada",
414- "context_allow_jailbreak": "Incluye Jailbreak al final del mensaje, si está definido en la tarjeta de personaje Y está habilitado \"Prefer Char. Jailbreak\".\nESTO NO SE RECOMIENDA PARA MODELOS DE COMPLETO DE TEXTO, PUEDE PRODUCIR UN RESULTADO INCORRECTO.",
415414 "Allow Jailbreak": "Permitir Jailbreak",
416415 "Context Order": "Orden de contexto",
417416 "Summary": "Resumen",
public/locales/fr-fr.json+0 -1
@@ -1448,7 +1448,6 @@
14481448 "Add Character and User names to a list of stopping strings.": "Ajouter les noms de personnages et d'utilisateurs à une liste de chaînes d'arrêt.",
14491449 "Names as Stop Strings": "Noms comme chaînes d'arrêt",
14501450 "context_allow_post_history_instructions": "Inclut les instructions post-historiques à la fin du prompt, si elles sont définies dans la fiche de personnage ET si l'option 'Préférer les instructions de personnage' est activée.\nN'EST PAS RECOMMANDÉ POUR LES MODÈLES DE COMPLÉTION DE TEXTE, CAR IL PEUT ENTRAÎNER DE MAUVAIS RÉSULTATS.",
1451- "Allow Post-History Instructions": "Autoriser les instructions post-histoire",
14521451 "Instruct Template": "Modèle d'instruction",
14531452 "instruct_derived": "Dériver des métadonnées du modèle, si possible.",
14541453 "instruct_enabled": "Activer le mode d'instruction",
public/locales/is-is.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Chat Start",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Bættu Chat Start og Example Separator við lista yfir stöðvunarstrengi.",
413413 "Use as Stop Strings": "Nota sem Stoppa Strengir",
414- "context_allow_jailbreak": "Inniheldur Jailbreak í lok hvetjunnar, ef það er skilgreint á stafkortinu OG ''Velst Char. Jailbreak'' er virkt.\nÞETTA ER EKKI MÆLT FYRIR TEXTAÚRSLUNARGERÐ, GETUR leitt til lélegrar úttaks.",
415414 "Allow Jailbreak": "Leyfa jailbreak",
416415 "Context Order": "Samhengisröð",
417416 "Summary": "Samantekt",
public/locales/it-it.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Inizio chat",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Aggiungi Inizio chat e Separatore di esempio a un elenco di stringhe di arresto.",
413413 "Use as Stop Strings": "Usa come stringhe di arresto",
414- "context_allow_jailbreak": "Include il jailbreak alla fine del prompt, se definito nella carta personaggio E ''Preferisci Char. Il jailbreak'' è abilitato.\nQUESTO NON È CONSIGLIATO PER I MODELLI DI COMPLETAMENTO DEL TESTO, PUÒ PORTARE A UN RISULTATO CATTIVO.",
415414 "Allow Jailbreak": "Consenti jailbreak",
416415 "Context Order": "Ordine del contesto",
417416 "Summary": "Riepilogo",
public/locales/ja-jp.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "チャット開始",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "停止文字列のリストにチャット開始と例の区切り文字を追加します。",
413413 "Use as Stop Strings": "ストップ文字列として使用",
414- "context_allow_jailbreak": "文字カードで定義されていて、「文字 Jailbreak を優先」が有効になっている場合は、プロンプトの最後に Jailbreak が含まれます。\nこれはテキスト補完モデルには推奨されません。出力が悪くなる可能性があります。",
415414 "Allow Jailbreak": "脱獄を許可する",
416415 "Context Order": "コンテキスト順序",
417416 "Summary": "まとめ",
public/locales/ko-kr.json+0 -2
@@ -421,7 +421,6 @@
421421 "Chat Start": "채팅 시작",
422422 "Add Chat Start and Example Separator to a list of stopping strings.": "중지 문자열 목록에 채팅 시작 및 예제 구분 기호를 추가합니다.",
423423 "Use as Stop Strings": "중지 문자열로 사용",
424- "context_allow_jailbreak": "캐릭터 카드에 정의되어 있고 ''Prefer Char. Jailbreak''가 활성화되어 있는 경우 프롬프트 끝에 Jailbreak를 포함합니다.\n이는 텍스트 완성 모델에 권장되지 않으며, 나쁜 출력으로 이어질 수 있습니다.",
425424 "Allow Jailbreak": "탈옥 허용",
426425 "Context Order": "컨텍스트 순서",
427426 "Summary": "요약",
@@ -1520,7 +1519,6 @@
15201519 "Always": "항상 추가함",
15211520 "Separators as Stop Strings": "구분 기호를 정지 문자열로 사용하기",
15221521 "Names as Stop Strings": "캐릭터의 이름들을 정지 문자열로 사용하기",
1523- "Allow Post-History Instructions": "Post-History 지침 허용",
15241522 "Image Captioning": "이미지 캡셔닝",
15251523 "Automatically caption images": "자동으로 이미지에 대한 설명 문장으로 나타내기",
15261524 "Edit captions before saving": "저장하기 전에 이미지에 대한 설명 문장 편집하기",
public/locales/nl-nl.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Chatstart",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Voeg Chat Start en Voorbeeldscheidingsteken toe aan een lijst met stoptekenreeksen.",
413413 "Use as Stop Strings": "Gebruik als stopreeksen",
414- "context_allow_jailbreak": "Inclusief jailbreak aan het einde van de prompt, indien gedefinieerd in de karakterkaart EN ''Prefer Char. Jailbreak'' is ingeschakeld.\nDIT WORDT NIET AANBEVOLEN VOOR MODELLEN VOOR HET INVOEREN VAN TEKST. KAN TOT SLECHTE UITVOER LEIDEN.",
415414 "Allow Jailbreak": "Jailbreak toestaan",
416415 "Context Order": "Contextvolgorde",
417416 "Summary": "Samenvatting",
public/locales/pt-pt.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Início do Chat",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Adicione o início do bate-papo e o separador de exemplo a uma lista de strings de parada.",
413413 "Use as Stop Strings": "Usar como Strings de Parada",
414- "context_allow_jailbreak": "Inclui Jailbreak no final do prompt, se definido no cartão de personagem E ''Prefer Char. Jailbreak'' está habilitado.\nISTO NÃO É RECOMENDADO PARA MODELOS DE COMPLEMENTAÇÃO DE TEXTO, PODE LEVAR A UMA SAÍDA RUIM.",
415414 "Allow Jailbreak": "Permitir jailbreak",
416415 "Context Order": "Ordem de Contexto",
417416 "Summary": "Resumo",
public/locales/ru-ru.json+105 -24
@@ -52,7 +52,7 @@
5252 "Presence Penalty": "Штраф за присутствие",
5353 "Top A": "Top А",
5454 "Tail Free Sampling": "Tail Free Sampling",
5555 "Rep. Pen. Slope": "Rep.Рост Pen.штрафа Slopeза повтор к концу промпта",
5656 "Top K": "Top K",
5757 "Top P": "Top P",
5858 "Do Sample": "Включить сэмплинг",
@@ -162,9 +162,9 @@
162162 "Story String": "Строка истории",
163163 "Example Separator": "Разделитель примеров сообщений",
164164 "Chat Start": "Начало чата",
165165 "Activation Regex": "RegexРег. выражение для активации",
166166 "Instruct Mode": "Режим Instruct",
167167 "Wrap Sequences with Newline": "ОтделятьКаждая строкистрока символомиз шаблона на новой строкистроке",
168168 "Include Names": "Добавлять имена",
169169 "Force for Groups and Personas": "Также для групп и персон",
170170 "System Prompt": "Системный промпт",
@@ -299,7 +299,7 @@
299299 "AI Horde": "AI Horde",
300300 "NovelAI": "NovelAI",
301301 "OpenAI API key": "Ключ для API OpenAI",
302302 "Trim spaces": "Обрезать пробелы в начале и конце",
303303 "Trim Incomplete Sentences": "Удалять неоконченные предложения",
304304 "Include Newline": "Добавлять новую строку",
305305 "Non-markdown strings": "Строки без разметки",
@@ -510,7 +510,7 @@
510510 "New preset": "Новый пресет",
511511 "Delete preset": "Удалить пресет",
512512 "API Connections": "Соединения с API",
513513 "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь с плохимипри ответамиплохих ставяответах, вделая очередьзапросы только подтвержденныхк работниковдоверенным рабочим машинам. Может замедлить время ответа.",
514514 "Clear your API key": "Стереть ключ от API",
515515 "Refresh models": "Обновить модели",
516516 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai",
@@ -551,7 +551,7 @@
551551 "Token counts may be inaccurate and provided just for reference.": "Счетчик токенов может быть неточным, используйте как ориентир",
552552 "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа",
553553 "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]",
554554 "Toggle grid view": "ПереключитьСменить вид сетки",
555555 "Add to Favorites": "Добавить в Избранное",
556556 "Advanced Definition": "Расширенное описание",
557557 "Character Lore": "Лор персонажа",
@@ -624,7 +624,7 @@
624624 "UI Theme": "Тема UI",
625625 "This message is invisible for the AI": "Это сообщение невидимо для ИИ",
626626 "Sampler Priority": "Приоритет сэмплеров",
627627 "Ooba only. Determines the order of samplers.": "Только для oobabooga. Определяет порядок сэмплеров.",
628628 "Load default order": "Загрузить стандартный порядок",
629629 "Max Tokens Second": "Макс. кол-во токенов в секунду",
630630 "CFG": "CFG",
@@ -695,7 +695,7 @@
695695 "Medium": "Средний",
696696 "Aggressive": "Агрессивный",
697697 "Very aggressive": "Очень агрессивный",
698698 "Eta_Cutoff_desc": "Eta cutoff - основной параметр специальной техники сэмплинга под названием Eta Sampling.&#13;В\nВ единицах 1e-4; разумное значение - 3.&#13;Установите\nУстановите в 0, чтобы отключить.&#13;См\nСм. статью Truncation Sampling as Language Model Desmoothing от Хьюитт и др. (2022) для получения подробной информации.",
699699 "Learn how to contribute your idle GPU cycles to the Horde": "Узнайте, как использовать время простоя вашего GPU для помощи Horde",
700700 "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Используйте соответствующий токенизатор для моделей Google через их API. Медленная обработка подсказок, но предлагает намного более точный подсчет токенов.",
701701 "Load koboldcpp order": "Загрузить порядок из koboldcpp",
@@ -964,7 +964,7 @@
964964 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",
965965 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",
966966 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
967967 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, заглянитенаходится в",
968968 "char_import_7": ")",
969969 "Grammar String": "Грамматика",
970970 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
@@ -1016,7 +1016,7 @@
10161016 "prompt_manager_relative": "Относительная",
10171017 "prompt_manager_depth": "Глубина",
10181018 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глубина вставки. 0 = после последнего сообщения, 1 = перед последним сообщением, и т.д.",
10191019 "The prompt to be sent.": "Отправляемый ИИТекст промптпромпта.",
10201020 "prompt_manager_forbid_overrides": "Запретить перезапись",
10211021 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",
10221022 "image_inlining_hint_1": "Отправлять картинки как часть промпта, если позволяет модель (такой функционал поддерживают GPT-4V, Claude 3 или Llava 13B). Чтобы добавить в чат изображение, используйте на нужном сообщении действие",
@@ -1232,7 +1232,7 @@
12321232 "Top P & Min P": "Top P & Min P",
12331233 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.",
12341234 "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.",
12351235 "character_names_default": "ExceptДобавлять forпрефиксы groupsдля andгрупповых pastчатов personasи предыдущих персон. Otherwise,В makeостальных sureслучаях youуказывайте provideимена namesв inпромпте theиными promptспособами.",
12361236 "Completion": "Completion Object",
12371237 "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",
12381238 "Use AI21 Tokenizer": "Использовать токенайзер AI21",
@@ -1257,7 +1257,6 @@
12571257 "Peek a password": "Посмотреть пароль",
12581258 "Clear your cookie": "Clear your cookie",
12591259 "Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",
1260- "context_allow_jailbreak": "Если в карточке есть джейлбрейк И ПРИ ЭТОМ включена опция \"Приоритет джейлбрейку из карточки персонажа\", то этот джейлбрейк добавляется в конец промпта.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",
12611260 "Context Order": "Context Order",
12621261 "Summary": "Summary",
12631262 "Example Dialogues": "Примеры диалогов",
@@ -1278,7 +1277,7 @@
12781277 "Will be inserted as a last prompt line when using system/neutral generation.": "Will be inserted as a last prompt line when using system/neutral generation.",
12791278 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "Если ИИ генерирует стоп-строку, то всё после неё будет вырезано из ответа (включая и саму стоп-строку).",
12801279 "Will be inserted at the start of the chat history if it doesn't start with a User message.": "Вставляется в начале истории чата, если она начинается не с сообщения пользователя.",
12811280 "Global World Info/Lorebook activation settings": "НастройкиГлобальные активациинастройки глобальногоактивации лорбука / Информации о мире",
12821281 "Click to expand": "Щёлкните, чтобы развернуть",
12831282 "Insertion Strategy": "Как инжектить",
12841283 "Only the entries with the most number of key matches will be selected for Inclusion Group filtering": "Only the entries with the most number of key matches will be selected for Inclusion Group filtering",
@@ -1647,10 +1646,9 @@
16471646 "mui_reset": "Сброс",
16481647 "Quick 'Impersonate' button": "Быстрое перевоплощение",
16491648 "Show a button in the input area to ask the AI to impersonate your character for a single message": "Показать в поле ввода кнопку, по нажатии на которую ИИ сгенерирует одно сообщение от лица вашего персонажа.",
16501649 "Separators as Stop Strings": "Разделители какв качестве стоп-строкистрок",
16511650 "Names as Stop Strings": "Имена какв качестве стоп-строкистрок",
16521651 "Add Character and User names to a list of stopping strings.": "Добавлять имена персонажа и пользователя в список стоп-строк.",
1653- "Allow Post-History Instructions": "Разрешить инструкции после истории",
16541652 "context_allow_post_history_instructions": "Добавлять в конец промпта инструкции после истории. Работает только при наличии таких инструкций в карточке И при включенной опции ''Приоритет инструкциям из карточек''.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",
16551653 "First User Prefix": "Первый префикс пользователя",
16561654 "Inserted before the first User's message.": "Вставляется перед первым сообщением пользователя.",
@@ -1916,8 +1914,8 @@
19161914 "Cannot restore GUI preset": "Пресет для Gui восстановить нельзя",
19171915 "Default preset cannot be restored": "Невозможно восстановить пресет по умолчанию",
19181916 "Default template cannot be restored": "Невозможно восстановить шаблон по умолчанию",
19191917 "Resetting a <b>default preset</b> will restore the default settings.": "Сброс <b>стандартногокомплектного пресета</b> восстановит настройки по умолчанию.",
19201918 "Resetting a <b>default template</b> will restore the default settings.": "Сброс <b>стандартногокомплектного шаблона</b> восстановит настройки по умолчанию.",
19211919 "Are you sure?": "Вы уверены?",
19221920 "Default preset restored": "Стандартный пресет восстановлен",
19231921 "Default template restored": "Стандартный шаблон восстановлен",
@@ -2048,11 +2046,11 @@
20482046 "prompt_post_processing_merge": "Объединять идущие подряд сообщения с одной ролью",
20492047 "prompt_post_processing_semi": "Semi-strict (чередовать роли)",
20502048 "prompt_post_processing_strict": "Strict (чередовать роли, сначала пользователь)",
20512049 "Select Horde models": "ВыбратьВыберите модель из Horde",
20522050 "Model ID (optional)": "Идентификатор модели (необязательно)",
20532051 "Derive context size from backend": "Использовать бэкенд для определения размера контекста",
20542052 "Rename current preset": "Переименовать пресет",
20552053 "No Worlds active. Click here to select.": "Нет активныхАктивных миров. Нажмитенет, чтобыЛКМ выбратьдля выбора.",
20562054 "Title/Memo": "Название",
20572055 "Strategy": "Статус",
20582056 "Position": "Позиция",
@@ -2171,7 +2169,7 @@
21712169 "instruct_derived": "Считывать из метаданных модели (по возможности)",
21722170 "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
21732171 "Reasoning Effort": "Рассуждения",
21742172 "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей, которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
21752173 "openai_reasoning_effort_low": "Поверхностные",
21762174 "openai_reasoning_effort_medium": "Обычные",
21772175 "openai_reasoning_effort_high": "Подробные",
@@ -2276,8 +2274,8 @@
22762274 "Persona Name Not Set": "У персоны отсутствует имя",
22772275 "You must bind a name to this persona before you can set a lorebook.": "Перед привязкой лорбука персоне необходимо присвоить имя.",
22782276 "Default Persona Removed": "Персона по умолчанию снята",
22792277 "Persona is locked to the current character": "Персона закреплена за этимтекущим персонажем",
22802278 "Persona is locked to the current chat": "Персона закреплена за этимтекущим чатом",
22812279 "characters": "перс.",
22822280 "character": "персонаж",
22832281 "in this group": "в группе",
@@ -2338,5 +2336,88 @@
23382336 "Reasoning already exists.": "Рассуждения уже присутствуют.",
23392337 "Edit Message": "Редактирование",
23402338 "Status check bypassed": "Проверка статуса отключена",
23412339 "Valid": "Работает",
2340+ "Use Group Scoring": "Использовать Group Scoring",
2341+ "Only the entries with the most number of key matches will be selected for Inclusion Group filtering": "До групповых фильтров будут допущены только записи с наибольшим кол-вом совпадений",
2342+ "Can be used to automatically activate Quick Replies": "Используется для автоматической активации быстрых ответов (Quick Replies)",
2343+ "( None )": "(Отсутствует)",
2344+ "Tie this entry to specific characters or characters with specific tags": "Привязать запись к опред. персонажам или персонажам с заданными тегами",
2345+ "Move Entry to Another Lorebook": "Переместить запись в другой лорбук",
2346+ "There are no other lorebooks to move to.": "Некуда перемещать: не найдено других лорбуков.",
2347+ "Select Target Lorebook": "Выберите куда переместить",
2348+ "Move '${0}' to:": "Переместить '${0}' в:",
2349+ "Please select a target lorebook.": "Выберите лорбук, в который будет перемещена запись.",
2350+ "Scan depth cannot be negative": "Глубина сканирования не может быть отрицательной",
2351+ "Scan depth cannot exceed ${0}": "Глубина сканирования не может превышать ${0}",
2352+ "Select your current Reasoning Template": "Выберите текущий Шаблон рассуждений",
2353+ "Delete template": "Удалить шаблон",
2354+ "Reasoning Template": "Шаблон рассуждений",
2355+ "openai_reasoning_effort_auto": "Авто",
2356+ "openai_reasoning_effort_minimum": "Минимальные",
2357+ "openai_reasoning_effort_maximum": "Максимальные",
2358+ "OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.": "OpenAI принимает следующее: low (Поверхностные), medium (Обычные), high (Подробные). Minimum (Минимальные) - то же самое, что low. Maximum (Максимальные) - то же самое, что high. При выборе Auto (Авто) значение не отсылается вообще.",
2359+ "Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%). Other options are model-dependent.": "Резервирует часть ответа для рассуждений (Поверхностные: 10% ответа, Обычные: 25%, Подробные: 50%). Остальные значения зависят от конкретной модели.",
2360+ "xAI Model": "Модель xAI",
2361+ "xAI API Key": "Ключ от API xAI",
2362+ "HuggingFace Token": "Токен HuggingFace",
2363+ "Endpoint URL": "Адрес эндпоинта",
2364+ "Example: https://****.endpoints.huggingface.cloud": "Пример: https://****.endpoints.huggingface.cloud",
2365+ "Featherless Model Selection": "Выбор модели из Featherless",
2366+ "category": "категория",
2367+ "Top": "Топовые",
2368+ "All Classes": "Все классы",
2369+ "Date Asc": "Дата, возрастание",
2370+ "Date Desc": "Дата, убывание",
2371+ "Background Image": "Фоновое изображение",
2372+ "Delete the background?": "Удалить фон?",
2373+ "Tags_as_Folders_desc": "Чтобы тег отображался как папка, его нужно отметить таковым в меню управления тегами. Нажмите сюда, чтобы открыть его.",
2374+ "tag_entries": "раз исп.",
2375+ "Multiple personas are connected to this character.\nSelect a persona to use for this chat.": "К этому персонажу привязано несколько персон.\nВыберите персону, которую хотите использовать в этом чате.",
2376+ "Select Persona": "Выберите персону",
2377+ "Completion Object": "Как часть Completion Object",
2378+ "Move ${0} to:": "Переместить '${0}' в:",
2379+ "Chat Scenario Override": "Перезапись сценария чата",
2380+ "Unique to this chat.": "Действует только в рамках текущего чата.",
2381+ "All group members will use the following scenario text instead of what is specified in their character cards.": "Все участники группы будут использовать этот сценарий вместо того, который указан в карточке.",
2382+ "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Чекпоинты наследуют сценарий родителя, после отделения его можно менять.",
2383+ "Delete Tag": "Удалить тег",
2384+ "Do you want to delete the tag": "Вы точно хотите удалить тег",
2385+ "If you want to merge all references to this tag into another tag, select it below:": "Если хотите заменить ссылки на этот тег на какой-то другой, то выберите из списка:",
2386+ "Open Folder (Show all characters even if not selected)": "Открытая папка (показать всех персонажей, включая невыбранных)",
2387+ "Closed Folder (Hide all characters unless selected)": "Закрытая папка (скрыть всех персонажей, кроме выбранных)",
2388+ "No Folder": "Не папка",
2389+ "Show only favorites": "Показать только избранных персонажей",
2390+ "Show only groups": "Показать только группы",
2391+ "Show only folders": "Показать только папки",
2392+ "Manage tags": "Панель управления тегами",
2393+ "Show Tag List": "Показать список тегов",
2394+ "Clear all filters": "Сбросить все фильтры",
2395+ "There are no items to display.": "Отображать абсолютно нечего.",
2396+ "Characters and groups hidden by filters or closed folders": "Персонажи и группы скрыты настройками фильтров либо закрытыми папками",
2397+ "Otterly empty": "Всё что можно, всё выдрано",
2398+ "Here be dragons": "Список настолько очистился, что в него вернулись драконы",
2399+ "Kiwibunga": "Настолько пусто, что киви прилетела посидеть",
2400+ "Pump-a-Rum": "Пу-пу-пу",
2401+ "Croak it": "Только кваканье лягушек и стрёкот сверчков",
2402+ "${0} character hidden.": "Персонажей скрыто: ${0}.",
2403+ "${0} characters hidden.": "Персонажей скрыто: ${0}.",
2404+ "/ page": "/ стр.",
2405+ "Context Length": "Размер контекста",
2406+ "Added On": "Добавлена",
2407+ "Class": "Класс",
2408+ "Bulk_edit_characters": "Массовое редактирование персонажей\n\nЛКМ, чтобы выделить либо отменить выделение персонажа\nShift+ЛКМ, чтобы массово выделить либо отменить выделение персонажей\nПКМ, чтобы выбрать действие",
2409+ "Bulk select all characters": "Выбрать всех персонажей",
2410+ "Duplicate": "Клонировать",
2411+ "Next page": "След. страница",
2412+ "Previous page": "Пред. страница",
2413+ "Group: ${0}": "Группа: ${0}",
2414+ "You deleted a character/chat and arrived back here for safety reasons! Pick another character!": "Вы удалили персонажа или чат, и мы из соображений безопасности перенесли вас на эту страницу! Выберите другого персонажа!",
2415+ "Group is empty.": "Группа пуста.",
2416+ "No characters available": "Персонажей нет",
2417+ "Choose what to export": "Выберите, что экспортировать",
2418+ "Text Completion Preset": "Пресет для режима Text Completion",
2419+ "Update enabled": "Обновить включенные",
2420+ "Could not connect to API": "Не удалось подключиться к API",
2421+ "Connected to API": "Соединение с API установлено",
2422+ "Go back": "Назад"
23422423}
public/locales/uk-ua.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Початок чату",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Додайте початок чату та роздільник прикладів до списку рядків зупинки.",
413413 "Use as Stop Strings": "Використовувати як рядки зупинки",
414- "context_allow_jailbreak": "Включає втечу з в’язниці в кінці підказки, якщо визначено в картці символів ТА «Переважати символ. Втечу з в'язниці'' увімкнено.\nЦЕ НЕ РЕКОМЕНДУЄТЬСЯ ДЛЯ МОДЕЛЕЙ ЗАВЕРШЕННЯ ТЕКСТУ, МОЖЕ ПРИЗВЕСТИ ДО ПОГАНОГО РЕЗУЛЬТАТУ.",
415414 "Allow Jailbreak": "Дозволити втечу з в'язниці",
416415 "Context Order": "Порядок контексту",
417416 "Summary": "Резюме",
public/locales/vi-vn.json+0 -1
@@ -411,7 +411,6 @@
411411 "Chat Start": "Bắt đầu Chat",
412412 "Add Chat Start and Example Separator to a list of stopping strings.": "Thêm Bắt đầu trò chuyện và Dấu phân cách ví dụ vào danh sách các chuỗi dừng.",
413413 "Use as Stop Strings": "Sử dụng như chuỗi dừng",
414- "context_allow_jailbreak": "Bao gồm Bẻ khóa ở cuối Prompt, nếu được xác định trong thẻ ký tự VÀ ''Thích Char. Bẻ khóa'' được bật.\nĐIỀU NÀY KHÔNG ĐƯỢC KHUYẾN NGHỊ CHO CÁC MÔ HÌNH HOÀN THÀNH VĂN BẢN, CÓ THỂ DẪN ĐẾN ĐẦU RA XẤU.",
415414 "Allow Jailbreak": "Cho phép bẻ khóa",
416415 "Context Order": "Thứ tự bối cảnh",
417416 "Summary": "Bản tóm tắt",
public/locales/zh-cn.json+24 -5
@@ -504,7 +504,6 @@
504504 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
505505 "Names as Stop Strings": "名称作为终止字符串",
506506 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",
507- "Allow Post-History Instructions": "允许后历史说明",
508507 "Instruct Template": "指导模板",
509508 "instruct_derived": "如果可能,从模型元数据中获取",
510509 "instruct_bind_to_context": "如果启用,上下文模板将根据所选的指导模板名称或偏好自动选择。",
@@ -1358,7 +1357,7 @@
13581357 "Image Captioning": "图像描述",
13591358 "Source": "来源",
13601359 "Local": "本地",
13611360 "Multimodal (OpenAI / Anthropic / llama / Google)": "多模式多模态(OpenAI / Anthropic / llama / Google)",
13621361 "Extras": "更多",
13631362 "Horde": "Horde",
13641363 "API": "API",
@@ -1588,8 +1587,8 @@
15881587 "sd_function_tool_txt": "Use function tool",
15891588 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",
15901589 "sd_interactive_mode_txt": "交互模式",
15911590 "sd_multimodal_captioning": "使用多模式字幕根据用户和角色的头像生成提示词使用多模态字幕根据用户和角色的头像生成提示词。",
15921591 "sd_multimodal_captioning_txt": "使用多模式字幕来描绘肖像使用多模态字幕来描绘肖像",
15931592 "sd_free_extend": "使用当前选择的 LLM 自动扩展自由模式主题提示(不是肖像或背景)。",
15941593 "sd_free_extend_txt": "延长自由模式提示",
15951594 "sd_free_extend_small": "(交互/命令)",
@@ -2109,5 +2108,25 @@
21092108 "Title/Memo": "标题(备忘)",
21102109 "Strategy": "触发策略",
21112110 "Position": "插入位置",
21122111 "Trigger %": "触发概率%",
2112+ "Generate Caption": "生成图片描述",
2113+ "(DEPRECATED)": "(已弃用)",
2114+ "[Currently loaded]": "[当前加载]",
2115+ "Change Persona Image": "更改角色图片",
2116+ "Delete Persona": "删除角色",
2117+ "Duplicate Persona": "复制角色",
2118+ "Enter a name for this persona:": "输入角色名",
2119+ "Enable web search": "启用联网搜索",
2120+ "Current Persona": "当前角色",
2121+ "Global Settings": "全局设置",
2122+ "Select a model": "选择模型",
2123+ "Thinking...": "思考中",
2124+ "Valid": "有效",
2125+ "Rename Persona": "重命名角色",
2126+ "Sort By: Name (Z-A)": "排序: 名称(Z-A)",
2127+ "Sort By: Name (A-Z)": "排序: 名称(A-Z)",
2128+ "Sort By: Date (Oldest First)": "排序: 日期(从最远到最新)",
2129+ "Sort By: Date (Newest First)": "排序: 日期(从最新到最远)",
2130+ "Set the reasoning block of a message. Returns the reasoning block content.": "设置消息的推理块。返回推理块内容。",
2131+ "Select providers. No selection = all providers.": "选择服务商。未选择 = 所有服务商。"
21132132}
public/locales/zh-tw.json+0 -2
@@ -412,7 +412,6 @@
412412 "Chat Start": "聊天開始符號",
413413 "Add Chat Start and Example Separator to a list of stopping strings.": "將聊天開始和範例分隔符號加入終止字串中。",
414414 "Use as Stop Strings": "用作停止字串",
415- "context_allow_jailbreak": "如果在角色卡中定義了越獄,且啟用了「角色卡越獄優先」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。",
416415 "Allow Jailbreak": "允許越獄",
417416 "Context Order": "上下文順序",
418417 "Summary": "摘要",
@@ -1555,7 +1554,6 @@
15551554 "All": "全部",
15561555 "Allow fallback models": "允許回退模型",
15571556 "Allow fallback providers": "允許回退供應商",
1558- "Allow Post-History Instructions": "允許聊天歷史後指示",
15591557 "Allow reverse proxy": "允許反向代理",
15601558 "Alternate Greeting #": "備選問候語 #",
15611559 "alternate_greetings_hint_1": "點選",
public/script.js+114 -52
@@ -50,6 +50,7 @@ import {
5050 importWorldInfo,
5151 wi_anchor_position,
5252 world_info_include_names,
53+ initWorldInfo,
5354} from './scripts/world-info.js';
5455
5556import {
@@ -142,6 +143,7 @@ import {
142143 getHordeModels,
143144 adjustHordeGenerationParams,
144145 MIN_LENGTH,
146+ initHorde,
145147} from './scripts/horde.js';
146148
147149import {
@@ -174,6 +176,9 @@ import {
174176 saveBase64AsFile,
175177 uuidv4,
176178 equalsIgnoreCaseAndAccents,
179+ localizePagination,
180+ renderPaginationDropdown,
181+ paginationDropdownChangeHandler,
177182} from './scripts/utils.js';
178183import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';
179184
@@ -651,7 +656,7 @@ export const extension_prompt_roles = {
651656 ASSISTANT: 2,
652657};
653658
654659export const MAX_INJECTION_DEPTH = 100010000;
655660
656661const SAFETY_CHAT = [
657662 {
@@ -992,6 +997,8 @@ async function firstLoadInit() {
992997 initBackgrounds();
993998 initAuthorsNote();
994999 await initPersonas();
1000+ initWorldInfo();
1001+ initHorde();
9951002 initRossMods();
9961003 initStats();
9971004 initCfg();
@@ -1421,30 +1428,26 @@ function getBackBlock() {
14211428 return template;
14221429}
14231430
14241431async function getEmptyBlock() {
14251432 const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog'];
14261433 const texts = ['t`Here be dragons'`, 't`Otterly empty'`, 't`Kiwibunga'`, 't`Pump-a-Rum'`, 't`Croak it'`];
14271434 const roll = new Date().getMinutes() % icons.length;
14281435 const emptyBlockparams = `{
1429- <div class="text_block empty_block">
1436+ text: texts[roll],
1430- <i class="fa-solid ${icons[roll]} fa-4x"></i>
1437+ icon: icons[roll],
1431- <h1>${texts[roll]}</h1>
1438+ };
1432- <p>There are no items to display.</p>
1439+ const emptyBlock = await renderTemplateAsync('emptyBlock', params);
1433- </div>`;
14341440 return $(emptyBlock);
14351441}
14361442
14371443/**
14381444 * @param {number} hidden Number of hidden characters
14391445 */
14401446async function getHiddenBlock(hidden) {
14411447 const hiddenBlockparams = `{
1442- <div class="text_block hidden_block">
1448+ text: (hidden > 1 ? t`${hidden} characters hidden.` : t`${hidden} character hidden.`),
1443- <small>
1449+ };
1444- <p>${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.</p>
1450+ const hiddenBlock = await renderTemplateAsync('hiddenBlock', params);
1445- <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Characters and groups hidden by filters or closed folders" title="Characters and groups hidden by filters or closed folders"></div>
1446- </small>
1447- </div>`;
14481451 return $(hiddenBlock);
14491452}
14501453
@@ -1524,10 +1527,11 @@ export async function printCharacters(fullRefresh = false) {
15241527
15251528 const entities = getEntitiesList({ doFilter: true });
15261529
1530+ const pageSize = Number(accountStorage.getItem(storageKey)) || per_page_default;
1531+ const sizeChangerOptions = [10, 25, 50, 100, 250, 500, 1000];
15271532 $('#rm_print_characters_pagination').pagination({
15281533 dataSource: entities,
1529- pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default,
1534+ pageSize,
1530- sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
15311535 pageRange: 1,
15321536 pageNumber: saveCharactersPage || 1,
15331537 position: 'top',
@@ -1536,14 +1540,16 @@ export async function printCharacters(fullRefresh = false) {
15361540 prevText: '<',
15371541 nextText: '>',
15381542 formatNavigator: PAGINATION_TEMPLATE,
1543+ formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
15391544 showNavigator: true,
15401545 callback: async function (/** @type {Entity[]} */ data) {
15411546 $(listId).empty();
15421547 if (power_user.bogus_folders && isBogusFolderOpen()) {
15431548 $(listId).append(getBackBlock());
15441549 }
15451550 if (!data.length) {
1546- $(listId).append(getEmptyBlock());
1551+ const emptyBlock = await getEmptyBlock();
1552+ $(listId).append(emptyBlock);
15471553 }
15481554 let displayCount = 0;
15491555 for (const i of data) {
@@ -1564,13 +1570,16 @@ export async function printCharacters(fullRefresh = false) {
15641570
15651571 const hidden = (characters.length + groups.length) - displayCount;
15661572 if (hidden > 0 && entitiesFilter.hasAnyFilter()) {
1567- $(listId).append(getHiddenBlock(hidden));
1573+ const hiddenBlock = await getHiddenBlock(hidden);
1574+ $(listId).append(hiddenBlock);
15681575 }
1576+ localizePagination($('#rm_print_characters_pagination'));
15691577
15701578 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
15711579 },
15721580 afterSizeSelectorChange: function (e, size) {
15731581 accountStorage.setItem(storageKey, e.target.value);
1582+ paginationDropdownChangeHandler(e, size);
15741583 },
15751584 afterPaging: function (e) {
15761585 saveCharactersPage = e;
@@ -2751,6 +2760,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
27512760 environment.charVersion = fields.version || '';
27522761 environment.char_version = fields.version || '';
27532762 environment.charDepthPrompt = fields.charDepthPrompt || '';
2763+ environment.creatorNotes = fields.creatorNotes || '';
27542764 }
27552765
27562766 // Must be substituted last so that they're replaced inside {{description}}
@@ -3047,6 +3057,20 @@ export async function getExtensionPromptByName(moduleName) {
30473057}
30483058
30493059/**
3060+ * Gets the maximum depth of extension prompts.
3061+ * @returns {number} Maximum depth of extension prompts
3062+ */
3063+export function getExtensionPromptMaxDepth() {
3064+ return MAX_INJECTION_DEPTH;
3065+ /*
3066+ const prompts = Object.values(extension_prompts);
3067+ const maxDepth = Math.max(...prompts.map(x => x.depth ?? 0));
3068+ // Clamp to 1 <= depth <= MAX_INJECTION_DEPTH
3069+ return Math.max(Math.min(maxDepth, MAX_INJECTION_DEPTH), 1);
3070+ */
3071+}
3072+
3073+/**
30503074 * Returns the extension prompt for the given position, depth, and role.
30513075 * If multiple prompts are found, they are joined with a separator.
30523076 * @param {number} [position] Position of the prompt
@@ -3115,6 +3139,7 @@ export function baseChatReplace(value, name1, name2) {
31153139 * @property {string} jailbreak Jailbreak instructions
31163140 * @property {string} version Character version
31173141 * @property {string} charDepthPrompt Character depth note
3142+ * @property {string} creatorNotes Character creator notes
31183143 * @returns {CharacterCardFields} Character card fields
31193144 */
31203145export function getCharacterCardFields({ chid = null } = {}) {
@@ -3130,6 +3155,7 @@ export function getCharacterCardFields({ chid = null } = {}) {
31303155 jailbreak: '',
31313156 version: '',
31323157 charDepthPrompt: '',
3158+ creatorNotes: '',
31333159 };
31343160 result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
31353161
@@ -3148,6 +3174,7 @@ export function getCharacterCardFields({ chid = null } = {}) {
31483174 result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : '';
31493175 result.version = character.data?.character_version ?? '';
31503176 result.charDepthPrompt = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2);
3177+ result.creatorNotes = baseChatReplace(character.data?.creator_notes?.trim(), name1, name2);
31513178
31523179 if (selected_group) {
31533180 const groupCards = getGroupCharacterCards(selected_group, Number(currentChid));
@@ -3975,11 +4002,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39754002 system,
39764003 jailbreak,
39774004 charDepthPrompt,
4005+ creatorNotes,
39784006 } = getCharacterCardFields();
39794007
39804008 if (main_api !== 'openai') {
39814009 if (power_user.sysprompt.enabled) {
39824010 system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.sysprompt.content, name1, name2);
4011+ ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? ''))
4012+ : baseChatReplace(power_user.sysprompt.content, name1, name2);
39834013 system = isInstruct ? formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.sysprompt.content)) : system;
39844014 } else {
39854015 // Nullify if it's not enabled
@@ -4129,7 +4159,15 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
41294159 // Make quiet prompt available for WIAN
41304160 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
41314161 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();
4132- const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun);
4162+ const globalScanData = {
4163+ personaDescription: persona,
4164+ characterDescription: description,
4165+ characterPersonality: personality,
4166+ characterDepthPrompt: charDepthPrompt,
4167+ scenario: scenario,
4168+ creatorNotes: creatorNotes,
4169+ };
4170+ const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);
41334171 setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);
41344172
41354173 // Add message example WI
@@ -4178,17 +4216,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
41784216 injectedIndices = await doChatInject(coreChat, isContinue);
41794217 }
41804218
4181- // Insert character jailbreak as the last user message (if exists, allowed, preferred, and not using Chat Completion)
4219+ if (main_api !== 'openai' && power_user.sysprompt.enabled) {
4182- if (power_user.context.allow_jailbreak && power_user.prefer_character_jailbreak && main_api !== 'openai' && jailbreak) {
4220+ jailbreak = power_user.prefer_character_jailbreak && jailbreak
4183- // Set "original" explicity to empty string since there's no original
4221+ ? substituteParams(jailbreak, name1, name2, (power_user.sysprompt.post_history ?? ''))
41844222 jailbreak = : substituteParamsbaseChatReplace(jailbreakpower_user.sysprompt.post_history, name1, name2, '');
41854223
4186- // When continuing generation of previous output, last user message precedes the message to continue
4224+ // Only inject the jb if there is one
41874225 if (isContinuejailbreak) {
4188- coreChat.splice(coreChat.length - 1, 0, { mes: jailbreak, is_user: true });
4226+ // When continuing generation of previous output, last user message precedes the message to continue
4189- }
4227+ if (isContinue) {
4190- else {
4228+ coreChat.splice(coreChat.length - 1, 0, { mes: jailbreak, is_user: true });
4191- coreChat.push({ mes: jailbreak, is_user: true });
4229+ }
4230+ else {
4231+ coreChat.push({ mes: jailbreak, is_user: true });
4232+ }
41924233 }
41934234 }
41944235
@@ -4221,12 +4262,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42214262
42224263 // Do not suffix the message for continuation
42234264 if (i === 0 && isContinue) {
4265+ // Pick something that's very unlikely to be in a message
4266+ const FORMAT_TOKEN = '\u0000\ufffc\u0000\ufffd';
4267+
42244268 if (isInstruct) {
4269+ const originalMessage = String(coreChat[j].mes ?? '');
4270+ coreChat[j].mes = originalMessage.replaceAll(FORMAT_TOKEN, '') + FORMAT_TOKEN;
42254271 // Reformat with the last output sequence (if any)
42264272 chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);
4273+ coreChat[j].mes = originalMessage;
42274274 }
42284275
4229- chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length);
4276+ chat2[i] = chat2[i].includes(FORMAT_TOKEN)
4277+ ? chat2[i].slice(0, chat2[i].lastIndexOf(FORMAT_TOKEN))
4278+ : chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length);
42304279 continue_mag = coreChat[j].mes;
42314280 }
42324281
@@ -4840,6 +4889,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
48404889 userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''),
48414890 tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '',
48424891 presetName: getPresetManager()?.getSelectedPresetName() || '',
4892+ messagesCount: main_api !== 'openai' ? mesSend.length : oaiMessages.length,
4893+ examplesCount: main_api !== 'openai' ? (pinExmString ? mesExamplesArray.length : count_exm_add) : oaiMessageExamples.length,
48434894 };
48444895
48454896 //console.log(additionalPromptStuff);
@@ -5099,7 +5150,8 @@ async function doChatInject(messages, isContinue) {
50995150 let totalInsertedMessages = 0;
51005151 messages.reverse();
51015152
5102- for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) {
5153+ const maxDepth = getExtensionPromptMaxDepth();
5154+ for (let i = 0; i <= maxDepth; i++) {
51035155 // Order of priority (most important go lower)
51045156 const roles = [extension_prompt_roles.SYSTEM, extension_prompt_roles.USER, extension_prompt_roles.ASSISTANT];
51055157 const names = {
@@ -5522,6 +5574,8 @@ export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMes
55225574 modelUsed: chat[incomingMesId]?.extra?.model,
55235575 apiUsed: chat[incomingMesId]?.extra?.api,
55245576 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
5577+ messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
5578+ examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
55255579 };
55265580
55275581 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
@@ -6940,15 +6994,23 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
69406994 throw new Error(result.statusText);
69416995 }
69426996
69436997 const forceSaveConfirmedpopupResult = await Popup.show.confirminput(
69446998 t`ERROR: Chat integrity check failed while saving the file.`,
6945- t`Continuing the operation may result in data loss. Would you like to overwrite the chat file anyway? Pressing "NO" will cancel the save operation.`,
6999+ t`<p>After you click OK, the page will be reloaded to prevent data corruption.</p>
6946- { okButton: t`Yes, overwrite`, cancelButton: t`No, cancel` },
7000+ <p>To confirm an overwrite (and potentially <b>LOSE YOUR DATA</b>), enter <code>OVERWRITE</code> (in all caps) in the box below before clicking OK.</p>`,
6947- ) === POPUP_RESULT.AFFIRMATIVE;
7001+ '',
7002+ { okButton: 'OK', cancelButton: false },
7003+ );
7004+
7005+ const forceSaveConfirmed = popupResult === 'OVERWRITE';
69487006
69497007 if (!forceSaveConfirmed) {
6950- await saveChat({ chatName, withMetadata, mesId, force: true });
7008+ console.warn('Chat integrity check failed, and user did not confirm the overwrite. Reloading the page.');
7009+ window.location.reload();
7010+ return;
69517011 }
7012+
7013+ await saveChat({ chatName, withMetadata, mesId, force: true });
69527014 } catch (error) {
69537015 console.error(error);
69547016 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);
@@ -8375,15 +8437,15 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w
83758437 function getOkButtonText() {
83768438 if (['text', 'char_not_selected'].includes(popup_type)) {
83778439 $dialoguePopupCancel.css('display', 'none');
83788440 return okButton ?? 't`Ok'`;
83798441 } else if (['delete_extension'].includes(popup_type)) {
83808442 return okButton ?? 't`Ok'`;
83818443 } else if (['new_chat', 'confirm'].includes(popup_type)) {
83828444 return okButton ?? 't`Yes'`;
83838445 } else if (['input'].includes(popup_type)) {
83848446 return okButton ?? t`Save`;
83858447 }
83868448 return okButton ?? 't`Delete'`;
83878449 }
83888450
83898451 dialogueCloseStop = true;
@@ -9089,7 +9151,7 @@ function formatSwipeCounter(current, total) {
90899151 * @param {string} [params.source] The source of the swipe event.
90909152 * @param {boolean} [params.repeated] Is the swipe event repeated.
90919153 */
90929154export function swipe_left(_event, { source, repeated } = {}) {
90939155 if (chat.length - 1 === Number(this_edit_mes_id)) {
90949156 closeMessageEditor();
90959157 }
@@ -9237,7 +9299,7 @@ function swipe_left(_event, { source, repeated } = {}) {
92379299 * @param {string} [params.source] The source of the swipe event.
92389300 * @param {boolean} [params.repeated] Is the swipe event repeated.
92399301 */
92409302export function swipe_right(_event, { source, repeated } = {}) {
92419303 if (chat.length - 1 === Number(this_edit_mes_id)) {
92429304 closeMessageEditor();
92439305 }
@@ -11832,8 +11894,8 @@ jQuery(async function () {
1183211894 return;
1183311895 }
1183411896 const drawer = $(this).closest('.inline-drawer');
1183511897 const icon = drawer.find('>.inline-drawer-header .inline-drawer-icon');
1183611898 const drawerContent = drawer.find('>.inline-drawer-content');
1183711899 icon.toggleClass('down up');
1183811900 icon.toggleClass('fa-circle-chevron-down fa-circle-chevron-up');
1183911901 drawerContent.stop().slideToggle({
public/scripts/RossAscends-mods.js+3 -2
@@ -409,6 +409,7 @@ function RA_autoconnect(PrevApi) {
409409 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
410410 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
411411 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
412+ || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
412413 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
413414 ) {
414415 $('#api_button_openai').trigger('click');
@@ -1047,7 +1048,7 @@ export function initRossMods() {
10471048 //Enter to send when send_textarea in focus
10481049 if (document.activeElement == hotkeyTargets['send_textarea']) {
10491050 const sendOnEnter = shouldSendOnEnter();
10501051 if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
10511052 event.preventDefault();
10521053 sendTextareaMessage();
10531054 return;
@@ -1119,7 +1120,7 @@ export function initRossMods() {
11191120 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
11201121 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
11211122 onClose: (popup) => {
11221123 regenerateWithCtrlEnter = Boolean(popup.inputResults.get('regenerateWithCtrlEnter') ?? false);
11231124 },
11241125 });
11251126 if (!result) {
public/scripts/backgrounds.js+2 -1
@@ -6,6 +6,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
66import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
77import { flashHighlight, stringFormat } from './utils.js';
88import { t } from './i18n.js';
9+import { Popup } from './popup.js';
910
1011const BG_METADATA_KEY = 'custom_background';
1112const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -291,7 +292,7 @@ async function onDeleteBackgroundClick(e) {
291292 const bgToDelete = $(this).closest('.bg_example');
292293 const url = bgToDelete.data('url');
293294 const isCustom = bgToDelete.attr('custom') === 'true';
294295 const confirm = await callPopupPopup.show.confirm('<h3>t`Delete the background?</h3>'`, 'confirm'null);
295296 const bg = bgToDelete.attr('bgfile');
296297
297298 if (confirm) {
public/scripts/char-data.js+6 -0
@@ -33,6 +33,12 @@
3333 * @property {number} role - The specific function or purpose of the extension.
3434 * @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.
3535 * @property {number} display_index - The order in which the extension should be displayed for user interfaces.
36+ * @property {boolean} match_persona_description - Wether to match against the persona description.
37+ * @property {boolean} match_character_description - Wether to match against the persona description.
38+ * @property {boolean} match_character_personality - Wether to match against the character personality.
39+ * @property {boolean} match_character_depth_prompt - Wether to match against the character depth prompt.
40+ * @property {boolean} match_scenario - Wether to match against the character scenario.
41+ * @property {boolean} match_creator_notes - Wether to match against the character creator notes.
3642 */
3743
3844/**
public/scripts/chat-templates.js+5 -0
@@ -74,6 +74,11 @@ const hash_derivations = {
7474 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
7575 'DeepSeek-V2.5'
7676 ,
77+
78+ // THUDM-GLM 4
79+ '854b703e44ca06bdb196cc471c728d15dbab61e744fe6cdce980086b61646ed1':
80+ 'GLM-4'
81+ ,
7782};
7883
7984const substr_derivations = {
public/scripts/custom-request.js+11 -5
@@ -43,10 +43,12 @@ import EventSourceStream from './sse-stream.js';
4343 * @property {boolean?} [stream=false] - Whether to stream the response
4444 * @property {ChatCompletionMessage[]} messages - Array of chat messages
4545 * @property {string} [model] - Optional model name to use for completion
4646 * @property {string} chat_completion_source - Source provider for chat completion
4747 * @property {number} max_tokens - Maximum number of tokens to generate
4848 * @property {number} [temperature] - Optional temperature parameter for response randomness
4949 * @property {string} [custom_url] - Optional custom URL for chat completion
50+ * @property {string} [reverse_proxy] - Optional reverse proxy URL
51+ * @property {string} [proxy_password] - Optional proxy password
5052 */
5153
5254/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
@@ -80,7 +82,6 @@ export class TextCompletionService {
8082 */
8183 static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
8284 const payload = {
83- ...props,
8485 stream,
8586 prompt,
8687 max_tokens,
@@ -90,6 +91,7 @@ export class TextCompletionService {
9091 api_server: api_server ?? getTextGenServer(api_type),
9192 temperature,
9293 min_p,
94+ ...props,
9395 };
9496
9597 // Remove undefined values to avoid API errors
@@ -387,9 +389,8 @@ export class ChatCompletionService {
387389 * @param {ChatCompletionPayload} custom
388390 * @returns {ChatCompletionPayload}
389391 */
390392 static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, reverse_proxy, proxy_password, ...props }) {
391393 const payload = {
392- ...props,
393394 stream,
394395 messages,
395396 model,
@@ -397,6 +398,11 @@ export class ChatCompletionService {
397398 max_tokens,
398399 temperature,
399400 custom_url,
401+ reverse_proxy,
402+ proxy_password,
403+ use_makersuite_sysprompt: true,
404+ claude_use_sysprompt: true,
405+ ...props,
400406 };
401407
402408 // Remove undefined values to avoid API errors
public/scripts/extensions.js+124 -3
@@ -661,6 +661,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
661661 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>` : '';
662662 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>` : '';
663663 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>` : '';
664+ let branchButton = isExternal && isUserAdmin ? `<button class="btn_branch menu_button" data-name="${externalId}" data-i18n="[title]Switch branch" title="Switch branch"><i class="fa-solid fa-code-branch fa-fw"></i></button>` : '';
664665 let modulesInfo = '';
665666
666667 if (isActive && Array.isArray(manifest.optional)) {
@@ -701,6 +702,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
701702
702703 <div class="extension_actions flex-container alignItemsCenter">
703704 ${updateButton}
705+ ${branchButton}
704706 ${moveButton}
705707 ${deleteButton}
706708 </div>
@@ -944,6 +946,44 @@ async function onDeleteClick() {
944946 }
945947}
946948
949+async function onBranchClick() {
950+ const extensionName = $(this).data('name');
951+ const isCurrentUserAdmin = isAdmin();
952+ const isGlobal = getExtensionType(extensionName) === 'global';
953+ if (isGlobal && !isCurrentUserAdmin) {
954+ toastr.error(t`You don't have permission to switch branch.`);
955+ return;
956+ }
957+
958+ let newBranch = '';
959+
960+ const branches = await getExtensionBranches(extensionName, isGlobal);
961+ const selectElement = document.createElement('select');
962+ selectElement.classList.add('text_pole', 'wide100p');
963+ selectElement.addEventListener('change', function () {
964+ newBranch = this.value;
965+ });
966+ for (const branch of branches) {
967+ const option = document.createElement('option');
968+ option.value = branch.name;
969+ option.textContent = `${branch.name} (${branch.commit}) [${branch.label}]`;
970+ option.selected = branch.current;
971+ selectElement.appendChild(option);
972+ }
973+
974+ const popup = new Popup(selectElement, POPUP_TYPE.CONFIRM, '', {
975+ okButton: t`Switch`,
976+ cancelButton: t`Cancel`,
977+ });
978+ const popupResult = await popup.show();
979+
980+ if (!popupResult || !newBranch) {
981+ return;
982+ }
983+
984+ await switchExtensionBranch(extensionName, isGlobal, newBranch);
985+}
986+
947987async function onMoveClick() {
948988 const extensionName = $(this).data('name');
949989 const isCurrentUserAdmin = isAdmin();
@@ -1056,12 +1096,82 @@ async function getExtensionVersion(extensionName, abortSignal) {
10561096}
10571097
10581098/**
1099+ * Gets the list of branches for a specific extension.
1100+ * @param {string} extensionName The name of the extension
1101+ * @param {boolean} isGlobal Whether the extension is global or not
1102+ * @returns {Promise<ExtensionBranch[]>} List of branches for the extension
1103+ * @typedef {object} ExtensionBranch
1104+ * @property {string} name The name of the branch
1105+ * @property {string} commit The commit hash of the branch
1106+ * @property {boolean} current Whether this branch is the current one
1107+ * @property {string} label The commit label of the branch
1108+ */
1109+async function getExtensionBranches(extensionName, isGlobal) {
1110+ try {
1111+ const response = await fetch('/api/extensions/branches', {
1112+ method: 'POST',
1113+ headers: getRequestHeaders(),
1114+ body: JSON.stringify({
1115+ extensionName,
1116+ global: isGlobal,
1117+ }),
1118+ });
1119+
1120+ if (!response.ok) {
1121+ const text = await response.text();
1122+ toastr.error(text || response.statusText, t`Extension branches fetch failed`);
1123+ console.error('Extension branches fetch failed', response.status, response.statusText, text);
1124+ return [];
1125+ }
1126+
1127+ return await response.json();
1128+ } catch (error) {
1129+ console.error('Error:', error);
1130+ return [];
1131+ }
1132+}
1133+
1134+/**
1135+ * Switches the branch of an extension.
1136+ * @param {string} extensionName The name of the extension
1137+ * @param {boolean} isGlobal If the extension is global
1138+ * @param {string} branch Branch name to switch to
1139+ * @returns {Promise<void>}
1140+ */
1141+async function switchExtensionBranch(extensionName, isGlobal, branch) {
1142+ try {
1143+ const response = await fetch('/api/extensions/switch', {
1144+ method: 'POST',
1145+ headers: getRequestHeaders(),
1146+ body: JSON.stringify({
1147+ extensionName,
1148+ branch,
1149+ global: isGlobal,
1150+ }),
1151+ });
1152+
1153+ if (!response.ok) {
1154+ const text = await response.text();
1155+ toastr.error(text || response.statusText, t`Extension branch switch failed`);
1156+ console.error('Extension branch switch failed', response.status, response.statusText, text);
1157+ return;
1158+ }
1159+
1160+ toastr.success(t`Extension ${extensionName} switched to ${branch}`);
1161+ await loadExtensionSettings({}, false, false);
1162+ void showExtensionsDetails();
1163+ } catch (error) {
1164+ console.error('Error:', error);
1165+ }
1166+}
1167+
1168+/**
10591169 * Installs a third-party extension via the API.
10601170 * @param {string} url Extension repository URL
10611171 * @param {boolean} global Is the extension global?
10621172 * @returns {Promise<void>}
10631173 */
10641174export async function installExtension(url, global, branch = '') {
10651175 console.debug('Extension installation started', url);
10661176
10671177 toastr.info(t`Please wait...`, t`Installing extension`);
@@ -1072,6 +1182,7 @@ export async function installExtension(url, global) {
10721182 body: JSON.stringify({
10731183 url,
10741184 global,
1185+ branch,
10751186 }),
10761187 });
10771188
@@ -1406,9 +1517,17 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
14061517 await popup.complete(POPUP_RESULT.AFFIRMATIVE);
14071518 },
14081519 };
1520+ /** @type {import('./popup.js').CustomPopupInput} */
1521+ const branchNameInput = {
1522+ id: 'extension_branch_name',
1523+ label: t`Branch or tag name (optional)`,
1524+ type: 'text',
1525+ tooltip: 'e.g. main, dev, v1.0.0',
1526+ };
14091527
14101528 const customButtons = isCurrentUserAdmin ? [installForAllButton] : [];
1411- const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons });
1529+ const customInputs = [branchNameInput];
1530+ const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons, customInputs });
14121531 const input = await popup.show();
14131532
14141533 if (!input) {
@@ -1417,7 +1536,8 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
14171536 }
14181537
14191538 const url = String(input).trim();
1420- await installExtension(url, global);
1539+ const branchName = String(popup.inputResults.get('extension_branch_name') ?? '').trim();
1540+ await installExtension(url, global, branchName);
14211541}
14221542
14231543export async function initExtensions() {
@@ -1433,6 +1553,7 @@ export async function initExtensions() {
14331553 $(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
14341554 $(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
14351555 $(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
1556+ $(document).on('click', '.extensions_info .extension_block .btn_branch', onBranchClick);
14361557
14371558 /**
14381559 * Handles the click event for the third-party extension import button.
public/scripts/extensions/assets/index.js+2 -2
@@ -291,7 +291,7 @@ async function installAsset(url, assetType, filename) {
291291 try {
292292 if (category === 'extension') {
293293 console.debug(DEBUG_PREFIX, 'Installing extension ', url);
294294 await installExtension(url, false);
295295 console.debug(DEBUG_PREFIX, 'Extension installed.');
296296 return;
297297 }
@@ -309,7 +309,7 @@ async function installAsset(url, assetType, filename) {
309309 console.debug(DEBUG_PREFIX, 'Importing character ', filename);
310310 const blob = await result.blob();
311311 const file = new File([blob], filename, { type: blob.type });
312312 await processDroppedFiles([file], true);
313313 console.debug(DEBUG_PREFIX, 'Character downloaded.');
314314 }
315315 }
public/scripts/extensions/assets/window.html+1 -1
@@ -39,7 +39,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
3939 <span data-i18n="Characters">Characters</span>
4040 </div>
4141 </div>
4242 <div class="inline-drawer-content" id="assets_menu">
4343 </div>
4444 </div>
4545 </div>
public/scripts/extensions/caption/index.js+1 -0
@@ -428,6 +428,7 @@ jQuery(async function () {
428428 'zerooneai': SECRET_KEYS.ZEROONEAI,
429429 'groq': SECRET_KEYS.GROQ,
430430 'cohere': SECRET_KEYS.COHERE,
431+ 'xai': SECRET_KEYS.XAI,
431432 };
432433
433434 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
public/scripts/extensions/caption/settings.html+36 -22
@@ -31,6 +31,7 @@
3131 <option value="openrouter">OpenRouter</option>
3232 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
3333 <option value="vllm">vLLM</option>
34+ <option value="xai">xAI (Grok)</option>
3435 </select>
3536 </div>
3637 <div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -46,13 +47,24 @@
4647 <option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>
4748 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
4849 <option data-type="zerooneai" value="yi-vision">yi-vision</option>
50+ <option data-type="openai" value="gpt-4.1">gpt-4.1</option>
51+ <option data-type="openai" value="gpt-4.1-2025-04-14">gpt-4.1-2025-04-14</option>
52+ <option data-type="openai" value="gpt-4.1-mini">gpt-4.1-mini</option>
53+ <option data-type="openai" value="gpt-4.1-mini-2025-04-14">gpt-4.1-mini-2025-04-14</option>
54+ <option data-type="openai" value="gpt-4.1-nano">gpt-4.1-nano</option>
55+ <option data-type="openai" value="gpt-4.1-nano-2025-04-14">gpt-4.1-nano-2025-04-14</option>
4956 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
5057 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
5158 <option data-type="openai" value="gpt-4o">gpt-4o</option>
5259 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
60+ <option data-type="openai" value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>
5361 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
5462 <option data-type="openai" value="o1">o1</option>
5563 <option data-type="openai" value="o1-2024-12-17">o1-2024-12-17</option>
64+ <option data-type="openai" value="o3">o3</option>
65+ <option data-type="openai" value="o3-2025-04-16">o3-2025-04-16</option>
66+ <option data-type="openai" value="o4-mini">o4-mini</option>
67+ <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
5668 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
5769 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
5870 <option data-type="anthropic" value="claude-3-7-sonnet-latest">claude-3-7-sonnet-latest</option>
@@ -67,33 +79,33 @@
6779 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
6880 <option data-type="google" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
6981 <option data-type="google" value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>
7082 <option data-type="google" value="gemini-2.05-proflash-exppreview-04-17">gemini-2.05-proflash-exppreview-04-17</option>
7183 <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option>
7284 <option data-type="google" value="gemini-2.0-flash-litepro-previewexp">gemini-2.0-flashpro-liteexp → 2.5-previewpro-exp-03-25</option>
7385 <option data-type="google" value="gemini-2.0-flash-lite-preview-02exp-051206">gemini-exp-1206 → 2.0-flash5-litepro-previewexp-0203-0525</option>
74- <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
7586 <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
76- <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
7787 <option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
7888 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
7989 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
8090 <option data-type="google" value="gemini-2.0-flash-thinking-exp-121901-21">gemini-2.0-flash-thinking-exp-121901-21 → 2.5-flash-preview-04-17</option>
8191 <option data-type="google" value="gemini-12.50-flash-thinking-exp-1219">gemini-12.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option>
92+ <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option>
93+ <option data-type="google" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
94+ <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
95+ <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
96+ <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
97+ <option data-type="google" value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
98+ <option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
99+ <option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
82100 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
83- <option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
84101 <option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
85102 <option data-type="google" value="gemini-1.5-flash-exp-0827001">gemini-1.5-flash-exp-0827001</option>
86103 <option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
104+ <option data-type="google" value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
87105 <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
88106 <option data-type="google" value="gemini-1.5-flash-8b-exp-11140827">gemini-1.5-flash-8b-exp-11140827</option>
89107 <option data-type="google" value="geminilearnlm-exp2.0-1121flash-experimental">geminilearnlm-exp2.0-1121flash-experimental</option>
90108 <option data-type="google" value="geminilearnlm-exp1.5-1206pro-experimental">geminilearnlm-exp1.5-1206pro-experimental</option>
91- <option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
92- <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
93- <option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
94- <option data-type="google" value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
95- <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
96- <option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>
97109 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
98110 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
99111 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
@@ -134,6 +146,8 @@
134146 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
135147 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
136148 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
149+ <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>
150+ <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>
137151 </select>
138152 </div>
139153 <div data-type="ollama">
public/scripts/extensions/memory/settings.html+1 -1
@@ -132,7 +132,7 @@
132132 </label>
133133 <label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
134134 <input type="radio" name="memory_position" value="1" />
135135 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span> <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="9999999" />
136136 <span data-i18n="as">as</span>
137137 <select id="memory_role" class="text_pole widthNatural">
138138 <option value="0" data-i18n="System">System</option>
public/scripts/extensions/regex/editor.html+2 -2
@@ -113,14 +113,14 @@
113113 <span data-i18n="Min Depth">Min Depth</span>
114114 <span class="fa-solid fa-circle-question note-link-span"></span>
115115 </small>
116116 <input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="9999999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
117117 </div>
118118 <div class="flex1 flex-container flexNoGap">
119119 <small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. Max must be greater than Min for regex to apply.">
120120 <span data-i18n="Max Depth">Max Depth</span>
121121 <span class="fa-solid fa-circle-question note-link-span"></span>
122122 </small>
123123 <input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="9999999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
124124 </div>
125125 </div>
126126 </div>
public/scripts/extensions/shared.js+13 -2
@@ -1,7 +1,7 @@
11import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
22import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
33import { t } from '../i18n.js';
44import { oai_settings, proxies } from '../openai.js';
55import { SECRET_KEYS, secret_state } from '../secrets.js';
66import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
77import { getTokenCountAsync } from '../tokenizers.js';
@@ -153,6 +153,10 @@ function throwIfInvalidModel(useReverseProxy) {
153153 throw new Error('Cohere API key is not set.');
154154 }
155155
156+ if (extension_settings.caption.multimodal_api === 'xai' && !secret_state[SECRET_KEYS.XAI]) {
157+ throw new Error('xAI API key is not set.');
158+ }
159+
156160 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
157161 throw new Error('Ollama server URL is not set.');
158162 }
@@ -306,9 +310,10 @@ export class ConnectionManagerRequestService {
306310 * @param {boolean?} [custom.includePreset=true]
307311 * @param {boolean?} [custom.includeInstruct=true]
308312 * @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings
313+ * @param {Record<string, any>} [overridePayload] - Override payload for the request
309314 * @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
310315 */
311316 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams, overridePayload = {}) {
312317 const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };
313318
314319 const context = SillyTavern.getContext();
@@ -326,6 +331,8 @@ export class ConnectionManagerRequestService {
326331 throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
327332 }
328333
334+ const proxyPreset = proxies.find((p) => p.name === profile.proxy);
335+
329336 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
330337 return await context.ChatCompletionService.processRequest({
331338 stream,
@@ -334,6 +341,9 @@ export class ConnectionManagerRequestService {
334341 model: profile.model,
335342 chat_completion_source: selectedApiMap.source,
336343 custom_url: profile['api-url'],
344+ reverse_proxy: proxyPreset?.url,
345+ proxy_password: proxyPreset?.password,
346+ ...overridePayload,
337347 }, {
338348 presetName: includePreset ? profile.preset : undefined,
339349 }, extractData, signal);
@@ -350,6 +360,7 @@ export class ConnectionManagerRequestService {
350360 model: profile.model,
351361 api_type: selectedApiMap.type,
352362 api_server: profile['api-url'],
363+ ...overridePayload,
353364 }, {
354365 instructName: includeInstruct ? profile.instruct : undefined,
355366 presetName: includePreset ? profile.preset : undefined,
public/scripts/extensions/stable-diffusion/index.js+55 -0
@@ -81,6 +81,7 @@ const sources = {
8181 nanogpt: 'nanogpt',
8282 bfl: 'bfl',
8383 falai: 'falai',
84+ xai: 'xai',
8485};
8586
8687const initiators = {
@@ -1303,6 +1304,7 @@ async function onModelChange() {
13031304 sources.nanogpt,
13041305 sources.bfl,
13051306 sources.falai,
1307+ sources.xai,
13061308 ];
13071309
13081310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1518,6 +1520,9 @@ async function loadSamplers() {
15181520 case sources.bfl:
15191521 samplers = ['N/A'];
15201522 break;
1523+ case sources.xai:
1524+ samplers = ['N/A'];
1525+ break;
15211526 }
15221527
15231528 for (const sampler of samplers) {
@@ -1708,6 +1713,9 @@ async function loadModels() {
17081713 case sources.falai:
17091714 models = await loadFalaiModels();
17101715 break;
1716+ case sources.xai:
1717+ models = await loadXAIModels();
1718+ break;
17111719 }
17121720
17131721 for (const model of models) {
@@ -1760,6 +1768,12 @@ async function loadFalaiModels() {
17601768 return [];
17611769}
17621770
1771+async function loadXAIModels() {
1772+ return [
1773+ { value: 'grok-2-image-1212', text: 'grok-2-image-1212' },
1774+ ];
1775+}
1776+
17631777async function loadPollinationsModels() {
17641778 const result = await fetch('/api/sd/pollinations/models', {
17651779 method: 'POST',
@@ -2081,6 +2095,9 @@ async function loadSchedulers() {
20812095 case sources.falai:
20822096 schedulers = ['N/A'];
20832097 break;
2098+ case sources.xai:
2099+ schedulers = ['N/A'];
2100+ break;
20842101 }
20852102
20862103 for (const scheduler of schedulers) {
@@ -2166,6 +2183,12 @@ async function loadVaes() {
21662183 case sources.bfl:
21672184 vaes = ['N/A'];
21682185 break;
2186+ case sources.falai:
2187+ vaes = ['N/A'];
2188+ break;
2189+ case sources.xai:
2190+ vaes = ['N/A'];
2191+ break;
21692192 }
21702193
21712194 for (const vae of vaes) {
@@ -2735,6 +2758,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27352758 case sources.falai:
27362759 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
27372760 break;
2761+ case sources.xai:
2762+ result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);
2763+ break;
27382764 }
27392765
27402766 if (!result.data) {
@@ -3464,6 +3490,33 @@ async function generateBflImage(prompt, signal) {
34643490}
34653491
34663492/**
3493+ * Generates an image using the xAI API.
3494+ * @param {string} prompt The main instruction used to guide the image generation.
3495+ * @param {string} _negativePrompt Negative prompt is not used in this API
3496+ * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
3497+ * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
3498+ */
3499+async function generateXAIImage(prompt, _negativePrompt, signal) {
3500+ const result = await fetch('/api/sd/xai/generate', {
3501+ method: 'POST',
3502+ headers: getRequestHeaders(),
3503+ signal: signal,
3504+ body: JSON.stringify({
3505+ prompt: prompt,
3506+ model: extension_settings.sd.model,
3507+ }),
3508+ });
3509+
3510+ if (result.ok) {
3511+ const data = await result.json();
3512+ return { format: 'jpg', data: data.image };
3513+ } else {
3514+ const text = await result.text();
3515+ throw new Error(text);
3516+ }
3517+}
3518+
3519+/**
34673520 * Generates an image using the FAL.AI API.
34683521 * @param {string} prompt - The main instruction used to guide the image generation.
34693522 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
@@ -3782,6 +3835,8 @@ function isValidState() {
37823835 return secret_state[SECRET_KEYS.BFL];
37833836 case sources.falai:
37843837 return secret_state[SECRET_KEYS.FALAI];
3838+ case sources.xai:
3839+ return secret_state[SECRET_KEYS.XAI];
37853840 }
37863841}
37873842
public/scripts/extensions/stable-diffusion/settings.html+1 -0
@@ -52,6 +52,7 @@
5252 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
5353 <option value="horde">Stable Horde</option>
5454 <option value="togetherai">TogetherAI</option>
55+ <option value="xai">xAI (Grok)</option>
5556 </select>
5657 <div data-sd-source="auto">
5758 <label for="sd_auto_url">SD Web UI URL</label>
public/scripts/extensions/tts/settings.html+1 -1
@@ -76,7 +76,7 @@
7676 <div id="tts_voicemap_block">
7777 </div>
7878 <hr>
7979 <form id="tts_provider_settings" class="inline-drawer-content">
8080 </form>
8181 <div class="tts_buttons">
8282 <input id="tts_voices" class="menu_button" type="submit" value="Available voices" />
public/scripts/extensions/tts/system.js+66 -17
@@ -79,6 +79,10 @@ class SystemTtsProvider {
7979 // Config //
8080 //########//
8181
82+ // Static constants for the simulated default voice
83+ static BROWSER_DEFAULT_VOICE_ID = '__browser_default__';
84+ static BROWSER_DEFAULT_VOICE_NAME = 'System Default Voice';
85+
8286 settings;
8387 ready = false;
8488 voices = [];
@@ -168,51 +172,97 @@ class SystemTtsProvider {
168172 //#################//
169173 fetchTtsVoiceObjects() {
170174 if (!('speechSynthesis' in window)) {
171175 return Promise.resolve([]);
172176 }
173177
174178 return new Promise((resolve) => {
175179 setTimeout(() => {
176180 constlet voices = speechSynthesis.getVoices();
177- .getVoices()
181+
178- .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
182+ if (voices.length === 0) {
179- .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
183+ // Edge compat: Provide default when voices empty
180-
184+ console.warn('SystemTTS: getVoices() returned empty list. Providing browser default option.');
181- resolve(voices);
185+ const defaultVoice = {
182- }, 1);
186+ name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME,
187+ voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID,
188+ preview_url: false,
189+ lang: navigator.language || 'en-US',
190+ };
191+ resolve([defaultVoice]);
192+ } else {
193+ const mappedVoices = voices
194+ .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
195+ .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
196+ resolve(mappedVoices);
197+ }
198+ }, 50);
183199 });
184200 }
185201
186202 previewTtsVoice(voiceId) {
187203 if (!('speechSynthesis' in window)) {
188204 throw new Error('Speech synthesis API is not supported');
189205 }
190206
191- const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId);
207+ let voice = null;
208+ if (voiceId !== SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID) {
209+ const voices = speechSynthesis.getVoices();
210+ voice = voices.find(x => x.voiceURI === voiceId);
192211
193212 if (!voice && voices.length > 0) {
194- throw `TTS Voice id ${voiceId} not found`;
213+ console.warn(`SystemTTS Preview: Voice ID "${voiceId}" not found among available voices. Using browser default.`);
214+ } else if (!voice && voices.length === 0) {
215+ console.warn('SystemTTS Preview: Voice list is empty. Using browser default.');
216+ }
217+ } else {
218+ console.log('SystemTTS Preview: Using browser default voice as requested.');
195219 }
196220
197221 speechSynthesis.cancel();
198- const text = getPreviewString(voice.lang);
222+ const langForPreview = voice ? voice.lang : (navigator.language || 'en-US');
223+ const text = getPreviewString(langForPreview);
199224 const utterance = new SpeechSynthesisUtterance(text);
200- utterance.voice = voice;
225+
226+ if (voice) {
227+ utterance.voice = voice;
228+ }
229+
201230 utterance.rate = this.settings.rate || 1;
202231 utterance.pitch = this.settings.pitch || 1;
232+
233+ utterance.onerror = (event) => {
234+ console.error(`SystemTTS Preview Error: ${event.error}`, event);
235+ };
236+
203237 speechSynthesis.speak(utterance);
204238 }
205239
206240 async getVoice(voiceName) {
207241 if (!('speechSynthesis' in window)) {
208242 return { voice_id: null, name: 'API Not Supported' };
243+ }
244+
245+ if (voiceName === SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME) {
246+ return {
247+ voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID,
248+ name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME,
249+ };
209250 }
210251
211252 const voices = speechSynthesis.getVoices();
253+
254+ if (voices.length === 0) {
255+ console.warn('SystemTTS: Empty voice list, using default fallback');
256+ return {
257+ voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID,
258+ name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME,
259+ };
260+ }
261+
212262 const match = voices.find(x => x.name == voiceName);
213263
214264 if (!match) {
215265 throw new Error(`SystemTTS getVoice: TTS Voice name "${voiceName}" not found`);
216266 }
217267
218268 return { voice_id: match.voiceURI, name: match.name };
@@ -237,7 +287,6 @@ class SystemTtsProvider {
237287 speechUtteranceChunker(utterance, {
238288 chunkLength: 200,
239289 }, function () {
240- //some code to execute when done
241290 resolve(silence);
242291 console.log('System TTS done');
243292 });
public/scripts/extensions/vectors/index.js+35 -10
@@ -55,6 +55,8 @@ const getBatchSize = () => ['transformers', 'palm', 'ollama'].includes(settings.
5555const settings = {
5656 // For both
5757 source: 'transformers',
58+ alt_endpoint_url: '',
59+ use_alt_endpoint: false,
5860 include_wi: false,
5961 togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
6062 openai_model: 'text-embedding-ada-002',
@@ -109,6 +111,7 @@ const settings = {
109111const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
110112const webllmProvider = new WebLlmVectorProvider();
111113const cachedSummaries = new Map();
114+const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
112115
113116/**
114117 * Gets the Collection ID for a file embedded in the chat.
@@ -777,14 +780,14 @@ function getVectorsRequestBody(args = {}) {
777780 break;
778781 case 'ollama':
779782 body.model = extension_settings.vectors.ollama_model;
780783 body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
781784 body.keep = !!extension_settings.vectors.ollama_keep;
782785 break;
783786 case 'llamacpp':
784787 body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
785788 break;
786789 case 'vllm':
787790 body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.VLLM];
788791 body.model = extension_settings.vectors.vllm_model;
789792 break;
790793 case 'webllm':
@@ -826,11 +829,12 @@ async function getAdditionalArgs(items) {
826829* @returns {Promise<number[]>} Saved hashes
827830*/
828831async function getSavedHashes(collectionId) {
832+ const args = await getAdditionalArgs([]);
829833 const response = await fetch('/api/vector/list', {
830834 method: 'POST',
831835 headers: getRequestHeaders(),
832836 body: JSON.stringify({
833837 ...getVectorsRequestBody(args),
834838 collectionId: collectionId,
835839 source: settings.source,
836840 }),
@@ -883,11 +887,18 @@ function throwIfSourceInvalid() {
883887 throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
884888 }
885889
886- if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
890+ if (vectorApiRequiresUrl.includes(settings.source) && settings.use_alt_endpoint) {
887- settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||
891+ if (!settings.alt_endpoint_url) {
888- settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] ||
892+ throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
889- settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
893+ }
890- throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
894+ }
895+ else {
896+ if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
897+ settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||
898+ settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] ||
899+ settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
900+ throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
901+ }
891902 }
892903
893904 if (settings.source === 'ollama' && !settings.ollama_model || settings.source === 'vllm' && !settings.vllm_model) {
@@ -1087,6 +1098,7 @@ function toggleSettings() {
10871098 $('#webllm_vectorsModel').toggle(settings.source === 'webllm');
10881099 $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
10891100 $('#google_vectorsModel').toggle(settings.source === 'palm');
1101+ $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
10901102 if (settings.source === 'webllm') {
10911103 loadWebLlmModels();
10921104 }
@@ -1144,6 +1156,9 @@ function loadWebLlmModels() {
11441156 * @returns {Promise<Record<string, number[]>>} Calculated embeddings
11451157 */
11461158async function createWebLlmEmbeddings(items) {
1159+ if (items.length === 0) {
1160+ return /** @type {Record<string, number[]>} */ ({});
1161+ }
11471162 return executeWithWebLlmErrorHandling(async () => {
11481163 const embeddings = await webllmProvider.embedTexts(items, settings.webllm_model);
11491164 const result = /** @type {Record<string, number[]>} */ ({});
@@ -1165,7 +1180,7 @@ async function createKoboldCppEmbeddings(items) {
11651180 headers: getRequestHeaders(),
11661181 body: JSON.stringify({
11671182 items: items,
11681183 server: settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP],
11691184 }),
11701185 });
11711186
@@ -1467,6 +1482,16 @@ jQuery(async () => {
14671482 saveSettingsDebounced();
14681483 toggleSettings();
14691484 });
1485+ $('#vector_altEndpointUrl_enabled').prop('checked', settings.use_alt_endpoint).on('input', () => {
1486+ settings.use_alt_endpoint = $('#vector_altEndpointUrl_enabled').prop('checked');
1487+ Object.assign(extension_settings.vectors, settings);
1488+ saveSettingsDebounced();
1489+ });
1490+ $('#vector_altEndpoint_address').val(settings.alt_endpoint_url).on('change', () => {
1491+ settings.alt_endpoint_url = String($('#vector_altEndpoint_address').val());
1492+ Object.assign(extension_settings.vectors, settings);
1493+ saveSettingsDebounced();
1494+ });
14701495 $('#api_key_nomicai').on('click', async () => {
14711496 const popupText = 'NomicAI API Key:';
14721497 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
public/scripts/extensions/vectors/settings.html+13 -2
@@ -25,6 +25,16 @@
2525 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
2626 </select>
2727 </div>
28+ <div class="flex-container flexFlowColumn" id="vector_altEndpointUrl">
29+ <label class="checkbox_label" for="vector_altEndpointUrl_enabled" title="Enable secondary endpoint URL usage, instead of the main one.">
30+ <input id="vector_altEndpointUrl_enabled" type="checkbox" class="checkbox">
31+ <span data-i18n="Use secondary URL">Use secondary URL</span>
32+ </label>
33+ <label for="vector_altEndpoint_address" data-i18n="Secondary Embedding endpoint URL">
34+ Secondary Embedding endpoint URL
35+ </label>
36+ <input id="vector_altEndpoint_address" class="text_pole" type="text" placeholder="e.g. http://localhost:5001" />
37+ </div>
2838 <div class="flex-container flexFlowColumn" id="webllm_vectorsModel">
2939 <label for="vectors_webllm_model" data-i18n="Vectorization Model">
3040 Vectorization Model
@@ -87,6 +97,7 @@
8797 Vectorization Model
8898 </label>
8999 <select id="vectors_cohere_model" class="text_pole">
100+ <option value="embed-v4.0">embed-v4.0</option>
90101 <option value="embed-english-v3.0">embed-english-v3.0</option>
91102 <option value="embed-multilingual-v3.0">embed-multilingual-v3.0</option>
92103 <option value="embed-english-light-v3.0">embed-english-light-v3.0</option>
@@ -310,7 +321,7 @@
310321 <label for="vectors_file_depth_db" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
311322 <input type="radio" name="vectors_file_position_db" value="1" />
312323 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
313324 <input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="9999999" />
314325 <span>as</span>
315326 <select id="vectors_file_depth_role_db" class="text_pole widthNatural">
316327 <option value="0" data-i18n="System">System</option>
@@ -362,7 +373,7 @@
362373 <label for="vectors_depth" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
363374 <input type="radio" name="vectors_position" value="1" />
364375 <span data-i18n="In-chat @ Depth">In-chat @ Depth </span>
365376 <input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="9999999" />
366377 </label>
367378 </div>
368379 <div class="flex-container">
public/scripts/group-chats.js+19 -7
@@ -13,6 +13,9 @@ import {
1313 getBase64Async,
1414 resetScrollHeight,
1515 initScrollHeight,
16+ localizePagination,
17+ renderPaginationDropdown,
18+ paginationDropdownChangeHandler,
1619} from './utils.js';
1720import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
1821import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';
@@ -1374,6 +1377,8 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) {
13741377
13751378function printGroupCandidates() {
13761379 const storageKey = 'GroupCandidates_PerPage';
1380+ const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
1381+ const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
13771382 $('#rm_group_add_members_pagination').pagination({
13781383 dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }),
13791384 pageRange: 1,
@@ -1382,18 +1387,20 @@ function printGroupCandidates() {
13821387 prevText: '<',
13831388 nextText: '>',
13841389 formatNavigator: PAGINATION_TEMPLATE,
1390+ formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
13851391 showNavigator: true,
13861392 showSizeChanger: true,
1387- pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1393+ pageSize,
1388- sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1394+ afterSizeSelectorChange: function (e, size) {
1389- afterSizeSelectorChange: function (e) {
13901395 accountStorage.setItem(storageKey, e.target.value);
1396+ paginationDropdownChangeHandler(e, size);
13911397 },
13921398 callback: function (data) {
13931399 $('#rm_group_add_members').empty();
13941400 for (const i of data) {
13951401 $('#rm_group_add_members').append(getGroupCharacterBlock(i.item));
13961402 }
1403+ localizePagination($('#rm_group_add_members_pagination'));
13971404 },
13981405 });
13991406}
@@ -1401,6 +1408,9 @@ function printGroupCandidates() {
14011408function printGroupMembers() {
14021409 const storageKey = 'GroupMembers_PerPage';
14031410 $('.rm_group_members_pagination').each(function () {
1411+ let that = this;
1412+ const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
1413+ const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
14041414 $(this).pagination({
14051415 dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
14061416 pageRange: 1,
@@ -1411,16 +1421,18 @@ function printGroupMembers() {
14111421 formatNavigator: PAGINATION_TEMPLATE,
14121422 showNavigator: true,
14131423 showSizeChanger: true,
1414- pageSize: Number(accountStorage.getItem(storageKey)) || 5,
1424+ formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
1415- sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
1425+ pageSize,
14161426 afterSizeSelectorChange: function (e, size) {
14171427 accountStorage.setItem(storageKey, e.target.value);
1428+ paginationDropdownChangeHandler(e, size);
14181429 },
14191430 callback: function (data) {
14201431 $('.rm_group_members').empty();
14211432 for (const i of data) {
14221433 $('.rm_group_members').append(getGroupCharacterBlock(i.item));
14231434 }
1435+ localizePagination($(that));
14241436 },
14251437 });
14261438 });
@@ -1804,7 +1816,7 @@ async function createGroup() {
18041816 const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');
18051817
18061818 if (!name) {
18071819 name = t`Group: ${memberNames}`;
18081820 }
18091821
18101822 const avatar_url = $('#group_avatar_preview img').attr('src');
public/scripts/horde.js+3 -3
@@ -394,7 +394,7 @@ function getHordeModelTemplate(option) {
394394 `));
395395}
396396
397397jQuery(export function initHorde () {
398398 $('#horde_model').on('mousedown change', async function (e) {
399399 console.log('Horde model change', e);
400400 horde_settings.models = $('#horde_model').val();
@@ -441,7 +441,7 @@ jQuery(function () {
441441 if (!isMobile()) {
442442 $('#horde_model').select2({
443443 width: '100%',
444444 placeholder: 't`Select Horde models'`,
445445 allowClear: true,
446446 closeOnSelect: false,
447447 templateSelection: function (data) {
@@ -451,5 +451,5 @@ jQuery(function () {
451451 templateResult: getHordeModelTemplate,
452452 });
453453 }
454454});
455455
public/scripts/instruct-mode.js+26 -24
@@ -208,37 +208,39 @@ export function autoSelectInstructPreset(modelId) {
208208
209209 // Select matching instruct preset
210210 let foundMatch = false;
211- for (const instruct_preset of instruct_presets) {
211+
212- // If instruct preset matches the context template
212+ for (const preset of instruct_presets) {
213- if (power_user.instruct.bind_to_context && instruct_preset.name === power_user.context.preset) {
213+ // If activation regex is set, check if it matches the model id
214- foundMatch = true;
214+ if (preset.activation_regex) {
215- selectInstructPreset(instruct_preset.name, { isAuto: true });
215+ try {
216- break;
216+ const regex = regexFromString(preset.activation_regex);
217+
218+ // Stop on first match so it won't cycle back and forth between presets if multiple regexes match
219+ if (regex instanceof RegExp && regex.test(modelId)) {
220+ selectInstructPreset(preset.name, { isAuto: true });
221+ foundMatch = true;
222+ break;
223+ }
224+ } catch {
225+ // If regex is invalid, ignore it
226+ console.warn(`Invalid instruct activation regex in preset "${preset.name}"`);
227+ }
217228 }
218229 }
230+
219231 // If no match was found, auto-select instruct preset
220232 if (!foundMatch && power_user.instruct.bind_to_context) {
221233 for (const presetinstruct_preset of instruct_presets) {
222234 // If activation regex is set, check ifinstruct itpreset matches the modelcontext idtemplate
223235 if (presetinstruct_preset.activation_regexname === power_user.context.preset) {
224- try {
236+ selectInstructPreset(instruct_preset.name, { isAuto: true });
225- const regex = regexFromString(preset.activation_regex);
237+ foundMatch = true;
226-
238+ break;
227- // Stop on first match so it won't cycle back and forth between presets if multiple regexes match
228- if (regex instanceof RegExp && regex.test(modelId)) {
229- selectInstructPreset(preset.name, { isAuto: true });
230-
231- return true;
232- }
233- } catch {
234- // If regex is invalid, ignore it
235- console.warn(`Invalid instruct activation regex in preset "${preset.name}"`);
236- }
237239 }
238240 }
239241 }
240242
241243 return falsefoundMatch;
242244}
243245
244246/**
public/scripts/openai.js+255 -105
@@ -15,12 +15,12 @@ import {
1515 extension_prompt_types,
1616 Generate,
1717 getExtensionPrompt,
18+ getExtensionPromptMaxDepth,
1819 getNextMessageId,
1920 getRequestHeaders,
2021 getStoppingStrings,
2122 is_send_press,
2223 main_api,
23- MAX_INJECTION_DEPTH,
2424 name1,
2525 name2,
2626 replaceItemizedPromptText,
@@ -184,6 +184,7 @@ export const chat_completion_sources = {
184184 ZEROONEAI: '01ai',
185185 NANOGPT: 'nanogpt',
186186 DEEPSEEK: 'deepseek',
187+ XAI: 'xai',
187188};
188189
189190const character_names_behavior = {
@@ -215,6 +216,15 @@ const openrouter_middleout_types = {
215216 OFF: 'off',
216217};
217218
219+export const reasoning_effort_types = {
220+ auto: 'auto',
221+ low: 'low',
222+ medium: 'medium',
223+ high: 'high',
224+ min: 'min',
225+ max: 'max',
226+};
227+
218228const sensitiveFields = [
219229 'reverse_proxy',
220230 'proxy_password',
@@ -257,6 +267,7 @@ export const settingsToUpdate = {
257267 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false],
258268 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false],
259269 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],
270+ xai_model: ['#model_xai_select', 'xai_model', false],
260271 custom_model: ['#custom_model_id', 'custom_model', false],
261272 custom_url: ['#custom_api_url_text', 'custom_url', false],
262273 custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@@ -345,6 +356,7 @@ const default_settings = {
345356 nanogpt_model: 'gpt-4o-mini',
346357 zerooneai_model: 'yi-large',
347358 deepseek_model: 'deepseek-chat',
359+ xai_model: 'grok-3-beta',
348360 custom_model: '',
349361 custom_url: '',
350362 custom_include_body: '',
@@ -379,7 +391,7 @@ const default_settings = {
379391 continue_postfix: continue_postfix_types.SPACE,
380392 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
381393 show_thoughts: true,
382394 reasoning_effort: 'medium'reasoning_effort_types.auto,
383395 enable_web_search: false,
384396 request_images: false,
385397 seed: -1,
@@ -425,6 +437,7 @@ const oai_settings = {
425437 nanogpt_model: 'gpt-4o-mini',
426438 zerooneai_model: 'yi-large',
427439 deepseek_model: 'deepseek-chat',
440+ xai_model: 'grok-3-beta',
428441 custom_model: '',
429442 custom_url: '',
430443 custom_include_body: '',
@@ -459,7 +472,7 @@ const oai_settings = {
459472 continue_postfix: continue_postfix_types.SPACE,
460473 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
461474 show_thoughts: true,
462475 reasoning_effort: 'medium'reasoning_effort_types.auto,
463476 enable_web_search: false,
464477 request_images: false,
465478 seed: -1,
@@ -738,7 +751,8 @@ async function populationInjectionPrompts(prompts, messages) {
738751 'assistant': extension_prompt_roles.ASSISTANT,
739752 };
740753
741- for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) {
754+ const maxDepth = getExtensionPromptMaxDepth();
755+ for (let i = 0; i <= maxDepth; i++) {
742756 // Get prompts for current depth
743757 const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content);
744758
@@ -1407,9 +1421,9 @@ export async function prepareOpenAIMessages({
14071421 await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });
14081422 } catch (error) {
14091423 if (error instanceof TokenBudgetExceededError) {
14101424 toastr.error(t`An error occurred whileMandatory countingprompts tokens:exceed Tokenthe budgetcontext exceededsize.`);
14111425 chatCompletion.log('TokenMandatory budgetprompts exceededexceed the context size.');
14121426 promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token Limitlimit or disable custom prompts.`;
14131427 } else if (error instanceof InvalidCharacterNameError) {
14141428 toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
14151429 chatCompletion.log('Invalid character name');
@@ -1644,6 +1658,8 @@ export function getChatCompletionModel(source = null) {
16441658 return oai_settings.nanogpt_model;
16451659 case chat_completion_sources.DEEPSEEK:
16461660 return oai_settings.deepseek_model;
1661+ case chat_completion_sources.XAI:
1662+ return oai_settings.xai_model;
16471663 default:
16481664 throw new Error(`Unknown chat completion source: ${activeSource}`);
16491665 }
@@ -1687,6 +1703,11 @@ function calculateOpenRouterCost() {
16871703 }
16881704 }
16891705
1706+ if (oai_settings.enable_web_search) {
1707+ const webSearchCost = (0.02).toFixed(2);
1708+ cost = t`${cost} + $${webSearchCost}`;
1709+ }
1710+
16901711 $('#openrouter_max_prompt_cost').text(cost);
16911712}
16921713
@@ -1925,6 +1946,31 @@ async function sendAltScaleRequest(messages, logit_bias, signal, type) {
19251946 return data.output;
19261947}
19271948
1949+function getReasoningEffort() {
1950+ // These sources expect the effort as string.
1951+ const reasoningEffortSources = [
1952+ chat_completion_sources.OPENAI,
1953+ chat_completion_sources.CUSTOM,
1954+ chat_completion_sources.XAI,
1955+ chat_completion_sources.OPENROUTER,
1956+ ];
1957+
1958+ if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
1959+ return oai_settings.reasoning_effort;
1960+ }
1961+
1962+ switch (oai_settings.reasoning_effort) {
1963+ case reasoning_effort_types.auto:
1964+ return undefined;
1965+ case reasoning_effort_types.min:
1966+ return reasoning_effort_types.low;
1967+ case reasoning_effort_types.max:
1968+ return reasoning_effort_types.high;
1969+ default:
1970+ return oai_settings.reasoning_effort;
1971+ }
1972+}
1973+
19281974/**
19291975 * Send a chat completion request to backend
19301976 * @param {string} type (impersonate, quiet, continue, etc)
@@ -1961,13 +2007,14 @@ async function sendOpenAIRequest(type, messages, signal) {
19612007 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
19622008 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
19632009 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
2010+ const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
19642011 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
19652012 const isQuiet = type === 'quiet';
19662013 const isImpersonate = type === 'impersonate';
19672014 const isContinue = type === 'continue';
19682015 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
19692016 const useLogprobs = !!power_user.request_token_probabilities;
19702017 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI);
19712018
19722019 // If we're using the window.ai extension, use that instead
19732020 // Doesn't support logit bias yet
@@ -2010,7 +2057,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20102057 'char_name': name2,
20112058 'group_names': getGroupNames(),
20122059 'include_reasoning': Boolean(oai_settings.show_thoughts),
20132060 'reasoning_effort': StringgetReasoningEffort(oai_settings.reasoning_effort),
20142061 'enable_web_search': Boolean(oai_settings.enable_web_search),
20152062 'request_images': Boolean(oai_settings.request_images),
20162063 'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,
@@ -2026,14 +2073,14 @@ async function sendOpenAIRequest(type, messages, signal) {
20262073 }
20272074
20282075 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
20292076 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK, chat_completion_sources.XAI].includes(oai_settings.chat_completion_source)) {
20302077 await validateReverseProxy();
20312078 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
20322079 generate_data['proxy_password'] = oai_settings.proxy_password;
20332080 }
20342081
20352082 // Add logprobs request (currently OpenAI only, max 5 on their side)
20362083 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI)) {
20372084 generate_data['logprobs'] = 5;
20382085 }
20392086
@@ -2152,29 +2199,42 @@ async function sendOpenAIRequest(type, messages, signal) {
21522199 }
21532200 }
21542201
2155- if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
2202+ if (isXAI) {
2203+ if (generate_data.model.includes('grok-3-mini')) {
2204+ delete generate_data.presence_penalty;
2205+ delete generate_data.frequency_penalty;
2206+ }
2207+ if (generate_data.model.includes('grok-vision')) {
2208+ delete generate_data.tools;
2209+ delete generate_data.tool_choice;
2210+ }
2211+ }
2212+
2213+ if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI) && oai_settings.seed >= 0) {
21562214 generate_data['seed'] = oai_settings.seed;
21572215 }
21582216
21592217 if (isOAI && (oai_settings.openai_model.startsWith/^('o1') |o3| o4)/.test(oai_settings.openai_model.startsWith('o3'))) {
2160- generate_data.messages.forEach((msg) => {
2161- if (msg.role === 'system') {
2162- msg.role = 'user';
2163- }
2164- });
21652218 generate_data.max_completion_tokens = generate_data.max_tokens;
21662219 delete generate_data.max_tokens;
21672220 delete generate_data.logprobs;
21682221 delete generate_data.top_logprobs;
21692222 delete generate_data.nstop;
2223+ delete generate_data.logit_bias;
21702224 delete generate_data.temperature;
21712225 delete generate_data.top_p;
21722226 delete generate_data.frequency_penalty;
21732227 delete generate_data.presence_penalty;
2174- delete generate_data.tools;
2228+ if (oai_settings.openai_model.startsWith('o1')) {
2175- delete generate_data.tool_choice;
2229+ generate_data.messages.forEach((msg) => {
2176- delete generate_data.stop;
2230+ if (msg.role === 'system') {
2177- delete generate_data.logit_bias;
2231+ msg.role = 'user';
2232+ }
2233+ });
2234+ delete generate_data.n;
2235+ delete generate_data.tools;
2236+ delete generate_data.tool_choice;
2237+ }
21782238 }
21792239
21802240 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -2210,7 +2270,8 @@ async function sendOpenAIRequest(type, messages, signal) {
22102270
22112271 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
22122272 const swipeIndex = parsed.choices[0].index - 1;
2213- swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
2273+ // FIXME: state.reasoning should be an array to support multi-swipe
2274+ swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state, { overrideShowThoughts: false });
22142275 } else {
22152276 text += getStreamingReply(parsed, state);
22162277 }
@@ -2278,6 +2339,11 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
22782339 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
22792340 }
22802341 return data.choices?.[0]?.delta?.content || '';
2342+ } else if (chat_completion_source === chat_completion_sources.XAI) {
2343+ if (show_thoughts) {
2344+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2345+ }
2346+ return data.choices?.[0]?.delta?.content || '';
22812347 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
22822348 if (show_thoughts) {
22832349 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
@@ -2310,6 +2376,7 @@ function parseChatCompletionLogprobs(data) {
23102376 switch (oai_settings.chat_completion_source) {
23112377 case chat_completion_sources.OPENAI:
23122378 case chat_completion_sources.DEEPSEEK:
2379+ case chat_completion_sources.XAI:
23132380 case chat_completion_sources.CUSTOM:
23142381 if (!data.choices?.length) {
23152382 return null;
@@ -3231,6 +3298,7 @@ function loadOpenAISettings(data, settings) {
32313298 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
32323299 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
32333300 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
3301+ oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
32343302 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
32353303 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
32363304 oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@@ -3316,6 +3384,8 @@ function loadOpenAISettings(data, settings) {
33163384 $('#model_deepseek_select').val(oai_settings.deepseek_model);
33173385 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
33183386 $('#model_01ai_select').val(oai_settings.zerooneai_model);
3387+ $('#model_xai_select').val(oai_settings.xai_model);
3388+ $(`#model_xai_select option[value="${oai_settings.xai_model}"`).attr('selected', true);
33193389 $('#custom_model_id').val(oai_settings.custom_model);
33203390 $('#custom_api_url_text').val(oai_settings.custom_url);
33213391 $('#openai_max_context').val(oai_settings.openai_max_context);
@@ -3512,7 +3582,7 @@ async function getStatusOpen() {
35123582 chat_completion_source: oai_settings.chat_completion_source,
35133583 };
35143584
35153585 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK, chat_completion_sources.XAI].includes(oai_settings.chat_completion_source)) {
35163586 await validateReverseProxy();
35173587 }
35183588
@@ -3781,7 +3851,7 @@ function createLogitBiasListItem(entry) {
37813851}
37823852
37833853async function createNewLogitBiasPreset() {
37843854 const name = await callPopupPopup.show.input('t`Preset name:'`, 'input'null);
37853855
37863856 if (!name) {
37873857 return;
@@ -4092,9 +4162,15 @@ function getMaxContextOpenAI(value) {
40924162 if (oai_settings.max_context_unlocked) {
40934163 return unlocked_max;
40944164 }
40954165 else if (value.startsWithincludes('o1') || valuegpt-4.startsWith('o31')) {
4166+ return max_1mil;
4167+ }
4168+ else if (value.startsWith('o1')) {
40964169 return max_128k;
40974170 }
4171+ else if (value.startsWith('o4') || value.startsWith('o3')) {
4172+ return max_200k;
4173+ }
40984174 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
40994175 return max_128k;
41004176 }
@@ -4166,6 +4242,80 @@ function getMaxContextWindowAI(value) {
41664242}
41674243
41684244/**
4245+ * Get the maximum context size for the Mistral model
4246+ * @param {string} model Model identifier
4247+ * @param {boolean} isUnlocked Whether context limits are unlocked
4248+ * @returns {number} Maximum context size in tokens
4249+ */
4250+function getMistralMaxContext(model, isUnlocked) {
4251+ if (isUnlocked) {
4252+ return unlocked_max;
4253+ }
4254+
4255+ if (Array.isArray(model_list) && model_list.length > 0) {
4256+ const contextLength = model_list.find((record) => record.id === model)?.max_context_length;
4257+ if (contextLength) {
4258+ return contextLength;
4259+ }
4260+ }
4261+
4262+ const contextMap = {
4263+ 'codestral-2411-rc5': 262144,
4264+ 'codestral-2412': 262144,
4265+ 'codestral-2501': 262144,
4266+ 'codestral-latest': 262144,
4267+ 'codestral-mamba-2407': 262144,
4268+ 'codestral-mamba-latest': 262144,
4269+ 'open-codestral-mamba': 262144,
4270+ 'ministral-3b-2410': 131072,
4271+ 'ministral-3b-latest': 131072,
4272+ 'ministral-8b-2410': 131072,
4273+ 'ministral-8b-latest': 131072,
4274+ 'mistral-large-2407': 131072,
4275+ 'mistral-large-2411': 131072,
4276+ 'mistral-large-latest': 131072,
4277+ 'mistral-large-pixtral-2411': 131072,
4278+ 'mistral-tiny-2407': 131072,
4279+ 'mistral-tiny-latest': 131072,
4280+ 'open-mistral-nemo': 131072,
4281+ 'open-mistral-nemo-2407': 131072,
4282+ 'pixtral-12b': 131072,
4283+ 'pixtral-12b-2409': 131072,
4284+ 'pixtral-12b-latest': 131072,
4285+ 'pixtral-large-2411': 131072,
4286+ 'pixtral-large-latest': 131072,
4287+ 'open-mixtral-8x22b': 65536,
4288+ 'open-mixtral-8x22b-2404': 65536,
4289+ 'codestral-2405': 32768,
4290+ 'mistral-embed': 32768,
4291+ 'mistral-large-2402': 32768,
4292+ 'mistral-medium': 32768,
4293+ 'mistral-medium-2312': 32768,
4294+ 'mistral-medium-latest': 32768,
4295+ 'mistral-moderation-2411': 32768,
4296+ 'mistral-moderation-latest': 32768,
4297+ 'mistral-ocr-2503': 32768,
4298+ 'mistral-ocr-latest': 32768,
4299+ 'mistral-saba-2502': 32768,
4300+ 'mistral-saba-latest': 32768,
4301+ 'mistral-small': 32768,
4302+ 'mistral-small-2312': 32768,
4303+ 'mistral-small-2402': 32768,
4304+ 'mistral-small-2409': 32768,
4305+ 'mistral-small-2501': 32768,
4306+ 'mistral-small-2503': 32768,
4307+ 'mistral-small-latest': 32768,
4308+ 'mistral-tiny': 32768,
4309+ 'mistral-tiny-2312': 32768,
4310+ 'open-mistral-7b': 32768,
4311+ 'open-mixtral-8x7b': 32768,
4312+ };
4313+
4314+ // Return context size if model found, otherwise default to 32k
4315+ return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || 32768;
4316+}
4317+
4318+/**
41694319 * Get the maximum context size for the Groq model
41704320 * @param {string} model Model identifier
41714321 * @param {boolean} isUnlocked Whether context limits are unlocked
@@ -4312,6 +4462,11 @@ async function onModelChange() {
43124462 $('#custom_model_id').val(value).trigger('input');
43134463 }
43144464
4465+ if ($(this).is('#model_xai_select')) {
4466+ console.log('XAI model changed to', value);
4467+ oai_settings.xai_model = value;
4468+ }
4469+
43154470 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
43164471 if (oai_settings.max_context_unlocked) {
43174472 $('#openai_max_context').attr('max', unlocked_max);
@@ -4326,20 +4481,16 @@ async function onModelChange() {
43264481 if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
43274482 if (oai_settings.max_context_unlocked) {
43284483 $('#openai_max_context').attr('max', max_2mil);
43294484 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-21.0-flash-thinking-exp5-1219pro')) {
4330- $('#openai_max_context').attr('max', max_32k);
4331- } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
43324485 $('#openai_max_context').attr('max', max_2mil);
43334486 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash') || value.includes('gemini-2.50-pro') || value.includes('gemini-exp') || value.includes('gemini-032.5-25flash') || value.includes('gemini-2.5-pro-preview') || value.includes('learnlm-032.0-25flash')) {
43344487 $('#openai_max_context').attr('max', max_1mil);
43354488 } else if (value.includes('geminigemma-1.03-pro') || value === 'gemini27b-proit')) {
4336- $('#openai_max_context').attr('max', max_32k);
4337- } else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
4338- $('#openai_max_context').attr('max', max_32k);
4339- } else if (value.includes('gemma-3')) {
43404489 $('#openai_max_context').attr('max', max_128k);
4490+ } else if (value.includes('gemma-3') || value.includes('learnlm-1.5-pro-experimental')) {
4491+ $('#openai_max_context').attr('max', max_32k);
43414492 } else {
43424493 $('#openai_max_context').attr('max', max_4kmax_32k);
43434494 }
43444495 let makersuite_max_temp = (value.includes('vision') || value.includes('ultra') || value.includes('gemma')) ? 1.0 : 2.0;
43454496 oai_settings.temp_openai = Math.min(makersuite_max_temp, oai_settings.temp_openai);
@@ -4428,27 +4579,10 @@ async function onModelChange() {
44284579 }
44294580
44304581 if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
4431- if (oai_settings.max_context_unlocked) {
4582+ const maxContext = getMistralMaxContext(oai_settings.mistralai_model, oai_settings.max_context_unlocked);
44324583 $('#openai_max_context').attr('max', unlocked_maxmaxContext);
4433- } else if (['codestral-latest', 'codestral-mamba-2407', 'codestral-2411-rc5', 'codestral-2412', 'codestral-2501'].includes(oai_settings.mistralai_model)) {
4434- $('#openai_max_context').attr('max', max_256k);
4435- } else if (['mistral-large-2407', 'mistral-large-2411', 'mistral-large-pixtral-2411', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {
4436- $('#openai_max_context').attr('max', max_128k);
4437- } else if (oai_settings.mistralai_model.includes('mistral-nemo')) {
4438- $('#openai_max_context').attr('max', max_128k);
4439- } else if (oai_settings.mistralai_model.includes('mixtral-8x22b')) {
4440- $('#openai_max_context').attr('max', max_64k);
4441- } else if (oai_settings.mistralai_model.includes('pixtral')) {
4442- $('#openai_max_context').attr('max', max_128k);
4443- } else if (oai_settings.mistralai_model.includes('ministral')) {
4444- $('#openai_max_context').attr('max', max_32k);
4445- } else {
4446- $('#openai_max_context').attr('max', max_32k);
4447- }
44484584 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
44494585 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4450-
4451- //mistral also caps temp at 1.0
44524586 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
44534587 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
44544588 }
@@ -4584,6 +4718,22 @@ async function onModelChange() {
45844718 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
45854719 }
45864720
4721+ if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
4722+ if (oai_settings.max_context_unlocked) {
4723+ $('#openai_max_context').attr('max', unlocked_max);
4724+ } else if (oai_settings.xai_model.includes('grok-2-vision')) {
4725+ $('#openai_max_context').attr('max', max_32k);
4726+ } else if (oai_settings.xai_model.includes('grok-vision')) {
4727+ $('#openai_max_context').attr('max', max_8k);
4728+ } else {
4729+ $('#openai_max_context').attr('max', max_128k);
4730+ }
4731+
4732+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4733+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4734+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4735+ }
4736+
45874737 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
45884738 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
45894739 $('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');
@@ -4822,6 +4972,19 @@ async function onConnectButtonClick(e) {
48224972 }
48234973 }
48244974
4975+ if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
4976+ const api_key_xai = String($('#api_key_xai').val()).trim();
4977+
4978+ if (api_key_xai.length) {
4979+ await writeSecret(SECRET_KEYS.XAI, api_key_xai);
4980+ }
4981+
4982+ if (!secret_state[SECRET_KEYS.XAI] && !oai_settings.reverse_proxy) {
4983+ console.log('No secret key saved for XAI');
4984+ return;
4985+ }
4986+ }
4987+
48254988 startStatusLoading();
48264989 saveSettingsDebounced();
48274990 await getStatusOpen();
@@ -4878,6 +5041,9 @@ function toggleChatCompletionForms() {
48785041 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
48795042 $('#model_deepseek_select').trigger('change');
48805043 }
5044+ else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
5045+ $('#model_xai_select').trigger('change');
5046+ }
48815047 $('[data-source]').each(function () {
48825048 const validSources = $(this).data('source').split(',');
48835049 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -4962,63 +5128,43 @@ export function isImageInliningSupported() {
49625128
49635129 // gultra just isn't being offered as multimodal, thanks google.
49645130 const visionSupportedModels = [
4965- 'gpt-4-vision',
5131+ // OpenAI
4966- 'gemini-2.5-pro-exp-03-25',
5132+ 'chatgpt-4o-latest',
4967- 'gemini-2.5-pro-preview-03-25',
4968- 'gemini-2.0-pro-exp',
4969- 'gemini-2.0-pro-exp-02-05',
4970- 'gemini-2.0-flash-lite-preview',
4971- 'gemini-2.0-flash-lite-preview-02-05',
4972- 'gemini-2.0-flash',
4973- 'gemini-2.0-flash-001',
4974- 'gemini-2.0-flash-thinking-exp-1219',
4975- 'gemini-2.0-flash-thinking-exp-01-21',
4976- 'gemini-2.0-flash-thinking-exp',
4977- 'gemini-2.0-flash-exp',
4978- 'gemini-2.0-flash-exp-image-generation',
4979- 'gemini-1.5-flash',
4980- 'gemini-1.5-flash-latest',
4981- 'gemini-1.5-flash-001',
4982- 'gemini-1.5-flash-002',
4983- 'gemini-1.5-flash-exp-0827',
4984- 'gemini-1.5-flash-8b',
4985- 'gemini-1.5-flash-8b-exp-0827',
4986- 'gemini-1.5-flash-8b-exp-0924',
4987- 'gemini-exp-1114',
4988- 'gemini-exp-1121',
4989- 'gemini-exp-1206',
4990- 'gemini-1.0-pro-vision-latest',
4991- 'gemini-1.5-pro',
4992- 'gemini-1.5-pro-latest',
4993- 'gemini-1.5-pro-001',
4994- 'gemini-1.5-pro-002',
4995- 'gemini-1.5-pro-exp-0801',
4996- 'gemini-1.5-pro-exp-0827',
4997- 'claude-3',
4998- 'claude-3-5',
4999- 'claude-3-7',
50005133 'gpt-4-turbo',
50015134 'gpt-4o4-vision',
50025135 'gpt-4o-mini4.1',
50035136 'gpt-4.5-preview',
50045137 'gpt-4.5-preview-2025-02-274o',
50055138 'o1',
50065139 'o1-2024-12-17o3',
50075140 'chatgpt-4oo4-latestmini',
5141+ // 01.AI (Yi)
50085142 'yi-vision',
5009- 'pixtral-latest',
5143+ // Claude
50105144 'pixtral-12bclaude-latest3',
5011- 'pixtral-12b',
5145+ // Cohere
50125146 'pixtralc4ai-12baya-2409vision',
5013- 'pixtral-large-latest',
5147+ // Google AI Studio
50145148 'pixtral-largegemini-24111.5',
50155149 'c4ai-aya-visiongemini-8b2.0',
50165150 'c4ai-aya-visiongemini-32b2.5',
5151+ 'gemini-exp-1206',
5152+ 'learnlm',
5153+ // MistralAI
5154+ 'mistral-small-2503',
5155+ 'mistral-small-latest',
5156+ 'pixtral',
5157+ // xAI (Grok)
5158+ 'grok-2-vision',
5159+ 'grok-vision',
50175160 ];
50185161
50195162 switch (oai_settings.chat_completion_source) {
50205163 case chat_completion_sources.OPENAI:
5021- return visionSupportedModels.some(model => oai_settings.openai_model.includes(model) && !oai_settings.openai_model.includes('gpt-4-turbo-preview'));
5164+ return visionSupportedModels.some(model =>
5165+ oai_settings.openai_model.includes(model)
5166+ && ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !oai_settings.openai_model.includes(x)),
5167+ );
50225168 case chat_completion_sources.MAKERSUITE:
50235169 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
50245170 case chat_completion_sources.CLAUDE:
@@ -5033,6 +5179,8 @@ export function isImageInliningSupported() {
50335179 return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model));
50345180 case chat_completion_sources.COHERE:
50355181 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
5182+ case chat_completion_sources.XAI:
5183+ return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
50365184 default:
50375185 return false;
50385186 }
@@ -5573,6 +5721,7 @@ export function initOpenAI() {
55735721
55745722 $('#openai_enable_web_search').on('input', function () {
55755723 oai_settings.enable_web_search = !!$(this).prop('checked');
5724+ calculateOpenRouterCost();
55765725 saveSettingsDebounced();
55775726 });
55785727
@@ -5629,6 +5778,7 @@ export function initOpenAI() {
56295778 $('#model_deepseek_select').on('change', onModelChange);
56305779 $('#model_01ai_select').on('change', onModelChange);
56315780 $('#model_custom_select').on('change', onModelChange);
5781+ $('#model_xai_select').on('change', onModelChange);
56325782 $('#settings_preset_openai').on('change', onSettingsPresetChange);
56335783 $('#new_oai_preset').on('click', onNewPresetClick);
56345784 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/personas.js+7 -3
@@ -22,7 +22,7 @@ import {
2222} from '../script.js';
2323import { persona_description_positions, power_user } from './power-user.js';
2424import { getTokenCountAsync } from './tokenizers.js';
2525import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler } from './utils.js';
2626import { debounce_timeout } from './constants.js';
2727import { FILTER_TYPES, FilterHelper } from './filters.js';
2828import { groups, selected_group } from './group-chats.js';
@@ -250,16 +250,18 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
250250 const storageKey = 'Personas_PerPage';
251251 const listId = '#user_avatar_block';
252252 const perPage = Number(accountStorage.getItem(storageKey)) || 5;
253+ const sizeChangerOptions = [5, 10, 25, 50, 100, 250, 500, 1000];
253254
254255 $('#persona_pagination_container').pagination({
255256 dataSource: entities,
256257 pageSize: perPage,
257- sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000],
258+ sizeChangerOptions,
258259 pageRange: 1,
259260 pageNumber: savePersonasPage || 1,
260261 position: 'top',
261262 showPageNumbers: false,
262263 showSizeChanger: true,
264+ formatSizeChanger: renderPaginationDropdown(perPage, sizeChangerOptions),
263265 prevText: '<',
264266 nextText: '>',
265267 formatNavigator: PAGINATION_TEMPLATE,
@@ -270,9 +272,11 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
270272 $(listId).append(getUserAvatarBlock(item));
271273 }
272274 updatePersonaUIStates();
275+ localizePagination($('#persona_pagination_container'));
273276 },
274277 afterSizeSelectorChange: function (e, size) {
275278 accountStorage.setItem(storageKey, e.target.value);
279+ paginationDropdownChangeHandler(e, size);
276280 },
277281 afterPaging: function (e) {
278282 savePersonasPage = e;
public/scripts/popup.js+52 -25
@@ -71,7 +71,8 @@ export const POPUP_RESULT = {
7171 * @property {string} id - The id for the html element
7272 * @property {string} label - The label text for the input
7373 * @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label
7474 * @property {boolean?|string|undefined} [defaultState=false] - The default state when opening the popup (false if not set)
75+ * @property {string?} [type='checkbox'] - The type of the input (default is checkbox)
7576 */
7677
7778/**
@@ -157,7 +158,7 @@ export class Popup {
157158
158159 /** @type {POPUP_RESULT|number} */ result;
159160 /** @type {any} */ value;
160161 /** @type {Map<string,string|boolean>?} */ inputResults;
161162 /** @type {any} */ cropData;
162163
163164 /** @type {HTMLElement} */ lastFocus;
@@ -260,28 +261,53 @@ export class Popup {
260261 return;
261262 }
262263
263- const label = document.createElement('label');
264+ if (!input.type || input.type === 'checkbox') {
264- label.classList.add('checkbox_label', 'justifyCenter');
265+ const label = document.createElement('label');
265266 label.setAttributeclassList.add('forcheckbox_label', input.id'justifyCenter');
266- const inputElement = document.createElement('input');
267+ label.setAttribute('for', input.id);
267268 const inputElement.type = document.createElement('checkboxinput');
268269 inputElement.idtype = input.id'checkbox';
269270 inputElement.checkedid = input.defaultState ?? falseid;
270- label.appendChild(inputElement);
271+ inputElement.checked = Boolean(input.defaultState ?? false);
271- const labelText = document.createElement('span');
272+ label.appendChild(inputElement);
272- labelText.innerText = input.label;
273+ const labelText = document.createElement('span');
273274 labelText.dataset.i18ninnerText = input.label;
274- label.appendChild(labelText);
275+ labelText.dataset.i18n = input.label;
275-
276+ label.appendChild(labelText);
276- if (input.tooltip) {
277+
277- const tooltip = document.createElement('div');
278+ if (input.tooltip) {
278- tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
279+ const tooltip = document.createElement('div');
279- tooltip.title = input.tooltip;
280+ tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
280281 tooltip.dataset.i18n = '[title]' += input.tooltip;
281- label.appendChild(tooltip);
282+ tooltip.dataset.i18n = '[title]' + input.tooltip;
282- }
283+ label.appendChild(tooltip);
284+ }
285+
286+ this.inputControls.appendChild(label);
287+ } else if (input.type === 'text') {
288+ const label = document.createElement('label');
289+ label.classList.add('text_label', 'justifyCenter');
290+ label.setAttribute('for', input.id);
291+
292+ const inputElement = document.createElement('input');
293+ inputElement.classList.add('text_pole');
294+ inputElement.type = 'text';
295+ inputElement.id = input.id;
296+ inputElement.value = String(input.defaultState ?? '');
297+ inputElement.placeholder = input.tooltip ?? '';
298+
299+ const labelText = document.createElement('span');
300+ labelText.innerText = input.label;
301+ labelText.dataset.i18n = input.label;
283302
284303 this.inputControls label.appendChild(labellabelText);
304+ label.appendChild(inputElement);
305+
306+ this.inputControls.appendChild(label);
307+ } else {
308+ console.warn('Unknown custom input type. Only checkbox and text are supported.', input);
309+ return;
310+ }
285311 });
286312
287313 // Set the default button class
@@ -529,7 +555,8 @@ export class Popup {
529555 this.inputResults = new Map(this.customInputs.map(input => {
530556 /** @type {HTMLInputElement} */
531557 const inputControl = this.dlg.querySelector(`#${input.id}`);
532- return [inputControl.id, inputControl.checked];
558+ const value = input.type === 'text' ? inputControl.value : inputControl.checked;
559+ return [inputControl.id, value];
533560 }));
534561 }
535562
@@ -619,7 +646,7 @@ export class Popup {
619646 /** @readonly @type {Popup[]} Remember all popups */
620647 popups: [],
621648
622649 /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, string|boolean>?}?} Last popup result */
623650 lastResult: null,
624651
625652 /** @returns {boolean} Checks if any modal popup dialog is open */
public/scripts/power-user.js+3 -4
@@ -71,8 +71,8 @@ export {
7171
7272export const MAX_CONTEXT_DEFAULT = 8192;
7373export const MAX_RESPONSE_DEFAULT = 2048;
7474const MAX_CONTEXT_UNLOCKED = 200512 * 1024;
7575const MAX_RESPONSE_UNLOCKED = 3264 * 1024;
7676const unlockedMaxContextStep = 512;
7777const maxContextMin = 512;
7878const maxContextStep = 64;
@@ -244,7 +244,6 @@ let power_user = {
244244 chat_start: defaultChatStart,
245245 example_separator: defaultExampleSeparator,
246246 use_stop_strings: true,
247- allow_jailbreak: false,
248247 names_as_stop_strings: true,
249248 },
250249
@@ -255,6 +254,7 @@ let power_user = {
255254 enabled: true,
256255 name: 'Neutral - Chat',
257256 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
257+ post_history: '',
258258 },
259259
260260 reasoning: {
@@ -334,7 +334,6 @@ const contextControls = [
334334 { id: 'context_example_separator', property: 'example_separator', isCheckbox: false, isGlobalSetting: false },
335335 { id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
336336 { id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
337- { id: 'context_allow_jailbreak', property: 'allow_jailbreak', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
338337 { id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
339338
340339 // Existing power user settings
public/scripts/preset-manager.js+6 -0
@@ -902,6 +902,12 @@ export async function initPresetManager() {
902902
903903 await presetManager.renamePreset(newName);
904904
905+ if (apiId === 'openai') {
906+ // This is a horrible mess, but prevents the renamed preset from being corrupted.
907+ $('#update_oai_preset').trigger('click');
908+ return;
909+ }
910+
905911 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;
906912 toastr.success(successToast);
907913 });
public/scripts/reasoning.js+2 -0
@@ -109,6 +109,8 @@ export function extractReasoningFromData(data, {
109109 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
110110 case chat_completion_sources.DEEPSEEK:
111111 return data?.choices?.[0]?.message?.reasoning_content ?? '';
112+ case chat_completion_sources.XAI:
113+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
112114 case chat_completion_sources.OPENROUTER:
113115 return data?.choices?.[0]?.message?.reasoning ?? '';
114116 case chat_completion_sources.MAKERSUITE:
public/scripts/secrets.js+2 -0
@@ -42,6 +42,7 @@ export const SECRET_KEYS = {
4242 DEEPSEEK: 'api_key_deepseek',
4343 SERPER: 'api_key_serper',
4444 FALAI: 'api_key_falai',
45+ XAI: 'api_key_xai',
4546};
4647
4748const INPUT_MAP = {
@@ -76,6 +77,7 @@ const INPUT_MAP = {
7677 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
7778 [SECRET_KEYS.GENERIC]: '#api_key_generic',
7879 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
80+ [SECRET_KEYS.XAI]: '#api_key_xai',
7981};
8082
8183async function clearSecret() {
public/scripts/slash-commands.js+162 -7
@@ -1,4 +1,5 @@
11import { Fuse, DOMPurify } from '../lib.js';
2+import { flashHighlight } from './utils.js';
23
34import {
45 Generate,
@@ -21,6 +22,7 @@ import {
2122 extractMessageBias,
2223 generateQuietPrompt,
2324 generateRaw,
25+ getFirstDisplayedMessageId,
2426 getThumbnailUrl,
2527 is_send_press,
2628 main_api,
@@ -314,6 +316,11 @@ export function initDefaultSlashCommands() {
314316 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
315317 }),
316318 SlashCommandNamedArgument.fromProps({
319+ name: 'name',
320+ description: 'Optional custom display name to use for this system narrator message.',
321+ typeList: [ARGUMENT_TYPE.STRING],
322+ }),
323+ SlashCommandNamedArgument.fromProps({
317324 name: 'return',
318325 description: 'The way how you want the return value to be provided',
319326 typeList: [ARGUMENT_TYPE.STRING],
@@ -2077,8 +2084,9 @@ export function initDefaultSlashCommands() {
20772084 name: 'replace',
20782085 aliases: ['re'],
20792086 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
20802087 if (!pattern === '') {
20812088 throw new Error('Argument of \'pattern=\' cannot be empty');
2089+ }
20822090 switch (mode) {
20832091 case 'literal':
20842092 return text.replaceAll(pattern, replacer);
@@ -2121,15 +2129,161 @@ export function initDefaultSlashCommands() {
21212129 </div>
21222130 <div>
21232131 <strong>Example:</strong>
21242132 <pre><code class="language-stscript">/let x Blue house and blue car || </code></pre>
21252133 <pre><code class="language-stscript">/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</code></pre>
21262134 <pre><code class="language-stscript">/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</code></pre>
21272135 <pre><code class="language-stscript">/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</code></pre>
21282136 <pre><code class="language-stscript">/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</code></pre>
2137+ </div>
2138+ `,
2139+ }));
2140+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2141+ name: 'test',
2142+ callback: (({ pattern }, text) => {
2143+ if (!pattern) {
2144+ throw new Error('Argument of \'pattern=\' cannot be empty');
2145+ }
2146+ const re = regexFromString(pattern.toString());
2147+ if (!re) {
2148+ throw new Error('The value of \'pattern\' argument is not a valid regular expression.');
2149+ }
2150+ return JSON.stringify(re.test(text.toString()));
2151+ }),
2152+ returns: 'true | false',
2153+ namedArgumentList: [
2154+ new SlashCommandNamedArgument(
2155+ 'pattern', 'pattern to find', [ARGUMENT_TYPE.STRING], true, false,
2156+ ),
2157+ ],
2158+ unnamedArgumentList: [
2159+ new SlashCommandArgument(
2160+ 'text to test', [ARGUMENT_TYPE.STRING], true, false,
2161+ ),
2162+ ],
2163+ helpString: `
2164+ <div>
2165+ Tests text for a regular expression match.
2166+ </div>
2167+ <div>
2168+ Returns <code>true</code> if the match is found, <code>false</code> otherwise.
2169+ </div>
2170+ <div>
2171+ <strong>Example:</strong>
2172+ <pre><code class="language-stscript">/let x Blue house and green car ||</code></pre>
2173+ <pre><code class="language-stscript">/test pattern="green" {{var::x}} | /echo |/# true ||</code></pre>
2174+ <pre><code class="language-stscript">/test pattern="blue" {{var::x}} | /echo |/# false ||</code></pre>
2175+ <pre><code class="language-stscript">/test pattern="/blue/i" {{var::x}} | /echo |/# true ||</code></pre>
2176+ </div>
2177+ `,
2178+ }));
2179+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2180+ name: 'match',
2181+ callback: (({ pattern }, text) => {
2182+ if (!pattern) {
2183+ throw new Error('Argument of \'pattern=\' cannot be empty');
2184+ }
2185+ const re = regexFromString(pattern.toString());
2186+ if (!re) {
2187+ throw new Error('The value of \'pattern\' argument is not a valid regular expression.');
2188+ }
2189+ if (re.flags.includes('g')) {
2190+ return JSON.stringify([...text.toString().matchAll(re)]);
2191+ } else {
2192+ const match = text.toString().match(re);
2193+ return match ? JSON.stringify(match) : '';
2194+ }
2195+ }),
2196+ returns: 'group array for each match',
2197+ namedArgumentList: [
2198+ new SlashCommandNamedArgument(
2199+ 'pattern', 'pattern to find', [ARGUMENT_TYPE.STRING], true, false,
2200+ ),
2201+ ],
2202+ unnamedArgumentList: [
2203+ new SlashCommandArgument(
2204+ 'text to match against', [ARGUMENT_TYPE.STRING], true, false,
2205+ ),
2206+ ],
2207+ helpString: `
2208+ <div>
2209+ Retrieves regular expression matches in the given text
2210+ </div>
2211+ <div>
2212+ Returns an array of groups (with the first group being the full match). If the regex contains the global flag (i.e. <code>/g</code>),
2213+ multiple nested arrays are returned for each match. If the regex is global, returns <code>[]</code> if no matches are found,
2214+ otherwise it returns an empty string.
2215+ </div>
2216+ <div>
2217+ <strong>Example:</strong>
2218+ <pre><code class="language-stscript">/let x color_green green lamp color_blue ||</code></pre>
2219+ <pre><code class="language-stscript">/match pattern="green" {{var::x}} | /echo |/# [ "green" ] ||</code></pre>
2220+ <pre><code class="language-stscript">/match pattern="color_(\\w+)" {{var::x}} | /echo |/# [ "color_green", "green" ] ||</code></pre>
2221+ <pre><code class="language-stscript">/match pattern="/color_(\\w+)/g" {{var::x}} | /echo |/# [ [ "color_green", "green" ], [ "color_blue", "blue" ] ] ||</code></pre>
2222+ <pre><code class="language-stscript">/match pattern="orange" {{var::x}} | /echo |/# ||</code></pre>
2223+ <pre><code class="language-stscript">/match pattern="/orange/g" {{var::x}} | /echo |/# [] ||</code></pre>
21292224 </div>
21302225 `,
21312226 }));
21322227
2228+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2229+ name: 'chat-jump',
2230+ aliases: ['chat-scrollto', 'floor-teleport'],
2231+ callback: async (_, index) => {
2232+ const messageIndex = Number(index);
2233+
2234+ if (isNaN(messageIndex) || messageIndex < 0 || messageIndex >= chat.length) {
2235+ toastr.warning(t`Invalid message index: ${index}. Please enter a number between 0 and ${chat.length}.`);
2236+ console.warn(`WARN: Invalid message index provided for /chat-jump: ${index}. Max index: ${chat.length}`);
2237+ return '';
2238+ }
2239+
2240+ // Load more messages if needed
2241+ const firstDisplayedMessageId = getFirstDisplayedMessageId();
2242+ if (isFinite(firstDisplayedMessageId) && messageIndex < firstDisplayedMessageId) {
2243+ const needToLoadCount = firstDisplayedMessageId - messageIndex;
2244+ await showMoreMessages(needToLoadCount);
2245+ await delay(1);
2246+ }
2247+
2248+ const chatContainer = document.getElementById('chat');
2249+ const messageElement = document.querySelector(`#chat .mes[mesid="${messageIndex}"]`);
2250+
2251+ if (messageElement instanceof HTMLElement && chatContainer instanceof HTMLElement) {
2252+ const elementRect = messageElement.getBoundingClientRect();
2253+ const containerRect = chatContainer.getBoundingClientRect();
2254+
2255+ const scrollPosition = elementRect.top - containerRect.top + chatContainer.scrollTop;
2256+ chatContainer.scrollTo({
2257+ top: scrollPosition,
2258+ behavior: 'smooth',
2259+ });
2260+
2261+ flashHighlight($(messageElement), 2000);
2262+ } else {
2263+ toastr.warning(t`Could not find element for message ${messageIndex}. It might not be rendered yet or the index is invalid.`);
2264+ console.warn(`WARN: Element not found for message index ${messageIndex} in /chat-jump.`);
2265+ }
2266+
2267+ return '';
2268+ },
2269+ unnamedArgumentList: [
2270+ SlashCommandArgument.fromProps({
2271+ description: 'The message index (0-based) to scroll to.',
2272+ typeList: [ARGUMENT_TYPE.NUMBER],
2273+ isRequired: true,
2274+ enumProvider: commonEnumProviders.messages(),
2275+ }),
2276+ ],
2277+ helpString: `
2278+ <div>
2279+ Scrolls the chat view to the specified message index. Index starts at 0.
2280+ </div>
2281+ <div>
2282+ <strong>Example:</strong> <pre><code>/chat-jump 10</code></pre> Scrolls to the 11th message (id=10).
2283+ </div>
2284+ `,
2285+ }));
2286+
21332287 registerVariableCommands();
21342288}
21352289
@@ -3702,7 +3856,7 @@ export async function sendMessageAs(args, text) {
37023856
37033857export async function sendNarratorMessage(args, text) {
37043858 text = String(text ?? '');
37053859 const name = args.name ?? (chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT);
37063860 // Messages that do nothing but set bias will be hidden from the context
37073861 const bias = extractMessageBias(text);
37083862 const isSystem = bias && !removeMacros(text).length;
@@ -3942,6 +4096,7 @@ function getModelOptions(quiet) {
39424096 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
39434097 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
39444098 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
4099+ { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
39454100 { id: 'model_novel_select', api: 'novel', type: null },
39464101 { id: 'horde_model', api: 'koboldhorde', type: null },
39474102 ];
public/scripts/st-context.js+3 -0
@@ -50,6 +50,8 @@ import {
5050 unshallowCharacter,
5151 deleteLastMessage,
5252 getCharacterCardFields,
53+ swipe_right,
54+ swipe_left,
5355} from '../script.js';
5456import {
5557 extension_settings,
@@ -196,6 +198,7 @@ export function getContext() {
196198 humanizedDateTime,
197199 updateMessageBlock,
198200 appendMediaToMessage,
201+ swipe: { left: swipe_left, right: swipe_right },
199202 variables: {
200203 local: {
201204 get: getLocalVariable,
public/scripts/sysprompt.js+15 -3
@@ -17,6 +17,7 @@ export let system_prompts = [];
1717const $enabled = $('#sysprompt_enabled');
1818const $select = $('#sysprompt_select');
1919const $content = $('#sysprompt_content');
20+const $postHistory = $('#sysprompt_post_history');
2021const $contentBlock = $('#SystemPromptBlock');
2122
2223async function migrateSystemPromptFromInstructMode() {
@@ -25,6 +26,7 @@ async function migrateSystemPromptFromInstructMode() {
2526 delete power_user.instruct.system_prompt;
2627 power_user.sysprompt.enabled = power_user.instruct.enabled;
2728 power_user.sysprompt.content = prompt;
29+ power_user.sysprompt.post_history = '';
2830
2931 const existingPromptName = system_prompts.find(x => x.content === prompt)?.name;
3032
@@ -59,7 +61,8 @@ export async function loadSystemPrompts(data) {
5961
6062 $enabled.prop('checked', power_user.sysprompt.enabled);
6163 $select.val(power_user.sysprompt.name);
6264 $content.val(power_user.sysprompt.content || '');
65+ $postHistory.val(power_user.sysprompt.post_history || '');
6366 if (!CSS.supports('field-sizing', 'content')) {
6467 await resetScrollHeight($content);
6568 }
@@ -165,13 +168,17 @@ export function initSystemPrompts() {
165168 const name = String($(this).val());
166169 const prompt = system_prompts.find(p => p.name === name);
167170 if (prompt) {
168171 $content.val(prompt.content || '');
172+ $postHistory.val(prompt.post_history || '');
173+
169174 if (!CSS.supports('field-sizing', 'content')) {
170175 await resetScrollHeight($content);
176+ await resetScrollHeight($postHistory);
171177 }
172178
173179 power_user.sysprompt.name = name;
174180 power_user.sysprompt.content = prompt.content || '';
181+ power_user.sysprompt.post_history = prompt.post_history || '';
175182 }
176183 saveSettingsDebounced();
177184 });
@@ -181,6 +188,11 @@ export function initSystemPrompts() {
181188 saveSettingsDebounced();
182189 });
183190
191+ $postHistory.on('input', function () {
192+ power_user.sysprompt.post_history = String($(this).val());
193+ saveSettingsDebounced();
194+ });
195+
184196 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
185197 name: 'sysprompt',
186198 aliases: ['system-prompt'],
public/scripts/tags.js+5 -11
@@ -27,7 +27,7 @@ import { debounce_timeout } from './constants.js';
2727import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
2828import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
2929import { renderTemplateAsync } from './templates.js';
3030import { t, translate } from './i18n.js';
3131
3232export {
3333 TAG_FOLDER_TYPES,
@@ -318,7 +318,7 @@ function getTagBlock(tag, entities, hidden = 0, isUseless = false) {
318318 template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`);
319319 template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`);
320320 template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : '');
321321 template.find('.bogus_folder_counter').text(`${count} ${` + (count != 1 ? 't`characters'` : 't`character'}`));
322322 template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);
323323 if (isUseless) template.addClass('useless');
324324
@@ -1057,7 +1057,7 @@ function appendTagToList(listElement, tag, { removable = false, isFilter = false
10571057 tagElement.attr('title', tag.title);
10581058 }
10591059 if (tag.icon) {
10601060 tagElement.find('.tag_name').text('').attr('title', `${translate(tag.name)} ${tag.title || ''}`.trim()).addClass(tag.icon);
10611061 tagElement.addClass('actionable');
10621062 }
10631063
@@ -1644,6 +1644,7 @@ function updateDrawTagFolder(element, tag) {
16441644
16451645 // Draw/update css attributes for this class
16461646 folderElement.attr('title', tagFolder.tooltip);
1647+ folderElement.attr('data-i18n', '[title]' + tagFolder.tooltip);
16471648 const indicator = folderElement.find('.tag_folder_indicator');
16481649 indicator.text(tagFolder.icon);
16491650 indicator.css('color', tagFolder.color);
@@ -1655,14 +1656,7 @@ async function onTagDeleteClick() {
16551656 const tag = tags.find(x => x.id === id);
16561657 const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));
16571658
16581659 const popupContent = $(`await renderTemplateAsync('deleteTag', { otherTags }));
1659- <h3>Delete Tag</h3>
1660- <div>Do you want to delete the tag <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
1661- <div class="m-t-2 marginBot5">If you want to merge all references to this tag into another tag, select it below:</div>
1662- <select id="merge_tag_select">
1663- <option value="">--- None ---</option>
1664- ${otherTags.map(x => `<option value="${x.id}">${x.name}</option>`).join('')}
1665- </select>`);
16661660
16671661 appendTagToList(popupContent.find('#tag_to_delete'), tag);
16681662
public/scripts/templates/deleteTag.html+9 -0
@@ -0,0 +1,9 @@
1+<h3 data-i18n="Delete Tag">Delete Tag</h3>
2+<div><span data-i18n="Do you want to delete the tag">Do you want to delete the tag</span> <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
3+<div class="m-t-2 marginBot5" data-i18n="If you want to merge all references to this tag into another tag, select it below:">If you want to merge all references to this tag into another tag, select it below:</div>
4+<select id="merge_tag_select">
5+ <option value="">--- None ---</option>
6+ {{#each otherTags}}
7+ <option value="{{this.id}}">{{this.name}}</option>
8+ {{/each}}
9+</select>
9 \ No newline at end of file
public/scripts/templates/emptyBlock.html+7 -0
@@ -0,0 +1,7 @@
1+<div class="text_block empty_block">
2+ <i class="fa-solid {{icon}} fa-4x"></i>
3+ <h1>{{text}}</h1>
4+ <p data-i18n="There are no items to display.">
5+ There are no items to display.
6+ </p>
7+</div>
public/scripts/templates/hiddenBlock.html+6 -0
@@ -0,0 +1,6 @@
1+<div class="text_block hidden_block">
2+ <small>
3+ <p>{{text}}</p>
4+ <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Characters and groups hidden by filters or closed folders" title="Characters and groups hidden by filters or closed folders"></div>
5+ </small>
6+</div>
public/scripts/templates/itemizationChat.html+8 -2
@@ -83,7 +83,10 @@
8383 <div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
8484 </div>
8585 <div class="flex-container ">
8686 <div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
87+ <span>-- Examples:</span>
88+ {{#if examplesCount}}<small>({{examplesCount}})</small>{{/if}}
89+ </div>
8790 <div class="tokenItemizingSubclass">{{examplesStringTokens}}</div>
8891 </div>
8992 <div class="flex-container ">
@@ -96,7 +99,10 @@
9699 <div class="">{{worldInfoStringTokens}}</div>
97100 </div>
98101 <div class="wide100p flex-container">
99102 <div class="flex1" style="color: palegreen;"><span data-i18n="Chat History:">Chat History:</span></div>
103+ <span data-i18n="Chat History:">Chat History:</span>
104+ {{#if messagesCount}}<small>({{messagesCount}})</small>{{/if}}
105+ </div>
100106 <div class="">{{ActualChatHistoryTokens}}</div>
101107 </div>
102108 <div class="wide100p flex-container flexNoGap flexFlowColumn">
public/scripts/templates/itemizationText.html+8 -2
@@ -51,7 +51,10 @@
5151 <div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
5252 </div>
5353 <div class="flex-container">
5454 <div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
55+ <span>-- Examples:</span>
56+ {{#if examplesCount}}<small>({{examplesCount}})</small>{{/if}}
57+ </div>
5558 <div class="tokenItemizingSubclass"> {{examplesStringTokens}}</div>
5659 </div>
5760 <div class="flex-container">
@@ -68,7 +71,10 @@
6871 <div class="">{{worldInfoStringTokens}}</div>
6972 </div>
7073 <div class="wide100p flex-container">
7174 <div class="flex1" style="color: palegreen;">Chat History:</div>
75+ <span data-i18n="Chat History:">Chat History:</span>
76+ {{#if messagesCount}}<small>({{messagesCount}})</small>{{/if}}
77+ </div>
7278 <div class=""> {{ActualChatHistoryTokens}}</div>
7379 </div>
7480 <div class="wide100p flex-container flexNoGap flexFlowColumn">
public/scripts/textgen-models.js+19 -16
@@ -7,6 +7,7 @@ import { renderTemplateAsync } from './templates.js';
77import { POPUP_TYPE, callGenericPopup } from './popup.js';
88import { t } from './i18n.js';
99import { accountStorage } from './util/AccountStorage.js';
10+import { localizePagination, PAGINATION_TEMPLATE } from './utils.js';
1011
1112let mancerModels = [];
1213let togetherModels = [];
@@ -41,12 +42,7 @@ const OPENROUTER_PROVIDERS = [
4142 'Avian',
4243 'Lambda',
4344 'Azure',
44- 'Modal',
45- 'AnyScale',
46- 'Replicate',
4745 'Perplexity',
48- 'Recursal',
49- 'OctoAI',
5046 'DeepSeek',
5147 'Infermatic',
5248 'AI21',
@@ -54,10 +50,12 @@ const OPENROUTER_PROVIDERS = [
5450 'Inflection',
5551 'xAI',
5652 'Cloudflare',
57- 'SF Compute',
5853 'Minimax',
5954 'Nineteen',
6055 'Liquid',
56+ 'GMICloud',
57+ 'Stealth',
58+ 'NCompass',
6159 'InferenceNet',
6260 'Friendli',
6361 'AionLabs',
@@ -69,14 +67,16 @@ const OPENROUTER_PROVIDERS = [
6967 'Targon',
7068 'Ubicloud',
7169 'Parasail',
7270 '01.AIPhala',
7371 'HuggingFaceCent-ML',
72+ 'Venice',
73+ 'OpenInference',
74+ 'Atoma',
75+ 'Enfer',
7476 'Mancer',
7577 'Mancer 2',
7678 'Hyperbolic',
7779 'Hyperbolic 2',
78- 'Lynn 2',
79- 'Lynn',
8080 'Reflection',
8181];
8282
@@ -362,9 +362,7 @@ export async function loadFeatherlessModels(data) {
362362 showSizeChanger: false,
363363 prevText: '<',
364364 nextText: '>',
365365 formatNavigator: function (currentPagePAGINATION_TEMPLATE, totalPage) {
366- return (currentPage - 1) * perPage + 1 + ' - ' + currentPage * perPage + ' of ' + totalPage * perPage;
367- },
368366 showNavigator: true,
369367 callback: function (modelsOnPage, pagination) {
370368 modelCardBlock.innerHTML = '';
@@ -386,15 +384,15 @@ export async function loadFeatherlessModels(data) {
386384
387385 const modelClassDiv = document.createElement('div');
388386 modelClassDiv.classList.add('model-class');
389387 modelClassDiv.textContent = t`Class` + `: ${model.model_class || 'N/A'}`;
390388
391389 const contextLengthDiv = document.createElement('div');
392390 contextLengthDiv.classList.add('model-context-length');
393391 contextLengthDiv.textContent = t`Context Length` + `: ${model.context_length}`;
394392
395393 const dateAddedDiv = document.createElement('div');
396394 dateAddedDiv.classList.add('model-date-added');
397395 dateAddedDiv.textContent = t`Added On` + `: ${new Date(model.created * 1000).toLocaleDateString()}`;
398396
399397 detailsContainer.appendChild(modelClassDiv);
400398 detailsContainer.appendChild(contextLengthDiv);
@@ -418,6 +416,7 @@ export async function loadFeatherlessModels(data) {
418416
419417 // Update the current page value whenever the page changes
420418 featherlessCurrentPage = pagination.pageNumber;
419+ localizePagination(paginationContainer);
421420 },
422421 afterSizeSelectorChange: function (e) {
423422 const newPerPage = e.target.value;
@@ -923,6 +922,10 @@ export function getCurrentDreamGenModelTokenizer() {
923922 return tokenizers.YI;
924923 } else if (model.id.startsWith('opus-v1-xl')) {
925924 return tokenizers.LLAMA;
925+ } else if (model.id.startsWith('lucid-v1-medium')) {
926+ return tokenizers.NEMO;
927+ } else if (model.id.startsWith('lucid-v1-extra-large')) {
928+ return tokenizers.LLAMA3;
926929 } else {
927930 return tokenizers.MISTRAL;
928931 }
public/scripts/textgen-settings.js+1 -1
@@ -1146,7 +1146,7 @@ function tryParseStreamingError(response, decoded) {
11461146 // No JSON. Do nothing.
11471147 }
11481148
11491149 const message = data?.error?.message || data?.error || data?.message || data?.detail;
11501150
11511151 if (message) {
11521152 toastr.error(message, 'Text Completion API');
public/scripts/tool-calling.js+1 -0
@@ -586,6 +586,7 @@ export class ToolManager {
586586 chat_completion_sources.DEEPSEEK,
587587 chat_completion_sources.MAKERSUITE,
588588 chat_completion_sources.AI21,
589+ chat_completion_sources.XAI,
589590 ];
590591 return supportedSources.includes(oai_settings.chat_completion_source);
591592 }
public/scripts/utils.js+41 -2
@@ -20,7 +20,46 @@ import { getCurrentLocale, t } from './i18n.js';
2020 * Pagination status string template.
2121 * @type {string}
2222 */
2323export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of.. <%= totalNumber %>';
24+
25+export const localizePagination = function(container) {
26+ container.find('[title="Next page"]').attr('title', t`Next page`);
27+ container.find('[title="Previous page"]').attr('title', t`Previous page`);
28+};
29+
30+/**
31+ * Renders a dropdown for selecting page size in pagination.
32+ * @param {number} pageSize Page size
33+ * @param {number[]} sizeChangerOptions Array of page size options
34+ * @returns {string} The rendered dropdown element as a string
35+ */
36+export const renderPaginationDropdown = function(pageSize, sizeChangerOptions) {
37+ const sizeSelect = document.createElement('select');
38+ sizeSelect.classList.add('J-paginationjs-size-select');
39+
40+ if (sizeChangerOptions.indexOf(pageSize) === -1) {
41+ sizeChangerOptions.unshift(pageSize);
42+ sizeChangerOptions.sort((a, b) => a - b);
43+ }
44+
45+ for (let i = 0; i < sizeChangerOptions.length; i++) {
46+ const option = document.createElement('option');
47+ option.value = `${sizeChangerOptions[i]}`;
48+ option.textContent = `${sizeChangerOptions[i]} ${t`/ page`}`;
49+ if (sizeChangerOptions[i] === pageSize) {
50+ option.setAttribute('selected', 'selected');
51+ }
52+ sizeSelect.appendChild(option);
53+ }
54+
55+ return sizeSelect.outerHTML;
56+};
57+
58+export const paginationDropdownChangeHandler = function(event, size) {
59+ let dropdown = $(event?.originalEvent?.currentTarget || event.delegateTarget).find('select');
60+ dropdown.find('[selected]').removeAttr('selected');
61+ dropdown.find(`[value=${size}]`).attr('selected', '');
62+};
2463
2564/**
2665 * Navigation options for pagination.
@@ -1047,7 +1086,7 @@ export function getImageSizeFromDataURL(dataUrl) {
10471086
10481087/**
10491088 * Gets the filename of the character avatar without extension
10501089 * @param {string|number?} [chid=null] - Character ID. If not provided, uses the current character ID
10511090 * @param {object} [options={}] - Options arguments
10521091 * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
10531092 * @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
public/scripts/world-info.js+212 -109
@@ -98,11 +98,28 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
9898
9999// Typedef area
100100/**
101+ * @typedef {object} WIGlobalScanData The chat-independent data to be scanned. Each of
102+ * these fields can be enabled for scanning per entry.
103+ * @property {string} personaDescription User persona description
104+ * @property {string} characterDescription Character description
105+ * @property {string} characterPersonality Character personality
106+ * @property {string} characterDepthPrompt Character depth prompt (sometimes referred to as character notes)
107+ * @property {string} scenario Character defined scenario
108+ * @property {string} creatorNotes Character creator notes
109+ */
110+
111+/**
101112 * @typedef {object} WIScanEntry The entry that triggered the scan
102113 * @property {number} [scanDepth] The depth of the scan
103114 * @property {boolean} [caseSensitive] If the scan is case sensitive
104115 * @property {boolean} [matchWholeWords] If the scan should match whole words
105116 * @property {boolean} [useGroupScoring] If the scan should use group scoring
117+ * @property {boolean} [matchPersonaDescription] If the scan should match against the persona description
118+ * @property {boolean} [matchCharacterDescription] If the scan should match against the character description
119+ * @property {boolean} [matchCharacterPersonality] If the scan should match against the character personality
120+ * @property {boolean} [matchCharacterDepthPrompt] If the scan should match against the character depth prompt
121+ * @property {boolean} [matchScenario] If the scan should match against the character scenario
122+ * @property {boolean} [matchCreatorNotes] If the scan should match against the creator notes
106123 * @property {number} [uid] The UID of the entry that triggered the scan
107124 * @property {string} [world] The world info book of origin of the entry
108125 * @property {string[]} [key] The primary keys to scan for
@@ -139,6 +156,11 @@ class WorldInfoBuffer {
139156 static externalActivations = new Map();
140157
141158 /**
159+ * @type {WIGlobalScanData} Chat independent data to be scanned, such as persona and character descriptions
160+ */
161+ #globalScanData = null;
162+
163+ /**
142164 * @type {string[]} Array of messages sorted by ascending depth
143165 */
144166 #depthBuffer = [];
@@ -166,9 +188,11 @@ class WorldInfoBuffer {
166188 /**
167189 * Initialize the buffer with the given messages.
168190 * @param {string[]} messages Array of messages to add to the buffer
191+ * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
169192 */
170193 constructor(messages, globalScanData) {
171194 this.#initDepthBuffer(messages);
195+ this.#globalScanData = globalScanData;
172196 }
173197
174198 /**
@@ -225,6 +249,25 @@ class WorldInfoBuffer {
225249 const JOINER = '\n' + MATCHER;
226250 let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);
227251
252+ if (entry.matchPersonaDescription && this.#globalScanData.personaDescription) {
253+ result += JOINER + this.#globalScanData.personaDescription;
254+ }
255+ if (entry.matchCharacterDescription && this.#globalScanData.characterDescription) {
256+ result += JOINER + this.#globalScanData.characterDescription;
257+ }
258+ if (entry.matchCharacterPersonality && this.#globalScanData.characterPersonality) {
259+ result += JOINER + this.#globalScanData.characterPersonality;
260+ }
261+ if (entry.matchCharacterDepthPrompt && this.#globalScanData.characterDepthPrompt) {
262+ result += JOINER + this.#globalScanData.characterDepthPrompt;
263+ }
264+ if (entry.matchScenario && this.#globalScanData.scenario) {
265+ result += JOINER + this.#globalScanData.scenario;
266+ }
267+ if (entry.matchCreatorNotes && this.#globalScanData.creatorNotes) {
268+ result += JOINER + this.#globalScanData.creatorNotes;
269+ }
270+
228271 if (this.#injectBuffer.length > 0) {
229272 result += JOINER + this.#injectBuffer.join(JOINER);
230273 }
@@ -756,6 +799,7 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
756799 * @param {string[]} chat - The chat messages to scan, in reverse order.
757800 * @param {number} maxContext - The maximum context size of the generation.
758801 * @param {boolean} isDryRun - If true, the function will not emit any events.
802+ * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
759803 * @typedef {object} WIPromptResult
760804 * @property {string} worldInfoString - Complete world info string
761805 * @property {string} worldInfoBefore - World info that goes before the prompt
@@ -766,10 +810,10 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
766810 * @property {Array} anAfter - Array of entries after Author's Note
767811 * @returns {Promise<WIPromptResult>} The world info string and depth.
768812 */
769813export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanData) {
770814 let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
771815
772816 const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun, globalScanData);
773817 worldInfoBefore = activatedWorldInfo.worldInfoBefore;
774818 worldInfoAfter = activatedWorldInfo.worldInfoAfter;
775819 worldInfoString = worldInfoBefore + worldInfoAfter;
@@ -966,7 +1010,7 @@ function registerWorldInfoSlashCommands() {
9661010 /**
9671011 * Gets the name of the character-bound lorebook.
9681012 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
9691013 * @param {import('./slash-commands/SlashCommand.js').UnnamedArgumentsstring} name Character name
9701014 * @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string
9711015 */
9721016 function getCharBookCallback({ type }, name) {
@@ -1338,6 +1382,18 @@ function registerWorldInfoSlashCommands() {
13381382 }
13391383 }
13401384
1385+ async function getGlobalBooksCallback() {
1386+ if (!selected_world_info?.length) {
1387+ return JSON.stringify([]);
1388+ }
1389+
1390+ let entries = selected_world_info.slice();
1391+
1392+ console.debug(`[WI] Selected global world info has ${entries.length} entries`, selected_world_info);
1393+
1394+ return JSON.stringify(entries);
1395+ }
1396+
13411397 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13421398 name: 'world',
13431399 callback: onWorldInfoChange,
@@ -1380,6 +1436,13 @@ function registerWorldInfoSlashCommands() {
13801436 aliases: ['getchatlore', 'getchatwi'],
13811437 }));
13821438 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1439+ name: 'getglobalbooks',
1440+ callback: getGlobalBooksCallback,
1441+ returns: 'list of selected lorebook names',
1442+ helpString: 'Get a list of names of the selected global lorebooks and pass it down the pipe.',
1443+ aliases: ['getgloballore', 'getglobalwi'],
1444+ }));
1445+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13831446 name: 'getpersonabook',
13841447 callback: getPersonaBookCallback,
13851448 returns: 'lorebook name',
@@ -2172,6 +2235,12 @@ export const originalWIDataKeyMap = {
21722235 'matchWholeWords': 'extensions.match_whole_words',
21732236 'useGroupScoring': 'extensions.use_group_scoring',
21742237 'caseSensitive': 'extensions.case_sensitive',
2238+ 'matchPersonaDescription': 'extensions.match_persona_description',
2239+ 'matchCharacterDescription': 'extensions.match_character_description',
2240+ 'matchCharacterPersonality': 'extensions.match_character_personality',
2241+ 'matchCharacterDepthPrompt': 'extensions.match_character_depth_prompt',
2242+ 'matchScenario': 'extensions.match_scenario',
2243+ 'matchCreatorNotes': 'extensions.match_creator_notes',
21752244 'scanDepth': 'extensions.scan_depth',
21762245 'automationId': 'extensions.automation_id',
21772246 'vectorized': 'extensions.vectorized',
@@ -2589,7 +2658,7 @@ export async function getWorldEntry(name, data, entry) {
25892658 if (!isMobile()) {
25902659 $(characterFilter).select2({
25912660 width: '100%',
25922661 placeholder: 't`Tie this entry to specific characters or characters with specific tags'`,
25932662 allowClear: true,
25942663 closeOnSelect: false,
25952664 });
@@ -3189,7 +3258,7 @@ export async function getWorldEntry(name, data, entry) {
31893258
31903259 // Create wrapper div
31913260 const wrapper = document.createElement('div');
31923261 wrapper.textContent = t`Move "'${sourceName}"' to:`;
31933262
31943263 // Create container and append elements
31953264 const container = document.createElement('div');
@@ -3289,6 +3358,28 @@ export async function getWorldEntry(name, data, entry) {
32893358 });
32903359 useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
32913360
3361+ function handleMatchCheckbox(fieldName) {
3362+ const key = originalWIDataKeyMap[fieldName];
3363+ const checkBoxElem = template.find(`input[type="checkbox"][name="${fieldName}"]`);
3364+ checkBoxElem.data('uid', entry.uid);
3365+ checkBoxElem.on('input', async function () {
3366+ const uid = $(this).data('uid');
3367+ const value = $(this).prop('checked');
3368+
3369+ data.entries[uid][fieldName] = value;
3370+ setWIOriginalDataValue(data, uid, key, data.entries[uid][fieldName]);
3371+ await saveWorldInfo(name, data);
3372+ });
3373+ checkBoxElem.prop('checked', !!entry[fieldName]).trigger('input');
3374+ }
3375+
3376+ handleMatchCheckbox('matchPersonaDescription');
3377+ handleMatchCheckbox('matchCharacterDescription');
3378+ handleMatchCheckbox('matchCharacterPersonality');
3379+ handleMatchCheckbox('matchCharacterDepthPrompt');
3380+ handleMatchCheckbox('matchScenario');
3381+ handleMatchCheckbox('matchCreatorNotes');
3382+
32923383 // automation id
32933384 const automationIdInput = template.find('input[name="automationId"]');
32943385 automationIdInput.data('uid', entry.uid);
@@ -3424,7 +3515,7 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false }
34243515 });
34253516
34263517 $(input).on('focus click', function () {
34273518 $(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : String($(input).val()));
34283519 });
34293520}
34303521
@@ -3495,6 +3586,12 @@ export const newWorldInfoEntryDefinition = {
34953586 disable: { default: false, type: 'boolean' },
34963587 excludeRecursion: { default: false, type: 'boolean' },
34973588 preventRecursion: { default: false, type: 'boolean' },
3589+ matchPersonaDescription: { default: false, type: 'boolean' },
3590+ matchCharacterDescription: { default: false, type: 'boolean' },
3591+ matchCharacterPersonality: { default: false, type: 'boolean' },
3592+ matchCharacterDepthPrompt: { default: false, type: 'boolean' },
3593+ matchScenario: { default: false, type: 'boolean' },
3594+ matchCreatorNotes: { default: false, type: 'boolean' },
34983595 delayUntilRecursion: { default: 0, type: 'number' },
34993596 probability: { default: 100, type: 'number' },
35003597 useProbability: { default: true, type: 'boolean' },
@@ -3959,6 +4056,7 @@ function parseDecorators(content) {
39594056 * @param {string[]} chat The chat messages to scan, in reverse order.
39604057 * @param {number} maxContext The maximum context size of the generation.
39614058 * @param {boolean} isDryRun Whether to perform a dry run.
4059+ * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
39624060 * @typedef {object} WIActivated
39634061 * @property {string} worldInfoBefore The world info before the chat.
39644062 * @property {string} worldInfoAfter The world info after the chat.
@@ -3969,9 +4067,9 @@ function parseDecorators(content) {
39694067 * @property {Set<any>} allActivatedEntries All entries.
39704068 * @returns {Promise<WIActivated>} The world info activated.
39714069 */
39724070export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) {
39734071 const context = getContext();
39744072 const buffer = new WorldInfoBuffer(chat, globalScanData);
39754073
39764074 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
39774075
@@ -4829,6 +4927,12 @@ export function convertCharacterBook(characterBook) {
48294927 sticky: entry.extensions?.sticky ?? null,
48304928 cooldown: entry.extensions?.cooldown ?? null,
48314929 delay: entry.extensions?.delay ?? null,
4930+ matchPersonaDescription: entry.extensions?.match_persona_description ?? false,
4931+ matchCharacterDescription: entry.extensions?.match_character_description ?? false,
4932+ matchCharacterPersonality: entry.extensions?.match_character_personality ?? false,
4933+ matchCharacterDepthPrompt: entry.extensions?.match_character_depth_prompt ?? false,
4934+ matchScenario: entry.extensions?.match_scenario ?? false,
4935+ matchCreatorNotes: entry.extensions?.match_creator_notes ?? false,
48324936 extensions: entry.extensions ?? {},
48334937 };
48344938 });
@@ -5099,7 +5203,7 @@ export function openWorldInfoEditor(worldName) {
50995203
51005204/**
51015205 * Assigns a lorebook to the current chat.
51025206 * @param {PointerEventJQuery.ClickEvent<Document, undefined, any, any>} event Pointer event
51035207 * @returns {Promise<void>}
51045208 */
51055209export async function assignLorebookToChat(event) {
@@ -5138,11 +5242,106 @@ export async function assignLorebookToChat(event) {
51385242 saveMetadata();
51395243 });
51405244
51415245 returnawait callGenericPopup(template, POPUP_TYPE.TEXT);
51425246}
51435247
5144-jQuery(() => {
5248+/**
5249+ * Moves a World Info entry from a source lorebook to a target lorebook.
5250+ *
5251+ * @param {string} sourceName - The name of the source lorebook file.
5252+ * @param {string} targetName - The name of the target lorebook file.
5253+ * @param {string|number} uid - The UID of the entry to move from the source lorebook.
5254+ * @returns {Promise<boolean>} True if the move was successful, false otherwise.
5255+ */
5256+export async function moveWorldInfoEntry(sourceName, targetName, uid) {
5257+ if (sourceName === targetName) {
5258+ return false;
5259+ }
51455260
5261+ if (!world_names.includes(sourceName)) {
5262+ toastr.error(t`Source lorebook '${sourceName}' not found.`);
5263+ console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
5264+ return false;
5265+ }
5266+
5267+ if (!world_names.includes(targetName)) {
5268+ toastr.error(t`Target lorebook '${targetName}' not found.`);
5269+ console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
5270+ return false;
5271+ }
5272+
5273+ const entryUidString = String(uid);
5274+
5275+ try {
5276+ const sourceData = await loadWorldInfo(sourceName);
5277+ const targetData = await loadWorldInfo(targetName);
5278+
5279+ if (!sourceData || !sourceData.entries) {
5280+ toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
5281+ console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
5282+ return false;
5283+ }
5284+ if (!targetData || !targetData.entries) {
5285+ toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
5286+ console.error(`[WI Move] Could not load target data for '${targetName}'.`);
5287+ return false;
5288+ }
5289+
5290+ if (!sourceData.entries[entryUidString]) {
5291+ toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
5292+ console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
5293+ return false;
5294+ }
5295+
5296+ const entryToMove = structuredClone(sourceData.entries[entryUidString]);
5297+
5298+
5299+ const newUid = getFreeWorldEntryUid(targetData);
5300+ if (newUid === null) {
5301+ console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
5302+ return false;
5303+ }
5304+
5305+ entryToMove.uid = newUid;
5306+ // Place the entry at the end of the target lorebook
5307+ const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
5308+ entryToMove.displayIndex = maxDisplayIndex + 1;
5309+
5310+ targetData.entries[newUid] = entryToMove;
5311+
5312+ delete sourceData.entries[entryUidString];
5313+ // Remove from originalData if it exists
5314+ deleteWIOriginalDataValue(sourceData, entryUidString);
5315+ // TODO: setWIOriginalDataValue
5316+ console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
5317+
5318+
5319+ await saveWorldInfo(targetName, targetData, true);
5320+ console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
5321+ await saveWorldInfo(sourceName, sourceData, true);
5322+ console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
5323+
5324+
5325+ console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
5326+
5327+ // Check if the currently viewed book in the editor is the source or target and reload it
5328+ const currentEditorBookIndex = Number($('#world_editor_select').val());
5329+ if (!isNaN(currentEditorBookIndex)) {
5330+ const currentEditorBookName = world_names[currentEditorBookIndex];
5331+ if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
5332+ reloadEditor(currentEditorBookName);
5333+ }
5334+ }
5335+
5336+ return true;
5337+ } catch (error) {
5338+ toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
5339+ console.error('[WI Move] Unexpected error:', error);
5340+ return false;
5341+ }
5342+}
5343+
5344+export function initWorldInfo() {
51465345 $('#world_info').on('mousedown change', async function (e) {
51475346 // If there's no world names, don't do anything
51485347 if (world_names.length === 0) {
@@ -5329,7 +5528,7 @@ jQuery(() => {
53295528 if (!isMobile()) {
53305529 $('#world_info').select2({
53315530 width: '100%',
53325531 placeholder: 't`No Worlds active. Click here to select.'`,
53335532 allowClear: true,
53345533 closeOnSelect: false,
53355534 });
@@ -5354,100 +5553,4 @@ jQuery(() => {
53545553 }
53555554 });
53565555 });
5357-});
5358-
5359-/**
5360- * Moves a World Info entry from a source lorebook to a target lorebook.
5361- *
5362- * @param {string} sourceName - The name of the source lorebook file.
5363- * @param {string} targetName - The name of the target lorebook file.
5364- * @param {string|number} uid - The UID of the entry to move from the source lorebook.
5365- * @returns {Promise<boolean>} True if the move was successful, false otherwise.
5366- */
5367-export async function moveWorldInfoEntry(sourceName, targetName, uid) {
5368- if (sourceName === targetName) {
5369- return false;
5370- }
5371-
5372- if (!world_names.includes(sourceName)) {
5373- toastr.error(t`Source lorebook '${sourceName}' not found.`);
5374- console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
5375- return false;
5376- }
5377-
5378- if (!world_names.includes(targetName)) {
5379- toastr.error(t`Target lorebook '${targetName}' not found.`);
5380- console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
5381- return false;
5382- }
5383-
5384- const entryUidString = String(uid);
5385-
5386- try {
5387- const sourceData = await loadWorldInfo(sourceName);
5388- const targetData = await loadWorldInfo(targetName);
5389-
5390- if (!sourceData || !sourceData.entries) {
5391- toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
5392- console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
5393- return false;
5394- }
5395- if (!targetData || !targetData.entries) {
5396- toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
5397- console.error(`[WI Move] Could not load target data for '${targetName}'.`);
5398- return false;
5399- }
5400-
5401- if (!sourceData.entries[entryUidString]) {
5402- toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
5403- console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
5404- return false;
5405- }
5406-
5407- const entryToMove = structuredClone(sourceData.entries[entryUidString]);
5408-
5409-
5410- const newUid = getFreeWorldEntryUid(targetData);
5411- if (newUid === null) {
5412- console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
5413- return false;
5414- }
5415-
5416- entryToMove.uid = newUid;
5417- // Place the entry at the end of the target lorebook
5418- const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
5419- entryToMove.displayIndex = maxDisplayIndex + 1;
5420-
5421- targetData.entries[newUid] = entryToMove;
5422-
5423- delete sourceData.entries[entryUidString];
5424- // Remove from originalData if it exists
5425- deleteWIOriginalDataValue(sourceData, entryUidString);
5426- // TODO: setWIOriginalDataValue
5427- console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
5428-
5429-
5430- await saveWorldInfo(targetName, targetData, true);
5431- console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
5432- await saveWorldInfo(sourceName, sourceData, true);
5433- console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
5434-
5435-
5436- console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
5437-
5438- // Check if the currently viewed book in the editor is the source or target and reload it
5439- const currentEditorBookIndex = Number($('#world_editor_select').val());
5440- if (!isNaN(currentEditorBookIndex)) {
5441- const currentEditorBookName = world_names[currentEditorBookIndex];
5442- if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
5443- reloadEditor(currentEditorBookName);
5444- }
5445- }
5446-
5447- return true;
5448- } catch (error) {
5449- toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
5450- console.error('[WI Move] Unexpected error:', error);
5451- return false;
5452- }
54535556}
public/style.css+19 -3
@@ -157,11 +157,26 @@ body {
157157}
158158
159159::-webkit-scrollbar {
160160 width: 10px0.7rem;
161- height: 10px;
162161 scrollbar-gutter: stable;
163162}
164163
164+::-webkit-scrollbar-track {
165+ cursor: default;
166+}
167+
168+::-webkit-scrollbar-track:hover {
169+ background-color: rgba(126, 126, 126, 0.2); /* Adaptive, but won't contrast with neutral-gray. */
170+}
171+
172+::-webkit-scrollbar-thumb {
173+ cursor: grab;
174+}
175+
176+::-webkit-scrollbar-thumb:active {
177+ cursor: grabbing;
178+}
179+
165180.scrollY {
166181 overflow-y: auto !important;
167182}
@@ -5091,7 +5106,7 @@ body:not(.sd) .mes_img_swipes {
50915106 display: flex;
50925107 flex-direction: column;
50935108 justify-content: center;
50945109 padding: 10px0px;
50955110 height: 100%;
50965111 width: 100%;
50975112}
@@ -5700,6 +5715,7 @@ body:not(.movingUI) .drawer-content.maximized {
57005715 width: unset;
57015716 margin: 0;
57025717 font-size: calc(var(--mainFontSize) * 0.85);
5718+ padding-right: 20px;
57035719}
57045720
57055721.paginationjs-pages ul li a {
server.js+5 -383
@@ -1,393 +1,15 @@
11#!/usr/bin/env node
2-
3-// native node modules
4-import path from 'node:path';
5-import util from 'node:util';
6-import net from 'node:net';
7-import dns from 'node:dns';
8-import process from 'node:process';
9-import { fileURLToPath } from 'node:url';
10-
11-import cors from 'cors';
12-import { csrfSync } from 'csrf-sync';
13-import express from 'express';
14-import compression from 'compression';
15-import cookieSession from 'cookie-session';
16-import multer from 'multer';
17-import responseTime from 'response-time';
18-import helmet from 'helmet';
19-import bodyParser from 'body-parser';
20-import open from 'open';
21-
22-// local library imports
23-import './src/fetch-patch.js';
24-import { serverEvents, EVENT_NAMES } from './src/server-events.js';
252import { CommandLineParser } from './src/command-line.js';
263import { loadPluginsserverDirectory } from './src/pluginserver-loaderdirectory.js';
27-import {
28- initUserStorage,
29- getCookieSecret,
30- getCookieSessionName,
31- ensurePublicDirectoriesExist,
32- getUserDirectoriesList,
33- migrateSystemPrompts,
34- migrateUserData,
35- requireLoginMiddleware,
36- setUserDataMiddleware,
37- shouldRedirectToLogin,
38- cleanUploads,
39- getSessionCookieAge,
40- verifySecuritySettings,
41- loginPageMiddleware,
42-} from './src/users.js';
43-
44-import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
45-import basicAuthMiddleware from './src/middleware/basicAuth.js';
46-import getWhitelistMiddleware from './src/middleware/whitelist.js';
47-import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';
48-import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
49-import initRequestProxy from './src/request-proxy.js';
50-import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
51-import corsProxyMiddleware from './src/middleware/corsProxy.js';
52-import {
53- getVersion,
54- color,
55- removeColorFormatting,
56- getSeparator,
57- safeReadFileSync,
58- setupLogLevel,
59- setWindowTitle,
60-} from './src/util.js';
61-import { UPLOADS_DIRECTORY } from './src/constants.js';
62-import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
63-
64-// Routers
65-import { router as usersPublicRouter } from './src/endpoints/users-public.js';
66-import { init as statsInit, onExit as statsOnExit } from './src/endpoints/stats.js';
67-import { checkForNewContent } from './src/endpoints/content-manager.js';
68-import { init as settingsInit } from './src/endpoints/settings.js';
69-import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './src/server-startup.js';
70-import { diskCache } from './src/endpoints/characters.js';
71-
72-// Unrestrict console logs display limit
73-util.inspect.defaultOptions.maxArrayLength = null;
74-util.inspect.defaultOptions.maxStringLength = null;
75-util.inspect.defaultOptions.depth = 4;
76-
77-// Set a working directory for the server
78-const serverDirectory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
79-console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
80-process.chdir(serverDirectory);
81-
82-// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
83-// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
84-// Safe to remove once support for Node v20 is dropped.
85-if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) {
86- // @ts-ignore
87- if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
88-}
894
5+// config.yaml will be set when parsing command line arguments
906const cliArgs = new CommandLineParser().parse(process.argv);
917globalThis.DATA_ROOT = cliArgs.dataRoot;
928globalThis.COMMAND_LINE_ARGS = cliArgs;
93-
9+process.chdir(serverDirectory);
94-if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
95- console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
96- process.exit(1);
97-}
9810
9911try {
100- if (cliArgs.dnsPreferIPv6) {
12+ await import('./src/server-main.js');
101- dns.setDefaultResultOrder('ipv6first');
102- console.log('Preferring IPv6 for DNS resolution');
103- } else {
104- dns.setDefaultResultOrder('ipv4first');
105- console.log('Preferring IPv4 for DNS resolution');
106- }
10713} catch (error) {
10814 console.warnerror('Failed to set DNSA resolutioncritical order.error Possiblyhas unsupportedoccurred inwhile thisstarting Nodethe version.server:', error);
109-}
110-
111-const app = express();
112-app.use(helmet({
113- contentSecurityPolicy: false,
114-}));
115-app.use(compression());
116-app.use(responseTime());
117-
118-app.use(bodyParser.json({ limit: '200mb' }));
119-app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
120-
121-// CORS Settings //
122-const CORS = cors({
123- origin: 'null',
124- methods: ['OPTIONS'],
125-});
126-
127-app.use(CORS);
128-
129-if (cliArgs.listen && cliArgs.basicAuthMode) {
130- app.use(basicAuthMiddleware);
131-}
132-
133-if (cliArgs.whitelistMode) {
134- const whitelistMiddleware = await getWhitelistMiddleware();
135- app.use(whitelistMiddleware);
136-}
137-
138-if (cliArgs.listen) {
139- app.use(accessLoggerMiddleware());
140-}
141-
142-if (cliArgs.enableCorsProxy) {
143- app.use('/proxy/:url(*)', corsProxyMiddleware);
144-} else {
145- app.use('/proxy/:url(*)', async (_, res) => {
146- const message = 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';
147- console.log(message);
148- res.status(404).send(message);
149- });
150-}
151-
152-app.use(cookieSession({
153- name: getCookieSessionName(),
154- sameSite: 'lax',
155- httpOnly: true,
156- maxAge: getSessionCookieAge(),
157- secret: getCookieSecret(globalThis.DATA_ROOT),
158-}));
159-
160-app.use(setUserDataMiddleware);
161-
162-// CSRF Protection //
163-if (!cliArgs.disableCsrf) {
164- const csrfSyncProtection = csrfSync({
165- getTokenFromState: (req) => {
166- if (!req.session) {
167- console.error('(CSRF error) getTokenFromState: Session object not initialized');
168- return;
169- }
170- return req.session.csrfToken;
171- },
172- getTokenFromRequest: (req) => {
173- return req.headers['x-csrf-token']?.toString();
174- },
175- storeTokenInState: (req, token) => {
176- if (!req.session) {
177- console.error('(CSRF error) storeTokenInState: Session object not initialized');
178- return;
179- }
180- req.session.csrfToken = token;
181- },
182- size: 32,
183- });
184-
185- app.get('/csrf-token', (req, res) => {
186- res.json({
187- 'token': csrfSyncProtection.generateToken(req),
188- });
189- });
190-
191- // Customize the error message
192- csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
193- csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
194-
195- app.use(csrfSyncProtection.csrfSynchronisedProtection);
196-} else {
197- console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
198- app.get('/csrf-token', (req, res) => {
199- res.json({
200- 'token': 'disabled',
201- });
202- });
203-}
204-
205-// Static files
206-// Host index page
207-app.get('/', getCacheBusterMiddleware(), (request, response) => {
208- if (shouldRedirectToLogin(request)) {
209- const query = request.url.split('?')[1];
210- const redirectUrl = query ? `/login?${query}` : '/login';
211- return response.redirect(redirectUrl);
212- }
213-
214- return response.sendFile('index.html', { root: path.join(process.cwd(), 'public') });
215-});
216-
217-// Callback endpoint for OAuth PKCE flows (e.g. OpenRouter)
218-app.get('/callback/:source?', (request, response) => {
219- const source = request.params.source;
220- const query = request.url.split('?')[1];
221- const searchParams = new URLSearchParams();
222- source && searchParams.set('source', source);
223- query && searchParams.set('query', query);
224- const path = `/?${searchParams.toString()}`;
225- return response.redirect(307, path);
226-});
227-
228-// Host login page
229-app.get('/login', loginPageMiddleware);
230-
231-// Host frontend assets
232-const webpackMiddleware = getWebpackServeMiddleware();
233-app.use(webpackMiddleware);
234-app.use(express.static(process.cwd() + '/public', {}));
235-
236-// Public API
237-app.use('/api/users', usersPublicRouter);
238-
239-// Everything below this line requires authentication
240-app.use(requireLoginMiddleware);
241-app.get('/api/ping', (request, response) => {
242- if (request.query.extend && request.session) {
243- request.session.touch = Date.now();
244- }
245-
246- response.sendStatus(204);
247-});
248-
249-// File uploads
250-const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
251-app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
252-app.use(multerMonkeyPatch);
253-
254-app.get('/version', async function (_, response) {
255- const data = await getVersion();
256- response.send(data);
257-});
258-
259-redirectDeprecatedEndpoints(app);
260-setupPrivateEndpoints(app);
261-
262-/**
263- * Tasks that need to be run before the server starts listening.
264- * @returns {Promise<void>}
265- */
266-async function preSetupTasks() {
267- const version = await getVersion();
268-
269- // Print formatted header
270- console.log();
271- console.log(`SillyTavern ${version.pkgVersion}`);
272- if (version.gitBranch) {
273- console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`);
274- if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) {
275- console.log('INFO: Currently not on the latest commit.');
276- console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.');
277- }
278- }
279- console.log();
280-
281- const directories = await getUserDirectoriesList();
282- await checkForNewContent(directories);
283- await ensureThumbnailCache(directories);
284- await diskCache.verify(directories);
285- cleanUploads();
286- migrateAccessLog();
287-
288- await settingsInit();
289- await statsInit();
290-
291- const pluginsDirectory = path.join(serverDirectory, 'plugins');
292- const cleanupPlugins = await loadPlugins(app, pluginsDirectory);
293- const consoleTitle = process.title;
294-
295- let isExiting = false;
296- const exitProcess = async () => {
297- if (isExiting) return;
298- isExiting = true;
299- await statsOnExit();
300- if (typeof cleanupPlugins === 'function') {
301- await cleanupPlugins();
302- }
303- diskCache.dispose();
304- setWindowTitle(consoleTitle);
305- process.exit();
306- };
307-
308- // Set up event listeners for a graceful shutdown
309- process.on('SIGINT', exitProcess);
310- process.on('SIGTERM', exitProcess);
311- process.on('uncaughtException', (err) => {
312- console.error('Uncaught exception:', err);
313- exitProcess();
314- });
315-
316- // Add request proxy.
317- initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass });
318-
319- // Wait for frontend libs to compile
320- await webpackMiddleware.runWebpackCompiler();
321-}
322-
323-/**
324- * Tasks that need to be run after the server starts listening.
325- * @param {import('./src/server-startup.js').ServerStartupResult} result The result of the server startup
326- * @returns {Promise<void>}
327- */
328-async function postSetupTasks(result) {
329- const autorunHostname = await cliArgs.getAutorunHostname(result);
330- const autorunUrl = cliArgs.getAutorunUrl(autorunHostname);
331-
332- if (cliArgs.autorun) {
333- try {
334- console.log('Launching in a browser...');
335- await open(autorunUrl.toString());
336- } catch (error) {
337- console.error('Failed to launch the browser. Open the URL manually.');
338- }
339- }
340-
341- setWindowTitle('SillyTavern WebServer');
342-
343- let logListen = 'SillyTavern is listening on';
344-
345- if (result.useIPv6 && !result.v6Failed) {
346- logListen += color.green(
347- ' IPv6: ' + cliArgs.getIPv6ListenUrl().host,
348- );
349- }
350-
351- if (result.useIPv4 && !result.v4Failed) {
352- logListen += color.green(
353- ' IPv4: ' + cliArgs.getIPv4ListenUrl().host,
354- );
355- }
356-
357- const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
358- const plainGoToLog = removeColorFormatting(goToLog);
359-
360- console.log(logListen);
361- if (cliArgs.listen) {
362- console.log();
363- console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
364- console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
365- }
366- console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
367- console.log(goToLog);
368- console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
369-
370- setupLogLevel();
371- serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
372-}
373-
374-/**
375- * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
376- */
377-function apply404Middleware() {
378- const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
379- app.use((req, res) => {
380- res.status(404).send(notFoundWebpage);
381- });
38215}
383-
384-// User storage module needs to be initialized before starting the server
385-initUserStorage(globalThis.DATA_ROOT)
386- .then(ensurePublicDirectoriesExist)
387- .then(migrateUserData)
388- .then(migrateSystemPrompts)
389- .then(verifySecuritySettings)
390- .then(preSetupTasks)
391- .then(apply404Middleware)
392- .then(() => new ServerStartup(app, cliArgs).start())
393- .then(postSetupTasks);
src/command-line.js+11 -0
@@ -2,9 +2,11 @@ import yargs from 'yargs/yargs';
22import { hideBin } from 'yargs/helpers';
33import ipRegex from 'ip-regex';
44import { canResolve, color, getConfigValue, stringToBool } from './util.js';
5+import { initConfig } from './config-init.js';
56
67/**
78 * @typedef {object} CommandLineArguments Parsed command line arguments
9+ * @property {string} configPath Path to the config file
810 * @property {string} dataRoot Data root directory
911 * @property {number} port Port number
1012 * @property {boolean} listen If SillyTavern is listening on all network interfaces
@@ -40,6 +42,7 @@ export class CommandLineParser {
4042 constructor() {
4143 /** @type {CommandLineArguments} */
4244 this.default = Object.freeze({
45+ configPath: './config.yaml',
4346 dataRoot: './data',
4447 port: 8000,
4548 listen: false,
@@ -88,6 +91,11 @@ export class CommandLineParser {
8891 parse(args) {
8992 const cliArguments = yargs(hideBin(args))
9093 .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.')
94+ .option('configPath', {
95+ type: 'string',
96+ default: null,
97+ describe: 'Path to the config file',
98+ })
9199 .option('enableIPv6', {
92100 type: 'string',
93101 default: null,
@@ -177,8 +185,11 @@ export class CommandLineParser {
177185 describe: 'Request proxy bypass list (space separated list of hosts)',
178186 }).parseSync();
179187
188+ const configPath = cliArguments.configPath ?? this.default.configPath;
189+ initConfig(configPath);
180190 /** @type {CommandLineArguments} */
181191 const result = {
192+ configPath: configPath,
182193 dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot),
183194 port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'),
184195 listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'),
src/config-init.js+197 -0
@@ -0,0 +1,197 @@
1+import fs from 'node:fs';
2+import path from 'node:path';
3+import yaml from 'yaml';
4+import color from 'chalk';
5+import _ from 'lodash';
6+import { serverDirectory } from './server-directory.js';
7+import { setConfigFilePath } from './util.js';
8+
9+const keyMigrationMap = [
10+ {
11+ oldKey: 'disableThumbnails',
12+ newKey: 'thumbnails.enabled',
13+ migrate: (value) => !value,
14+ },
15+ {
16+ oldKey: 'thumbnailsQuality',
17+ newKey: 'thumbnails.quality',
18+ migrate: (value) => value,
19+ },
20+ {
21+ oldKey: 'avatarThumbnailsPng',
22+ newKey: 'thumbnails.format',
23+ migrate: (value) => (value ? 'png' : 'jpg'),
24+ },
25+ {
26+ oldKey: 'disableChatBackup',
27+ newKey: 'backups.chat.enabled',
28+ migrate: (value) => !value,
29+ },
30+ {
31+ oldKey: 'numberOfBackups',
32+ newKey: 'backups.common.numberOfBackups',
33+ migrate: (value) => value,
34+ },
35+ {
36+ oldKey: 'maxTotalChatBackups',
37+ newKey: 'backups.chat.maxTotalBackups',
38+ migrate: (value) => value,
39+ },
40+ {
41+ oldKey: 'chatBackupThrottleInterval',
42+ newKey: 'backups.chat.throttleInterval',
43+ migrate: (value) => value,
44+ },
45+ {
46+ oldKey: 'enableExtensions',
47+ newKey: 'extensions.enabled',
48+ migrate: (value) => value,
49+ },
50+ {
51+ oldKey: 'enableExtensionsAutoUpdate',
52+ newKey: 'extensions.autoUpdate',
53+ migrate: (value) => value,
54+ },
55+ {
56+ oldKey: 'extras.disableAutoDownload',
57+ newKey: 'extensions.models.autoDownload',
58+ migrate: (value) => !value,
59+ },
60+ {
61+ oldKey: 'extras.classificationModel',
62+ newKey: 'extensions.models.classification',
63+ migrate: (value) => value,
64+ },
65+ {
66+ oldKey: 'extras.captioningModel',
67+ newKey: 'extensions.models.captioning',
68+ migrate: (value) => value,
69+ },
70+ {
71+ oldKey: 'extras.embeddingModel',
72+ newKey: 'extensions.models.embedding',
73+ migrate: (value) => value,
74+ },
75+ {
76+ oldKey: 'extras.speechToTextModel',
77+ newKey: 'extensions.models.speechToText',
78+ migrate: (value) => value,
79+ },
80+ {
81+ oldKey: 'extras.textToSpeechModel',
82+ newKey: 'extensions.models.textToSpeech',
83+ migrate: (value) => value,
84+ },
85+ {
86+ oldKey: 'minLogLevel',
87+ newKey: 'logging.minLogLevel',
88+ migrate: (value) => value,
89+ },
90+ {
91+ oldKey: 'cardsCacheCapacity',
92+ newKey: 'performance.memoryCacheCapacity',
93+ migrate: (value) => `${value}mb`,
94+ },
95+ {
96+ oldKey: 'cookieSecret',
97+ newKey: 'cookieSecret',
98+ migrate: () => void 0,
99+ remove: true,
100+ },
101+];
102+
103+/**
104+ * Gets all keys from an object recursively.
105+ * @param {object} obj Object to get all keys from
106+ * @param {string} prefix Prefix to prepend to all keys
107+ * @returns {string[]} Array of all keys in the object
108+ */
109+function getAllKeys(obj, prefix = '') {
110+ if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
111+ return [];
112+ }
113+
114+ return _.flatMap(Object.keys(obj), key => {
115+ const newPrefix = prefix ? `${prefix}.${key}` : key;
116+ if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
117+ return getAllKeys(obj[key], newPrefix);
118+ } else {
119+ return [newPrefix];
120+ }
121+ });
122+}
123+
124+/**
125+ * Compares the current config.yaml with the default config.yaml and adds any missing values.
126+ * @param {string} configPath Path to config.yaml
127+ */
128+export function addMissingConfigValues(configPath) {
129+ try {
130+ const defaultConfig = yaml.parse(fs.readFileSync(path.join(serverDirectory, './default/config.yaml'), 'utf8'));
131+ let config = yaml.parse(fs.readFileSync(configPath, 'utf8'));
132+
133+ // Migrate old keys to new keys
134+ const migratedKeys = [];
135+ for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) {
136+ if (_.has(config, oldKey)) {
137+ if (remove) {
138+ _.unset(config, oldKey);
139+ migratedKeys.push({
140+ oldKey,
141+ newValue: void 0,
142+ });
143+ continue;
144+ }
145+
146+ const oldValue = _.get(config, oldKey);
147+ const newValue = migrate(oldValue);
148+ _.set(config, newKey, newValue);
149+ _.unset(config, oldKey);
150+
151+ migratedKeys.push({
152+ oldKey,
153+ newKey,
154+ oldValue,
155+ newValue,
156+ });
157+ }
158+ }
159+
160+ // Get all keys from the original config
161+ const originalKeys = getAllKeys(config);
162+
163+ // Use lodash's defaultsDeep function to recursively apply default properties
164+ config = _.defaultsDeep(config, defaultConfig);
165+
166+ // Get all keys from the updated config
167+ const updatedKeys = getAllKeys(config);
168+
169+ // Find the keys that were added
170+ const addedKeys = _.difference(updatedKeys, originalKeys);
171+
172+ if (addedKeys.length === 0 && migratedKeys.length === 0) {
173+ return;
174+ }
175+
176+ if (addedKeys.length > 0) {
177+ console.log('Adding missing config values to config.yaml:', addedKeys);
178+ }
179+
180+ if (migratedKeys.length > 0) {
181+ console.log('Migrating config values in config.yaml:', migratedKeys);
182+ }
183+
184+ fs.writeFileSync(configPath, yaml.stringify(config));
185+ } catch (error) {
186+ console.error(color.red('FATAL: Could not add missing config values to config.yaml'), error);
187+ }
188+}
189+
190+/**
191+ * Performs early initialization tasks before the server starts.
192+ * @param {string} configPath Path to config.yaml
193+ */
194+export function initConfig(configPath) {
195+ setConfigFilePath(configPath);
196+ addMissingConfigValues(configPath);
197+}
src/constants.js+3 -19
@@ -156,7 +156,7 @@ export const GEMINI_SAFETY = [
156156 },
157157 {
158158 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
159159 threshold: 'BLOCK_NONEOFF',
160160 },
161161];
162162
@@ -176,6 +176,7 @@ export const CHAT_COMPLETION_SOURCES = {
176176 ZEROONEAI: '01ai',
177177 NANOGPT: 'nanogpt',
178178 DEEPSEEK: 'deepseek',
179+ XAI: 'xai',
179180};
180181
181182/**
@@ -267,23 +268,6 @@ export const FEATHERLESS_KEYS = [
267268 'guided_whitespace_pattern',
268269];
269270
270-// https://dreamgen.com/docs/api#openai-text
271-export const DREAMGEN_KEYS = [
272- 'model',
273- 'prompt',
274- 'max_tokens',
275- 'temperature',
276- 'top_p',
277- 'top_k',
278- 'min_p',
279- 'repetition_penalty',
280- 'frequency_penalty',
281- 'presence_penalty',
282- 'stop',
283- 'stream',
284- 'minimum_message_content_tokens',
285-];
286-
287271// https://docs.together.ai/reference/completions
288272export const TOGETHERAI_KEYS = [
289273 'model',
@@ -300,7 +284,7 @@ export const TOGETHERAI_KEYS = [
300284 'stop',
301285];
302286
303287// https://github.com/jmorgancaollama/ollama/blob/main/docs/api.md#request-with-options
304288export const OLLAMA_KEYS = [
305289 'num_predict',
306290 'num_ctx',
src/endpoints/assets.js+2 -2
@@ -235,14 +235,14 @@ router.post('/download', async (request, response) => {
235235 const contentType = mime.lookup(temp_path) || 'application/octet-stream';
236236 response.setHeader('Content-Type', contentType);
237237 response.send(fileContent);
238238 fs.rmSyncunlinkSync(temp_path);
239239 return;
240240 }
241241
242242 // Move into asset place
243243 console.info('Download finished, moving file from', temp_path, 'to', file_path);
244244 fs.copyFileSync(temp_path, file_path);
245245 fs.rmSyncunlinkSync(temp_path);
246246 response.sendStatus(200);
247247 }
248248 catch (error) {
src/endpoints/avatars.js+2 -2
@@ -28,7 +28,7 @@ router.post('/delete', getFileNameValidationFunction('avatar'), function (reques
2828 const fileName = path.join(request.user.directories.avatars, sanitize(request.body.avatar));
2929
3030 if (fs.existsSync(fileName)) {
3131 fs.rmSyncunlinkSync(fileName);
3232 return response.send({ result: 'ok' });
3333 }
3434
@@ -53,7 +53,7 @@ router.post('/upload', async (request, response) => {
5353 const filename = request.body.overwrite_name || `${Date.now()}.png`;
5454 const pathToNewFile = path.join(request.user.directories.avatars, filename);
5555 writeFileAtomicSync(pathToNewFile, image);
5656 fs.rmSyncunlinkSync(pathToUpload);
5757 return response.send({ path: filename });
5858 } catch (err) {
5959 return response.status(400).send('Is not a valid image');
src/endpoints/backends/chat-completions.js+161 -32
@@ -1,4 +1,5 @@
11import process from 'node:process';
2+import util from 'node:util';
23import express from 'express';
34import fetch from 'node-fetch';
45
@@ -23,11 +24,13 @@ import {
2324 convertCohereMessages,
2425 convertMistralMessages,
2526 convertAI21Messages,
27+ convertXAIMessages,
2628 mergeMessages,
2729 cachingAtDepthForOpenRouterClaude,
2830 cachingAtDepthForClaude,
2931 getPromptNames,
3032 calculateBudgetTokenscalculateClaudeBudgetTokens,
33+ calculateGoogleBudgetTokens,
3134} from '../../prompt-converters.js';
3235
3336import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -53,6 +56,7 @@ const API_01AI = 'https://api.lingyiwanwu.com/v1';
5356const API_AI21 = 'https://api.ai21.com/studio/v1';
5457const API_NANOGPT = 'https://nano-gpt.com/api/v1';
5558const API_DEEPSEEK = 'https://api.deepseek.com/beta';
59+const API_XAI = 'https://api.x.ai/v1';
5660
5761/**
5862 * Applies a post-processing step to the generated messages.
@@ -200,7 +204,7 @@ async function sendClaudeRequest(request, response) {
200204 // No prefill when thinking
201205 voidPrefill = true;
202206 const reasoningEffort = request.body.reasoning_effort;
203207 const budgetTokens = calculateBudgetTokenscalculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);
204208 const minThinkTokens = 1024;
205209 if (requestBody.max_tokens <= minThinkTokens) {
206210 const newValue = requestBody.max_tokens + minThinkTokens;
@@ -248,7 +252,7 @@ async function sendClaudeRequest(request, response) {
248252 if (!generateResponse.ok) {
249253 const generateResponseText = await generateResponse.text();
250254 console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
251255 return response.status(generateResponse.status500).send({ error: true });
252256 }
253257
254258 /** @type {any} */
@@ -338,8 +342,9 @@ async function sendMakerSuiteRequest(request, response) {
338342 const stream = Boolean(request.body.stream);
339343 const enableWebSearch = Boolean(request.body.enable_web_search);
340344 const requestImages = Boolean(request.body.request_images);
341345 const isThinkingreasoningEffort = model.includesString('thinking'request.body.reasoning_effort);
342346 const isGemma = model.includes('gemma');
347+ const isLearnLM = model.includes('learnlm');
343348
344349 const generationConfig = {
345350 stopSequences: request.body.stop,
@@ -353,47 +358,60 @@ async function sendMakerSuiteRequest(request, response) {
353358 };
354359
355360 function getGeminiBody() {
361+ // #region UGLY MODEL LISTS AREA
362+ const imageGenerationModels = [
363+ 'gemini-2.0-flash-exp',
364+ 'gemini-2.0-flash-exp-image-generation',
365+ ];
366+
367+ // These models do not support setting the threshold to OFF at all.
368+ const blockNoneModels = [
369+ 'gemini-1.5-pro-001',
370+ 'gemini-1.5-flash-001',
371+ 'gemini-1.5-flash-8b-exp-0827',
372+ 'gemini-1.5-flash-8b-exp-0924',
373+ ];
374+
375+ const thinkingBudgetModels = [
376+ 'gemini-2.5-flash-preview-04-17',
377+ ];
378+
379+ const noSearchModels = [
380+ 'gemini-2.0-flash-lite',
381+ 'gemini-2.0-flash-lite-001',
382+ 'gemini-2.0-flash-lite-preview-02-05',
383+ 'gemini-1.5-flash-8b-exp-0924',
384+ 'gemini-1.5-flash-8b-exp-0827',
385+ ];
386+ // #endregion
387+
356388 if (!Array.isArray(generationConfig.stopSequences) || !generationConfig.stopSequences.length) {
357389 delete generationConfig.stopSequences;
358390 }
359391
360392 const useMultiModalenableImageModality = requestImages && ['gemini-2.0-flash-exp', 'gemini-2.0-flash-exp-image-generation']imageGenerationModels.includes(model);
361393 if (useMultiModalenableImageModality) {
362394 generationConfig.responseModalities = ['text', 'image'];
363395 }
364396
365397 const useSystemPrompt = !useMultiModalenableImageModality && (!isGemma && request.body.use_makersuite_sysprompt;
366- model.includes('gemini-2.5-pro') ||
367- model.includes('gemini-2.0-pro') ||
368- model.includes('gemini-2.0-flash') ||
369- model.includes('gemini-2.0-flash-thinking-exp') ||
370- model.includes('gemini-1.5-flash') ||
371- model.includes('gemini-1.5-pro') ||
372- model.startsWith('gemini-exp')
373- ) && request.body.use_makersuite_sysprompt;
374398
375399 const tools = [];
376400 const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request));
377401 let safetySettings = GEMINI_SAFETY;
378402
379- // These models do not support setting the threshold to OFF at all.
403+ if (blockNoneModels.includes(model)) {
380- 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', 'gemma-3-27b-it'].includes(model)) {
381404 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));
382405 }
383- // Interestingly, Gemini 2.0 Flash does support setting the threshold for HARM_CATEGORY_CIVIC_INTEGRITY to OFF.
384- else if (['gemini-2.0-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash-exp', 'gemini-2.0-flash-exp-image-generation'].includes(model)) {
385- safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'OFF' }));
386- }
387- // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
388406
389407 if (enableWebSearch && !useMultiModalenableImageModality && !isGemma && !isLearnLM && !noSearchModels.includes(model)) {
390408 const searchTool = model.includes('1.5') || model.includes('1.0')
391409 ? ({ google_search_retrieval: {} })
392410 : ({ google_search: {} });
393411 tools.push(searchTool);
394412 }
395413
396414 if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !useMultiModalenableImageModality && !isGemma) {
397415 const functionDeclarations = [];
398416 for (const tool of request.body.tools) {
399417 if (tool.type === 'function') {
@@ -409,13 +427,21 @@ async function sendMakerSuiteRequest(request, response) {
409427 tools.push({ function_declarations: functionDeclarations });
410428 }
411429
430+ if (thinkingBudgetModels.includes(model)) {
431+ const thinkingBudget = calculateGoogleBudgetTokens(generationConfig.maxOutputTokens, reasoningEffort);
432+
433+ if (Number.isInteger(thinkingBudget)) {
434+ generationConfig.thinkingConfig = { thinkingBudget: thinkingBudget };
435+ }
436+ }
437+
412438 let body = {
413439 contents: prompt.contents,
414440 safetySettings: safetySettings,
415441 generationConfig: generationConfig,
416442 };
417443
418- if (useSystemPrompt) {
444+ if (useSystemPrompt && Array.isArray(prompt.system_instruction.parts) && prompt.system_instruction.parts.length) {
419445 body.systemInstruction = prompt.system_instruction;
420446 }
421447
@@ -436,7 +462,7 @@ async function sendMakerSuiteRequest(request, response) {
436462 controller.abort();
437463 });
438464
439465 const apiVersion = isThinking ? getConfigValue('v1alphagemini.apiVersion' :, 'v1beta');
440466 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
441467
442468 const generateResponse = await fetch(`${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
@@ -447,7 +473,7 @@ async function sendMakerSuiteRequest(request, response) {
447473 },
448474 signal: controller.signal,
449475 });
450- // have to do this because of their busted ass streaming endpoint
476+
451477 if (stream) {
452478 try {
453479 // Pipe remote SSE stream to Express response
@@ -461,7 +487,7 @@ async function sendMakerSuiteRequest(request, response) {
461487 } else {
462488 if (!generateResponse.ok) {
463489 console.warn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
464490 return response.status(generateResponse.status500).send({ error: true });
465491 }
466492
467493 /** @type {any} */
@@ -480,7 +506,7 @@ async function sendMakerSuiteRequest(request, response) {
480506 const responseContent = candidates[0].content ?? candidates[0].output;
481507 const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);
482508 const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData);
483509 console.warndebug('Google AI Studio response:', responseContentutil.inspect(generateResponseJson, { depth: 5, colors: true }));
484510
485511 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
486512 if (!responseText && !functionCall && !inlineData) {
@@ -828,6 +854,100 @@ async function sendDeepSeekRequest(request, response) {
828854 }
829855}
830856
857+/**
858+ * Sends a request to XAI API.
859+ * @param {express.Request} request Express request
860+ * @param {express.Response} response Express response
861+ */
862+async function sendXaiRequest(request, response) {
863+ const apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();
864+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
865+
866+ if (!apiKey && !request.body.reverse_proxy) {
867+ console.warn('xAI API key is missing.');
868+ return response.status(400).send({ error: true });
869+ }
870+
871+ const controller = new AbortController();
872+ request.socket.removeAllListeners('close');
873+ request.socket.on('close', function () {
874+ controller.abort();
875+ });
876+
877+ try {
878+ let bodyParams = {};
879+
880+ if (request.body.logprobs > 0) {
881+ bodyParams['top_logprobs'] = request.body.logprobs;
882+ bodyParams['logprobs'] = true;
883+ }
884+
885+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
886+ bodyParams['tools'] = request.body.tools;
887+ bodyParams['tool_choice'] = request.body.tool_choice;
888+ }
889+
890+ if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {
891+ bodyParams['stop'] = request.body.stop;
892+ }
893+
894+ if (request.body.reasoning_effort && ['grok-3-mini-beta', 'grok-3-mini-fast-beta'].includes(request.body.model)) {
895+ bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low';
896+ }
897+
898+ const processedMessages = request.body.messages = convertXAIMessages(request.body.messages, getPromptNames(request));
899+
900+ const requestBody = {
901+ 'messages': processedMessages,
902+ 'model': request.body.model,
903+ 'temperature': request.body.temperature,
904+ 'max_tokens': request.body.max_tokens,
905+ 'max_completion_tokens': request.body.max_completion_tokens,
906+ 'stream': request.body.stream,
907+ 'presence_penalty': request.body.presence_penalty,
908+ 'frequency_penalty': request.body.frequency_penalty,
909+ 'top_p': request.body.top_p,
910+ 'seed': request.body.seed,
911+ 'n': request.body.n,
912+ ...bodyParams,
913+ };
914+
915+ const config = {
916+ method: 'POST',
917+ headers: {
918+ 'Content-Type': 'application/json',
919+ 'Authorization': 'Bearer ' + apiKey,
920+ },
921+ body: JSON.stringify(requestBody),
922+ signal: controller.signal,
923+ };
924+
925+ console.debug('xAI request:', requestBody);
926+
927+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
928+
929+ if (request.body.stream) {
930+ forwardFetchResponse(generateResponse, response);
931+ } else {
932+ if (!generateResponse.ok) {
933+ const errorText = await generateResponse.text();
934+ console.warn(`xAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
935+ const errorJson = tryParse(errorText) ?? { error: true };
936+ return response.status(500).send(errorJson);
937+ }
938+ const generateResponseJson = await generateResponse.json();
939+ console.debug('xAI response:', generateResponseJson);
940+ return response.send(generateResponseJson);
941+ }
942+ } catch (error) {
943+ console.error('Error communicating with xAI API: ', error);
944+ if (!response.headersSent) {
945+ response.send({ error: true });
946+ } else {
947+ response.end();
948+ }
949+ }
950+}
831951
832952export const router = express.Router();
833953
@@ -872,6 +992,10 @@ router.post('/status', async function (request, response_getstatus_openai) {
872992 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
873993 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
874994 headers = {};
995+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {
996+ api_url = new URL(request.body.reverse_proxy || API_XAI);
997+ api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
998+ headers = {};
875999 } else {
8761000 console.warn('This chat completion source is not supported yet.');
8771001 return response_getstatus_openai.status(400).send({ error: true });
@@ -1039,6 +1163,7 @@ router.post('/generate', function (request, response) {
10391163 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
10401164 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
10411165 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
1166+ case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
10421167 }
10431168
10441169 let apiUrl;
@@ -1108,6 +1233,10 @@ router.post('/generate', function (request, response) {
11081233 bodyParams['route'] = 'fallback';
11091234 }
11101235
1236+ if (request.body.reasoning_effort) {
1237+ bodyParams['reasoning'] = { effort: request.body.reasoning_effort };
1238+ }
1239+
11111240 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
11121241 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
11131242 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -1156,8 +1285,8 @@ router.post('/generate', function (request, response) {
11561285 }
11571286
11581287 // A few of OpenAIs reasoning models support reasoning effort
11591288 if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
11601289 if (['o1', 'o3-mini', 'o3-mini-2025-01-31', 'o4-mini', 'o4-mini-2025-04-16', 'o3', 'o3-2025-04-16'].includes(request.body.model)) {
11611290 bodyParams['reasoning_effort'] = request.body.reasoning_effort;
11621291 }
11631292 }
src/endpoints/backends/kobold.js+1 -1
@@ -204,7 +204,7 @@ router.post('/transcribe-audio', async function (request, response) {
204204 console.debug('Transcribing audio with KoboldCpp', server);
205205
206206 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
207207 fs.rmSyncunlinkSync(request.file.path);
208208
209209 const headers = {};
210210 setAdditionalHeadersByType(headers, TEXTGEN_TYPES.KOBOLDCPP, server, request.user.directories);
src/endpoints/backends/text-completions.js+3 -7
@@ -10,7 +10,6 @@ import {
1010 INFERMATICAI_KEYS,
1111 OPENROUTER_KEYS,
1212 VLLM_KEYS,
13- DREAMGEN_KEYS,
1413 FEATHERLESS_KEYS,
1514 OPENAI_KEYS,
1615} from '../../constants.js';
@@ -340,9 +339,6 @@ router.post('/generate', async function (request, response) {
340339 }
341340
342341 if (request.body.api_type === TEXTGEN_TYPES.DREAMGEN) {
343- request.body = _.pickBy(request.body, (_, key) => DREAMGEN_KEYS.includes(key));
344- // NOTE: DreamGen sometimes get confused by the unusual formatting in the character cards.
345- request.body.stop?.push('### User', '## User');
346342 args.body = JSON.stringify(request.body);
347343 }
348344
@@ -450,7 +446,7 @@ ollama.post('/download', async function (request, response) {
450446
451447 if (!fetchResponse.ok) {
452448 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
453449 return response.status(fetchResponse.status500).send({ error: true });
454450 }
455451
456452 console.debug('Ollama pull response:', await fetchResponse.json());
@@ -659,14 +655,14 @@ tabby.post('/download', async function (request, response) {
659655 }
660656 } else {
661657 console.error('API Permission error:', permissionResponse.status, permissionResponse.statusText);
662658 return response.status(permissionResponse.status500).send({ error: true });
663659 }
664660
665661 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
666662
667663 if (!fetchResponse.ok) {
668664 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
669665 return response.status(fetchResponse.status500).send({ error: true });
670666 }
671667
672668 return response.send({ ok: true });
src/endpoints/backgrounds.js+3 -3
@@ -30,7 +30,7 @@ router.post('/delete', getFileNameValidationFunction('bg'), function (request, r
3030 return response.sendStatus(400);
3131 }
3232
3333 fs.rmSyncunlinkSync(fileName);
3434 invalidateThumbnail(request.user.directories, 'bg', request.body.bg);
3535 return response.send('ok');
3636});
@@ -52,7 +52,7 @@ router.post('/rename', function (request, response) {
5252 }
5353
5454 fs.copyFileSync(oldFileName, newFileName);
5555 fs.rmSyncunlinkSync(oldFileName);
5656 invalidateThumbnail(request.user.directories, 'bg', request.body.old_bg);
5757 return response.send('ok');
5858});
@@ -65,7 +65,7 @@ router.post('/upload', function (request, response) {
6565
6666 try {
6767 fs.copyFileSync(img_path, path.join(request.user.directories.backgrounds, filename));
6868 fs.rmSyncunlinkSync(img_path);
6969 invalidateThumbnail(request.user.directories, 'bg', filename);
7070 response.send(filename);
7171 } catch (err) {
src/endpoints/characters.js+10 -4
@@ -702,6 +702,12 @@ function convertWorldInfoToCharacterBook(name, entries) {
702702 sticky: entry.sticky ?? null,
703703 cooldown: entry.cooldown ?? null,
704704 delay: entry.delay ?? null,
705+ match_persona_description: entry.matchPersonaDescription ?? false,
706+ match_character_description: entry.matchCharacterDescription ?? false,
707+ match_character_personality: entry.matchCharacterPersonality ?? false,
708+ match_character_depth_prompt: entry.matchCharacterDepthPrompt ?? false,
709+ match_scenario: entry.matchScenario ?? false,
710+ match_creator_notes: entry.matchCreatorNotes ?? false,
705711 },
706712 };
707713
@@ -720,7 +726,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
720726 */
721727async function importFromYaml(uploadPath, context, preservedFileName) {
722728 const fileText = fs.readFileSync(uploadPath, 'utf8');
723729 fs.rmSyncunlinkSync(uploadPath);
724730 const yamlData = yaml.parse(fileText);
725731 console.info('Importing from YAML');
726732 yamlData.name = sanitize(yamlData.name);
@@ -754,7 +760,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
754760 */
755761async function importFromCharX(uploadPath, { request }, preservedFileName) {
756762 const data = fs.readFileSync(uploadPath).buffer;
757763 fs.rmSyncunlinkSync(uploadPath);
758764 console.info('Importing from CharX');
759765 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
760766
@@ -995,7 +1001,7 @@ router.post('/rename', validateAvatarUrlMiddleware, async function (request, res
9951001 }
9961002
9971003 // Remove the old character file
9981004 fs.rmSyncunlinkSync(oldAvatarPath);
9991005
10001006 // Return new avatar name to ST
10011007 return response.send({ avatar: newAvatarName });
@@ -1150,7 +1156,7 @@ router.post('/delete', validateAvatarUrlMiddleware, async function (request, res
11501156 return response.sendStatus(400);
11511157 }
11521158
11531159 fs.rmSyncunlinkSync(avatarPath);
11541160 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
11551161 let dir_name = (request.body.avatar_url.replace('.png', ''));
11561162
src/endpoints/chats.js+3 -3
@@ -433,7 +433,7 @@ router.post('/rename', validateAvatarUrlMiddleware, async function (request, res
433433 }
434434
435435 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
436436 fs.rmSyncunlinkSync(pathToOriginalFile);
437437 console.info('Successfully renamed.');
438438 return response.send({ ok: true, sanitizedFileName });
439439});
@@ -449,7 +449,7 @@ router.post('/delete', validateAvatarUrlMiddleware, function (request, response)
449449 return response.sendStatus(400);
450450 }
451451
452452 fs.rmSyncunlinkSync(filePath);
453453 console.info(`Deleted chat file: ${filePath}`);
454454 return response.send('ok');
455455});
@@ -665,7 +665,7 @@ router.post('/group/delete', (request, response) => {
665665 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
666666
667667 if (fs.existsSync(pathToFile)) {
668668 fs.rmSyncunlinkSync(pathToFile);
669669 return response.send({ ok: true });
670670 }
671671
src/endpoints/content-manager.js+20 -6
@@ -1,6 +1,5 @@
11import fs from 'node:fs';
22import path from 'node:path';
3-import process from 'node:process';
43import { Buffer } from 'node:buffer';
54
65import express from 'express';
@@ -8,11 +7,12 @@ import fetch from 'node-fetch';
87import sanitize from 'sanitize-filename';
98import { sync as writeFileAtomicSync } from 'write-file-atomic';
109
1110import { getConfigValue, color, setPermissionsSync } from '../util.js';
1211import { write } from '../character-card-parser.js';
12+import { serverDirectory } from '../server-directory.js';
1313
1414const contentDirectory = path.join(process.cwd()serverDirectory, 'default/content');
1515const scaffoldDirectory = path.join(process.cwd()serverDirectory, 'default/scaffold');
1616const contentIndexPath = path.join(contentDirectory, 'index.json');
1717const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
1818
@@ -149,6 +149,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
149149 }
150150
151151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
152+ setPermissionsSync(targetPath);
152153 console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);
153154 anyContentAdded = true;
154155 }
@@ -540,9 +541,21 @@ async function downloadGenericPng(url) {
540541
541542 if (result.ok) {
542543 const buffer = Buffer.from(await result.arrayBuffer());
543544 constlet fileName = sanitize(result.url.split('?')[0].split('/').reverse()[0]);
544545 const contentType = result.headers.get('content-type') || 'image/png'; //yoink it from AICC function lol
545546
547+ // The `importCharacter()` function detects the MIME (content-type) of the file
548+ // using its file extension. The problem is that not all third-party APIs serve
549+ // their cards with a `.png` extension. To support more third-party sites,
550+ // dynamically append the `.png` extension to the filename if it doesn't
551+ // already have a file extension.
552+ if (contentType === 'image/png') {
553+ const ext = fileName.match(/\.(\w+)$/); // Same regex used by `importCharacter()`
554+ if (!ext) {
555+ fileName += '.png';
556+ }
557+ }
558+
546559 return {
547560 buffer: buffer,
548561 fileName: fileName,
@@ -694,10 +707,11 @@ router.post('/importURL', async (request, response) => {
694707 type = 'character';
695708 result = await downloadRisuCharacter(uuid);
696709 } else if (isGeneric) {
697710 console.info('Downloading from generic url.:', url);
698711 type = 'character';
699712 result = await downloadGenericPng(url);
700713 } else {
714+ console.error(`Received an import for "${getHostFromUrl(url)}", but site is not whitelisted. This domain must be added to the config key "whitelistImportDomains" to allow import from this source.`);
701715 return response.sendStatus(404);
702716 }
703717
src/endpoints/extensions.js+134 -20
@@ -30,17 +30,23 @@ async function getManifest(extensionPath) {
3030 * @returns {Promise<Object>} - Returns the extension information as an object
3131 */
3232async function checkIfRepoIsUpToDate(extensionPath) {
3333 const git = simpleGit({ baseDir: extensionPath });
3434 await git.cwd(extensionPath).fetch('origin');
3535 const currentBranch = await git.cwd(extensionPath).branch();
3636 const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
3737 const log = await git.cwd(extensionPath).log({
3838 from: currentCommitHash,
3939 to: `origin/${currentBranch.current}`,
4040 });
4141
4242 // Fetch remote repository information
4343 const remotes = await git.cwd(extensionPath).getRemotes(true);
44+ if (remotes.length === 0) {
45+ return {
46+ isUpToDate: true,
47+ remoteUrl: '',
48+ };
49+ }
4450
4551 return {
4652 isUpToDate: log.total === 0,
@@ -76,7 +82,7 @@ router.post('/install', async (request, response) => {
7682 fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);
7783 }
7884
7985 const { url, global, branch } = request.body;
8086
8187 if (global && !request.user.profile.admin) {
8288 console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
@@ -90,8 +96,12 @@ router.post('/install', async (request, response) => {
9096 return response.status(409).send(`Directory already exists at ${extensionPath}`);
9197 }
9298
9399 awaitconst git.clone(url,cloneOptions extensionPath,= { '--depth': 1 });
94- console.info(`Extension has been cloned at ${extensionPath}`);
100+ if (branch) {
101+ cloneOptions['--branch'] = branch;
102+ }
103+ await git.clone(url, extensionPath, cloneOptions);
104+ console.info(`Extension has been cloned to ${extensionPath} from ${url} at ${branch || '(default)'} branch`);
95105
96106 const { version, author, display_name } = await getManifest(extensionPath);
97107
@@ -114,7 +124,6 @@ router.post('/install', async (request, response) => {
114124 * @returns {void}
115125 */
116126router.post('/update', async (request, response) => {
117- const git = simpleGit();
118127 if (!request.body.extensionName) {
119128 return response.status(400).send('Bad Request: extensionName is required in the request body.');
120129 }
@@ -128,22 +137,23 @@ router.post('/update', async (request, response) => {
128137 }
129138
130139 const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
131140 const extensionPath = path.join(basePath, sanitize(extensionName));
132141
133142 if (!fs.existsSync(extensionPath)) {
134143 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
135144 }
136145
137146 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
138147 const currentBranchgit = await git.cwdsimpleGit({ baseDir: extensionPath).branch( });
148+ const currentBranch = await git.branch();
139149 if (!isUpToDate) {
140150 await git.cwd(extensionPath).pull('origin', currentBranch.current);
141151 console.info(`Extension has been updated at ${extensionPath}`);
142152 } else {
143153 console.info(`Extension is up to date at ${extensionPath}`);
144154 }
145155 await git.cwd(extensionPath).fetch('origin');
146156 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
147157 const shortCommitHash = fullCommitHash.slice(0, 7);
148158
149159 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
@@ -154,6 +164,110 @@ router.post('/update', async (request, response) => {
154164 }
155165});
156166
167+router.post('/branches', async (request, response) => {
168+ try {
169+ const { extensionName, global } = request.body;
170+
171+ if (!extensionName) {
172+ return response.status(400).send('Bad Request: extensionName is required in the request body.');
173+ }
174+
175+ if (global && !request.user.profile.admin) {
176+ console.error(`User ${request.user.profile.handle} does not have permission to list branches of global extensions.`);
177+ return response.status(403).send('Forbidden: No permission to list branches of global extensions.');
178+ }
179+
180+ const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
181+ const extensionPath = path.join(basePath, sanitize(extensionName));
182+
183+ if (!fs.existsSync(extensionPath)) {
184+ return response.status(404).send(`Directory does not exist at ${extensionPath}`);
185+ }
186+
187+ const git = simpleGit({ baseDir: extensionPath });
188+ // Unshallow the repository if it is shallow
189+ const isShallow = await git.revparse(['--is-shallow-repository']) === 'true';
190+ if (isShallow) {
191+ console.info(`Unshallowing the repository at ${extensionPath}`);
192+ await git.fetch('origin', ['--unshallow']);
193+ }
194+
195+ // Fetch all branches
196+ await git.remote(['set-branches', 'origin', '*']);
197+ await git.fetch('origin');
198+ const localBranches = await git.branchLocal();
199+ const remoteBranches = await git.branch(['-r', '--list', 'origin/*']);
200+ const result = [
201+ ...Object.values(localBranches.branches),
202+ ...Object.values(remoteBranches.branches),
203+ ].map(b => ({ current: b.current, commit: b.commit, name: b.name, label: b.label }));
204+
205+ return response.send(result);
206+ } catch (error) {
207+ console.error('Getting branches failed', error);
208+ return response.status(500).send('Internal Server Error. Check the server logs for more details.');
209+ }
210+});
211+
212+router.post('/switch', async (request, response) => {
213+ try {
214+ const { extensionName, branch, global } = request.body;
215+
216+ if (!extensionName || !branch) {
217+ return response.status(400).send('Bad Request: extensionName and branch are required in the request body.');
218+ }
219+
220+ if (global && !request.user.profile.admin) {
221+ console.error(`User ${request.user.profile.handle} does not have permission to switch branches of global extensions.`);
222+ return response.status(403).send('Forbidden: No permission to switch branches of global extensions.');
223+ }
224+
225+ const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
226+ const extensionPath = path.join(basePath, sanitize(extensionName));
227+
228+ if (!fs.existsSync(extensionPath)) {
229+ return response.status(404).send(`Directory does not exist at ${extensionPath}`);
230+ }
231+
232+ const git = simpleGit({ baseDir: extensionPath });
233+ const branches = await git.branchLocal();
234+
235+ if (String(branch).startsWith('origin/')) {
236+ const localBranch = branch.replace('origin/', '');
237+ if (branches.all.includes(localBranch)) {
238+ console.info(`Branch ${localBranch} already exists locally, checking it out`);
239+ await git.checkout(localBranch);
240+ return response.sendStatus(204);
241+ }
242+
243+ console.info(`Branch ${localBranch} does not exist locally, creating it from ${branch}`);
244+ await git.checkoutBranch(localBranch, branch);
245+ return response.sendStatus(204);
246+ }
247+
248+ if (!branches.all.includes(branch)) {
249+ console.error(`Branch ${branch} does not exist locally`);
250+ return response.status(404).send(`Branch ${branch} does not exist locally`);
251+ }
252+
253+ // Check if the branch is already checked out
254+ const currentBranch = await git.branch();
255+ if (currentBranch.current === branch) {
256+ console.info(`Branch ${branch} is already checked out`);
257+ return response.sendStatus(204);
258+ }
259+
260+ // Checkout the branch
261+ await git.checkout(branch);
262+ console.info(`Checked out branch ${branch} at ${extensionPath}`);
263+
264+ return response.sendStatus(204);
265+ } catch (error) {
266+ console.error('Switching branches failed', error);
267+ return response.status(500).send('Internal Server Error. Check the server logs for more details.');
268+ }
269+});
270+
157271router.post('/move', async (request, response) => {
158272 try {
159273 const { extensionName, source, destination } = request.body;
@@ -194,7 +308,7 @@ router.post('/move', async (request, response) => {
194308 return response.sendStatus(204);
195309 } catch (error) {
196310 console.error('Moving extension failed', error);
197311 return response.status(500).send('Internal Server Error. TryCheck againthe laterserver logs for more details.');
198312 }
199313});
200314
@@ -209,7 +323,6 @@ router.post('/move', async (request, response) => {
209323 * @returns {void}
210324 */
211325router.post('/version', async (request, response) => {
212- const git = simpleGit();
213326 if (!request.body.extensionName) {
214327 return response.status(400).send('Bad Request: extensionName is required in the request body.');
215328 }
@@ -223,19 +336,20 @@ router.post('/version', async (request, response) => {
223336 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
224337 }
225338
339+ const git = simpleGit({ baseDir: extensionPath });
226340 let currentCommitHash;
227341 try {
228342 currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
229343 } catch (error) {
230344 // it is not a git repo, or has no commits yet, or is a bare repo
231345 // not possible to update it, most likely can't get the branch name either
232346 return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' });
233347 }
234348
235349 const currentBranch = await git.cwd(extensionPath).branch();
236350 // get only the working branch
237351 const currentBranchName = currentBranch.current;
238352 await git.cwd(extensionPath).fetch('origin');
239353 console.debug(extensionName, currentBranchName, currentCommitHash);
240354 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
241355
src/endpoints/files.js+1 -1
@@ -66,7 +66,7 @@ router.post('/delete', async (request, response) => {
6666 return response.status(404).send('File not found');
6767 }
6868
6969 fs.rmSyncunlinkSync(pathToDelete);
7070 console.info(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
7171 return response.sendStatus(200);
7272 } catch (error) {
src/endpoints/google.js+1 -1
@@ -45,7 +45,7 @@ router.post('/caption-image', async (request, response) => {
4545 if (!result.ok) {
4646 const error = await result.json();
4747 console.error(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
4848 return response.status(result.status500).send({ error: true });
4949 }
5050
5151 /** @type {any} */
src/endpoints/groups.js+2 -2
@@ -117,7 +117,7 @@ router.post('/delete', async (request, response) => {
117117 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
118118
119119 if (fs.existsSync(pathToFile)) {
120120 fs.rmSyncunlinkSync(pathToFile);
121121 }
122122 }
123123 }
@@ -126,7 +126,7 @@ router.post('/delete', async (request, response) => {
126126 }
127127
128128 if (fs.existsSync(pathToGroup)) {
129129 fs.rmSyncunlinkSync(pathToGroup);
130130 }
131131
132132 return response.send({ ok: true });
src/endpoints/novelai.js+1 -1
@@ -270,7 +270,7 @@ router.post('/generate', async function (req, res) {
270270 // ignore
271271 }
272272
273273 return res.status(response.status500).send({ error: { message } });
274274 }
275275
276276 /** @type {any} */
src/endpoints/openai.js+9 -1
@@ -65,6 +65,10 @@ router.post('/caption-image', async (request, response) => {
6565 key = readSecret(request.user.directories, SECRET_KEYS.COHERE);
6666 }
6767
68+ if (request.body.api === 'xai') {
69+ key = readSecret(request.user.directories, SECRET_KEYS.XAI);
70+ }
71+
6872 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
6973 console.warn('No key found for API', request.body.api);
7074 return response.sendStatus(400);
@@ -134,6 +138,10 @@ router.post('/caption-image', async (request, response) => {
134138 apiUrl = 'https://api.cohere.ai/v2/chat';
135139 }
136140
141+ if (request.body.api === 'xai') {
142+ apiUrl = 'https://api.x.ai/v1/chat/completions';
143+ }
144+
137145 if (request.body.api === 'ooba') {
138146 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
139147 const imgMessage = body.messages.pop();
@@ -226,7 +234,7 @@ router.post('/transcribe-audio', async (request, response) => {
226234 return response.status(500).send(text);
227235 }
228236
229237 fs.rmSyncunlinkSync(request.file.path);
230238 const data = await result.json();
231239 console.debug('OpenAI transcription response', data);
232240 return response.json(data);
src/endpoints/presets.js+1 -1
@@ -124,7 +124,7 @@ router.post('/delete-openai', function (request, response) {
124124 const pathToFile = path.join(request.user.directories.openAI_Settings, `${name}.json`);
125125
126126 if (fs.existsSync(pathToFile)) {
127127 fs.rmSyncunlinkSync(pathToFile);
128128 return response.send({ ok: true });
129129 }
130130
src/endpoints/secrets.js+1 -0
@@ -52,6 +52,7 @@ export const SECRET_KEYS = {
5252 GENERIC: 'api_key_generic',
5353 DEEPSEEK: 'api_key_deepseek',
5454 SERPER: 'api_key_serper',
55+ XAI: 'api_key_xai',
5556};
5657
5758// These are the keys that are safe to expose, even if allowKeysExposure is false
src/endpoints/sprites.js+5 -5
@@ -165,7 +165,7 @@ router.post('/delete', async (request, response) => {
165165 // Remove existing sprite with the same label
166166 for (const file of files) {
167167 if (path.parse(file).name === spriteName) {
168168 fs.rmSyncunlinkSync(path.join(spritesPath, file));
169169 }
170170 }
171171
@@ -206,7 +206,7 @@ router.post('/upload-zip', async (request, response) => {
206206 const existingFile = files.find(file => path.parse(file).name === path.parse(filename).name);
207207
208208 if (existingFile) {
209209 fs.rmSyncunlinkSync(path.join(spritesPath, existingFile));
210210 }
211211
212212 // Write sprite buffer to disk
@@ -215,7 +215,7 @@ router.post('/upload-zip', async (request, response) => {
215215 }
216216
217217 // Remove uploaded ZIP file
218218 fs.rmSyncunlinkSync(spritePackPath);
219219 return response.send({ count: sprites.length });
220220 } catch (error) {
221221 console.error(error);
@@ -251,7 +251,7 @@ router.post('/upload', async (request, response) => {
251251 // Remove existing sprite with the same label
252252 for (const file of files) {
253253 if (path.parse(file).name === spriteName) {
254254 fs.rmSyncunlinkSync(path.join(spritesPath, file));
255255 }
256256 }
257257
@@ -261,7 +261,7 @@ router.post('/upload', async (request, response) => {
261261 // Copy uploaded file to sprites folder
262262 fs.cpSync(spritePath, pathToFile);
263263 // Remove uploaded file
264264 fs.rmSyncunlinkSync(spritePath);
265265 return response.sendStatus(200);
266266 } catch (error) {
267267 console.error(error);
src/endpoints/stable-diffusion.js+56 -3
@@ -627,8 +627,8 @@ together.post('/models', async (request, response) => {
627627 }
628628
629629 const models = data
630630 .filter(x => x.display_typetype === 'image')
631631 .map(x => ({ value: x.nameid, text: x.display_name }));
632632
633633 return response.send(models);
634634 } catch (error) {
@@ -1166,7 +1166,8 @@ falai.post('/models', async (_request, response) => {
11661166 const models = data
11671167 .filter(x => !x.title.toLowerCase().includes('inpainting') &&
11681168 !x.title.toLowerCase().includes('control') &&
11691169 !x.title.toLowerCase().includes('upscale')) &&
1170+ !x.title.toLowerCase().includes('lora'))
11701171 .sort((a, b) => a.title.localeCompare(b.title))
11711172 .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }));
11721173 return response.send(models);
@@ -1245,6 +1246,7 @@ falai.post('/generate', async (request, response) => {
12451246 'Authorization': `Key ${key}`,
12461247 },
12471248 });
1249+ /** @type {any} */
12481250 const resultData = await resultFetch.json();
12491251
12501252 if (resultData.detail !== null && resultData.detail !== undefined) {
@@ -1270,6 +1272,56 @@ falai.post('/generate', async (request, response) => {
12701272 }
12711273});
12721274
1275+const xai = express.Router();
1276+
1277+xai.post('/generate', async (request, response) => {
1278+ try {
1279+ const key = readSecret(request.user.directories, SECRET_KEYS.XAI);
1280+
1281+ if (!key) {
1282+ console.warn('xAI key not found.');
1283+ return response.sendStatus(400);
1284+ }
1285+
1286+ const requestBody = {
1287+ prompt: request.body.prompt,
1288+ model: request.body.model,
1289+ response_format: 'b64_json',
1290+ };
1291+
1292+ console.debug('xAI request:', requestBody);
1293+
1294+ const result = await fetch('https://api.x.ai/v1/images/generations', {
1295+ method: 'POST',
1296+ body: JSON.stringify(requestBody),
1297+ headers: {
1298+ 'Content-Type': 'application/json',
1299+ 'Authorization': `Bearer ${key}`,
1300+ },
1301+ });
1302+
1303+ if (!result.ok) {
1304+ const text = await result.text();
1305+ console.warn('xAI returned an error.', text);
1306+ return response.sendStatus(500);
1307+ }
1308+
1309+ /** @type {any} */
1310+ const data = await result.json();
1311+
1312+ const image = data?.data?.[0]?.b64_json;
1313+ if (!image) {
1314+ console.warn('xAI returned invalid data.');
1315+ return response.sendStatus(500);
1316+ }
1317+
1318+ return response.send({ image });
1319+ } catch (error) {
1320+ console.error('Error communicating with xAI', error);
1321+ return response.sendStatus(500);
1322+ }
1323+});
1324+
12731325router.use('/comfy', comfy);
12741326router.use('/together', together);
12751327router.use('/drawthings', drawthings);
@@ -1279,3 +1331,4 @@ router.use('/huggingface', huggingface);
12791331router.use('/nanogpt', nanogpt);
12801332router.use('/bfl', bfl);
12811333router.use('/falai', falai);
1334+router.use('/xai', xai);
src/endpoints/themes.js+1 -1
@@ -29,7 +29,7 @@ router.post('/delete', function (request, response) {
2929 console.error('Theme file not found:', filename);
3030 return response.sendStatus(404);
3131 }
3232 fs.rmSyncunlinkSync(filename);
3333 return response.sendStatus(200);
3434 } catch (error) {
3535 console.error(error);
src/endpoints/thumbnails.js+1 -1
@@ -75,7 +75,7 @@ export function invalidateThumbnail(directories, type, file) {
7575 const pathToThumbnail = path.join(folder, file);
7676
7777 if (fs.existsSync(pathToThumbnail)) {
7878 fs.rmSyncunlinkSync(pathToThumbnail);
7979 }
8080}
8181
src/endpoints/tokenizers.js+5 -1
@@ -407,11 +407,15 @@ export function getTokenizerModel(requestModel) {
407407 return 'o1';
408408 }
409409
410+ if (requestModel.includes('o3') || requestModel.includes('o4-mini')) {
411+ return 'o1';
412+ }
413+
410414 if (requestModel.includes('gpt-4o') || requestModel.includes('chatgpt-4o-latest')) {
411415 return 'gpt-4o';
412416 }
413417
414418 if (requestModel.includes('gpt-4.1') || requestModel.includes('gpt-4.5')) {
415419 return 'gpt-4o';
416420 }
417421
src/endpoints/worldinfo.js+1 -1
@@ -57,7 +57,7 @@ router.post('/delete', (request, response) => {
5757 throw new Error(`World info file ${filename} doesn't exist.`);
5858 }
5959
6060 fs.rmSyncunlinkSync(pathToWorldInfo);
6161
6262 return response.sendStatus(200);
6363});
src/fetch-patch.js+4 -4
@@ -2,6 +2,7 @@ import fs from 'node:fs';
22import path from 'node:path';
33import { fileURLToPath } from 'node:url';
44import mime from 'mime-types';
5+import { serverDirectory } from './server-directory.js';
56
67const originalFetch = globalThis.fetch;
78
@@ -67,10 +68,9 @@ globalThis.fetch = async (/** @type {string | URL | Request} */ request, /** @ty
6768 }
6869 const url = getRequestURL(request);
6970 const filePath = path.resolve(fileURLToPath(url));
70- const cwd = path.resolve(process.cwd()) + path.sep;
71+ const isUnderServerDirectory = isPathUnderParent(serverDirectory, filePath);
71- const isUnderCwd = isPathUnderParent(cwd, filePath);
72+ if (!isUnderServerDirectory) {
72- if (!isUnderCwd) {
73+ throw new Error('Requested file path is outside of the server directory.');
73- throw new Error('Requested file path is outside of the current working directory.');
7474 }
7575 const parsedPath = path.parse(filePath);
7676 if (!ALLOWED_EXTENSIONS.includes(parsedPath.ext)) {
src/prompt-converters.js+106 -53
@@ -3,6 +3,15 @@ import { getConfigValue, tryParse } from './util.js';
33
44const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
55
6+const REASONING_EFFORT = {
7+ auto: 'auto',
8+ low: 'low',
9+ medium: 'medium',
10+ high: 'high',
11+ min: 'min',
12+ max: 'max',
13+};
14+
615/**
716 * @typedef {object} PromptNames
817 * @property {string} charName Character name
@@ -342,59 +351,20 @@ export function convertCohereMessages(messages, names) {
342351 }
343352 });
344353
345- // A prompt should end with a user/tool message
346- if (messages.length && !['user', 'tool'].includes(messages[messages.length - 1].role)) {
347- messages[messages.length - 1].role = 'user';
348- }
349-
350354 return { chatHistory: messages };
351355}
352356
353357/**
354358 * Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.
355359 * @param {object[]} messages Array of messages
356360 * @param {string} model_model Model name
357361 * @param {boolean} useSysPrompt Use system prompt
358362 * @param {PromptNames} names Prompt names
359363 * @returns {{contents: *[], system_instruction: {parts: {text: string}[]}}} Prompt for Google MakerSuite models
360364 */
361365export function convertGooglePrompt(messages, model_model, useSysPrompt, names) {
362366 const visionSupportedModelssysPrompt = [];
363- 'gemini-2.5-pro-preview-03-25',
367+
364- 'gemini-2.5-pro-exp-03-25',
365- 'gemini-2.0-pro-exp',
366- 'gemini-2.0-pro-exp-02-05',
367- 'gemini-2.0-flash-lite-preview',
368- 'gemini-2.0-flash-lite-preview-02-05',
369- 'gemini-2.0-flash',
370- 'gemini-2.0-flash-001',
371- 'gemini-2.0-flash-thinking-exp',
372- 'gemini-2.0-flash-thinking-exp-01-21',
373- 'gemini-2.0-flash-thinking-exp-1219',
374- 'gemini-2.0-flash-exp',
375- 'gemini-2.0-flash-exp-image-generation',
376- 'gemini-1.5-flash',
377- 'gemini-1.5-flash-latest',
378- 'gemini-1.5-flash-001',
379- 'gemini-1.5-flash-002',
380- 'gemini-1.5-flash-exp-0827',
381- 'gemini-1.5-flash-8b',
382- 'gemini-1.5-flash-8b-exp-0827',
383- 'gemini-1.5-flash-8b-exp-0924',
384- 'gemini-exp-1114',
385- 'gemini-exp-1121',
386- 'gemini-exp-1206',
387- 'gemini-1.5-pro',
388- 'gemini-1.5-pro-latest',
389- 'gemini-1.5-pro-001',
390- 'gemini-1.5-pro-002',
391- 'gemini-1.5-pro-exp-0801',
392- 'gemini-1.5-pro-exp-0827',
393- ];
394-
395- const isMultimodal = visionSupportedModels.includes(model);
396-
397- let sys_prompt = '';
398368 if (useSysPrompt) {
399369 while (messages.length > 1 && messages[0].role === 'system') {
400370 // Append example names if not already done by the frontend (e.g. for group chats).
@@ -408,12 +378,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
408378 messages[0].content = `${names.charName}: ${messages[0].content}`;
409379 }
410380 }
411- sys_prompt += `${messages[0].content}\n\n`;
381+ sysPrompt.push(messages[0].content);
412382 messages.shift();
413383 }
414384 }
415385
416386 const system_instruction = { parts: [sysPrompt.map(text => ({ text: sys_prompt.trim(})) }]};
417387 const toolNameMap = {};
418388
419389 const contents = [];
@@ -492,7 +462,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
492462
493463 toolNameMap[toolCall.id] = toolCall.function.name;
494464 });
495465 } else if (part.type === 'image_url' && isMultimodal) {
496466 const mimeType = part.image_url.url.split(';')[0].split(':')[1];
497467 const base64Data = part.image_url.url.split(',')[1];
498468 parts.push({
@@ -508,7 +478,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
508478 if (index > 0 && message.role === contents[contents.length - 1].role) {
509479 parts.forEach((part) => {
510480 if (part.text) {
511481 const textPart = contents[contents.length - 1].parts[0].textfind(p += '\n\n'> +typeof partp.text === 'string');
482+ if (textPart) {
483+ textPart.text += '\n\n' + part.text;
484+ } else {
485+ contents[contents.length - 1].parts.push(part);
486+ }
512487 }
513488 if (part.inlineData || part.functionCall || part.functionResponse) {
514489 contents[contents.length - 1].parts.push(part);
@@ -680,6 +655,43 @@ export function convertMistralMessages(messages, names) {
680655}
681656
682657/**
658+ * Convert a prompt from the messages objects to the format used by xAI.
659+ * @param {object[]} messages Array of messages
660+ * @param {PromptNames} names Prompt names
661+ * @returns {object[]} Prompt for xAI
662+ */
663+export function convertXAIMessages(messages, names) {
664+ if (!Array.isArray(messages)) {
665+ return [];
666+ }
667+
668+ messages.forEach(msg => {
669+ if (!msg.name || msg.role === 'user') {
670+ return;
671+ }
672+
673+ const needsCharNamePrefix = [
674+ { role: 'assistant', condition: names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(msg.content) },
675+ { role: 'system', name: 'example_assistant', condition: names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(msg.content) },
676+ { role: 'system', name: 'example_user', condition: names.userName && !msg.content.startsWith(`${names.userName}: `) },
677+ ];
678+
679+ const matchingRule = needsCharNamePrefix.find(rule =>
680+ msg.role === rule.role && (!rule.name || msg.name === rule.name) && rule.condition,
681+ );
682+
683+ if (matchingRule) {
684+ const prefix = msg.role === 'system' && msg.name === 'example_user' ? names.userName : names.charName;
685+ msg.content = `${prefix}: ${msg.content}`;
686+ }
687+
688+ delete msg.name;
689+ });
690+
691+ return messages;
692+}
693+
694+/**
683695 * Merge messages with the same consecutive role, removing names if they exist.
684696 * @param {any[]} messages Messages to merge
685697 * @param {PromptNames} names Prompt names
@@ -901,25 +913,32 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth) {
901913}
902914
903915/**
904916 * Calculate the Claude budget tokens for a given reasoning effort.
905917 * @param {number} maxTokens Maximum tokens
906918 * @param {string} reasoningEffort Reasoning effort
907919 * @param {boolean} stream If streaming is enabled
908920 * @returns {number} Budget tokens
909921 */
910922export function calculateBudgetTokenscalculateClaudeBudgetTokens(maxTokens, reasoningEffort, stream) {
911923 let budgetTokens = 0;
912924
913925 switch (reasoningEffort) {
914926 case 'low'REASONING_EFFORT.min:
927+ budgetTokens = 1024;
928+ break;
929+ case REASONING_EFFORT.low:
915930 budgetTokens = Math.floor(maxTokens * 0.1);
916931 break;
917932 case 'medium'REASONING_EFFORT.auto:
933+ case REASONING_EFFORT.medium:
918934 budgetTokens = Math.floor(maxTokens * 0.25);
919935 break;
920936 case 'REASONING_EFFORT.high':
921937 budgetTokens = Math.floor(maxTokens * 0.5);
922938 break;
939+ case REASONING_EFFORT.max:
940+ budgetTokens = Math.floor(maxTokens * 0.95);
941+ break;
923942 }
924943
925944 budgetTokens = Math.max(budgetTokens, 1024);
@@ -930,3 +949,37 @@ export function calculateBudgetTokens(maxTokens, reasoningEffort, stream) {
930949
931950 return budgetTokens;
932951}
952+
953+/**
954+ * Calculate the Google budget tokens for a given reasoning effort.
955+ * @param {number} maxTokens Maximum tokens
956+ * @param {string} reasoningEffort Reasoning effort
957+ * @returns {number?} Budget tokens
958+ */
959+export function calculateGoogleBudgetTokens(maxTokens, reasoningEffort) {
960+ let budgetTokens = 0;
961+
962+ switch (reasoningEffort) {
963+ case REASONING_EFFORT.auto:
964+ return null;
965+ case REASONING_EFFORT.min:
966+ budgetTokens = 0;
967+ break;
968+ case REASONING_EFFORT.low:
969+ budgetTokens = Math.floor(maxTokens * 0.1);
970+ break;
971+ case REASONING_EFFORT.medium:
972+ budgetTokens = Math.floor(maxTokens * 0.25);
973+ break;
974+ case REASONING_EFFORT.high:
975+ budgetTokens = Math.floor(maxTokens * 0.5);
976+ break;
977+ case REASONING_EFFORT.max:
978+ budgetTokens = maxTokens;
979+ break;
980+ }
981+
982+ budgetTokens = Math.min(budgetTokens, 24576);
983+
984+ return budgetTokens;
985+}
src/server-directory.js+3 -0
@@ -0,0 +1,3 @@
1+import path from 'node:path';
2+import { fileURLToPath } from 'node:url';
3+export const serverDirectory = path.dirname(import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)));
src/server-main.js+386 -0
@@ -0,0 +1,386 @@
1+// native node modules
2+import path from 'node:path';
3+import util from 'node:util';
4+import net from 'node:net';
5+import dns from 'node:dns';
6+import process from 'node:process';
7+
8+import cors from 'cors';
9+import { csrfSync } from 'csrf-sync';
10+import express from 'express';
11+import compression from 'compression';
12+import cookieSession from 'cookie-session';
13+import multer from 'multer';
14+import responseTime from 'response-time';
15+import helmet from 'helmet';
16+import bodyParser from 'body-parser';
17+import open from 'open';
18+
19+// local library imports
20+import './fetch-patch.js';
21+import { serverDirectory } from './server-directory.js';
22+
23+console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
24+
25+// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
26+// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
27+// Safe to remove once support for Node v20 is dropped.
28+if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) {
29+ // @ts-ignore
30+ if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
31+}
32+
33+import { serverEvents, EVENT_NAMES } from './server-events.js';
34+import { loadPlugins } from './plugin-loader.js';
35+import {
36+ initUserStorage,
37+ getCookieSecret,
38+ getCookieSessionName,
39+ ensurePublicDirectoriesExist,
40+ getUserDirectoriesList,
41+ migrateSystemPrompts,
42+ migrateUserData,
43+ requireLoginMiddleware,
44+ setUserDataMiddleware,
45+ shouldRedirectToLogin,
46+ cleanUploads,
47+ getSessionCookieAge,
48+ verifySecuritySettings,
49+ loginPageMiddleware,
50+} from './users.js';
51+
52+import getWebpackServeMiddleware from './middleware/webpack-serve.js';
53+import basicAuthMiddleware from './middleware/basicAuth.js';
54+import getWhitelistMiddleware from './middleware/whitelist.js';
55+import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './middleware/accessLogWriter.js';
56+import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
57+import initRequestProxy from './request-proxy.js';
58+import getCacheBusterMiddleware from './middleware/cacheBuster.js';
59+import corsProxyMiddleware from './middleware/corsProxy.js';
60+import {
61+ getVersion,
62+ color,
63+ removeColorFormatting,
64+ getSeparator,
65+ safeReadFileSync,
66+ setupLogLevel,
67+ setWindowTitle,
68+} from './util.js';
69+import { UPLOADS_DIRECTORY } from './constants.js';
70+import { ensureThumbnailCache } from './endpoints/thumbnails.js';
71+
72+// Routers
73+import { router as usersPublicRouter } from './endpoints/users-public.js';
74+import { init as statsInit, onExit as statsOnExit } from './endpoints/stats.js';
75+import { checkForNewContent } from './endpoints/content-manager.js';
76+import { init as settingsInit } from './endpoints/settings.js';
77+import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './server-startup.js';
78+import { diskCache } from './endpoints/characters.js';
79+
80+// Unrestrict console logs display limit
81+util.inspect.defaultOptions.maxArrayLength = null;
82+util.inspect.defaultOptions.maxStringLength = null;
83+util.inspect.defaultOptions.depth = 4;
84+
85+const cliArgs = globalThis.COMMAND_LINE_ARGS;
86+
87+if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
88+ console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
89+ process.exit(1);
90+}
91+
92+try {
93+ if (cliArgs.dnsPreferIPv6) {
94+ dns.setDefaultResultOrder('ipv6first');
95+ console.log('Preferring IPv6 for DNS resolution');
96+ } else {
97+ dns.setDefaultResultOrder('ipv4first');
98+ console.log('Preferring IPv4 for DNS resolution');
99+ }
100+} catch (error) {
101+ console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.');
102+}
103+
104+const app = express();
105+app.use(helmet({
106+ contentSecurityPolicy: false,
107+}));
108+app.use(compression());
109+app.use(responseTime());
110+
111+app.use(bodyParser.json({ limit: '200mb' }));
112+app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
113+
114+// CORS Settings //
115+const CORS = cors({
116+ origin: 'null',
117+ methods: ['OPTIONS'],
118+});
119+
120+app.use(CORS);
121+
122+if (cliArgs.listen && cliArgs.basicAuthMode) {
123+ app.use(basicAuthMiddleware);
124+}
125+
126+if (cliArgs.whitelistMode) {
127+ const whitelistMiddleware = await getWhitelistMiddleware();
128+ app.use(whitelistMiddleware);
129+}
130+
131+if (cliArgs.listen) {
132+ app.use(accessLoggerMiddleware());
133+}
134+
135+if (cliArgs.enableCorsProxy) {
136+ app.use('/proxy/:url(*)', corsProxyMiddleware);
137+} else {
138+ app.use('/proxy/:url(*)', async (_, res) => {
139+ const message = 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';
140+ console.log(message);
141+ res.status(404).send(message);
142+ });
143+}
144+
145+app.use(cookieSession({
146+ name: getCookieSessionName(),
147+ sameSite: 'lax',
148+ httpOnly: true,
149+ maxAge: getSessionCookieAge(),
150+ secret: getCookieSecret(globalThis.DATA_ROOT),
151+}));
152+
153+app.use(setUserDataMiddleware);
154+
155+// CSRF Protection //
156+if (!cliArgs.disableCsrf) {
157+ const csrfSyncProtection = csrfSync({
158+ getTokenFromState: (req) => {
159+ if (!req.session) {
160+ console.error('(CSRF error) getTokenFromState: Session object not initialized');
161+ return;
162+ }
163+ return req.session.csrfToken;
164+ },
165+ getTokenFromRequest: (req) => {
166+ return req.headers['x-csrf-token']?.toString();
167+ },
168+ storeTokenInState: (req, token) => {
169+ if (!req.session) {
170+ console.error('(CSRF error) storeTokenInState: Session object not initialized');
171+ return;
172+ }
173+ req.session.csrfToken = token;
174+ },
175+ size: 32,
176+ });
177+
178+ app.get('/csrf-token', (req, res) => {
179+ res.json({
180+ 'token': csrfSyncProtection.generateToken(req),
181+ });
182+ });
183+
184+ // Customize the error message
185+ csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
186+ csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
187+
188+ app.use(csrfSyncProtection.csrfSynchronisedProtection);
189+} else {
190+ console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
191+ app.get('/csrf-token', (req, res) => {
192+ res.json({
193+ 'token': 'disabled',
194+ });
195+ });
196+}
197+
198+// Static files
199+// Host index page
200+app.get('/', getCacheBusterMiddleware(), (request, response) => {
201+ if (shouldRedirectToLogin(request)) {
202+ const query = request.url.split('?')[1];
203+ const redirectUrl = query ? `/login?${query}` : '/login';
204+ return response.redirect(redirectUrl);
205+ }
206+
207+ return response.sendFile('index.html', { root: path.join(serverDirectory, 'public') });
208+});
209+
210+// Callback endpoint for OAuth PKCE flows (e.g. OpenRouter)
211+app.get('/callback/:source?', (request, response) => {
212+ const source = request.params.source;
213+ const query = request.url.split('?')[1];
214+ const searchParams = new URLSearchParams();
215+ source && searchParams.set('source', source);
216+ query && searchParams.set('query', query);
217+ const path = `/?${searchParams.toString()}`;
218+ return response.redirect(307, path);
219+});
220+
221+// Host login page
222+app.get('/login', loginPageMiddleware);
223+
224+// Host frontend assets
225+const webpackMiddleware = getWebpackServeMiddleware();
226+app.use(webpackMiddleware);
227+app.use(express.static(path.join(serverDirectory, 'public'), {}));
228+
229+// Public API
230+app.use('/api/users', usersPublicRouter);
231+
232+// Everything below this line requires authentication
233+app.use(requireLoginMiddleware);
234+app.get('/api/ping', (request, response) => {
235+ if (request.query.extend && request.session) {
236+ request.session.touch = Date.now();
237+ }
238+
239+ response.sendStatus(204);
240+});
241+
242+// File uploads
243+const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
244+app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
245+app.use(multerMonkeyPatch);
246+
247+app.get('/version', async function (_, response) {
248+ const data = await getVersion();
249+ response.send(data);
250+});
251+
252+redirectDeprecatedEndpoints(app);
253+setupPrivateEndpoints(app);
254+
255+/**
256+ * Tasks that need to be run before the server starts listening.
257+ * @returns {Promise<void>}
258+ */
259+async function preSetupTasks() {
260+ const version = await getVersion();
261+
262+ // Print formatted header
263+ console.log();
264+ console.log(`SillyTavern ${version.pkgVersion}`);
265+ if (version.gitBranch) {
266+ console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`);
267+ if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) {
268+ console.log('INFO: Currently not on the latest commit.');
269+ console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.');
270+ }
271+ }
272+ console.log();
273+
274+ const directories = await getUserDirectoriesList();
275+ await checkForNewContent(directories);
276+ await ensureThumbnailCache(directories);
277+ await diskCache.verify(directories);
278+ cleanUploads();
279+ migrateAccessLog();
280+
281+ await settingsInit();
282+ await statsInit();
283+
284+ const pluginsDirectory = path.join(serverDirectory, 'plugins');
285+ const cleanupPlugins = await loadPlugins(app, pluginsDirectory);
286+ const consoleTitle = process.title;
287+
288+ let isExiting = false;
289+ const exitProcess = async () => {
290+ if (isExiting) return;
291+ isExiting = true;
292+ await statsOnExit();
293+ if (typeof cleanupPlugins === 'function') {
294+ await cleanupPlugins();
295+ }
296+ diskCache.dispose();
297+ setWindowTitle(consoleTitle);
298+ process.exit();
299+ };
300+
301+ // Set up event listeners for a graceful shutdown
302+ process.on('SIGINT', exitProcess);
303+ process.on('SIGTERM', exitProcess);
304+ process.on('uncaughtException', (err) => {
305+ console.error('Uncaught exception:', err);
306+ exitProcess();
307+ });
308+
309+ // Add request proxy.
310+ initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass });
311+
312+ // Wait for frontend libs to compile
313+ await webpackMiddleware.runWebpackCompiler();
314+}
315+
316+/**
317+ * Tasks that need to be run after the server starts listening.
318+ * @param {import('./server-startup.js').ServerStartupResult} result The result of the server startup
319+ * @returns {Promise<void>}
320+ */
321+async function postSetupTasks(result) {
322+ const autorunHostname = await cliArgs.getAutorunHostname(result);
323+ const autorunUrl = cliArgs.getAutorunUrl(autorunHostname);
324+
325+ if (cliArgs.autorun) {
326+ try {
327+ console.log('Launching in a browser...');
328+ await open(autorunUrl.toString());
329+ } catch (error) {
330+ console.error('Failed to launch the browser. Open the URL manually.');
331+ }
332+ }
333+
334+ setWindowTitle('SillyTavern WebServer');
335+
336+ let logListen = 'SillyTavern is listening on';
337+
338+ if (result.useIPv6 && !result.v6Failed) {
339+ logListen += color.green(
340+ ' IPv6: ' + cliArgs.getIPv6ListenUrl().host,
341+ );
342+ }
343+
344+ if (result.useIPv4 && !result.v4Failed) {
345+ logListen += color.green(
346+ ' IPv4: ' + cliArgs.getIPv4ListenUrl().host,
347+ );
348+ }
349+
350+ const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
351+ const plainGoToLog = removeColorFormatting(goToLog);
352+
353+ console.log(logListen);
354+ if (cliArgs.listen) {
355+ console.log();
356+ console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
357+ console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
358+ }
359+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
360+ console.log(goToLog);
361+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
362+
363+ setupLogLevel();
364+ serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
365+}
366+
367+/**
368+ * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
369+ */
370+function apply404Middleware() {
371+ const notFoundWebpage = safeReadFileSync(path.join(serverDirectory, 'public/error/url-not-found.html')) ?? '';
372+ app.use((req, res) => {
373+ res.status(404).send(notFoundWebpage);
374+ });
375+}
376+
377+// User storage module needs to be initialized before starting the server
378+initUserStorage(globalThis.DATA_ROOT)
379+ .then(ensurePublicDirectoriesExist)
380+ .then(migrateUserData)
381+ .then(migrateSystemPrompts)
382+ .then(verifySecuritySettings)
383+ .then(preSetupTasks)
384+ .then(apply404Middleware)
385+ .then(() => new ServerStartup(app, cliArgs).start())
386+ .then(postSetupTasks);
src/transformers.js+2 -1
@@ -5,6 +5,7 @@ import { Buffer } from 'node:buffer';
55
66import { pipeline, env, RawImage } from 'sillytavern-transformers';
77import { getConfigValue } from './util.js';
8+import { serverDirectory } from './server-directory.js';
89
910configureTransformers();
1011
@@ -12,7 +13,7 @@ function configureTransformers() {
1213 // Limit the number of threads to 1 to avoid issues on Android
1314 env.backends.onnx.wasm.numThreads = 1;
1415 // Use WASM from a local folder to avoid CDN connections
1516 env.backends.onnx.wasm.wasmPaths = path.join(process.cwd()serverDirectory, 'node_modules', 'sillytavern-transformers', 'dist') + path.sep;
1617}
1718
1819const tasks = {
src/users.js+2 -1
@@ -18,6 +18,7 @@ import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FIL
1818import { getConfigValue, color, delay, generateTimestamp } from './util.js';
1919import { readSecret, writeSecret } from './endpoints/secrets.js';
2020import { getContentOfType } from './endpoints/content-manager.js';
21+import { serverDirectory } from './server-directory.js';
2122
2223export const KEY_PREFIX = 'user:';
2324const AVATAR_PREFIX = 'avatar:';
@@ -905,7 +906,7 @@ export async function loginPageMiddleware(request, response) {
905906 console.error('Error during auto-login:', error);
906907 }
907908
908909 return response.sendFile('login.html', { root: path.join(process.cwd()serverDirectory, 'public') });
909910}
910911
911912/**
src/util.js+66 -14
@@ -15,13 +15,15 @@ import yauzl from 'yauzl';
1515import mime from 'mime-types';
1616import { default as simpleGit } from 'simple-git';
1717import chalk from 'chalk';
18-import { LOG_LEVELS } from './constants.js';
1918import bytes from 'bytes';
19+import { LOG_LEVELS } from './constants.js';
20+import { serverDirectory } from './server-directory.js';
2021
2122/**
2223 * Parsed config object.
2324 */
2425let CACHED_CONFIG = null;
26+let CONFIG_PATH = null;
2527
2628/**
2729 * Converts a configuration key to an environment variable key.
@@ -32,22 +34,37 @@ let CACHED_CONFIG = null;
3234export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_');
3335
3436/**
37+ * Set the config file path.
38+ * @param {string} configFilePath Path to the config file
39+ */
40+export function setConfigFilePath(configFilePath) {
41+ if (CONFIG_PATH !== null) {
42+ console.error(color.red('Config file path already set. Please restart the server to change the config file path.'));
43+ }
44+ CONFIG_PATH = path.resolve(configFilePath);
45+}
46+
47+/**
3548 * Returns the config object from the config.yaml file.
3649 * @returns {object} Config object
3750 */
3851export function getConfig() {
52+ if (CONFIG_PATH === null) {
53+ console.trace();
54+ console.error(color.red('No config file path set. Please set the config file path using setConfigFilePath().'));
55+ process.exit(1);
56+ }
3957 if (CACHED_CONFIG) {
4058 return CACHED_CONFIG;
4159 }
42-
60+ if (!fs.existsSync(CONFIG_PATH)) {
43- if (!fs.existsSync('./config.yaml')) {
4461 console.error(color.red('No config file found. Please create a config.yaml file. The default config file can be found in the /default folder.'));
4562 console.error(color.red('The program will now exit.'));
4663 process.exit(1);
4764 }
4865
4966 try {
5067 const config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml')CONFIG_PATH, 'utf8'));
5168 CACHED_CONFIG = config;
5269 return config;
5370 } catch (error) {
@@ -121,20 +138,19 @@ export async function getVersion() {
121138
122139 try {
123140 const require = createRequire(import.meta.url);
124141 const pkgJson = require(path.join(process.cwd()serverDirectory, './package.json'));
125142 pkgVersion = pkgJson.version;
126143 if (commandExistsSync('git')) {
127144 const git = simpleGit({ baseDir: serverDirectory });
128- const cwd = process.cwd();
145+ gitRevision = await git.revparse(['--short', 'HEAD']);
129146 gitRevisiongitBranch = await git.cwd(cwd).revparse(['--shortabbrev-ref', 'HEAD']);
130147 gitBranchcommitDate = await git.cwd(cwd).revparseshow(['--abbrev-refs', 'HEAD--format=%ci', gitRevision]);
131- commitDate = await git.cwd(cwd).show(['-s', '--format=%ci', gitRevision]);
132148
133149 const trackingBranch = await git.cwd(cwd).revparse(['--abbrev-ref', '@{u}']);
134150
135151 // Might fail, but exception is caught. Just don't run anything relevant after in this block...
136152 const localLatest = await git.cwd(cwd).revparse(['HEAD']);
137153 const remoteLatest = await git.cwd(cwd).revparse([trackingBranch]);
138154 isLatest = localLatest === remoteLatest;
139155 }
140156 }
@@ -419,7 +435,7 @@ export function removeOldBackups(directory, prefix, limit = null) {
419435 break;
420436 }
421437
422438 fs.rmSyncunlinkSync(oldest);
423439 }
424440 }
425441}
@@ -1071,3 +1087,39 @@ export function mutateJsonString(jsonString, mutation) {
10711087 return jsonString;
10721088 }
10731089}
1090+
1091+/**
1092+ * Sets the permissions of a file or directory to be writable.
1093+ * @param {string} targetPath Path to the file or directory
1094+ */
1095+export function setPermissionsSync(targetPath) {
1096+ /**
1097+ * Appends writable permission to the file mode.
1098+ * @param {string} filePath Path to the file
1099+ * @param {fs.Stats} stats File stats
1100+ */
1101+ function appendWritablePermission(filePath, stats) {
1102+ const currentMode = stats.mode;
1103+ const newMode = currentMode | 0o200;
1104+ if (newMode != currentMode) {
1105+ fs.chmodSync(filePath, newMode);
1106+ }
1107+ }
1108+
1109+ try {
1110+ const stats = fs.statSync(targetPath);
1111+
1112+ if (stats.isDirectory()) {
1113+ appendWritablePermission(targetPath, stats);
1114+ const files = fs.readdirSync(targetPath);
1115+
1116+ files.forEach((file) => {
1117+ setPermissionsSync(path.join(targetPath, file));
1118+ });
1119+ } else {
1120+ appendWritablePermission(targetPath, stats);
1121+ }
1122+ } catch (error) {
1123+ console.error(`Error setting write permissions for ${targetPath}:`, error);
1124+ }
1125+}
tests/package-lock.json+153 -302
@@ -1038,20 +1038,19 @@
10381038 }
10391039 },
10401040 "node_modules/@puppeteer/browsers": {
10411041 "version": "2.210.32",
10421042 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.210.32.tgz",
10431043 "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjadi4Ez+rhFwdaG1Lus9oRWQbNjtI/Wzq58GHYCDTlZ9z6mldf4g3+NTbjxr7OH508mjAKvza0ekPJem0ZtmsYHP3B5dq62+TXEfe0PpnsQIaBHKaGCOuqJxXzvFLUhJvQ6jtsQ==",
10441044 "license": "Apache-2.0",
10451045 "peer": true,
10461046 "dependencies": {
10471047 "debug": "^4.3.4.0",
10481048 "extract-zip": "^2.0.1",
10491049 "progress": "^2.0.3",
10501050 "proxy-agent": "^6.45.0",
10511051 "semver": "^7.67.01",
10521052 "tar-fs": "^3.0.58",
10531053 "unbzip2-streamyargs": "1^17.47.32",
1054- "yargs": "17.7.2"
10551054 },
10561055 "bin": {
10571056 "browsers": "lib/cjs/main-cli.js"
@@ -1060,46 +1059,12 @@
10601059 "node": ">=18"
10611060 }
10621061 },
1063- "node_modules/@puppeteer/browsers/node_modules/debug": {
1064- "version": "4.3.4",
1065- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1066- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
1067- "license": "MIT",
1068- "peer": true,
1069- "dependencies": {
1070- "ms": "2.1.2"
1071- },
1072- "engines": {
1073- "node": ">=6.0"
1074- },
1075- "peerDependenciesMeta": {
1076- "supports-color": {
1077- "optional": true
1078- }
1079- }
1080- },
1081- "node_modules/@puppeteer/browsers/node_modules/lru-cache": {
1082- "version": "6.0.0",
1083- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
1084- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
1085- "license": "ISC",
1086- "peer": true,
1087- "dependencies": {
1088- "yallist": "^4.0.0"
1089- },
1090- "engines": {
1091- "node": ">=10"
1092- }
1093- },
10941062 "node_modules/@puppeteer/browsers/node_modules/semver": {
10951063 "version": "7.67.01",
10961064 "resolved": "https://registry.npmjs.org/semver/-/semver-7.67.01.tgz",
10971065 "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5svhlq8tAfn0m/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+X5COhOVgqXjkMDvPlOFFSGwQjoEa6gyMA==",
10981066 "license": "ISC",
10991067 "peer": true,
1100- "dependencies": {
1101- "lru-cache": "^6.0.0"
1102- },
11031068 "bin": {
11041069 "semver": "bin/semver.js"
11051070 },
@@ -1107,13 +1072,6 @@
11071072 "node": ">=10"
11081073 }
11091074 },
1110- "node_modules/@puppeteer/browsers/node_modules/yallist": {
1111- "version": "4.0.0",
1112- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
1113- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
1114- "license": "ISC",
1115- "peer": true
1116- },
11171075 "node_modules/@sideway/address": {
11181076 "version": "4.1.5",
11191077 "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -1452,14 +1410,11 @@
14521410 }
14531411 },
14541412 "node_modules/agent-base": {
14551413 "version": "7.1.13",
14561414 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.13.tgz",
14571415 "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOAjRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
14581416 "license": "MIT",
14591417 "peer": true,
1460- "dependencies": {
1461- "debug": "^4.3.4"
1462- },
14631418 "engines": {
14641419 "node": ">= 14"
14651420 }
@@ -1581,9 +1536,9 @@
15811536 }
15821537 },
15831538 "node_modules/b4a": {
15841539 "version": "1.6.67",
15851540 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.67.tgz",
15861541 "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmgOnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
15871542 "license": "Apache-2.0",
15881543 "peer": true
15891544 },
@@ -1701,76 +1656,81 @@
17011656 "license": "MIT"
17021657 },
17031658 "node_modules/bare-events": {
17041659 "version": "2.45.24",
17051660 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.45.24.tgz",
17061661 "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24gFfDkR8pj4/QTrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
17071662 "license": "Apache-2.0",
17081663 "optional": true,
17091664 "peer": true
17101665 },
17111666 "node_modules/bare-fs": {
17121667 "version": "2.34.1.3",
17131668 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.34.1.3.tgz",
17141669 "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsClOeEZYIg+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA2qepaWLyphaOXHAHKo3xkM8y3BeGAvHdMN8GNWvEAU1Yw6rYpGzu/wDDbKxgEjVeVDpgGhDzaeMpjg==",
17151670 "license": "Apache-2.0",
17161671 "optional": true,
17171672 "peer": true,
17181673 "dependencies": {
17191674 "bare-events": "^2.05.04",
17201675 "bare-path": "^23.0.0",
17211676 "bare-stream": "^2.06.04"
1677+ },
1678+ "engines": {
1679+ "bare": ">=1.16.0"
1680+ },
1681+ "peerDependencies": {
1682+ "bare-buffer": "*"
1683+ },
1684+ "peerDependenciesMeta": {
1685+ "bare-buffer": {
1686+ "optional": true
1687+ }
17221688 }
17231689 },
17241690 "node_modules/bare-os": {
17251691 "version": "23.46.01",
17261692 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-23.46.01.tgz",
17271693 "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNguaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
17281694 "license": "Apache-2.0",
17291695 "optional": true,
17301696 "peer": true,
1697+ "engines": {
1698+ "bare": ">=1.14.0"
1699+ }
17311700 },
17321701 "node_modules/bare-path": {
17331702 "version": "23.10.30",
17341703 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-23.10.30.tgz",
17351704 "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/xtyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+PisgcXAs47kot8Mw==",
17361705 "license": "Apache-2.0",
17371706 "optional": true,
17381707 "peer": true,
17391708 "dependencies": {
17401709 "bare-os": "^2.13.0.1"
17411710 }
17421711 },
17431712 "node_modules/bare-stream": {
17441713 "version": "2.16.35",
17451714 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.16.35.tgz",
17461715 "integrity": "sha512-tiDAH9HjSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQiOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
17471716 "license": "Apache-2.0",
17481717 "optional": true,
17491718 "peer": true,
17501719 "dependencies": {
17511720 "streamx": "^2.1821.0"
17521721 },
1753- },
1722+ "peerDependencies": {
1754- "node_modules/base64-js": {
1723+ "bare-buffer": "*",
1755- "version": "1.5.1",
1724+ "bare-events": "*"
1756- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
1725+ },
1757- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
1726+ "peerDependenciesMeta": {
17581727 "fundingbare-buffer": [{
1759- {
1728+ "optional": true
1760- "type": "github",
1761- "url": "https://github.com/sponsors/feross"
1762- },
1763- {
1764- "type": "patreon",
1765- "url": "https://www.patreon.com/feross"
17661729 },
1767- {
1730+ "bare-events": {
17681731 "typeoptional": "consulting",true
1769- "url": "https://feross.org/support"
17701732 }
1771- ],
1733+ }
1772- "license": "MIT",
1773- "peer": true
17741734 },
17751735 "node_modules/basic-ftp": {
17761736 "version": "5.0.5",
@@ -1845,31 +1805,6 @@
18451805 "node-int64": "^0.4.0"
18461806 }
18471807 },
1848- "node_modules/buffer": {
1849- "version": "5.7.1",
1850- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
1851- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
1852- "funding": [
1853- {
1854- "type": "github",
1855- "url": "https://github.com/sponsors/feross"
1856- },
1857- {
1858- "type": "patreon",
1859- "url": "https://www.patreon.com/feross"
1860- },
1861- {
1862- "type": "consulting",
1863- "url": "https://feross.org/support"
1864- }
1865- ],
1866- "license": "MIT",
1867- "peer": true,
1868- "dependencies": {
1869- "base64-js": "^1.3.1",
1870- "ieee754": "^1.1.13"
1871- }
1872- },
18731808 "node_modules/buffer-crc32": {
18741809 "version": "0.2.13",
18751810 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -1950,15 +1885,14 @@
19501885 }
19511886 },
19521887 "node_modules/chromium-bidi": {
19531888 "version": "04.51.241",
19541889 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-04.51.241.tgz",
19551890 "integrity": "sha512-5xQNN2SVBdZv4TxeMLaIbiR7t4vF3YluE6RlMSk9IWk+PelrnZsHDhn8h2JtyriLrb9U+0qHcZS8BMuo93qN6J1VmtmrgYPWWyzHp+rmcLHcbpnA8QJhN2pL9vRTk+wUXHYRTVp7jTK58ZNzMLBgoLMHY4QyJMbeuw3eKxqg==",
19561891 "license": "Apache-2.0",
19571892 "peer": true,
19581893 "dependencies": {
19591894 "mitt": "^3.0.1",
19601895 "urlpattern-polyfillzod": "10^3.024.01",
1961- "zod": "3.23.8"
19621896 },
19631897 "peerDependencies": {
19641898 "devtools-protocol": "*"
@@ -2169,12 +2103,12 @@
21692103 }
21702104 },
21712105 "node_modules/debug": {
21722106 "version": "4.34.50",
21732107 "resolved": "https://registry.npmjs.org/debug/-/debug-4.34.50.tgz",
21742108 "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ6WTZ/wrQRkXDT4AUWlNZNPKvW5xIxCY/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13VT6BALoZHaE4ctp9xm+jgZ5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
21752109 "license": "MIT",
21762110 "dependencies": {
21772111 "ms": "^2.1.23"
21782112 },
21792113 "engines": {
21802114 "node": ">=6.0"
@@ -2248,9 +2182,9 @@
22482182 }
22492183 },
22502184 "node_modules/devtools-protocol": {
22512185 "version": "0.0.12990701425554",
22522186 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.12990701425554.tgz",
22532187 "integrity": "sha512-+qtL3eX50qsJ7cuRfxR6Nlzdzt0ihVIkV+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7/sLztKgs7rgquY/Xe8yhMmQ+elj3uAqSgMhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw==",
22542188 "license": "BSD-3-Clause",
22552189 "peer": true
22562190 },
@@ -2987,21 +2921,6 @@
29872921 "node": ">=0.10.0"
29882922 }
29892923 },
2990- "node_modules/fs-extra": {
2991- "version": "11.2.0",
2992- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
2993- "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
2994- "license": "MIT",
2995- "peer": true,
2996- "dependencies": {
2997- "graceful-fs": "^4.2.0",
2998- "jsonfile": "^6.0.1",
2999- "universalify": "^2.0.0"
3000- },
3001- "engines": {
3002- "node": ">=14.14"
3003- }
3004- },
30052924 "node_modules/fs.realpath": {
30062925 "version": "1.0.0",
30072926 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3071,16 +2990,15 @@
30712990 }
30722991 },
30732992 "node_modules/get-uri": {
30742993 "version": "6.0.34",
30752994 "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.34.tgz",
30762995 "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanwE1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
30772996 "license": "MIT",
30782997 "peer": true,
30792998 "dependencies": {
30802999 "basic-ftp": "^5.0.2",
30813000 "data-uri-to-buffer": "^6.0.2",
30823001 "debug": "^4.3.4",
3083- "fs-extra": "^11.2.0"
30843002 },
30853003 "engines": {
30863004 "node": ">= 14"
@@ -3254,13 +3172,13 @@
32543172 }
32553173 },
32563174 "node_modules/https-proxy-agent": {
32573175 "version": "7.0.56",
32583176 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.56.tgz",
32593177 "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvwvK9P5/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalwiUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
32603178 "license": "MIT",
32613179 "peer": true,
32623180 "dependencies": {
32633181 "agent-base": "^7.01.2",
32643182 "debug": "4"
32653183 },
32663184 "engines": {
@@ -3276,27 +3194,6 @@
32763194 "node": ">=10.17.0"
32773195 }
32783196 },
3279- "node_modules/ieee754": {
3280- "version": "1.2.1",
3281- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
3282- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
3283- "funding": [
3284- {
3285- "type": "github",
3286- "url": "https://github.com/sponsors/feross"
3287- },
3288- {
3289- "type": "patreon",
3290- "url": "https://www.patreon.com/feross"
3291- },
3292- {
3293- "type": "consulting",
3294- "url": "https://feross.org/support"
3295- }
3296- ],
3297- "license": "BSD-3-Clause",
3298- "peer": true
3299- },
33003197 "node_modules/ignore": {
33013198 "version": "5.3.1",
33023199 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -4287,19 +4184,6 @@
42874184 "node": ">=6"
42884185 }
42894186 },
4290- "node_modules/jsonfile": {
4291- "version": "6.1.0",
4292- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
4293- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
4294- "license": "MIT",
4295- "peer": true,
4296- "dependencies": {
4297- "universalify": "^2.0.0"
4298- },
4299- "optionalDependencies": {
4300- "graceful-fs": "^4.1.6"
4301- }
4302- },
43034187 "node_modules/keyv": {
43044188 "version": "4.5.4",
43054189 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4501,9 +4385,9 @@
45014385 "peer": true
45024386 },
45034387 "node_modules/ms": {
45044388 "version": "2.1.23",
45054389 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.23.tgz",
45064390 "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk6FlzubTLZG3J2a/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2wNVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
45074391 "license": "MIT"
45084392 },
45094393 "node_modules/natural-compare": {
@@ -4657,20 +4541,20 @@
46574541 }
46584542 },
46594543 "node_modules/pac-proxy-agent": {
46604544 "version": "7.0.2.0",
46614545 "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.0.tgz",
46624546 "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBATEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+bTfxYzeKW73eHgpIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
46634547 "license": "MIT",
46644548 "peer": true,
46654549 "dependencies": {
46664550 "@tootallnate/quickjs-emscripten": "^0.23.0",
46674551 "agent-base": "^7.01.2",
46684552 "debug": "^4.3.4",
46694553 "get-uri": "^6.0.1",
46704554 "http-proxy-agent": "^7.0.0",
46714555 "https-proxy-agent": "^7.0.56",
46724556 "pac-resolver": "^7.0.1",
46734557 "socks-proxy-agent": "^8.0.45"
46744558 },
46754559 "engines": {
46764560 "node": ">= 14"
@@ -4876,20 +4760,20 @@
48764760 }
48774761 },
48784762 "node_modules/proxy-agent": {
48794763 "version": "6.45.0",
48804764 "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.45.0.tgz",
48814765 "integrity": "sha512-u0piLUTmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+nCOHMgGjRbimiXmA9kMEaCzoInSu/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQA==",
48824766 "license": "MIT",
48834767 "peer": true,
48844768 "dependencies": {
48854769 "agent-base": "^7.01.2",
48864770 "debug": "^4.3.4",
48874771 "http-proxy-agent": "^7.0.1",
48884772 "https-proxy-agent": "^7.0.36",
48894773 "lru-cache": "^7.14.1",
48904774 "pac-proxy-agent": "^7.0.1.0",
48914775 "proxy-from-env": "^1.1.0",
48924776 "socks-proxy-agent": "^8.0.25"
48934777 },
48944778 "engines": {
48954779 "node": ">= 14"
@@ -4912,9 +4796,9 @@
49124796 "license": "MIT"
49134797 },
49144798 "node_modules/pump": {
49154799 "version": "3.0.02",
49164800 "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.02.tgz",
49174801 "integrity": "sha512-LwZy+p3SFs1PytdtUPXtzlGM8FE3P0ZL6DVs/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
49184802 "license": "MIT",
49194803 "peer": true,
49204804 "dependencies": {
@@ -4932,37 +4816,40 @@
49324816 }
49334817 },
49344818 "node_modules/puppeteer": {
49354819 "version": "2224.127.12",
49364820 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2224.127.12.tgz",
49374821 "integrity": "sha512-1GxY8dnEnHr1SLzdSDr0FCjM6JQfAh2E2IifYqoY6wGs0yZeFuFPn8BE9FhuveXkarF+eO18I2e/EqzeF8a58DbGVk9oVjj4lFdqNoVbpgFSpAbz7VER9St7S1wDpNgaxdoCh4Qh1AE+qXdJBhdaeoPt6eRNTY4Dih29Jbq8wow==",
49384822 "hasInstallScript": true,
49394823 "license": "Apache-2.0",
49404824 "peer": true,
49414825 "dependencies": {
49424826 "@puppeteer/browsers": "2.210.32",
4827+ "chromium-bidi": "4.1.1",
49434828 "cosmiconfig": "^9.0.0",
49444829 "devtools-protocol": "0.0.12990701425554",
49454830 "puppeteer-core": "2224.127.12",
4831+ "typed-query-selector": "^2.12.0"
49464832 },
49474833 "bin": {
49484834 "puppeteer": "lib/esmcjs/puppeteer/node/cli.js"
49494835 },
49504836 "engines": {
49514837 "node": ">=18"
49524838 }
49534839 },
49544840 "node_modules/puppeteer-core": {
49554841 "version": "2224.127.12",
49564842 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-2224.127.12.tgz",
49574843 "integrity": "sha512-XmqeDPVdC5/3nGJys1jbgeoZ02wP0WV1GBlPtrP9pZyTmJqKODFCnkZgemCpoFA4LbAa8+NumHVQKyP5X9IgdNS1ZnAnIh1sMAwhF8/ULRbGXJFuqgXMcKQ3eeNtFpBzGRbpeoCGWHge1ZWKWl0ExwxEUGf7jt+qmNLlKieFw1Q==",
49584844 "license": "Apache-2.0",
49594845 "peer": true,
49604846 "dependencies": {
49614847 "@puppeteer/browsers": "2.210.32",
49624848 "chromium-bidi": "04.51.241",
49634849 "debug": "^4.34.50",
49644850 "devtools-protocol": "0.0.12990701425554",
49654851 "wstyped-query-selector": "^82.1712.10",
4852+ "ws": "^8.18.1"
49664853 },
49674854 "engines": {
49684855 "node": ">=18"
@@ -5051,13 +4938,6 @@
50514938 ],
50524939 "license": "MIT"
50534940 },
5054- "node_modules/queue-tick": {
5055- "version": "1.0.1",
5056- "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
5057- "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
5058- "license": "MIT",
5059- "peer": true
5060- },
50614941 "node_modules/react-is": {
50624942 "version": "18.3.1",
50634943 "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -5254,9 +5134,9 @@
52545134 }
52555135 },
52565136 "node_modules/socks": {
52575137 "version": "2.8.34",
52585138 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.34.tgz",
52595139 "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsFD3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/AJPYCW8Zwb3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
52605140 "license": "MIT",
52615141 "peer": true,
52625142 "dependencies": {
@@ -5269,13 +5149,13 @@
52695149 }
52705150 },
52715151 "node_modules/socks-proxy-agent": {
52725152 "version": "8.0.45",
52735153 "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.45.tgz",
52745154 "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSGHehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRwE+YTzJw==",
52755155 "license": "MIT",
52765156 "peer": true,
52775157 "dependencies": {
52785158 "agent-base": "^7.1.12",
52795159 "debug": "^4.3.4",
52805160 "socks": "^2.8.3"
52815161 },
@@ -5345,14 +5225,13 @@
53455225 }
53465226 },
53475227 "node_modules/streamx": {
53485228 "version": "2.1822.0",
53495229 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.1822.0.tgz",
53505230 "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4osLh1evHOzBy/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQiWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
53515231 "license": "MIT",
53525232 "peer": true,
53535233 "dependencies": {
53545234 "fast-fifo": "^1.3.2",
5355- "queue-tick": "^1.0.1",
53565235 "text-decoder": "^1.1.0"
53575236 },
53585237 "optionalDependencies": {
@@ -5453,9 +5332,9 @@
54535332 }
54545333 },
54555334 "node_modules/tar-fs": {
54565335 "version": "3.0.58",
54575336 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.58.tgz",
54585337 "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGgZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
54595338 "license": "MIT",
54605339 "peer": true,
54615340 "dependencies": {
@@ -5463,8 +5342,8 @@
54635342 "tar-stream": "^3.1.5"
54645343 },
54655344 "optionalDependencies": {
54665345 "bare-fs": "^24.10.1",
54675346 "bare-path": "^23.10.0"
54685347 }
54695348 },
54705349 "node_modules/tar-stream": {
@@ -5494,9 +5373,9 @@
54945373 }
54955374 },
54965375 "node_modules/text-decoder": {
54975376 "version": "1.12.13",
54985377 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.12.13.tgz",
54995378 "integrity": "sha512-8zll7REEv4GDD3x43/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+fLBql4zbt5z83GEQGGV5VAbC2ol/zqpW37NHxLaA==",
55005379 "license": "Apache-2.0",
55015380 "peer": true,
55025381 "dependencies": {
@@ -5509,13 +5388,6 @@
55095388 "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
55105389 "license": "MIT"
55115390 },
5512- "node_modules/through": {
5513- "version": "2.3.8",
5514- "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
5515- "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
5516- "license": "MIT",
5517- "peer": true
5518- },
55195391 "node_modules/tmpl": {
55205392 "version": "1.0.5",
55215393 "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -5594,6 +5466,13 @@
55945466 "url": "https://github.com/sponsors/sindresorhus"
55955467 }
55965468 },
5469+ "node_modules/typed-query-selector": {
5470+ "version": "2.12.0",
5471+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
5472+ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
5473+ "license": "MIT",
5474+ "peer": true
5475+ },
55975476 "node_modules/typescript": {
55985477 "version": "5.5.3",
55995478 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
@@ -5608,33 +5487,12 @@
56085487 "node": ">=14.17"
56095488 }
56105489 },
5611- "node_modules/unbzip2-stream": {
5612- "version": "1.4.3",
5613- "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
5614- "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
5615- "license": "MIT",
5616- "peer": true,
5617- "dependencies": {
5618- "buffer": "^5.2.1",
5619- "through": "^2.3.8"
5620- }
5621- },
56225490 "node_modules/undici-types": {
56235491 "version": "5.26.5",
56245492 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
56255493 "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
56265494 "license": "MIT"
56275495 },
5628- "node_modules/universalify": {
5629- "version": "2.0.1",
5630- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
5631- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
5632- "license": "MIT",
5633- "peer": true,
5634- "engines": {
5635- "node": ">= 10.0.0"
5636- }
5637- },
56385496 "node_modules/update-browserslist-db": {
56395497 "version": "1.1.0",
56405498 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -5674,13 +5532,6 @@
56745532 "punycode": "^2.1.0"
56755533 }
56765534 },
5677- "node_modules/urlpattern-polyfill": {
5678- "version": "10.0.0",
5679- "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
5680- "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
5681- "license": "MIT",
5682- "peer": true
5683- },
56845535 "node_modules/v8-to-istanbul": {
56855536 "version": "9.3.0",
56865537 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -5784,9 +5635,9 @@
57845635 }
57855636 },
57865637 "node_modules/ws": {
57875638 "version": "8.18.01",
57885639 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.01.tgz",
57895640 "integrity": "sha512-8VbfWfHLbbwu3RKW2aJZMXeMxVpnZ6bck+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1eRswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pwuurzwa0rVI16n/Xzjdz1w==",
57905641 "license": "MIT",
57915642 "peer": true,
57925643 "engines": {
@@ -5871,9 +5722,9 @@
58715722 }
58725723 },
58735724 "node_modules/zod": {
58745725 "version": "3.2324.83",
58755726 "resolved": "https://registry.npmjs.org/zod/-/zod-3.2324.83.tgz",
58765727 "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0EajHhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+5Y0lftuGBqC4+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6gMsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
58775728 "license": "MIT",
58785729 "peer": true,
58795730 "funding": {
webpack.config.js+2 -1
@@ -1,6 +1,7 @@
11import process from 'node:process';
22import path from 'node:path';
33import isDocker from 'is-docker';
4+import { serverDirectory } from './src/server-directory.js';
45
56/**
67 * Get the Webpack configuration for the public/lib.js file.
@@ -40,7 +41,7 @@ export default function getPublicLibConfig(forceDist = false) {
4041
4142 return {
4243 mode: 'production',
4344 entry: 'path./join(serverDirectory, 'public/lib.js'),
4445 cache: {
4546 type: 'filesystem',
4647 cacheDirectory: cacheDirectory,