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
13/cache13/cache
14.DS_Store14.DS_Store
15/public/scripts/extensions/third-party15/public/scripts/extensions/third-party
16/colab
.github/readme.md+1 -0
@@ -350,6 +350,7 @@ Start.bat --port 8000 --listen false
350| Option | Description | Type |350| Option | Description | Type |
351|-------------------------|----------------------------------------------------------------------|----------|351|-------------------------|----------------------------------------------------------------------|----------|
352| `--version` | Show version number | boolean |352| `--version` | Show version number | boolean |
353| `--configPath` | Override the path to the config.yaml file | string |
353| `--dataRoot` | Root directory for data storage | string |354| `--dataRoot` | Root directory for data storage | string |
354| `--port` | Sets the port under which SillyTavern will run | number |355| `--port` | Sets the port under which SillyTavern will run | number |
355| `--listen` | SillyTavern will listen on all network interfaces | boolean |356| `--listen` | SillyTavern will listen on all network interfaces | boolean |
.github/workflows/pr-auto-manager.yml+3 -2
@@ -30,7 +30,8 @@ jobs:
30 # https://github.com/marketplace/actions/checkout30 # https://github.com/marketplace/actions/checkout
31 uses: actions/checkout@v4.2.231 uses: actions/checkout@v4.2.2
32 with:32 with:
33 ref: ${{ github.head_ref }}33 ref: ${{ github.event.pull_request.head.sha }}
34 repository: ${{ github.event.pull_request.head.repo.full_name }}
3435
35 - name: Setup Node.js36 - name: Setup Node.js
36 # Setup Node.js environment37 # Setup Node.js environment
@@ -49,7 +50,7 @@ jobs:
49 with:50 with:
50 token: ${{ secrets.GITHUB_TOKEN }}51 token: ${{ secrets.GITHUB_TOKEN }}
51 eslint-args: '--ignore-path=.gitignore --quiet'52 eslint-args: '--ignore-path=.gitignore --quiet'
52 extensions: 'js,ts'53 extensions: 'js'
53 annotations: true54 annotations: true
54 ignore-patterns: |55 ignore-patterns: |
55 dist/56 dist/
.npmignore+1 -0
@@ -12,3 +12,4 @@ access.log
12.vscode12.vscode
13.git13.git
14/public/scripts/extensions/third-party14/public/scripts/extensions/third-party
15/colab
Dockerfile+3 -5
@@ -12,15 +12,13 @@ WORKDIR ${APP_HOME}
12# Set NODE_ENV to production12# Set NODE_ENV to production
13ENV NODE_ENV=production13ENV NODE_ENV=production
1414
15# Install app dependencies15# Bundle app source
16COPY package*.json post-install.js ./16COPY . ./
17
17RUN \18RUN \
18 echo "*** Install npm packages ***" && \19 echo "*** Install npm packages ***" && \
19 npm i --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force20 npm i --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force
2021
21# Bundle app source
22COPY . ./
23
24# Copy default chats, characters and user avatars to <folder>.default folder22# Copy default chats, characters and user avatars to <folder>.default folder
25RUN \23RUN \
26 rm -f "config.yaml" || true && \24 rm -f "config.yaml" || true && \
default/config.yaml+5 -0
@@ -155,6 +155,7 @@ whitelistImportDomains:
155 - cdn.discordapp.com155 - cdn.discordapp.com
156 - files.catbox.moe156 - files.catbox.moe
157 - raw.githubusercontent.com157 - raw.githubusercontent.com
158 - char-archive.evulid.cc
158# API request overrides (for KoboldAI and Text Completion APIs)159# API request overrides (for KoboldAI and Text Completion APIs)
159## Note: host includes the port number if it's not the default (80 or 443)160## Note: host includes the port number if it's not the default (80 or 443)
160## Format is an array of objects:161## Format is an array of objects:
@@ -233,6 +234,10 @@ claude:
233 # should be ideal for most use cases.234 # should be ideal for most use cases.
234 # Any value other than a non-negative integer will be ignored and caching at depth will not be enabled.235 # Any value other than a non-negative integer will be ignored and caching at depth will not be enabled.
235 cachingAtDepth: -1236 cachingAtDepth: -1
237# -- GOOGLE GEMINI API CONFIGURATION --
238gemini:
239 # API endpoint version ("v1beta" or "v1alpha")
240 apiVersion: 'v1beta'
236# -- SERVER PLUGIN CONFIGURATION --241# -- SERVER PLUGIN CONFIGURATION --
237enableServerPlugins: false242enableServerPlugins: false
238# Attempt to automatically update server plugins on startup243# Attempt to automatically update server plugins on startup
default/content/index.json+21 -5
@@ -540,7 +540,7 @@
540 "type": "context"540 "type": "context"
541 },541 },
542 {542 {
543 "filename": "presets/context/Pygmalion.json",543 "filename": "presets/context/Metharme.json",
544 "type": "context"544 "type": "context"
545 },545 },
546 {546 {
@@ -564,6 +564,10 @@
564 "type": "context"564 "type": "context"
565 },565 },
566 {566 {
567 "filename": "presets/context/Llama 4 Instruct.json",
568 "type": "context"
569 },
570 {
567 "filename": "presets/context/Phi.json",571 "filename": "presets/context/Phi.json",
568 "type": "context"572 "type": "context"
569 },573 },
@@ -616,10 +620,6 @@
616 "type": "instruct"620 "type": "instruct"
617 },621 },
618 {622 {
619 "filename": "presets/instruct/Pygmalion.json",
620 "type": "instruct"
621 },
622 {
623 "filename": "presets/instruct/Story.json",623 "filename": "presets/instruct/Story.json",
624 "type": "instruct"624 "type": "instruct"
625 },625 },
@@ -664,6 +664,10 @@
664 "type": "instruct"664 "type": "instruct"
665 },665 },
666 {666 {
667 "filename": "presets/instruct/Llama 4 Instruct.json",
668 "type": "instruct"
669 },
670 {
667 "filename": "presets/instruct/Phi.json",671 "filename": "presets/instruct/Phi.json",
668 "type": "instruct"672 "type": "instruct"
669 },673 },
@@ -748,6 +752,10 @@
748 "type": "sysprompt"752 "type": "sysprompt"
749 },753 },
750 {754 {
755 "filename": "presets/sysprompt/Lightning 1.1.json",
756 "type": "sysprompt"
757 },
758 {
751 "filename": "presets/instruct/Mistral V1.json",759 "filename": "presets/instruct/Mistral V1.json",
752 "type": "instruct"760 "type": "instruct"
753 },761 },
@@ -788,6 +796,14 @@
788 "type": "context"796 "type": "context"
789 },797 },
790 {798 {
799 "filename": "presets/instruct/GLM-4.json",
800 "type": "instruct"
801 },
802 {
803 "filename": "presets/context/GLM-4.json",
804 "type": "context"
805 },
806 {
791 "filename": "presets/reasoning/DeepSeek.json",807 "filename": "presets/reasoning/DeepSeek.json",
792 "type": "reasoning"808 "type": "reasoning"
793 },809 },
default/content/presets/context/Adventure.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": false,6 "always_force_name2": false,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": true,8 "single_line": true,
default/content/presets/context/Alpaca-Single-Turn.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": false,6 "always_force_name2": false,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Alpaca.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/ChatML-Names.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/ChatML.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Command R.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>New Roleplay:<|END_OF_TURN_TOKEN|>",4 "chat_start": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>New Roleplay:<|END_OF_TURN_TOKEN|>",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/DeepSeek-V2.5.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Default.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "***",3 "example_separator": "***",
4 "chat_start": "***",4 "chat_start": "***",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/DreamGen Role-Play V1 ChatML.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": false,6 "always_force_name2": false,
8 "trim_sentences": true,7 "trim_sentences": true,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/DreamGen Role-Play V1 Llama3.json+0 -1
@@ -3,7 +3,6 @@
3 "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.",3 "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.",
4 "chat_start": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nStart the role-play between {{char}} and {{user}}.",4 "chat_start": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nStart the role-play between {{char}} and {{user}}.",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": false,6 "always_force_name2": false,
8 "trim_sentences": true,7 "trim_sentences": true,
9 "single_line": false,8 "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 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Libra-32B.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "### Example:",3 "example_separator": "### Example:",
4 "chat_start": "### START ROLEPLAY:",4 "chat_start": "### START ROLEPLAY:",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Lightning 1.1.json+3 -4
@@ -1,9 +1,8 @@
1{1{
2 "story_string": "{{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}}",2 "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",
3 "example_separator": "Example of an interaction:",3 "example_separator": "Example of an interaction:\n",
4 "chat_start": "This is the history of the roleplay:",4 "chat_start": "This is the history of the roleplay:\n",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Llama 2 Chat.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Llama 3 Instruct.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "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 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Pygmalion.json → default/content/presets/context/Metharme.json+1 -2
@@ -3,9 +3,8 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
10 "name": "Pygmalion"9 "name": "Metharme"
11}10}
default/content/presets/context/Minimalist.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Mistral V1.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Mistral V2 & V3.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Mistral V3-Tekken.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Mistral V7.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/NovelAI.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "***",3 "example_separator": "***",
4 "chat_start": "***",4 "chat_start": "***",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/OldDefault.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "This is how {{char}} should talk",3 "example_separator": "This is how {{char}} should talk",
4 "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n",4 "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Phi.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Story.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Synthia.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/Tulu.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "",3 "example_separator": "",
4 "chat_start": "",4 "chat_start": "",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/context/simple-proxy-for-tavern.json+0 -1
@@ -3,7 +3,6 @@
3 "example_separator": "### New Roleplay:",3 "example_separator": "### New Roleplay:",
4 "chat_start": "### New Roleplay:",4 "chat_start": "### New Roleplay:",
5 "use_stop_strings": false,5 "use_stop_strings": false,
6 "allow_jailbreak": false,
7 "always_force_name2": true,6 "always_force_name2": true,
8 "trim_sentences": false,7 "trim_sentences": false,
9 "single_line": false,8 "single_line": false,
default/content/presets/instruct/ChatML.json+1 -1
@@ -6,7 +6,7 @@
6 "stop_sequence": "<|im_end|>",6 "stop_sequence": "<|im_end|>",
7 "wrap": true,7 "wrap": true,
8 "macro": true,8 "macro": true,
9 "names_behavior": "always",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
default/content/presets/instruct/Command R.json+1 -1
@@ -8,7 +8,7 @@
8 "stop_sequence": "<|END_OF_TURN_TOKEN|>",8 "stop_sequence": "<|END_OF_TURN_TOKEN|>",
9 "wrap": false,9 "wrap": false,
10 "macro": true,10 "macro": true,
11 "names_behavior": "always",11 "names_behavior": "force",
12 "activation_regex": "",12 "activation_regex": "",
13 "skip_examples": false,13 "skip_examples": false,
14 "output_suffix": "<|END_OF_TURN_TOKEN|>",14 "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 @@
1{1{
2 "input_sequence": "<|user|>",2 "input_sequence": "<|user|>\n",
3 "output_sequence": "<|model|>",3 "output_sequence": "<|assistant|>\n",
4 "first_output_sequence": "",
4 "last_output_sequence": "",5 "last_output_sequence": "",
5 "system_sequence": "",6 "system_sequence_prefix": "<|system|>\n",
6 "stop_sequence": "<|user|>",7 "system_sequence_suffix": "",
8 "stop_sequence": "",
7 "wrap": false,9 "wrap": false,
8 "macro": true,10 "macro": true,
9 "names_behavior": "always",11 "names_behavior": "force",
10 "activation_regex": "",12 "activation_regex": "",
11 "system_sequence_prefix": "<|system|>",
12 "system_sequence_suffix": "",
13 "first_output_sequence": "",
14 "skip_examples": false,13 "skip_examples": false,
15 "output_suffix": "",14 "output_suffix": "",
16 "input_suffix": "",15 "input_suffix": "",
16 "system_sequence": "",
17 "system_suffix": "",17 "system_suffix": "",
18 "user_alignment_message": "",18 "user_alignment_message": "",
19 "system_same_as_user": true,
20 "last_system_sequence": "",19 "last_system_sequence": "",
21 "name": "Pygmalion"20 "system_same_as_user": true,
21 "name": "GLM-4"
22}22}
default/content/presets/instruct/Gemma 2.json+1 -1
@@ -6,7 +6,7 @@
6 "stop_sequence": "<end_of_turn>",6 "stop_sequence": "<end_of_turn>",
7 "wrap": true,7 "wrap": true,
8 "macro": true,8 "macro": true,
9 "names_behavior": "none",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
default/content/presets/instruct/Lightning 1.1.json+4 -4
@@ -1,7 +1,7 @@
1{1{
2 "input_sequence": "### Instruction:",2 "input_sequence": "### Instruction:",
3 "output_sequence": "### Response: (length = unlimited)",3 "output_sequence": "### Response:",
4 "last_output_sequence": "",4 "last_output_sequence": "### Response: (length = unlimited)",
5 "system_sequence": "",5 "system_sequence": "",
6 "stop_sequence": "",6 "stop_sequence": "",
7 "wrap": true,7 "wrap": true,
@@ -12,8 +12,8 @@
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
13 "first_output_sequence": "",13 "first_output_sequence": "",
14 "skip_examples": false,14 "skip_examples": false,
15 "output_suffix": "",15 "output_suffix": "\n\n",
16 "input_suffix": "",16 "input_suffix": "\n\n",
17 "system_suffix": "",17 "system_suffix": "",
18 "user_alignment_message": "",18 "user_alignment_message": "",
19 "system_same_as_user": true,19 "system_same_as_user": true,
default/content/presets/instruct/Llama 3 Instruct.json+2 -2
@@ -6,7 +6,7 @@
6 "stop_sequence": "<|eot_id|>",6 "stop_sequence": "<|eot_id|>",
7 "wrap": false,7 "wrap": false,
8 "macro": true,8 "macro": true,
9 "names_behavior": "always",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
@@ -16,7 +16,7 @@
16 "input_suffix": "<|eot_id|>",16 "input_suffix": "<|eot_id|>",
17 "system_suffix": "<|eot_id|>",17 "system_suffix": "<|eot_id|>",
18 "user_alignment_message": "",18 "user_alignment_message": "",
19 "system_same_as_user": true,19 "system_same_as_user": false,
20 "last_system_sequence": "",20 "last_system_sequence": "",
21 "name": "Llama 3 Instruct"21 "name": "Llama 3 Instruct"
22}22}
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 @@
6 "stop_sequence": "",6 "stop_sequence": "",
7 "wrap": false,7 "wrap": false,
8 "macro": true,8 "macro": true,
9 "names_behavior": "always",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
default/content/presets/instruct/Mistral V2 & V3.json+1 -1
@@ -6,7 +6,7 @@
6 "stop_sequence": "",6 "stop_sequence": "",
7 "wrap": false,7 "wrap": false,
8 "macro": true,8 "macro": true,
9 "names_behavior": "always",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
default/content/presets/instruct/Mistral V3-Tekken.json+1 -1
@@ -6,7 +6,7 @@
6 "stop_sequence": "",6 "stop_sequence": "",
7 "wrap": false,7 "wrap": false,
8 "macro": true,8 "macro": true,
9 "names_behavior": "always",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
default/content/presets/instruct/Mistral V7.json+1 -1
@@ -6,7 +6,7 @@
6 "stop_sequence": "</s>",6 "stop_sequence": "</s>",
7 "wrap": false,7 "wrap": false,
8 "macro": true,8 "macro": true,
9 "names_behavior": "always",9 "names_behavior": "force",
10 "activation_regex": "",10 "activation_regex": "",
11 "system_sequence_prefix": "",11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",12 "system_sequence_suffix": "",
default/content/presets/instruct/Phi.json+1 -1
@@ -8,7 +8,7 @@
8 "stop_sequence": "<|end|>",8 "stop_sequence": "<|end|>",
9 "wrap": false,9 "wrap": false,
10 "macro": true,10 "macro": true,
11 "names_behavior": "always",11 "names_behavior": "force",
12 "activation_regex": "",12 "activation_regex": "",
13 "skip_examples": false,13 "skip_examples": false,
14 "output_suffix": "<|end|>\n",14 "output_suffix": "<|end|>\n",
default/content/presets/instruct/Tulu.json+1 -1
@@ -8,7 +8,7 @@
8 "stop_sequence": "<|end_of_text|>",8 "stop_sequence": "<|end_of_text|>",
9 "wrap": false,9 "wrap": false,
10 "macro": true,10 "macro": true,
11 "names_behavior": "always",11 "names_behavior": "force",
12 "activation_regex": "",12 "activation_regex": "",
13 "skip_examples": false,13 "skip_examples": false,
14 "output_suffix": "<|end_of_text|>\n",14 "output_suffix": "<|end_of_text|>\n",
default/content/presets/sysprompt/Actor.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Actor",2 "name": "Actor",
3 "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}}."3 "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": ""
4}5}
default/content/presets/sysprompt/Assistant - Expert.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Assistant - Expert",2 "name": "Assistant - Expert",
3 "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."3 "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": ""
4}5}
default/content/presets/sysprompt/Assistant - Simple.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Assistant - Simple",2 "name": "Assistant - Simple",
3 "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."3 "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": ""
4}5}
default/content/presets/sysprompt/Blank.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Blank",2 "name": "Blank",
3 "content": ""3 "content": "",
4 "post_history": ""
4}5}
default/content/presets/sysprompt/Chain of Thought.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Chain of Thought",2 "name": "Chain of Thought",
3 "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."3 "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": ""
4}5}
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 @@
1{1{
2 "name": "Neutral - Chat",2 "name": "Neutral - Chat",
3 "content": "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}."3 "content": "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}.",
4 "post_history": ""
4}5}
default/content/presets/sysprompt/Roleplay - Detailed.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Roleplay - Detailed",2 "name": "Roleplay - Detailed",
3 "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."3 "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": ""
4}5}
default/content/presets/sysprompt/Roleplay - Immersive.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Roleplay - Immersive",2 "name": "Roleplay - Immersive",
3 "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.]"3 "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": ""
4}5}
default/content/presets/sysprompt/Roleplay - Simple.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Roleplay - Simple",2 "name": "Roleplay - Simple",
3 "content": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}."3 "content": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.",
4 "post_history": ""
4}5}
default/content/presets/sysprompt/Text Adventure.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Text Adventure",2 "name": "Text Adventure",
3 "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.]"3 "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": ""
4}5}
default/content/presets/sysprompt/Writer - Creative.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Writer - Creative",2 "name": "Writer - Creative",
3 "content": "You are an intelligent, skilled, versatile writer.\n\nYour task is to write a role-play based on the information below."3 "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": ""
4}5}
default/content/presets/sysprompt/Writer - Realistic.json+2 -1
@@ -1,4 +1,5 @@
1{1{
2 "name": "Writer - Realistic",2 "name": "Writer - Realistic",
3 "content": "Continue writing this story and portray characters realistically."3 "content": "Continue writing this story and portray characters realistically.",
4 "post_history": ""
4}5}
package-lock.json+2 -2
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.13",3 "version": "1.12.14",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.13",9 "version": "1.12.14",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
package.json+1 -1
@@ -109,7 +109,7 @@
109 "type": "git",109 "type": "git",
110 "url": "https://github.com/SillyTavern/SillyTavern.git"110 "url": "https://github.com/SillyTavern/SillyTavern.git"
111 },111 },
112 "version": "1.12.13",112 "version": "1.12.14",
113 "scripts": {113 "scripts": {
114 "start": "node server.js",114 "start": "node server.js",
115 "debug": "node --inspect server.js",115 "debug": "node --inspect server.js",
post-install.js+3 -229
@@ -3,133 +3,17 @@
3 */3 */
4import fs from 'node:fs';4import fs from 'node:fs';
5import path from 'node:path';5import path from 'node:path';
6import crypto from 'node:crypto';
7import process from 'node:process';6import process from 'node:process';
8import yaml from 'yaml';7import yaml from 'yaml';
9import _ from 'lodash';
10import chalk from 'chalk';8import chalk from 'chalk';
11import { createRequire } from 'node:module';9import { createRequire } from 'node:module';
10import { addMissingConfigValues } from './src/config-init.js';
1211
13/**12/**
14 * Colorizes console output.13 * Colorizes console output.
15 */14 */
16const color = chalk;15const color = chalk;
1716
18const 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 */
118function 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
133/**17/**
134 * Converts the old config.conf file to the new config.yaml format.18 * Converts the old config.conf file to the new config.yaml format.
135 */19 */
@@ -157,71 +41,6 @@ function convertConfig() {
157}41}
15842
159/**43/**
160 * Compares the current config.yaml with the default config.yaml and adds any missing values.
161 */
162function 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/**
225 * Creates the default config files if they don't exist yet.44 * Creates the default config files if they don't exist yet.
226 */45 */
227function createDefaultFiles() {46function createDefaultFiles() {
@@ -283,58 +102,13 @@ function createDefaultFiles() {
283 }102 }
284}103}
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 */
291function 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 */
301function 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
329try {105try {
330 // 0. Convert config.conf to config.yaml106 // 0. Convert config.conf to config.yaml
331 convertConfig();107 convertConfig();
332 // 1. Create default config files108 // 1. Create default config files
333 createDefaultFiles();109 createDefaultFiles();
334 // 2. Copy transformers WASM binaries from node_modules110 // 2. Add missing config values
335 copyWasmFiles();111 addMissingConfigValues(path.join(process.cwd(), './config.yaml'));
336 // 3. Add missing config values
337 addMissingConfigValues();
338} catch (error) {112} catch (error) {
339 console.error(error);113 console.error(error);
340}114}
public/css/rm-groups.css+2 -2
@@ -87,7 +87,7 @@
87}87}
8888
89#rm_group_members:empty::before {89#rm_group_members:empty::before {
90 content: 'Group is empty';90 content: attr(group_empty_text);
9191
92 font-weight: bolder;92 font-weight: bolder;
93 width: 100%;93 width: 100%;
@@ -115,7 +115,7 @@
115}115}
116116
117#rm_group_add_members:empty::before {117#rm_group_add_members:empty::before {
118 content: 'No characters available';118 content: attr(no_characters_text);
119119
120 font-weight: bolder;120 font-weight: bolder;
121 width: 100%;121 width: 100%;
public/css/world-info.css+4 -0
@@ -124,6 +124,10 @@
124 cursor: initial;124 cursor: initial;
125}125}
126126
127.world_entry .inline-drawer-header-pointer {
128 cursor: pointer;
129}
130
127.world_entry .killSwitch {131.world_entry .killSwitch {
128 cursor: pointer;132 cursor: pointer;
129}133}
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 @@
646 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">646 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
647 </div>647 </div>
648 </div>648 </div>
649 <div class="range-block" data-source="openai,custom">649 <div class="range-block" data-source="openai,custom,xai">
650 <div class="range-block-title" data-i18n="Multiple swipes per generation">650 <div class="range-block-title" data-i18n="Multiple swipes per generation">
651 Multiple swipes per generation651 Multiple swipes per generation
652 </div>652 </div>
@@ -685,7 +685,7 @@
685 </span>685 </span>
686 </div>686 </div>
687 </div>687 </div>
688 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek">688 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai">
689 <div class="range-block-title" data-i18n="Temperature">689 <div class="range-block-title" data-i18n="Temperature">
690 Temperature690 Temperature
691 </div>691 </div>
@@ -698,7 +698,7 @@
698 </div>698 </div>
699 </div>699 </div>
700 </div>700 </div>
701 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek">701 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai">
702 <div class="range-block-title" data-i18n="Frequency Penalty">702 <div class="range-block-title" data-i18n="Frequency Penalty">
703 Frequency Penalty703 Frequency Penalty
704 </div>704 </div>
@@ -711,7 +711,7 @@
711 </div>711 </div>
712 </div>712 </div>
713 </div>713 </div>
714 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek">714 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai">
715 <div class="range-block-title" data-i18n="Presence Penalty">715 <div class="range-block-title" data-i18n="Presence Penalty">
716 Presence Penalty716 Presence Penalty
717 </div>717 </div>
@@ -737,7 +737,7 @@
737 </div>737 </div>
738 </div>738 </div>
739 </div>739 </div>
740 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek">740 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai">
741 <div class="range-block-title" data-i18n="Top P">741 <div class="range-block-title" data-i18n="Top P">
742 Top P742 Top P
743 </div>743 </div>
@@ -974,7 +974,7 @@
974 </div>974 </div>
975 </div>975 </div>
976 </div>976 </div>
977 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt">977 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai">
978 <div class="range-block-title justifyLeft" data-i18n="Seed">978 <div class="range-block-title justifyLeft" data-i18n="Seed">
979 Seed979 Seed
980 </div>980 </div>
@@ -1419,7 +1419,7 @@
1419 </div>1419 </div>
1420 </div>1420 </div>
14211421
1422 <div data-tg-type="aphrodite, ooba, koboldcpp, tabby, llamacpp" id="dryBlock" class="wide100p">1422 <div data-tg-type="aphrodite, ooba, koboldcpp, tabby, llamacpp, dreamgen" id="dryBlock" class="wide100p">
1423 <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">1423 <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">
1424 <label data-i18n="DRY Repetition Penalty">DRY Repetition Penalty</label>1424 <label data-i18n="DRY Repetition Penalty">DRY Repetition Penalty</label>
1425 <a href="https://github.com/oobabooga/text-generation-webui/pull/5677" target="_blank">1425 <a href="https://github.com/oobabooga/text-generation-webui/pull/5677" target="_blank">
@@ -1574,7 +1574,7 @@
1574 <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>1574 <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>
1575 </label>1575 </label>
1576 </label>1576 </label>
1577 <label data-tg-type="ooba, llamacpp, tabby, koboldcpp" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">1577 <label data-tg-type="ooba, llamacpp, tabby, koboldcpp, dreamgen" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
1578 <input type="checkbox" id="ban_eos_token_textgenerationwebui" />1578 <input type="checkbox" id="ban_eos_token_textgenerationwebui" />
1579 <label>1579 <label>
1580 <small data-i18n="Ban EOS Token">Ban EOS Token</small>1580 <small data-i18n="Ban EOS Token">Ban EOS Token</small>
@@ -1963,9 +1963,12 @@
1963 <span data-i18n="Use search capabilities provided by the backend.">1963 <span data-i18n="Use search capabilities provided by the backend.">
1964 Use search capabilities provided by the backend.1964 Use search capabilities provided by the backend.
1965 </span>1965 </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>
1966 </div>1969 </div>
1967 </div>1970 </div>
1968 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,ai21">1971 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,ai21,xai">
1969 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1972 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1970 <input id="openai_function_calling" type="checkbox" />1973 <input id="openai_function_calling" type="checkbox" />
1971 <span data-i18n="Enable function calling">Enable function calling</span>1974 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1975,7 +1978,7 @@
1975 <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>1978 <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
1976 </div>1979 </div>
1977 </div>1980 </div>
1978 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom,01ai">1981 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,claude,custom,01ai,xai">
1979 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">1982 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
1980 <input id="openai_image_inlining" type="checkbox" />1983 <input id="openai_image_inlining" type="checkbox" />
1981 <span data-i18n="Send inline images">Send inline images</span>1984 <span data-i18n="Send inline images">Send inline images</span>
@@ -1987,7 +1990,7 @@
1987 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>1990 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
1988 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>1991 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
1989 </div>1992 </div>
1990 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">1993 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai">
1991 <div class="flex-container oneline-dropdown">1994 <div class="flex-container oneline-dropdown">
1992 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">1995 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
1993 Inline Image Quality1996 Inline Image Quality
@@ -2022,7 +2025,7 @@
2022 <input id="use_makersuite_sysprompt" type="checkbox" />2025 <input id="use_makersuite_sysprompt" type="checkbox" />
2023 <span>2026 <span>
2024 <span data-i18n="Use system prompt">Use system prompt</span>2027 <span data-i18n="Use system prompt">Use system prompt</span>
2025 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 1.5/2.0 Pro/Flash"></i>2028 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 1.5+, LearnLM"></i>
2026 </span>2029 </span>
2027 </label>2030 </label>
2028 <div class="toggle-description justifyLeft marginBot5">2031 <div class="toggle-description justifyLeft marginBot5">
@@ -2031,7 +2034,7 @@
2031 </span>2034 </span>
2032 </div>2035 </div>
2033 </div>2036 </div>
2034 <div class="range-block" data-source="deepseek,openrouter,custom,claude">2037 <div class="range-block" data-source="deepseek,openrouter,custom,claude,xai">
2035 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2038 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
2036 <input id="openai_show_thoughts" type="checkbox" />2039 <input id="openai_show_thoughts" type="checkbox" />
2037 <span>2040 <span>
@@ -2045,16 +2048,20 @@
2045 </span>2048 </span>
2046 </div>2049 </div>
2047 </div>2050 </div>
2048 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude">2051 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,openrouter">
2049 <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.">2052 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#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.">
2050 <label for="openai_reasoning_effort">2053 <label for="openai_reasoning_effort">
2051 <span data-i18n="Reasoning Effort">Reasoning Effort</span>2054 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
2052 <i data-source="claude" class="opacity50p fa-solid fa-circle-info" title="Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%), but minimum 1024 tokens."></i>2055 <i data-source="openai,custom,xai,openrouter" class="opacity50p fa-solid fa-circle-info" title="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level." data-i18n="[title]OpenAI-style options: low, medium, high. Minimum and maximum are aliased 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>
2053 </label>2057 </label>
2054 <select id="openai_reasoning_effort">2058 <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>
2055 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>2061 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
2056 <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>2062 <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option>
2057 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>2063 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2064 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
2058 </select>2065 </select>
2059 </div>2066 </div>
2060 </div>2067 </div>
@@ -2491,8 +2498,8 @@
2491 <option value="search" data-i18n="Search" hidden>A-Z</option>2498 <option value="search" data-i18n="Search" hidden>A-Z</option>
2492 <option value="asc">A-Z</option>2499 <option value="asc">A-Z</option>
2493 <option value="desc">Z-A</option>2500 <option value="desc">Z-A</option>
2494 <option value="date_asc">Date Asc</option>2501 <option data-i18n="Date Asc" value="date_asc">Date Asc</option>
2495 <option value="date_desc">Date Desc</option>2502 <option data-i18n="Date Desc" value="date_desc">Date Desc</option>
2496 </select>2503 </select>
2497 <select id="featherless_category_selection" class="text_pole">2504 <select id="featherless_category_selection" class="text_pole">
2498 <option value="" disabled selected data-i18n="category">category</option>2505 <option value="" disabled selected data-i18n="category">category</option>
@@ -2502,7 +2509,7 @@
2502 <option value="All" data-i18n="All">All</option>2509 <option value="All" data-i18n="All">All</option>
2503 </select>2510 </select>
2504 <select id="featherless_class_selection" class="text_pole">2511 <select id="featherless_class_selection" class="text_pole">
2505 <option value="" selected data-i18n="class">All Classes</option>2512 <option value="" selected data-i18n="All Classes">All Classes</option>
2506 </select>2513 </select>
2507 <div id="featherless_model_pagination_container" class="flex1"></div>2514 <div id="featherless_model_pagination_container" class="flex1"></div>
2508 <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>2515 <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 @@
2615 </div>2622 </div>
2616 <div data-tg-type="ollama">2623 <div data-tg-type="ollama">
2617 <div class="flex-container flexFlowColumn">2624 <div class="flex-container flexFlowColumn">
2618 <a href="https://github.com/jmorganca/ollama" target="_blank">2625 <a href="https://github.com/ollama/ollama" target="_blank">
2619 jmorganca/ollama2626 ollama/ollama
2620 </a>2627 </a>
2621 </div>2628 </div>
2622 <div class="flex1">2629 <div class="flex1">
@@ -2756,9 +2763,10 @@
2756 <option value="perplexity">Perplexity</option>2763 <option value="perplexity">Perplexity</option>
2757 <option value="scale">Scale</option>2764 <option value="scale">Scale</option>
2758 <option value="windowai">Window AI</option>2765 <option value="windowai">Window AI</option>
2766 <option value="xai">xAI (Grok)</option>
2759 </optgroup>2767 </optgroup>
2760 </select>2768 </select>
2761 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">2769 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
2762 <div class="inline-drawer-toggle inline-drawer-header">2770 <div class="inline-drawer-toggle inline-drawer-header">
2763 <b data-i18n="Reverse Proxy">Reverse Proxy</b>2771 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
2764 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>2772 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2821,7 +2829,7 @@
2821 </div>2829 </div>
2822 </div>2830 </div>
2823 </div>2831 </div>
2824 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">2832 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
2825 <div class="reverse_proxy_warning">2833 <div class="reverse_proxy_warning">
2826 <b>2834 <b>
2827 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">2835 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -2880,12 +2888,17 @@
2880 </optgroup>2888 </optgroup>
2881 <optgroup label="GPT-4o mini">2889 <optgroup label="GPT-4o mini">
2882 <option value="gpt-4o-mini">gpt-4o-mini</option>2890 <option value="gpt-4o-mini">gpt-4o-mini</option>
2883 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>2891 <option value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</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">
2886 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>2894 <option value="gpt-4.1">gpt-4.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>
2887 </optgroup>2900 </optgroup>
2888 <optgroup label="o1 and o1-mini">2901 <optgroup label="o1">
2889 <option value="o1">o1</option>2902 <option value="o1">o1</option>
2890 <option value="o1-2024-12-17">o1-2024-12-17</option>2903 <option value="o1-2024-12-17">o1-2024-12-17</option>
2891 <option value="o1-mini">o1-mini</option>2904 <option value="o1-mini">o1-mini</option>
@@ -2894,9 +2907,15 @@
2894 <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>2907 <option value="o1-preview-2024-09-12">o1-preview-2024-09-12</option>
2895 </optgroup>2908 </optgroup>
2896 <optgroup label="o3">2909 <optgroup label="o3">
2910 <option value="o3">o3</option>
2911 <option value="o3-2025-04-16">o3-2025-04-16</option>
2897 <option value="o3-mini">o3-mini</option>2912 <option value="o3-mini">o3-mini</option>
2898 <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>2913 <option value="o3-mini-2025-01-31">o3-mini-2025-01-31</option>
2899 </optgroup>2914 </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>
2900 <optgroup label="GPT-4.5">2919 <optgroup label="GPT-4.5">
2901 <option value="gpt-4.5-preview">gpt-4.5-preview</option>2920 <option value="gpt-4.5-preview">gpt-4.5-preview</option>
2902 <option value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>2921 <option value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
@@ -3127,48 +3146,49 @@
3127 <div>3146 <div>
3128 <h4 data-i18n="Google Model">Google Model</h4>3147 <h4 data-i18n="Google Model">Google Model</h4>
3129 <select id="model_google_select">3148 <select id="model_google_select">
3130 <optgroup label="Primary">3149 <optgroup label="Gemini 2.5">
3131 <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>3150 <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
3132 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>3151 <option value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>
3133 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>3152 <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-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>
3137 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>3156 <option value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 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>
3138 </optgroup>3182 </optgroup>
3139 <optgroup label="Gemma">3183 <optgroup label="Gemma">
3140 <option value="gemma-3-27b-it">Gemma 3 27B</option>3184 <option value="gemma-3-27b-it">gemma-3-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>
3141 </optgroup>3188 </optgroup>
3142 <optgroup label="Subversions">3189 <optgroup label="LearnLM">
3143 <option value="gemini-2.5-pro-preview-03-25">Gemini 2.5 Pro Preview 2025-03-25</option>3190 <option value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
3144 <option value="gemini-2.5-pro-exp-03-25">Gemini 2.5 Pro Experimental 2025-03-25</option>3191 <option value="learnlm-1.5-pro-experimental">learnlm-1.5-pro-experimental</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>
3172 </optgroup>3192 </optgroup>
3173 </select>3193 </select>
3174 </div>3194 </div>
@@ -3424,6 +3444,31 @@
3424 <select id="model_01ai_select">3444 <select id="model_01ai_select">
3425 </select>3445 </select>
3426 </div>3446 </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>
3427 <div id="prompt_post_porcessing_form" data-source="custom,openrouter">3472 <div id="prompt_post_porcessing_form" data-source="custom,openrouter">
3428 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>3473 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
3429 <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.">3474 <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 @@
3458 <div class="drawer-toggle">3503 <div class="drawer-toggle">
3459 <div class="drawer-icon fa-solid fa-font fa-fw closedIcon" title="AI Response Formatting" data-i18n="[title]AI Response Formatting"></div>3504 <div class="drawer-icon fa-solid fa-font fa-fw closedIcon" title="AI Response Formatting" data-i18n="[title]AI Response Formatting"></div>
3460 </div>3505 </div>
3461 <div id="AdvancedFormatting" class="drawer-content">3506 <div id="AdvancedFormatting" class="drawer-content closedDrawer">
3462 <div class="flex-container alignItemsBaseline">3507 <div class="flex-container alignItemsBaseline">
3463 <h3 class="margin0 flex1 flex-container alignItemsBaseline">3508 <h3 class="margin0 flex1 flex-container alignItemsBaseline">
3464 <span data-i18n="Advanced Formatting">3509 <span data-i18n="Advanced Formatting">
@@ -3577,11 +3622,6 @@
3577 <small data-i18n="Names as Stop Strings">Names as Stop Strings</small>3622 <small data-i18n="Names as Stop Strings">Names as Stop Strings</small>
3578 </label>3623 </label>
3579 </div>3624 </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>
3585 </div>3625 </div>
3586 </div>3626 </div>
3587 </div>3627 </div>
@@ -3783,9 +3823,7 @@
3783 </label>3823 </label>
3784 </div>3824 </div>
3785 </h4>3825 </h4>
3786 <div id="SystemPromptBlock">3826 <div id="SystemPromptBlock" class="marginBot10">
3787
3788
3789 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">3827 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
3790 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>3828 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>
3791 <div class="flex-container margin0 justifyCenter gap3px">3829 <div class="flex-container margin0 justifyCenter gap3px">
@@ -3807,10 +3845,14 @@
3807 </label>3845 </label>
3808 <textarea id="sysprompt_content" class="text_pole textarea_compact autoSetHeight"></textarea>3846 <textarea id="sysprompt_content" class="text_pole textarea_compact autoSetHeight"></textarea>
3809 </div>3847 </div>
3810 </div>
38113848
3812 <div>3849 <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>
3814 </div>3856 </div>
38153857
3816 <div>3858 <div>
@@ -5113,7 +5155,7 @@
5113 <div id="persona_depth_position_settings" class="flex-container">5155 <div id="persona_depth_position_settings" class="flex-container">
5114 <div class="flex1">5156 <div class="flex1">
5115 <label for="persona_depth_value" data-i18n="Depth:">Depth:</label>5157 <label for="persona_depth_value" data-i18n="Depth:">Depth:</label>
5116 <input id="persona_depth_value" class="text_pole" type="number" min="0" max="999" step="1">5158 <input id="persona_depth_value" class="text_pole" type="number" min="0" max="9999" step="1">
5117 </div>5159 </div>
5118 <div class="flex1">5160 <div class="flex1">
5119 <label for="persona_depth_role" data-i18n="Role:">Role:</label>5161 <label for="persona_depth_role" data-i18n="Role:">Role:</label>
@@ -5489,7 +5531,7 @@
5489 <div class="inline-drawer-content">5531 <div class="inline-drawer-content">
5490 <div id="currentGroupMembers" name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">5532 <div id="currentGroupMembers" name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">
5491 <div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div>5533 <div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div>
5492 <div id="rm_group_members" class="rm_group_members overflowYAuto flex-container"></div>5534 <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>
5493 </div>5535 </div>
5494 </div>5536 </div>
5495 </div>5537 </div>
@@ -5507,7 +5549,7 @@
5507 <div class="tags rm_tag_filter"></div>5549 <div class="tags rm_tag_filter"></div>
5508 </div>5550 </div>
5509 <div id="rm_group_add_members_pagination" class="group_pagination"></div>5551 <div id="rm_group_add_members_pagination" class="group_pagination"></div>
5510 <div id="rm_group_add_members" class="overflowYAuto flex-container"></div>5552 <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>
5511 </div>5553 </div>
5512 </div>5554 </div>
5513 </div>5555 </div>
@@ -5766,7 +5808,7 @@
5766 @ Depth5808 @ Depth
5767 </span>5809 </span>
5768 </h4>5810 </h4>
5769 <input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole textarea_compact m-t-0" type="number" min="0" max="999" value="4" form="form_create" />5811 <input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole textarea_compact m-t-0" type="number" min="0" max="9999" value="4" form="form_create" />
5770 <h4>5812 <h4>
5771 <span data-i18n="Role">5813 <span data-i18n="Role">
5772 Role5814 Role
@@ -5927,7 +5969,7 @@
5927 <div class="tag_view_color_picker" data-value="color"></div>5969 <div class="tag_view_color_picker" data-value="color"></div>
5928 <div class="tag_view_color_picker" data-value="color2"></div>5970 <div class="tag_view_color_picker" data-value="color2"></div>
5929 <div class="tag_view_name" contenteditable="true"></div>5971 <div class="tag_view_name" contenteditable="true"></div>
5930 <div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;entries</div>5972 <div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;<span data-i18n="tag_entries">entries</span></div>
5931 <div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>5973 <div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>
5932 </div>5974 </div>
5933 </div>5975 </div>
@@ -5987,11 +6029,11 @@
5987 </div>6029 </div>
5988 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">6030 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">
5989 <label for="depth" class="WIEntryHeaderTitleMobile" data-i18n="Depth:">Depth:</label>6031 <label for="depth" class="WIEntryHeaderTitleMobile" data-i18n="Depth:">Depth:</label>
5990 <input title="Depth" class="text_pole wideMax100px margin0" type="number" name="depth" data-i18n="[title]Depth" placeholder="" min="0" max="999" />6032 <input title="Depth" class="text_pole wideMax100px margin0" type="number" name="depth" data-i18n="[title]Depth" placeholder="" min="0" max="9999" />
5991 </div>6033 </div>
5992 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">6034 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">
5993 <label for="order" class="WIEntryHeaderTitleMobile" data-i18n="Order:">Order:</label>6035 <label for="order" class="WIEntryHeaderTitleMobile" data-i18n="Order:">Order:</label>
5994 <input title="Order" data-i18n="[title]Order" class="text_pole wideMax100px margin0" type="number" name="order" placeholder="" min="0" max="999" />6036 <input title="Order" data-i18n="[title]Order" class="text_pole wideMax100px margin0" type="number" name="order" placeholder="" min="0" max="9999" />
5995 </div>6037 </div>
5996 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap probabilityContainer">6038 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap probabilityContainer">
5997 <label for="order" class="WIEntryHeaderTitleMobile" data-i18n="Trigger %:">Trigger %:</label>6039 <label for="order" class="WIEntryHeaderTitleMobile" data-i18n="Trigger %:">Trigger %:</label>
@@ -6247,6 +6289,54 @@
6247 </label>6289 </label>
6248 </div>6290 </div>
6249 </div>6291 </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>
6250 </div>6340 </div>
6251 </div>6341 </div>
6252 </form>6342 </form>
@@ -6353,7 +6443,7 @@
6353 <span data-i18n="prompt_manager_depth">Depth</span>6443 <span data-i18n="prompt_manager_depth">Depth</span>
6354 </label>6444 </label>
6355 <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>6445 <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>
6356 <input id="completion_prompt_manager_popup_entry_form_injection_depth" class="text_pole" type="number" name="injection_depth" min="0" max="999" value="4" />6446 <input id="completion_prompt_manager_popup_entry_form_injection_depth" class="text_pole" type="number" name="injection_depth" min="0" max="9999" value="4" />
6357 </div>6447 </div>
6358 </div>6448 </div>
6359 <div class="completion_prompt_manager_popup_entry_form_control">6449 <div class="completion_prompt_manager_popup_entry_form_control">
@@ -6614,7 +6704,7 @@
6614 </div>6704 </div>
6615 <div class="flex-container wide100pLess70px character_select_container height100p alignitemscenter">6705 <div class="flex-container wide100pLess70px character_select_container height100p alignitemscenter">
6616 <div class="wide100p character_name_block">6706 <div class="wide100p character_name_block">
6617 <span class="ch_name">Go back</span>6707 <span class="ch_name" data-i18n="Go back">Go back</span>
6618 </div>6708 </div>
6619 </div>6709 </div>
6620 </div>6710 </div>
@@ -6713,7 +6803,7 @@
6713 <label class="checkbox_label alignItemsCenter" for="extension_floating_position_depth">6803 <label class="checkbox_label alignItemsCenter" for="extension_floating_position_depth">
6714 <input type="radio" id="extension_floating_position_depth" name="extension_floating_position" value="1" />6804 <input type="radio" id="extension_floating_position_depth" name="extension_floating_position" value="1" />
6715 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>6805 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
6716 <input id="extension_floating_depth" class="text_pole textarea_compact widthNatural" type="number" min="0" max="999" />6806 <input id="extension_floating_depth" class="text_pole textarea_compact widthNatural" type="number" min="0" max="9999" />
6717 <span data-i18n="as">as</span>6807 <span data-i18n="as">as</span>
6718 <select id="extension_floating_role" class="text_pole widthNatural">6808 <select id="extension_floating_role" class="text_pole widthNatural">
6719 <option data-i18n="System" value="0">System</option>6809 <option data-i18n="System" value="0">System</option>
@@ -6728,7 +6818,7 @@
6728 <span data-i18n="Insertion Frequency">Insertion Frequency</span>6818 <span data-i18n="Insertion Frequency">Insertion Frequency</span>
6729 <small data-i18n="(0 = Disable, 1 = Always)">(0 = Disable, 1 = Always)</small>6819 <small data-i18n="(0 = Disable, 1 = Always)">(0 = Disable, 1 = Always)</small>
6730 </label>6820 </label>
6731 <input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="999" />6821 <input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="9999" />
6732 </div>6822 </div>
6733 <br>6823 <br>
6734 <span><span data-i18n="User inputs until next insertion:">User inputs until next insertion:</span> <span id="extension_floating_counter">(disabled)</span></span>6824 <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 @@
6798 <label class="checkbox_label alignItemsCenter" for="extension_default_position_depth">6888 <label class="checkbox_label alignItemsCenter" for="extension_default_position_depth">
6799 <input type="radio" id="extension_default_position_depth" name="extension_default_position" value="1" />6889 <input type="radio" id="extension_default_position_depth" name="extension_default_position" value="1" />
6800 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>6890 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
6801 <input id="extension_default_depth" class="text_pole textarea_compact widthNatural" type="number" min="0" max="999" />6891 <input id="extension_default_depth" class="text_pole textarea_compact widthNatural" type="number" min="0" max="9999" />
6802 <span data-i18n="as">as</span>6892 <span data-i18n="as">as</span>
6803 <select id="extension_default_role" class="text_pole widthNatural">6893 <select id="extension_default_role" class="text_pole widthNatural">
6804 <option data-i18n="System" value="0">System</option>6894 <option data-i18n="System" value="0">System</option>
@@ -6812,7 +6902,7 @@
6812 <span data-i18n="Insertion Frequency">Insertion Frequency</span>6902 <span data-i18n="Insertion Frequency">Insertion Frequency</span>
6813 <small data-i18n="(0 = Disable, 1 = Always)">(0 = Disable, 1 = Always)</small>6903 <small data-i18n="(0 = Disable, 1 = Always)">(0 = Disable, 1 = Always)</small>
6814 </label>6904 </label>
6815 <input id="extension_default_interval" class="text_pole widthUnset" type="number" min="0" max="999" />6905 <input id="extension_default_interval" class="text_pole widthUnset" type="number" min="0" max="9999" />
6816 </div>6906 </div>
6817 </div>6907 </div>
6818 </div>6908 </div>
public/locales/ar-sa.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "بداية الدردشة",411 "Chat Start": "بداية الدردشة",
412 "Add Chat Start and Example Separator to a list of stopping strings.": "أضف بداية الدردشة وفاصل الأمثلة إلى قائمة سلاسل التوقف.",412 "Add Chat Start and Example Separator to a list of stopping strings.": "أضف بداية الدردشة وفاصل الأمثلة إلى قائمة سلاسل التوقف.",
413 "Use as Stop Strings": "استخدم كسلاسل التوقف",413 "Use as Stop Strings": "استخدم كسلاسل التوقف",
414 "context_allow_jailbreak": "يتضمن كسر الحماية في نهاية المطالبة، إذا تم تحديده في بطاقة الشخصية و''Prefer Char. تم تمكين الهروب من السجن.\nلا يُنصح بهذا بالنسبة لنماذج إكمال النص، فقد يؤدي إلى نتائج سيئة.",
415 "Allow Jailbreak": "السماح بالجيلبريك",414 "Allow Jailbreak": "السماح بالجيلبريك",
416 "Context Order": "ترتيب السياق",415 "Context Order": "ترتيب السياق",
417 "Summary": "ملخص",416 "Summary": "ملخص",
public/locales/de-de.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Chat-Start",411 "Chat Start": "Chat-Start",
412 "Add Chat Start and Example Separator to a list of stopping strings.": "Fügen Sie einer Liste von Stoppzeichenfolgen „Chat-Start“ und „Beispieltrennzeichen“ hinzu.",412 "Add Chat Start and Example Separator to a list of stopping strings.": "Fügen Sie einer Liste von Stoppzeichenfolgen „Chat-Start“ und „Beispieltrennzeichen“ hinzu.",
413 "Use as Stop Strings": "Verwende als Stoppzeichenfolgen",413 "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.",
415 "Allow Jailbreak": "Jailbreak zulassen",414 "Allow Jailbreak": "Jailbreak zulassen",
416 "Context Order": "Kontextreihenfolge",415 "Context Order": "Kontextreihenfolge",
417 "Summary": "Zusammenfassung",416 "Summary": "Zusammenfassung",
public/locales/es-es.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Inicio de chat",411 "Chat Start": "Inicio de chat",
412 "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.",412 "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.",
413 "Use as Stop Strings": "Usar como Cadenas de Parada",413 "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.",
415 "Allow Jailbreak": "Permitir Jailbreak",414 "Allow Jailbreak": "Permitir Jailbreak",
416 "Context Order": "Orden de contexto",415 "Context Order": "Orden de contexto",
417 "Summary": "Resumen",416 "Summary": "Resumen",
public/locales/fr-fr.json+0 -1
@@ -1448,7 +1448,6 @@
1448 "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.",1448 "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.",
1449 "Names as Stop Strings": "Noms comme chaînes d'arrêt",1449 "Names as Stop Strings": "Noms comme chaînes d'arrêt",
1450 "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.",1450 "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",
1452 "Instruct Template": "Modèle d'instruction",1451 "Instruct Template": "Modèle d'instruction",
1453 "instruct_derived": "Dériver des métadonnées du modèle, si possible.",1452 "instruct_derived": "Dériver des métadonnées du modèle, si possible.",
1454 "instruct_enabled": "Activer le mode d'instruction",1453 "instruct_enabled": "Activer le mode d'instruction",
public/locales/is-is.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Chat Start",411 "Chat Start": "Chat Start",
412 "Add Chat Start and Example Separator to a list of stopping strings.": "Bættu Chat Start og Example Separator við lista yfir stöðvunarstrengi.",412 "Add Chat Start and Example Separator to a list of stopping strings.": "Bættu Chat Start og Example Separator við lista yfir stöðvunarstrengi.",
413 "Use as Stop Strings": "Nota sem Stoppa Strengir",413 "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.",
415 "Allow Jailbreak": "Leyfa jailbreak",414 "Allow Jailbreak": "Leyfa jailbreak",
416 "Context Order": "Samhengisröð",415 "Context Order": "Samhengisröð",
417 "Summary": "Samantekt",416 "Summary": "Samantekt",
public/locales/it-it.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Inizio chat",411 "Chat Start": "Inizio chat",
412 "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.",412 "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.",
413 "Use as Stop Strings": "Usa come stringhe di arresto",413 "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.",
415 "Allow Jailbreak": "Consenti jailbreak",414 "Allow Jailbreak": "Consenti jailbreak",
416 "Context Order": "Ordine del contesto",415 "Context Order": "Ordine del contesto",
417 "Summary": "Riepilogo",416 "Summary": "Riepilogo",
public/locales/ja-jp.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "チャット開始",411 "Chat Start": "チャット開始",
412 "Add Chat Start and Example Separator to a list of stopping strings.": "停止文字列のリストにチャット開始と例の区切り文字を追加します。",412 "Add Chat Start and Example Separator to a list of stopping strings.": "停止文字列のリストにチャット開始と例の区切り文字を追加します。",
413 "Use as Stop Strings": "ストップ文字列として使用",413 "Use as Stop Strings": "ストップ文字列として使用",
414 "context_allow_jailbreak": "文字カードで定義されていて、「文字 Jailbreak を優先」が有効になっている場合は、プロンプトの最後に Jailbreak が含まれます。\nこれはテキスト補完モデルには推奨されません。出力が悪くなる可能性があります。",
415 "Allow Jailbreak": "脱獄を許可する",414 "Allow Jailbreak": "脱獄を許可する",
416 "Context Order": "コンテキスト順序",415 "Context Order": "コンテキスト順序",
417 "Summary": "まとめ",416 "Summary": "まとめ",
public/locales/ko-kr.json+0 -2
@@ -421,7 +421,6 @@
421 "Chat Start": "채팅 시작",421 "Chat Start": "채팅 시작",
422 "Add Chat Start and Example Separator to a list of stopping strings.": "중지 문자열 목록에 채팅 시작 및 예제 구분 기호를 추가합니다.",422 "Add Chat Start and Example Separator to a list of stopping strings.": "중지 문자열 목록에 채팅 시작 및 예제 구분 기호를 추가합니다.",
423 "Use as Stop Strings": "중지 문자열로 사용",423 "Use as Stop Strings": "중지 문자열로 사용",
424 "context_allow_jailbreak": "캐릭터 카드에 정의되어 있고 ''Prefer Char. Jailbreak''가 활성화되어 있는 경우 프롬프트 끝에 Jailbreak를 포함합니다.\n이는 텍스트 완성 모델에 권장되지 않으며, 나쁜 출력으로 이어질 수 있습니다.",
425 "Allow Jailbreak": "탈옥 허용",424 "Allow Jailbreak": "탈옥 허용",
426 "Context Order": "컨텍스트 순서",425 "Context Order": "컨텍스트 순서",
427 "Summary": "요약",426 "Summary": "요약",
@@ -1520,7 +1519,6 @@
1520 "Always": "항상 추가함",1519 "Always": "항상 추가함",
1521 "Separators as Stop Strings": "구분 기호를 정지 문자열로 사용하기",1520 "Separators as Stop Strings": "구분 기호를 정지 문자열로 사용하기",
1522 "Names as Stop Strings": "캐릭터의 이름들을 정지 문자열로 사용하기",1521 "Names as Stop Strings": "캐릭터의 이름들을 정지 문자열로 사용하기",
1523 "Allow Post-History Instructions": "Post-History 지침 허용",
1524 "Image Captioning": "이미지 캡셔닝",1522 "Image Captioning": "이미지 캡셔닝",
1525 "Automatically caption images": "자동으로 이미지에 대한 설명 문장으로 나타내기",1523 "Automatically caption images": "자동으로 이미지에 대한 설명 문장으로 나타내기",
1526 "Edit captions before saving": "저장하기 전에 이미지에 대한 설명 문장 편집하기",1524 "Edit captions before saving": "저장하기 전에 이미지에 대한 설명 문장 편집하기",
public/locales/nl-nl.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Chatstart",411 "Chat Start": "Chatstart",
412 "Add Chat Start and Example Separator to a list of stopping strings.": "Voeg Chat Start en Voorbeeldscheidingsteken toe aan een lijst met stoptekenreeksen.",412 "Add Chat Start and Example Separator to a list of stopping strings.": "Voeg Chat Start en Voorbeeldscheidingsteken toe aan een lijst met stoptekenreeksen.",
413 "Use as Stop Strings": "Gebruik als stopreeksen",413 "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.",
415 "Allow Jailbreak": "Jailbreak toestaan",414 "Allow Jailbreak": "Jailbreak toestaan",
416 "Context Order": "Contextvolgorde",415 "Context Order": "Contextvolgorde",
417 "Summary": "Samenvatting",416 "Summary": "Samenvatting",
public/locales/pt-pt.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Início do Chat",411 "Chat Start": "Início do Chat",
412 "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.",412 "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.",
413 "Use as Stop Strings": "Usar como Strings de Parada",413 "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.",
415 "Allow Jailbreak": "Permitir jailbreak",414 "Allow Jailbreak": "Permitir jailbreak",
416 "Context Order": "Ordem de Contexto",415 "Context Order": "Ordem de Contexto",
417 "Summary": "Resumo",416 "Summary": "Resumo",
public/locales/ru-ru.json+105 -24
@@ -52,7 +52,7 @@
52 "Presence Penalty": "Штраф за присутствие",52 "Presence Penalty": "Штраф за присутствие",
53 "Top A": "Top А",53 "Top A": "Top А",
54 "Tail Free Sampling": "Tail Free Sampling",54 "Tail Free Sampling": "Tail Free Sampling",
55 "Rep. Pen. Slope": "Rep. Pen. Slope",55 "Rep. Pen. Slope": "Рост штрафа за повтор к концу промпта",
56 "Top K": "Top K",56 "Top K": "Top K",
57 "Top P": "Top P",57 "Top P": "Top P",
58 "Do Sample": "Включить сэмплинг",58 "Do Sample": "Включить сэмплинг",
@@ -162,9 +162,9 @@
162 "Story String": "Строка истории",162 "Story String": "Строка истории",
163 "Example Separator": "Разделитель примеров сообщений",163 "Example Separator": "Разделитель примеров сообщений",
164 "Chat Start": "Начало чата",164 "Chat Start": "Начало чата",
165 "Activation Regex": "Regex для активации",165 "Activation Regex": "Рег. выражение для активации",
166 "Instruct Mode": "Режим Instruct",166 "Instruct Mode": "Режим Instruct",
167 "Wrap Sequences with Newline": "Отделять строки символом новой строки",167 "Wrap Sequences with Newline": "Каждая строка из шаблона на новой строке",
168 "Include Names": "Добавлять имена",168 "Include Names": "Добавлять имена",
169 "Force for Groups and Personas": "Также для групп и персон",169 "Force for Groups and Personas": "Также для групп и персон",
170 "System Prompt": "Системный промпт",170 "System Prompt": "Системный промпт",
@@ -299,7 +299,7 @@
299 "AI Horde": "AI Horde",299 "AI Horde": "AI Horde",
300 "NovelAI": "NovelAI",300 "NovelAI": "NovelAI",
301 "OpenAI API key": "Ключ для API OpenAI",301 "OpenAI API key": "Ключ для API OpenAI",
302 "Trim spaces": "Обрезать пробелы",302 "Trim spaces": "Обрезать пробелы в начале и конце",
303 "Trim Incomplete Sentences": "Удалять неоконченные предложения",303 "Trim Incomplete Sentences": "Удалять неоконченные предложения",
304 "Include Newline": "Добавлять новую строку",304 "Include Newline": "Добавлять новую строку",
305 "Non-markdown strings": "Строки без разметки",305 "Non-markdown strings": "Строки без разметки",
@@ -510,7 +510,7 @@
510 "New preset": "Новый пресет",510 "New preset": "Новый пресет",
511 "Delete preset": "Удалить пресет",511 "Delete preset": "Удалить пресет",
512 "API Connections": "Соединения с API",512 "API Connections": "Соединения с API",
513 "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь с плохими ответами ставя в очередь только подтвержденных работников. Может замедлить время ответа.",513 "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь при плохих ответах, делая запросы только к доверенным рабочим машинам. Может замедлить время ответа.",
514 "Clear your API key": "Стереть ключ от API",514 "Clear your API key": "Стереть ключ от API",
515 "Refresh models": "Обновить модели",515 "Refresh models": "Обновить модели",
516 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai",516 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai",
@@ -551,7 +551,7 @@
551 "Token counts may be inaccurate and provided just for reference.": "Счетчик токенов может быть неточным, используйте как ориентир",551 "Token counts may be inaccurate and provided just for reference.": "Счетчик токенов может быть неточным, используйте как ориентир",
552 "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа",552 "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа",
553 "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]",553 "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]",
554 "Toggle grid view": "Переключить вид сетки",554 "Toggle grid view": "Сменить вид сетки",
555 "Add to Favorites": "Добавить в Избранное",555 "Add to Favorites": "Добавить в Избранное",
556 "Advanced Definition": "Расширенное описание",556 "Advanced Definition": "Расширенное описание",
557 "Character Lore": "Лор персонажа",557 "Character Lore": "Лор персонажа",
@@ -624,7 +624,7 @@
624 "UI Theme": "Тема UI",624 "UI Theme": "Тема UI",
625 "This message is invisible for the AI": "Это сообщение невидимо для ИИ",625 "This message is invisible for the AI": "Это сообщение невидимо для ИИ",
626 "Sampler Priority": "Приоритет сэмплеров",626 "Sampler Priority": "Приоритет сэмплеров",
627 "Ooba only. Determines the order of samplers.": "Только oobabooga. Определяет порядок сэмплеров.",627 "Ooba only. Determines the order of samplers.": "Только для oobabooga. Определяет порядок сэмплеров.",
628 "Load default order": "Загрузить стандартный порядок",628 "Load default order": "Загрузить стандартный порядок",
629 "Max Tokens Second": "Макс. кол-во токенов в секунду",629 "Max Tokens Second": "Макс. кол-во токенов в секунду",
630 "CFG": "CFG",630 "CFG": "CFG",
@@ -695,7 +695,7 @@
695 "Medium": "Средний",695 "Medium": "Средний",
696 "Aggressive": "Агрессивный",696 "Aggressive": "Агрессивный",
697 "Very aggressive": "Очень агрессивный",697 "Very aggressive": "Очень агрессивный",
698 "Eta_Cutoff_desc": "Eta cutoff - основной параметр специальной техники сэмплинга под названием Eta Sampling.&#13;В единицах 1e-4; разумное значение - 3.&#13;Установите в 0, чтобы отключить.&#13;См. статью Truncation Sampling as Language Model Desmoothing от Хьюитт и др. (2022) для получения подробной информации.",698 "Eta_Cutoff_desc": "Eta cutoff - основной параметр специальной техники сэмплинга под названием Eta Sampling.\nВ единицах 1e-4; разумное значение - 3.\nУстановите в 0, чтобы отключить.\nСм. статью Truncation Sampling as Language Model Desmoothing от Хьюитт и др. (2022) для получения подробной информации.",
699 "Learn how to contribute your idle GPU cycles to the Horde": "Узнайте, как использовать время простоя вашего GPU для помощи Horde",699 "Learn how to contribute your idle GPU cycles to the Horde": "Узнайте, как использовать время простоя вашего GPU для помощи Horde",
700 "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Используйте соответствующий токенизатор для моделей Google через их API. Медленная обработка подсказок, но предлагает намного более точный подсчет токенов.",700 "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Используйте соответствующий токенизатор для моделей Google через их API. Медленная обработка подсказок, но предлагает намного более точный подсчет токенов.",
701 "Load koboldcpp order": "Загрузить порядок из koboldcpp",701 "Load koboldcpp order": "Загрузить порядок из koboldcpp",
@@ -964,7 +964,7 @@
964 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",964 "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)",
965 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",965 "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)",
966 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",966 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
967 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",967 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
968 "char_import_7": ")",968 "char_import_7": ")",
969 "Grammar String": "Грамматика",969 "Grammar String": "Грамматика",
970 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",970 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
@@ -1016,7 +1016,7 @@
1016 "prompt_manager_relative": "Относительная",1016 "prompt_manager_relative": "Относительная",
1017 "prompt_manager_depth": "Глубина",1017 "prompt_manager_depth": "Глубина",
1018 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глубина вставки. 0 = после последнего сообщения, 1 = перед последним сообщением, и т.д.",1018 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глубина вставки. 0 = после последнего сообщения, 1 = перед последним сообщением, и т.д.",
1019 "The prompt to be sent.": "Отправляемый ИИ промпт.",1019 "The prompt to be sent.": "Текст промпта.",
1020 "prompt_manager_forbid_overrides": "Запретить перезапись",1020 "prompt_manager_forbid_overrides": "Запретить перезапись",
1021 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",1021 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.",
1022 "image_inlining_hint_1": "Отправлять картинки как часть промпта, если позволяет модель (такой функционал поддерживают GPT-4V, Claude 3 или Llava 13B). Чтобы добавить в чат изображение, используйте на нужном сообщении действие",1022 "image_inlining_hint_1": "Отправлять картинки как часть промпта, если позволяет модель (такой функционал поддерживают GPT-4V, Claude 3 или Llava 13B). Чтобы добавить в чат изображение, используйте на нужном сообщении действие",
@@ -1232,7 +1232,7 @@
1232 "Top P & Min P": "Top P & Min P",1232 "Top P & Min P": "Top P & Min P",
1233 "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.",1233 "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.",
1234 "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.",1234 "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.",
1235 "character_names_default": "Except for groups and past personas. Otherwise, make sure you provide names in the prompt.",1235 "character_names_default": "Добавлять префиксы для групповых чатов и предыдущих персон. В остальных случаях указывайте имена в промпте иными способами.",
1236 "Completion": "Completion Object",1236 "Completion": "Completion Object",
1237 "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",1237 "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",
1238 "Use AI21 Tokenizer": "Использовать токенайзер AI21",1238 "Use AI21 Tokenizer": "Использовать токенайзер AI21",
@@ -1257,7 +1257,6 @@
1257 "Peek a password": "Посмотреть пароль",1257 "Peek a password": "Посмотреть пароль",
1258 "Clear your cookie": "Clear your cookie",1258 "Clear your cookie": "Clear your cookie",
1259 "Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",1259 "Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",
1260 "context_allow_jailbreak": "Если в карточке есть джейлбрейк И ПРИ ЭТОМ включена опция \"Приоритет джейлбрейку из карточки персонажа\", то этот джейлбрейк добавляется в конец промпта.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",
1261 "Context Order": "Context Order",1260 "Context Order": "Context Order",
1262 "Summary": "Summary",1261 "Summary": "Summary",
1263 "Example Dialogues": "Примеры диалогов",1262 "Example Dialogues": "Примеры диалогов",
@@ -1278,7 +1277,7 @@
1278 "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.",1277 "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.",
1279 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "Если ИИ генерирует стоп-строку, то всё после неё будет вырезано из ответа (включая и саму стоп-строку).",1278 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "Если ИИ генерирует стоп-строку, то всё после неё будет вырезано из ответа (включая и саму стоп-строку).",
1280 "Will be inserted at the start of the chat history if it doesn't start with a User message.": "Вставляется в начале истории чата, если она начинается не с сообщения пользователя.",1279 "Will be inserted at the start of the chat history if it doesn't start with a User message.": "Вставляется в начале истории чата, если она начинается не с сообщения пользователя.",
1281 "Global World Info/Lorebook activation settings": "Настройки активации глобального лорбука / Информации о мире",1280 "Global World Info/Lorebook activation settings": "Глобальные настройки активации лорбука / Информации о мире",
1282 "Click to expand": "Щёлкните, чтобы развернуть",1281 "Click to expand": "Щёлкните, чтобы развернуть",
1283 "Insertion Strategy": "Как инжектить",1282 "Insertion Strategy": "Как инжектить",
1284 "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",1283 "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 @@
1647 "mui_reset": "Сброс",1646 "mui_reset": "Сброс",
1648 "Quick 'Impersonate' button": "Быстрое перевоплощение",1647 "Quick 'Impersonate' button": "Быстрое перевоплощение",
1649 "Show a button in the input area to ask the AI to impersonate your character for a single message": "Показать в поле ввода кнопку, по нажатии на которую ИИ сгенерирует одно сообщение от лица вашего персонажа.",1648 "Show a button in the input area to ask the AI to impersonate your character for a single message": "Показать в поле ввода кнопку, по нажатии на которую ИИ сгенерирует одно сообщение от лица вашего персонажа.",
1650 "Separators as Stop Strings": "Разделители как стоп-строки",1649 "Separators as Stop Strings": "Разделители в качестве стоп-строк",
1651 "Names as Stop Strings": "Имена как стоп-строки",1650 "Names as Stop Strings": "Имена в качестве стоп-строк",
1652 "Add Character and User names to a list of stopping strings.": "Добавлять имена персонажа и пользователя в список стоп-строк.",1651 "Add Character and User names to a list of stopping strings.": "Добавлять имена персонажа и пользователя в список стоп-строк.",
1653 "Allow Post-History Instructions": "Разрешить инструкции после истории",
1654 "context_allow_post_history_instructions": "Добавлять в конец промпта инструкции после истории. Работает только при наличии таких инструкций в карточке И при включенной опции ''Приоритет инструкциям из карточек''.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",1652 "context_allow_post_history_instructions": "Добавлять в конец промпта инструкции после истории. Работает только при наличии таких инструкций в карточке И при включенной опции ''Приоритет инструкциям из карточек''.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",
1655 "First User Prefix": "Первый префикс пользователя",1653 "First User Prefix": "Первый префикс пользователя",
1656 "Inserted before the first User's message.": "Вставляется перед первым сообщением пользователя.",1654 "Inserted before the first User's message.": "Вставляется перед первым сообщением пользователя.",
@@ -1916,8 +1914,8 @@
1916 "Cannot restore GUI preset": "Пресет для Gui восстановить нельзя",1914 "Cannot restore GUI preset": "Пресет для Gui восстановить нельзя",
1917 "Default preset cannot be restored": "Невозможно восстановить пресет по умолчанию",1915 "Default preset cannot be restored": "Невозможно восстановить пресет по умолчанию",
1918 "Default template cannot be restored": "Невозможно восстановить шаблон по умолчанию",1916 "Default template cannot be restored": "Невозможно восстановить шаблон по умолчанию",
1919 "Resetting a <b>default preset</b> will restore the default settings": "Сброс <b>стандартного пресета</b> восстановит настройки по умолчанию.",1917 "Resetting a <b>default preset</b> will restore the default settings.": "Сброс <b>комплектного пресета</b> восстановит настройки по умолчанию.",
1920 "Resetting a <b>default template</b> will restore the default settings.": "Сброс <b>стандартного шаблона</b> восстановит настройки по умолчанию.",1918 "Resetting a <b>default template</b> will restore the default settings.": "Сброс <b>комплектного шаблона</b> восстановит настройки по умолчанию.",
1921 "Are you sure?": "Вы уверены?",1919 "Are you sure?": "Вы уверены?",
1922 "Default preset restored": "Стандартный пресет восстановлен",1920 "Default preset restored": "Стандартный пресет восстановлен",
1923 "Default template restored": "Стандартный шаблон восстановлен",1921 "Default template restored": "Стандартный шаблон восстановлен",
@@ -2048,11 +2046,11 @@
2048 "prompt_post_processing_merge": "Объединять идущие подряд сообщения с одной ролью",2046 "prompt_post_processing_merge": "Объединять идущие подряд сообщения с одной ролью",
2049 "prompt_post_processing_semi": "Semi-strict (чередовать роли)",2047 "prompt_post_processing_semi": "Semi-strict (чередовать роли)",
2050 "prompt_post_processing_strict": "Strict (чередовать роли, сначала пользователь)",2048 "prompt_post_processing_strict": "Strict (чередовать роли, сначала пользователь)",
2051 "Select Horde models": "Выбрать модель из Horde",2049 "Select Horde models": "Выберите модель из Horde",
2052 "Model ID (optional)": "Идентификатор модели (необязательно)",2050 "Model ID (optional)": "Идентификатор модели (необязательно)",
2053 "Derive context size from backend": "Использовать бэкенд для определения размера контекста",2051 "Derive context size from backend": "Использовать бэкенд для определения размера контекста",
2054 "Rename current preset": "Переименовать пресет",2052 "Rename current preset": "Переименовать пресет",
2055 "No Worlds active. Click here to select.": "Нет активных миров. Нажмите, чтобы выбрать.",2053 "No Worlds active. Click here to select.": "Активных миров нет, ЛКМ для выбора.",
2056 "Title/Memo": "Название",2054 "Title/Memo": "Название",
2057 "Strategy": "Статус",2055 "Strategy": "Статус",
2058 "Position": "Позиция",2056 "Position": "Позиция",
@@ -2171,7 +2169,7 @@
2171 "instruct_derived": "Считывать из метаданных модели (по возможности)",2169 "instruct_derived": "Считывать из метаданных модели (по возможности)",
2172 "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",2170 "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
2173 "Reasoning Effort": "Рассуждения",2171 "Reasoning Effort": "Рассуждения",
2174 "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",2172 "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей, которые поддерживают эту возможность.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
2175 "openai_reasoning_effort_low": "Поверхностные",2173 "openai_reasoning_effort_low": "Поверхностные",
2176 "openai_reasoning_effort_medium": "Обычные",2174 "openai_reasoning_effort_medium": "Обычные",
2177 "openai_reasoning_effort_high": "Подробные",2175 "openai_reasoning_effort_high": "Подробные",
@@ -2276,8 +2274,8 @@
2276 "Persona Name Not Set": "У персоны отсутствует имя",2274 "Persona Name Not Set": "У персоны отсутствует имя",
2277 "You must bind a name to this persona before you can set a lorebook.": "Перед привязкой лорбука персоне необходимо присвоить имя.",2275 "You must bind a name to this persona before you can set a lorebook.": "Перед привязкой лорбука персоне необходимо присвоить имя.",
2278 "Default Persona Removed": "Персона по умолчанию снята",2276 "Default Persona Removed": "Персона по умолчанию снята",
2279 "Persona is locked to the current character": "Персона закреплена за этим персонажем",2277 "Persona is locked to the current character": "Персона закреплена за текущим персонажем",
2280 "Persona is locked to the current chat": "Персона закреплена за этим чатом",2278 "Persona is locked to the current chat": "Персона закреплена за текущим чатом",
2281 "characters": "перс.",2279 "characters": "перс.",
2282 "character": "персонаж",2280 "character": "персонаж",
2283 "in this group": "в группе",2281 "in this group": "в группе",
@@ -2338,5 +2336,88 @@
2338 "Reasoning already exists.": "Рассуждения уже присутствуют.",2336 "Reasoning already exists.": "Рассуждения уже присутствуют.",
2339 "Edit Message": "Редактирование",2337 "Edit Message": "Редактирование",
2340 "Status check bypassed": "Проверка статуса отключена",2338 "Status check bypassed": "Проверка статуса отключена",
2341 "Valid": "Работает"2339 "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": "Назад"
2342}2423}
public/locales/uk-ua.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Початок чату",411 "Chat Start": "Початок чату",
412 "Add Chat Start and Example Separator to a list of stopping strings.": "Додайте початок чату та роздільник прикладів до списку рядків зупинки.",412 "Add Chat Start and Example Separator to a list of stopping strings.": "Додайте початок чату та роздільник прикладів до списку рядків зупинки.",
413 "Use as Stop Strings": "Використовувати як рядки зупинки",413 "Use as Stop Strings": "Використовувати як рядки зупинки",
414 "context_allow_jailbreak": "Включає втечу з в’язниці в кінці підказки, якщо визначено в картці символів ТА «Переважати символ. Втечу з в'язниці'' увімкнено.\nЦЕ НЕ РЕКОМЕНДУЄТЬСЯ ДЛЯ МОДЕЛЕЙ ЗАВЕРШЕННЯ ТЕКСТУ, МОЖЕ ПРИЗВЕСТИ ДО ПОГАНОГО РЕЗУЛЬТАТУ.",
415 "Allow Jailbreak": "Дозволити втечу з в'язниці",414 "Allow Jailbreak": "Дозволити втечу з в'язниці",
416 "Context Order": "Порядок контексту",415 "Context Order": "Порядок контексту",
417 "Summary": "Резюме",416 "Summary": "Резюме",
public/locales/vi-vn.json+0 -1
@@ -411,7 +411,6 @@
411 "Chat Start": "Bắt đầu Chat",411 "Chat Start": "Bắt đầu Chat",
412 "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.",412 "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.",
413 "Use as Stop Strings": "Sử dụng như chuỗi dừng",413 "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.",
415 "Allow Jailbreak": "Cho phép bẻ khóa",414 "Allow Jailbreak": "Cho phép bẻ khóa",
416 "Context Order": "Thứ tự bối cảnh",415 "Context Order": "Thứ tự bối cảnh",
417 "Summary": "Bản tóm tắt",416 "Summary": "Bản tóm tắt",
public/locales/zh-cn.json+24 -5
@@ -504,7 +504,6 @@
504 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",504 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
505 "Names as Stop Strings": "名称作为终止字符串",505 "Names as Stop Strings": "名称作为终止字符串",
506 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",506 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",
507 "Allow Post-History Instructions": "允许后历史说明",
508 "Instruct Template": "指导模板",507 "Instruct Template": "指导模板",
509 "instruct_derived": "如果可能,从模型元数据中获取",508 "instruct_derived": "如果可能,从模型元数据中获取",
510 "instruct_bind_to_context": "如果启用,上下文模板将根据所选的指导模板名称或偏好自动选择。",509 "instruct_bind_to_context": "如果启用,上下文模板将根据所选的指导模板名称或偏好自动选择。",
@@ -1358,7 +1357,7 @@
1358 "Image Captioning": "图像描述",1357 "Image Captioning": "图像描述",
1359 "Source": "来源",1358 "Source": "来源",
1360 "Local": "本地",1359 "Local": "本地",
1361 "Multimodal (OpenAI / Anthropic / llama / Google)": "多模式(OpenAI / Anthropic / llama / Google)",1360 "Multimodal (OpenAI / Anthropic / llama / Google)": "多模态(OpenAI / Anthropic / llama / Google)",
1362 "Extras": "更多",1361 "Extras": "更多",
1363 "Horde": "Horde",1362 "Horde": "Horde",
1364 "API": "API",1363 "API": "API",
@@ -1588,8 +1587,8 @@
1588 "sd_function_tool_txt": "Use function tool",1587 "sd_function_tool_txt": "Use function tool",
1589 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",1588 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",
1590 "sd_interactive_mode_txt": "交互模式",1589 "sd_interactive_mode_txt": "交互模式",
1591 "sd_multimodal_captioning": "使用多模式字幕根据用户和角色的头像生成提示词。",1590 "sd_multimodal_captioning": "使用多模态字幕根据用户和角色的头像生成提示词。",
1592 "sd_multimodal_captioning_txt": "使用多模式字幕来描绘肖像",1591 "sd_multimodal_captioning_txt": "使用多模态字幕来描绘肖像",
1593 "sd_free_extend": "使用当前选择的 LLM 自动扩展自由模式主题提示(不是肖像或背景)。",1592 "sd_free_extend": "使用当前选择的 LLM 自动扩展自由模式主题提示(不是肖像或背景)。",
1594 "sd_free_extend_txt": "延长自由模式提示",1593 "sd_free_extend_txt": "延长自由模式提示",
1595 "sd_free_extend_small": "(交互/命令)",1594 "sd_free_extend_small": "(交互/命令)",
@@ -2109,5 +2108,25 @@
2109 "Title/Memo": "标题(备忘)",2108 "Title/Memo": "标题(备忘)",
2110 "Strategy": "触发策略",2109 "Strategy": "触发策略",
2111 "Position": "插入位置",2110 "Position": "插入位置",
2112 "Trigger %": "触发概率%"2111 "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.": "选择服务商。未选择 = 所有服务商。"
2113}2132}
public/locales/zh-tw.json+0 -2
@@ -412,7 +412,6 @@
412 "Chat Start": "聊天開始符號",412 "Chat Start": "聊天開始符號",
413 "Add Chat Start and Example Separator to a list of stopping strings.": "將聊天開始和範例分隔符號加入終止字串中。",413 "Add Chat Start and Example Separator to a list of stopping strings.": "將聊天開始和範例分隔符號加入終止字串中。",
414 "Use as Stop Strings": "用作停止字串",414 "Use as Stop Strings": "用作停止字串",
415 "context_allow_jailbreak": "如果在角色卡中定義了越獄,且啟用了「角色卡越獄優先」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。",
416 "Allow Jailbreak": "允許越獄",415 "Allow Jailbreak": "允許越獄",
417 "Context Order": "上下文順序",416 "Context Order": "上下文順序",
418 "Summary": "摘要",417 "Summary": "摘要",
@@ -1555,7 +1554,6 @@
1555 "All": "全部",1554 "All": "全部",
1556 "Allow fallback models": "允許回退模型",1555 "Allow fallback models": "允許回退模型",
1557 "Allow fallback providers": "允許回退供應商",1556 "Allow fallback providers": "允許回退供應商",
1558 "Allow Post-History Instructions": "允許聊天歷史後指示",
1559 "Allow reverse proxy": "允許反向代理",1557 "Allow reverse proxy": "允許反向代理",
1560 "Alternate Greeting #": "備選問候語 #",1558 "Alternate Greeting #": "備選問候語 #",
1561 "alternate_greetings_hint_1": "點選",1559 "alternate_greetings_hint_1": "點選",
public/script.js+114 -52
@@ -50,6 +50,7 @@ import {
50 importWorldInfo,50 importWorldInfo,
51 wi_anchor_position,51 wi_anchor_position,
52 world_info_include_names,52 world_info_include_names,
53 initWorldInfo,
53} from './scripts/world-info.js';54} from './scripts/world-info.js';
5455
55import {56import {
@@ -142,6 +143,7 @@ import {
142 getHordeModels,143 getHordeModels,
143 adjustHordeGenerationParams,144 adjustHordeGenerationParams,
144 MIN_LENGTH,145 MIN_LENGTH,
146 initHorde,
145} from './scripts/horde.js';147} from './scripts/horde.js';
146148
147import {149import {
@@ -174,6 +176,9 @@ import {
174 saveBase64AsFile,176 saveBase64AsFile,
175 uuidv4,177 uuidv4,
176 equalsIgnoreCaseAndAccents,178 equalsIgnoreCaseAndAccents,
179 localizePagination,
180 renderPaginationDropdown,
181 paginationDropdownChangeHandler,
177} from './scripts/utils.js';182} from './scripts/utils.js';
178import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';183import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';
179184
@@ -651,7 +656,7 @@ export const extension_prompt_roles = {
651 ASSISTANT: 2,656 ASSISTANT: 2,
652};657};
653658
654export const MAX_INJECTION_DEPTH = 1000;659export const MAX_INJECTION_DEPTH = 10000;
655660
656const SAFETY_CHAT = [661const SAFETY_CHAT = [
657 {662 {
@@ -992,6 +997,8 @@ async function firstLoadInit() {
992 initBackgrounds();997 initBackgrounds();
993 initAuthorsNote();998 initAuthorsNote();
994 await initPersonas();999 await initPersonas();
1000 initWorldInfo();
1001 initHorde();
995 initRossMods();1002 initRossMods();
996 initStats();1003 initStats();
997 initCfg();1004 initCfg();
@@ -1421,30 +1428,26 @@ function getBackBlock() {
1421 return template;1428 return template;
1422}1429}
14231430
1424function getEmptyBlock() {1431async function getEmptyBlock() {
1425 const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog'];1432 const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog'];
1426 const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it'];1433 const texts = [t`Here be dragons`, t`Otterly empty`, t`Kiwibunga`, t`Pump-a-Rum`, t`Croak it`];
1427 const roll = new Date().getMinutes() % icons.length;1434 const roll = new Date().getMinutes() % icons.length;
1428 const emptyBlock = `1435 const params = {
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>`;
1434 return $(emptyBlock);1440 return $(emptyBlock);
1435}1441}
14361442
1437/**1443/**
1438 * @param {number} hidden Number of hidden characters1444 * @param {number} hidden Number of hidden characters
1439 */1445 */
1440function getHiddenBlock(hidden) {1446async function getHiddenBlock(hidden) {
1441 const hiddenBlock = `1447 const params = {
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>`;
1448 return $(hiddenBlock);1451 return $(hiddenBlock);
1449}1452}
14501453
@@ -1524,10 +1527,11 @@ export async function printCharacters(fullRefresh = false) {
15241527
1525 const entities = getEntitiesList({ doFilter: true });1528 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];
1527 $('#rm_print_characters_pagination').pagination({1532 $('#rm_print_characters_pagination').pagination({
1528 dataSource: entities,1533 dataSource: entities,
1529 pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default,1534 pageSize,
1530 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
1531 pageRange: 1,1535 pageRange: 1,
1532 pageNumber: saveCharactersPage || 1,1536 pageNumber: saveCharactersPage || 1,
1533 position: 'top',1537 position: 'top',
@@ -1536,14 +1540,16 @@ export async function printCharacters(fullRefresh = false) {
1536 prevText: '<',1540 prevText: '<',
1537 nextText: '>',1541 nextText: '>',
1538 formatNavigator: PAGINATION_TEMPLATE,1542 formatNavigator: PAGINATION_TEMPLATE,
1543 formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
1539 showNavigator: true,1544 showNavigator: true,
1540 callback: function (/** @type {Entity[]} */ data) {1545 callback: async function (/** @type {Entity[]} */ data) {
1541 $(listId).empty();1546 $(listId).empty();
1542 if (power_user.bogus_folders && isBogusFolderOpen()) {1547 if (power_user.bogus_folders && isBogusFolderOpen()) {
1543 $(listId).append(getBackBlock());1548 $(listId).append(getBackBlock());
1544 }1549 }
1545 if (!data.length) {1550 if (!data.length) {
1546 $(listId).append(getEmptyBlock());1551 const emptyBlock = await getEmptyBlock();
1552 $(listId).append(emptyBlock);
1547 }1553 }
1548 let displayCount = 0;1554 let displayCount = 0;
1549 for (const i of data) {1555 for (const i of data) {
@@ -1564,13 +1570,16 @@ export async function printCharacters(fullRefresh = false) {
15641570
1565 const hidden = (characters.length + groups.length) - displayCount;1571 const hidden = (characters.length + groups.length) - displayCount;
1566 if (hidden > 0 && entitiesFilter.hasAnyFilter()) {1572 if (hidden > 0 && entitiesFilter.hasAnyFilter()) {
1567 $(listId).append(getHiddenBlock(hidden));1573 const hiddenBlock = await getHiddenBlock(hidden);
1574 $(listId).append(hiddenBlock);
1568 }1575 }
1576 localizePagination($('#rm_print_characters_pagination'));
15691577
1570 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);1578 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
1571 },1579 },
1572 afterSizeSelectorChange: function (e) {1580 afterSizeSelectorChange: function (e, size) {
1573 accountStorage.setItem(storageKey, e.target.value);1581 accountStorage.setItem(storageKey, e.target.value);
1582 paginationDropdownChangeHandler(e, size);
1574 },1583 },
1575 afterPaging: function (e) {1584 afterPaging: function (e) {
1576 saveCharactersPage = e;1585 saveCharactersPage = e;
@@ -2751,6 +2760,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
2751 environment.charVersion = fields.version || '';2760 environment.charVersion = fields.version || '';
2752 environment.char_version = fields.version || '';2761 environment.char_version = fields.version || '';
2753 environment.charDepthPrompt = fields.charDepthPrompt || '';2762 environment.charDepthPrompt = fields.charDepthPrompt || '';
2763 environment.creatorNotes = fields.creatorNotes || '';
2754 }2764 }
27552765
2756 // Must be substituted last so that they're replaced inside {{description}}2766 // Must be substituted last so that they're replaced inside {{description}}
@@ -3047,6 +3057,20 @@ export async function getExtensionPromptByName(moduleName) {
3047}3057}
30483058
3049/**3059/**
3060 * Gets the maximum depth of extension prompts.
3061 * @returns {number} Maximum depth of extension prompts
3062 */
3063export 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/**
3050 * Returns the extension prompt for the given position, depth, and role.3074 * Returns the extension prompt for the given position, depth, and role.
3051 * If multiple prompts are found, they are joined with a separator.3075 * If multiple prompts are found, they are joined with a separator.
3052 * @param {number} [position] Position of the prompt3076 * @param {number} [position] Position of the prompt
@@ -3115,6 +3139,7 @@ export function baseChatReplace(value, name1, name2) {
3115 * @property {string} jailbreak Jailbreak instructions3139 * @property {string} jailbreak Jailbreak instructions
3116 * @property {string} version Character version3140 * @property {string} version Character version
3117 * @property {string} charDepthPrompt Character depth note3141 * @property {string} charDepthPrompt Character depth note
3142 * @property {string} creatorNotes Character creator notes
3118 * @returns {CharacterCardFields} Character card fields3143 * @returns {CharacterCardFields} Character card fields
3119 */3144 */
3120export function getCharacterCardFields({ chid = null } = {}) {3145export function getCharacterCardFields({ chid = null } = {}) {
@@ -3130,6 +3155,7 @@ export function getCharacterCardFields({ chid = null } = {}) {
3130 jailbreak: '',3155 jailbreak: '',
3131 version: '',3156 version: '',
3132 charDepthPrompt: '',3157 charDepthPrompt: '',
3158 creatorNotes: '',
3133 };3159 };
3134 result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);3160 result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
31353161
@@ -3148,6 +3174,7 @@ export function getCharacterCardFields({ chid = null } = {}) {
3148 result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : '';3174 result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : '';
3149 result.version = character.data?.character_version ?? '';3175 result.version = character.data?.character_version ?? '';
3150 result.charDepthPrompt = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2);3176 result.charDepthPrompt = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2);
3177 result.creatorNotes = baseChatReplace(character.data?.creator_notes?.trim(), name1, name2);
31513178
3152 if (selected_group) {3179 if (selected_group) {
3153 const groupCards = getGroupCharacterCards(selected_group, Number(currentChid));3180 const groupCards = getGroupCharacterCards(selected_group, Number(currentChid));
@@ -3975,11 +4002,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3975 system,4002 system,
3976 jailbreak,4003 jailbreak,
3977 charDepthPrompt,4004 charDepthPrompt,
4005 creatorNotes,
3978 } = getCharacterCardFields();4006 } = getCharacterCardFields();
39794007
3980 if (main_api !== 'openai') {4008 if (main_api !== 'openai') {
3981 if (power_user.sysprompt.enabled) {4009 if (power_user.sysprompt.enabled) {
3982 system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.sysprompt.content, name1, name2);4010 system = power_user.prefer_character_prompt && system
4011 ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? ''))
4012 : baseChatReplace(power_user.sysprompt.content, name1, name2);
3983 system = isInstruct ? formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.sysprompt.content)) : system;4013 system = isInstruct ? formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.sysprompt.content)) : system;
3984 } else {4014 } else {
3985 // Nullify if it's not enabled4015 // Nullify if it's not enabled
@@ -4129,7 +4159,15 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4129 // Make quiet prompt available for WIAN4159 // Make quiet prompt available for WIAN
4130 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);4160 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
4131 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();4161 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);
4133 setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);4171 setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);
41344172
4135 // Add message example WI4173 // Add message example WI
@@ -4178,17 +4216,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4178 injectedIndices = await doChatInject(coreChat, isContinue);4216 injectedIndices = await doChatInject(coreChat, isContinue);
4179 }4217 }
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 original4221 ? substituteParams(jailbreak, name1, name2, (power_user.sysprompt.post_history ?? ''))
4184 jailbreak = substituteParams(jailbreak, name1, name2, '');4222 : baseChatReplace(power_user.sysprompt.post_history, name1, name2);
41854223
4186 // When continuing generation of previous output, last user message precedes the message to continue4224 // Only inject the jb if there is one
4187 if (isContinue) {4225 if (jailbreak) {
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 }
4192 }4233 }
4193 }4234 }
41944235
@@ -4221,12 +4262,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42214262
4222 // Do not suffix the message for continuation4263 // Do not suffix the message for continuation
4223 if (i === 0 && isContinue) {4264 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
4224 if (isInstruct) {4268 if (isInstruct) {
4269 const originalMessage = String(coreChat[j].mes ?? '');
4270 coreChat[j].mes = originalMessage.replaceAll(FORMAT_TOKEN, '') + FORMAT_TOKEN;
4225 // Reformat with the last output sequence (if any)4271 // Reformat with the last output sequence (if any)
4226 chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);4272 chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);
4273 coreChat[j].mes = originalMessage;
4227 }4274 }
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);
4230 continue_mag = coreChat[j].mes;4279 continue_mag = coreChat[j].mes;
4231 }4280 }
42324281
@@ -4840,6 +4889,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4840 userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''),4889 userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''),
4841 tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '',4890 tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '',
4842 presetName: getPresetManager()?.getSelectedPresetName() || '',4891 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,
4843 };4894 };
48444895
4845 //console.log(additionalPromptStuff);4896 //console.log(additionalPromptStuff);
@@ -5099,7 +5150,8 @@ async function doChatInject(messages, isContinue) {
5099 let totalInsertedMessages = 0;5150 let totalInsertedMessages = 0;
5100 messages.reverse();5151 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++) {
5103 // Order of priority (most important go lower)5155 // Order of priority (most important go lower)
5104 const roles = [extension_prompt_roles.SYSTEM, extension_prompt_roles.USER, extension_prompt_roles.ASSISTANT];5156 const roles = [extension_prompt_roles.SYSTEM, extension_prompt_roles.USER, extension_prompt_roles.ASSISTANT];
5105 const names = {5157 const names = {
@@ -5522,6 +5574,8 @@ export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMes
5522 modelUsed: chat[incomingMesId]?.extra?.model,5574 modelUsed: chat[incomingMesId]?.extra?.model,
5523 apiUsed: chat[incomingMesId]?.extra?.api,5575 apiUsed: chat[incomingMesId]?.extra?.api,
5524 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,5576 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
5577 messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
5578 examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
5525 };5579 };
55265580
5527 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;5581 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 }
6940 throw new Error(result.statusText);6994 throw new Error(result.statusText);
6941 }6995 }
69426996
6943 const forceSaveConfirmed = await Popup.show.confirm(6997 const popupResult = await Popup.show.input(
6944 t`ERROR: Chat integrity check failed.`,6998 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
6949 if (forceSaveConfirmed) {7007 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;
6951 }7011 }
7012
7013 await saveChat({ chatName, withMetadata, mesId, force: true });
6952 } catch (error) {7014 } catch (error) {
6953 console.error(error);7015 console.error(error);
6954 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);7016 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
8375 function getOkButtonText() {8437 function getOkButtonText() {
8376 if (['text', 'char_not_selected'].includes(popup_type)) {8438 if (['text', 'char_not_selected'].includes(popup_type)) {
8377 $dialoguePopupCancel.css('display', 'none');8439 $dialoguePopupCancel.css('display', 'none');
8378 return okButton ?? 'Ok';8440 return okButton ?? t`Ok`;
8379 } else if (['delete_extension'].includes(popup_type)) {8441 } else if (['delete_extension'].includes(popup_type)) {
8380 return okButton ?? 'Ok';8442 return okButton ?? t`Ok`;
8381 } else if (['new_chat', 'confirm'].includes(popup_type)) {8443 } else if (['new_chat', 'confirm'].includes(popup_type)) {
8382 return okButton ?? 'Yes';8444 return okButton ?? t`Yes`;
8383 } else if (['input'].includes(popup_type)) {8445 } else if (['input'].includes(popup_type)) {
8384 return okButton ?? t`Save`;8446 return okButton ?? t`Save`;
8385 }8447 }
8386 return okButton ?? 'Delete';8448 return okButton ?? t`Delete`;
8387 }8449 }
83888450
8389 dialogueCloseStop = true;8451 dialogueCloseStop = true;
@@ -9089,7 +9151,7 @@ function formatSwipeCounter(current, total) {
9089 * @param {string} [params.source] The source of the swipe event.9151 * @param {string} [params.source] The source of the swipe event.
9090 * @param {boolean} [params.repeated] Is the swipe event repeated.9152 * @param {boolean} [params.repeated] Is the swipe event repeated.
9091 */9153 */
9092function swipe_left(_event, { source, repeated } = {}) {9154export function swipe_left(_event, { source, repeated } = {}) {
9093 if (chat.length - 1 === Number(this_edit_mes_id)) {9155 if (chat.length - 1 === Number(this_edit_mes_id)) {
9094 closeMessageEditor();9156 closeMessageEditor();
9095 }9157 }
@@ -9237,7 +9299,7 @@ function swipe_left(_event, { source, repeated } = {}) {
9237 * @param {string} [params.source] The source of the swipe event.9299 * @param {string} [params.source] The source of the swipe event.
9238 * @param {boolean} [params.repeated] Is the swipe event repeated.9300 * @param {boolean} [params.repeated] Is the swipe event repeated.
9239 */9301 */
9240function swipe_right(_event, { source, repeated } = {}) {9302export function swipe_right(_event, { source, repeated } = {}) {
9241 if (chat.length - 1 === Number(this_edit_mes_id)) {9303 if (chat.length - 1 === Number(this_edit_mes_id)) {
9242 closeMessageEditor();9304 closeMessageEditor();
9243 }9305 }
@@ -11832,8 +11894,8 @@ jQuery(async function () {
11832 return;11894 return;
11833 }11895 }
11834 const drawer = $(this).closest('.inline-drawer');11896 const drawer = $(this).closest('.inline-drawer');
11835 const icon = drawer.find('.inline-drawer-icon');11897 const icon = drawer.find('>.inline-drawer-header .inline-drawer-icon');
11836 const drawerContent = drawer.find('.inline-drawer-content');11898 const drawerContent = drawer.find('>.inline-drawer-content');
11837 icon.toggleClass('down up');11899 icon.toggleClass('down up');
11838 icon.toggleClass('fa-circle-chevron-down fa-circle-chevron-up');11900 icon.toggleClass('fa-circle-chevron-down fa-circle-chevron-up');
11839 drawerContent.stop().slideToggle({11901 drawerContent.stop().slideToggle({
public/scripts/RossAscends-mods.js+3 -2
@@ -409,6 +409,7 @@ function RA_autoconnect(PrevApi) {
409 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)409 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
410 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)410 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
411 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)411 || (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)
412 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)413 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
413 ) {414 ) {
414 $('#api_button_openai').trigger('click');415 $('#api_button_openai').trigger('click');
@@ -1047,7 +1048,7 @@ export function initRossMods() {
1047 //Enter to send when send_textarea in focus1048 //Enter to send when send_textarea in focus
1048 if (document.activeElement == hotkeyTargets['send_textarea']) {1049 if (document.activeElement == hotkeyTargets['send_textarea']) {
1049 const sendOnEnter = shouldSendOnEnter();1050 const sendOnEnter = shouldSendOnEnter();
1050 if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {1051 if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
1051 event.preventDefault();1052 event.preventDefault();
1052 sendTextareaMessage();1053 sendTextareaMessage();
1053 return;1054 return;
@@ -1119,7 +1120,7 @@ export function initRossMods() {
1119 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {1120 const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
1120 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],1121 customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
1121 onClose: (popup) => {1122 onClose: (popup) => {
1122 regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;1123 regenerateWithCtrlEnter = Boolean(popup.inputResults.get('regenerateWithCtrlEnter') ?? false);
1123 },1124 },
1124 });1125 });
1125 if (!result) {1126 if (!result) {
public/scripts/backgrounds.js+2 -1
@@ -6,6 +6,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
6import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';6import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
7import { flashHighlight, stringFormat } from './utils.js';7import { flashHighlight, stringFormat } from './utils.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { Popup } from './popup.js';
910
10const BG_METADATA_KEY = 'custom_background';11const BG_METADATA_KEY = 'custom_background';
11const LIST_METADATA_KEY = 'chat_backgrounds';12const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -291,7 +292,7 @@ async function onDeleteBackgroundClick(e) {
291 const bgToDelete = $(this).closest('.bg_example');292 const bgToDelete = $(this).closest('.bg_example');
292 const url = bgToDelete.data('url');293 const url = bgToDelete.data('url');
293 const isCustom = bgToDelete.attr('custom') === 'true';294 const isCustom = bgToDelete.attr('custom') === 'true';
294 const confirm = await callPopup('<h3>Delete the background?</h3>', 'confirm');295 const confirm = await Popup.show.confirm(t`Delete the background?`, null);
295 const bg = bgToDelete.attr('bgfile');296 const bg = bgToDelete.attr('bgfile');
296297
297 if (confirm) {298 if (confirm) {
public/scripts/char-data.js+6 -0
@@ -33,6 +33,12 @@
33 * @property {number} role - The specific function or purpose of the extension.33 * @property {number} role - The specific function or purpose of the extension.
34 * @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.34 * @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.
35 * @property {number} display_index - The order in which the extension should be displayed for user interfaces.35 * @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.
36 */42 */
3743
38/**44/**
public/scripts/chat-templates.js+5 -0
@@ -74,6 +74,11 @@ const hash_derivations = {
74 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':74 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
75 'DeepSeek-V2.5'75 'DeepSeek-V2.5'
76 ,76 ,
77
78 // THUDM-GLM 4
79 '854b703e44ca06bdb196cc471c728d15dbab61e744fe6cdce980086b61646ed1':
80 'GLM-4'
81 ,
77};82};
7883
79const substr_derivations = {84const substr_derivations = {
public/scripts/custom-request.js+11 -5
@@ -43,10 +43,12 @@ import EventSourceStream from './sse-stream.js';
43 * @property {boolean?} [stream=false] - Whether to stream the response43 * @property {boolean?} [stream=false] - Whether to stream the response
44 * @property {ChatCompletionMessage[]} messages - Array of chat messages44 * @property {ChatCompletionMessage[]} messages - Array of chat messages
45 * @property {string} [model] - Optional model name to use for completion45 * @property {string} [model] - Optional model name to use for completion
46 * @property {string} chat_completion_source - Source provider for chat completion46 * @property {string} chat_completion_source - Source provider
47 * @property {number} max_tokens - Maximum number of tokens to generate47 * @property {number} max_tokens - Maximum number of tokens to generate
48 * @property {number} [temperature] - Optional temperature parameter for response randomness48 * @property {number} [temperature] - Optional temperature parameter for response randomness
49 * @property {string} [custom_url] - Optional custom URL for chat completion49 * @property {string} [custom_url] - Optional custom URL
50 * @property {string} [reverse_proxy] - Optional reverse proxy URL
51 * @property {string} [proxy_password] - Optional proxy password
50 */52 */
5153
52/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */54/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
@@ -80,7 +82,6 @@ export class TextCompletionService {
80 */82 */
81 static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {83 static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
82 const payload = {84 const payload = {
83 ...props,
84 stream,85 stream,
85 prompt,86 prompt,
86 max_tokens,87 max_tokens,
@@ -90,6 +91,7 @@ export class TextCompletionService {
90 api_server: api_server ?? getTextGenServer(api_type),91 api_server: api_server ?? getTextGenServer(api_type),
91 temperature,92 temperature,
92 min_p,93 min_p,
94 ...props,
93 };95 };
9496
95 // Remove undefined values to avoid API errors97 // Remove undefined values to avoid API errors
@@ -387,9 +389,8 @@ export class ChatCompletionService {
387 * @param {ChatCompletionPayload} custom389 * @param {ChatCompletionPayload} custom
388 * @returns {ChatCompletionPayload}390 * @returns {ChatCompletionPayload}
389 */391 */
390 static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {392 static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, reverse_proxy, proxy_password, ...props }) {
391 const payload = {393 const payload = {
392 ...props,
393 stream,394 stream,
394 messages,395 messages,
395 model,396 model,
@@ -397,6 +398,11 @@ export class ChatCompletionService {
397 max_tokens,398 max_tokens,
398 temperature,399 temperature,
399 custom_url,400 custom_url,
401 reverse_proxy,
402 proxy_password,
403 use_makersuite_sysprompt: true,
404 claude_use_sysprompt: true,
405 ...props,
400 };406 };
401407
402 // Remove undefined values to avoid API errors408 // Remove undefined values to avoid API errors
public/scripts/extensions.js+124 -3
@@ -661,6 +661,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
661 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>` : '';661 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>` : '';
662 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>` : '';662 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>` : '';
663 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>` : '';663 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>` : '';
664 let modulesInfo = '';665 let modulesInfo = '';
665666
666 if (isActive && Array.isArray(manifest.optional)) {667 if (isActive && Array.isArray(manifest.optional)) {
@@ -701,6 +702,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
701702
702 <div class="extension_actions flex-container alignItemsCenter">703 <div class="extension_actions flex-container alignItemsCenter">
703 ${updateButton}704 ${updateButton}
705 ${branchButton}
704 ${moveButton}706 ${moveButton}
705 ${deleteButton}707 ${deleteButton}
706 </div>708 </div>
@@ -944,6 +946,44 @@ async function onDeleteClick() {
944 }946 }
945}947}
946948
949async 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
947async function onMoveClick() {987async function onMoveClick() {
948 const extensionName = $(this).data('name');988 const extensionName = $(this).data('name');
949 const isCurrentUserAdmin = isAdmin();989 const isCurrentUserAdmin = isAdmin();
@@ -1056,12 +1096,82 @@ async function getExtensionVersion(extensionName, abortSignal) {
1056}1096}
10571097
1058/**1098/**
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 */
1109async 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 */
1141async 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/**
1059 * Installs a third-party extension via the API.1169 * Installs a third-party extension via the API.
1060 * @param {string} url Extension repository URL1170 * @param {string} url Extension repository URL
1061 * @param {boolean} global Is the extension global?1171 * @param {boolean} global Is the extension global?
1062 * @returns {Promise<void>}1172 * @returns {Promise<void>}
1063 */1173 */
1064export async function installExtension(url, global) {1174export async function installExtension(url, global, branch = '') {
1065 console.debug('Extension installation started', url);1175 console.debug('Extension installation started', url);
10661176
1067 toastr.info(t`Please wait...`, t`Installing extension`);1177 toastr.info(t`Please wait...`, t`Installing extension`);
@@ -1072,6 +1182,7 @@ export async function installExtension(url, global) {
1072 body: JSON.stringify({1182 body: JSON.stringify({
1073 url,1183 url,
1074 global,1184 global,
1185 branch,
1075 }),1186 }),
1076 });1187 });
10771188
@@ -1406,9 +1517,17 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1406 await popup.complete(POPUP_RESULT.AFFIRMATIVE);1517 await popup.complete(POPUP_RESULT.AFFIRMATIVE);
1407 },1518 },
1408 };1519 };
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
1410 const customButtons = isCurrentUserAdmin ? [installForAllButton] : [];1528 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 });
1412 const input = await popup.show();1531 const input = await popup.show();
14131532
1414 if (!input) {1533 if (!input) {
@@ -1417,7 +1536,8 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1417 }1536 }
14181537
1419 const url = String(input).trim();1538 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);
1421}1541}
14221542
1423export async function initExtensions() {1543export async function initExtensions() {
@@ -1433,6 +1553,7 @@ export async function initExtensions() {
1433 $(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);1553 $(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
1434 $(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);1554 $(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
1435 $(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);1555 $(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
1556 $(document).on('click', '.extensions_info .extension_block .btn_branch', onBranchClick);
14361557
1437 /**1558 /**
1438 * Handles the click event for the third-party extension import button.1559 * 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) {
291 try {291 try {
292 if (category === 'extension') {292 if (category === 'extension') {
293 console.debug(DEBUG_PREFIX, 'Installing extension ', url);293 console.debug(DEBUG_PREFIX, 'Installing extension ', url);
294 await installExtension(url);294 await installExtension(url, false);
295 console.debug(DEBUG_PREFIX, 'Extension installed.');295 console.debug(DEBUG_PREFIX, 'Extension installed.');
296 return;296 return;
297 }297 }
@@ -309,7 +309,7 @@ async function installAsset(url, assetType, filename) {
309 console.debug(DEBUG_PREFIX, 'Importing character ', filename);309 console.debug(DEBUG_PREFIX, 'Importing character ', filename);
310 const blob = await result.blob();310 const blob = await result.blob();
311 const file = new File([blob], filename, { type: blob.type });311 const file = new File([blob], filename, { type: blob.type });
312 await processDroppedFiles([file], true);312 await processDroppedFiles([file]);
313 console.debug(DEBUG_PREFIX, 'Character downloaded.');313 console.debug(DEBUG_PREFIX, 'Character downloaded.');
314 }314 }
315 }315 }
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;
39 <span data-i18n="Characters">Characters</span>39 <span data-i18n="Characters">Characters</span>
40 </div>40 </div>
41 </div>41 </div>
42 <div class="inline-drawer-content" id="assets_menu">42 <div id="assets_menu">
43 </div>43 </div>
44 </div>44 </div>
45 </div>45 </div>
public/scripts/extensions/caption/index.js+1 -0
@@ -428,6 +428,7 @@ jQuery(async function () {
428 'zerooneai': SECRET_KEYS.ZEROONEAI,428 'zerooneai': SECRET_KEYS.ZEROONEAI,
429 'groq': SECRET_KEYS.GROQ,429 'groq': SECRET_KEYS.GROQ,
430 'cohere': SECRET_KEYS.COHERE,430 'cohere': SECRET_KEYS.COHERE,
431 'xai': SECRET_KEYS.XAI,
431 };432 };
432433
433 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {434 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
public/scripts/extensions/caption/settings.html+36 -22
@@ -31,6 +31,7 @@
31 <option value="openrouter">OpenRouter</option>31 <option value="openrouter">OpenRouter</option>
32 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>32 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
33 <option value="vllm">vLLM</option>33 <option value="vllm">vLLM</option>
34 <option value="xai">xAI (Grok)</option>
34 </select>35 </select>
35 </div>36 </div>
36 <div class="flex1 flex-container flexFlowColumn flexNoGap">37 <div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -46,13 +47,24 @@
46 <option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>47 <option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>
47 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>48 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
48 <option data-type="zerooneai" value="yi-vision">yi-vision</option>49 <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>
49 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>56 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
50 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>57 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
51 <option data-type="openai" value="gpt-4o">gpt-4o</option>58 <option data-type="openai" value="gpt-4o">gpt-4o</option>
52 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>59 <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>
53 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>61 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
54 <option data-type="openai" value="o1">o1</option>62 <option data-type="openai" value="o1">o1</option>
55 <option data-type="openai" value="o1-2024-12-17">o1-2024-12-17</option>63 <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>
56 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>68 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
57 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>69 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
58 <option data-type="anthropic" value="claude-3-7-sonnet-latest">claude-3-7-sonnet-latest</option>70 <option data-type="anthropic" value="claude-3-7-sonnet-latest">claude-3-7-sonnet-latest</option>
@@ -67,33 +79,33 @@
67 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>79 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
68 <option data-type="google" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>80 <option data-type="google" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
69 <option data-type="google" value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>81 <option data-type="google" value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>
70 <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>82 <option data-type="google" value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
71 <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>83 <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>
72 <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>84 <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option>
73 <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>85 <option data-type="google" value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option>
74 <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
75 <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>86 <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>
77 <option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>87 <option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
78 <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>88 <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
79 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>89 <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
80 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>90 <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-04-17</option>
81 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>91 <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.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>
82 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>100 <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>
84 <option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>101 <option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
85 <option data-type="google" value="gemini-1.5-flash-exp-0827">gemini-1.5-flash-exp-0827</option>102 <option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
86 <option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>103 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
104 <option data-type="google" value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
87 <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>105 <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
88 <option data-type="google" value="gemini-exp-1114">gemini-exp-1114</option>106 <option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
89 <option data-type="google" value="gemini-exp-1121">gemini-exp-1121</option>107 <option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
90 <option data-type="google" value="gemini-exp-1206">gemini-exp-1206</option>108 <option data-type="google" value="learnlm-1.5-pro-experimental">learnlm-1.5-pro-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>
97 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>109 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
98 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>110 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
99 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>111 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
@@ -134,6 +146,8 @@
134 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>146 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
135 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>147 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
136 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>148 <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>
137 </select>151 </select>
138 </div>152 </div>
139 <div data-type="ollama">153 <div data-type="ollama">
public/scripts/extensions/memory/settings.html+1 -1
@@ -132,7 +132,7 @@
132 </label>132 </label>
133 <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.">133 <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.">
134 <input type="radio" name="memory_position" value="1" />134 <input type="radio" name="memory_position" value="1" />
135 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span> <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />135 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span> <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="9999" />
136 <span data-i18n="as">as</span>136 <span data-i18n="as">as</span>
137 <select id="memory_role" class="text_pole widthNatural">137 <select id="memory_role" class="text_pole widthNatural">
138 <option value="0" data-i18n="System">System</option>138 <option value="0" data-i18n="System">System</option>
public/scripts/extensions/regex/editor.html+2 -2
@@ -113,14 +113,14 @@
113 <span data-i18n="Min Depth">Min Depth</span>113 <span data-i18n="Min Depth">Min Depth</span>
114 <span class="fa-solid fa-circle-question note-link-span"></span>114 <span class="fa-solid fa-circle-question note-link-span"></span>
115 </small>115 </small>
116 <input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />116 <input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="9999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
117 </div>117 </div>
118 <div class="flex1 flex-container flexNoGap">118 <div class="flex1 flex-container flexNoGap">
119 <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.">119 <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.">
120 <span data-i18n="Max Depth">Max Depth</span>120 <span data-i18n="Max Depth">Max Depth</span>
121 <span class="fa-solid fa-circle-question note-link-span"></span>121 <span class="fa-solid fa-circle-question note-link-span"></span>
122 </small>122 </small>
123 <input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />123 <input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="9999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
124 </div>124 </div>
125 </div>125 </div>
126 </div>126 </div>
public/scripts/extensions/shared.js+13 -2
@@ -1,7 +1,7 @@
1import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';1import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3import { t } from '../i18n.js';3import { t } from '../i18n.js';
4import { oai_settings } from '../openai.js';4import { oai_settings, proxies } from '../openai.js';
5import { SECRET_KEYS, secret_state } from '../secrets.js';5import { SECRET_KEYS, secret_state } from '../secrets.js';
6import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';6import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
7import { getTokenCountAsync } from '../tokenizers.js';7import { getTokenCountAsync } from '../tokenizers.js';
@@ -153,6 +153,10 @@ function throwIfInvalidModel(useReverseProxy) {
153 throw new Error('Cohere API key is not set.');153 throw new Error('Cohere API key is not set.');
154 }154 }
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
156 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {160 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
157 throw new Error('Ollama server URL is not set.');161 throw new Error('Ollama server URL is not set.');
158 }162 }
@@ -306,9 +310,10 @@ export class ConnectionManagerRequestService {
306 * @param {boolean?} [custom.includePreset=true]310 * @param {boolean?} [custom.includePreset=true]
307 * @param {boolean?} [custom.includeInstruct=true]311 * @param {boolean?} [custom.includeInstruct=true]
308 * @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings312 * @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings
313 * @param {Record<string, any>} [overridePayload] - Override payload for the request
309 * @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 AsyncGenerator314 * @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
310 */315 */
311 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {316 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams, overridePayload = {}) {
312 const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };317 const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };
313318
314 const context = SillyTavern.getContext();319 const context = SillyTavern.getContext();
@@ -326,6 +331,8 @@ export class ConnectionManagerRequestService {
326 throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);331 throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
327 }332 }
328333
334 const proxyPreset = proxies.find((p) => p.name === profile.proxy);
335
329 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];336 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
330 return await context.ChatCompletionService.processRequest({337 return await context.ChatCompletionService.processRequest({
331 stream,338 stream,
@@ -334,6 +341,9 @@ export class ConnectionManagerRequestService {
334 model: profile.model,341 model: profile.model,
335 chat_completion_source: selectedApiMap.source,342 chat_completion_source: selectedApiMap.source,
336 custom_url: profile['api-url'],343 custom_url: profile['api-url'],
344 reverse_proxy: proxyPreset?.url,
345 proxy_password: proxyPreset?.password,
346 ...overridePayload,
337 }, {347 }, {
338 presetName: includePreset ? profile.preset : undefined,348 presetName: includePreset ? profile.preset : undefined,
339 }, extractData, signal);349 }, extractData, signal);
@@ -350,6 +360,7 @@ export class ConnectionManagerRequestService {
350 model: profile.model,360 model: profile.model,
351 api_type: selectedApiMap.type,361 api_type: selectedApiMap.type,
352 api_server: profile['api-url'],362 api_server: profile['api-url'],
363 ...overridePayload,
353 }, {364 }, {
354 instructName: includeInstruct ? profile.instruct : undefined,365 instructName: includeInstruct ? profile.instruct : undefined,
355 presetName: includePreset ? profile.preset : undefined,366 presetName: includePreset ? profile.preset : undefined,
public/scripts/extensions/stable-diffusion/index.js+55 -0
@@ -81,6 +81,7 @@ const sources = {
81 nanogpt: 'nanogpt',81 nanogpt: 'nanogpt',
82 bfl: 'bfl',82 bfl: 'bfl',
83 falai: 'falai',83 falai: 'falai',
84 xai: 'xai',
84};85};
8586
86const initiators = {87const initiators = {
@@ -1303,6 +1304,7 @@ async function onModelChange() {
1303 sources.nanogpt,1304 sources.nanogpt,
1304 sources.bfl,1305 sources.bfl,
1305 sources.falai,1306 sources.falai,
1307 sources.xai,
1306 ];1308 ];
13071309
1308 if (cloudSources.includes(extension_settings.sd.source)) {1310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1518,6 +1520,9 @@ async function loadSamplers() {
1518 case sources.bfl:1520 case sources.bfl:
1519 samplers = ['N/A'];1521 samplers = ['N/A'];
1520 break;1522 break;
1523 case sources.xai:
1524 samplers = ['N/A'];
1525 break;
1521 }1526 }
15221527
1523 for (const sampler of samplers) {1528 for (const sampler of samplers) {
@@ -1708,6 +1713,9 @@ async function loadModels() {
1708 case sources.falai:1713 case sources.falai:
1709 models = await loadFalaiModels();1714 models = await loadFalaiModels();
1710 break;1715 break;
1716 case sources.xai:
1717 models = await loadXAIModels();
1718 break;
1711 }1719 }
17121720
1713 for (const model of models) {1721 for (const model of models) {
@@ -1760,6 +1768,12 @@ async function loadFalaiModels() {
1760 return [];1768 return [];
1761}1769}
17621770
1771async function loadXAIModels() {
1772 return [
1773 { value: 'grok-2-image-1212', text: 'grok-2-image-1212' },
1774 ];
1775}
1776
1763async function loadPollinationsModels() {1777async function loadPollinationsModels() {
1764 const result = await fetch('/api/sd/pollinations/models', {1778 const result = await fetch('/api/sd/pollinations/models', {
1765 method: 'POST',1779 method: 'POST',
@@ -2081,6 +2095,9 @@ async function loadSchedulers() {
2081 case sources.falai:2095 case sources.falai:
2082 schedulers = ['N/A'];2096 schedulers = ['N/A'];
2083 break;2097 break;
2098 case sources.xai:
2099 schedulers = ['N/A'];
2100 break;
2084 }2101 }
20852102
2086 for (const scheduler of schedulers) {2103 for (const scheduler of schedulers) {
@@ -2166,6 +2183,12 @@ async function loadVaes() {
2166 case sources.bfl:2183 case sources.bfl:
2167 vaes = ['N/A'];2184 vaes = ['N/A'];
2168 break;2185 break;
2186 case sources.falai:
2187 vaes = ['N/A'];
2188 break;
2189 case sources.xai:
2190 vaes = ['N/A'];
2191 break;
2169 }2192 }
21702193
2171 for (const vae of vaes) {2194 for (const vae of vaes) {
@@ -2735,6 +2758,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2735 case sources.falai:2758 case sources.falai:
2736 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);2759 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2737 break;2760 break;
2761 case sources.xai:
2762 result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);
2763 break;
2738 }2764 }
27392765
2740 if (!result.data) {2766 if (!result.data) {
@@ -3464,6 +3490,33 @@ async function generateBflImage(prompt, signal) {
3464}3490}
34653491
3466/**3492/**
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 */
3499async 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/**
3467 * Generates an image using the FAL.AI API.3520 * Generates an image using the FAL.AI API.
3468 * @param {string} prompt - The main instruction used to guide the image generation.3521 * @param {string} prompt - The main instruction used to guide the image generation.
3469 * @param {string} negativePrompt - The negative prompt used to guide the image generation.3522 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
@@ -3782,6 +3835,8 @@ function isValidState() {
3782 return secret_state[SECRET_KEYS.BFL];3835 return secret_state[SECRET_KEYS.BFL];
3783 case sources.falai:3836 case sources.falai:
3784 return secret_state[SECRET_KEYS.FALAI];3837 return secret_state[SECRET_KEYS.FALAI];
3838 case sources.xai:
3839 return secret_state[SECRET_KEYS.XAI];
3785 }3840 }
3786}3841}
37873842
public/scripts/extensions/stable-diffusion/settings.html+1 -0
@@ -52,6 +52,7 @@
52 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>52 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
53 <option value="horde">Stable Horde</option>53 <option value="horde">Stable Horde</option>
54 <option value="togetherai">TogetherAI</option>54 <option value="togetherai">TogetherAI</option>
55 <option value="xai">xAI (Grok)</option>
55 </select>56 </select>
56 <div data-sd-source="auto">57 <div data-sd-source="auto">
57 <label for="sd_auto_url">SD Web UI URL</label>58 <label for="sd_auto_url">SD Web UI URL</label>
public/scripts/extensions/tts/settings.html+1 -1
@@ -76,7 +76,7 @@
76 <div id="tts_voicemap_block">76 <div id="tts_voicemap_block">
77 </div>77 </div>
78 <hr>78 <hr>
79 <form id="tts_provider_settings" class="inline-drawer-content">79 <form id="tts_provider_settings">
80 </form>80 </form>
81 <div class="tts_buttons">81 <div class="tts_buttons">
82 <input id="tts_voices" class="menu_button" type="submit" value="Available voices" />82 <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 {
79 // Config //79 // Config //
80 //########//80 //########//
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
82 settings;86 settings;
83 ready = false;87 ready = false;
84 voices = [];88 voices = [];
@@ -168,51 +172,97 @@ class SystemTtsProvider {
168 //#################//172 //#################//
169 fetchTtsVoiceObjects() {173 fetchTtsVoiceObjects() {
170 if (!('speechSynthesis' in window)) {174 if (!('speechSynthesis' in window)) {
171 return [];175 return Promise.resolve([]);
172 }176 }
173177
174 return new Promise((resolve) => {178 return new Promise((resolve) => {
175 setTimeout(() => {179 setTimeout(() => {
176 const voices = speechSynthesis180 let 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
180184 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);
183 });199 });
184 }200 }
185201
186 previewTtsVoice(voiceId) {202 previewTtsVoice(voiceId) {
187 if (!('speechSynthesis' in window)) {203 if (!('speechSynthesis' in window)) {
188 throw 'Speech synthesis API is not supported';204 throw new Error('Speech synthesis API is not supported');
189 }205 }
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
193 if (!voice) {212 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.');
195 }219 }
196220
197 speechSynthesis.cancel();221 speechSynthesis.cancel();
198 const text = getPreviewString(voice.lang);222 const langForPreview = voice ? voice.lang : (navigator.language || 'en-US');
223 const text = getPreviewString(langForPreview);
199 const utterance = new SpeechSynthesisUtterance(text);224 const utterance = new SpeechSynthesisUtterance(text);
200 utterance.voice = voice;225
226 if (voice) {
227 utterance.voice = voice;
228 }
229
201 utterance.rate = this.settings.rate || 1;230 utterance.rate = this.settings.rate || 1;
202 utterance.pitch = this.settings.pitch || 1;231 utterance.pitch = this.settings.pitch || 1;
232
233 utterance.onerror = (event) => {
234 console.error(`SystemTTS Preview Error: ${event.error}`, event);
235 };
236
203 speechSynthesis.speak(utterance);237 speechSynthesis.speak(utterance);
204 }238 }
205239
206 async getVoice(voiceName) {240 async getVoice(voiceName) {
207 if (!('speechSynthesis' in window)) {241 if (!('speechSynthesis' in window)) {
208 return { voice_id: null };242 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 };
209 }250 }
210251
211 const voices = speechSynthesis.getVoices();252 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
212 const match = voices.find(x => x.name == voiceName);262 const match = voices.find(x => x.name == voiceName);
213263
214 if (!match) {264 if (!match) {
215 throw `TTS Voice name ${voiceName} not found`;265 throw new Error(`SystemTTS getVoice: TTS Voice name "${voiceName}" not found`);
216 }266 }
217267
218 return { voice_id: match.voiceURI, name: match.name };268 return { voice_id: match.voiceURI, name: match.name };
@@ -237,7 +287,6 @@ class SystemTtsProvider {
237 speechUtteranceChunker(utterance, {287 speechUtteranceChunker(utterance, {
238 chunkLength: 200,288 chunkLength: 200,
239 }, function () {289 }, function () {
240 //some code to execute when done
241 resolve(silence);290 resolve(silence);
242 console.log('System TTS done');291 console.log('System TTS done');
243 });292 });
public/scripts/extensions/vectors/index.js+35 -10
@@ -55,6 +55,8 @@ const getBatchSize = () => ['transformers', 'palm', 'ollama'].includes(settings.
55const settings = {55const settings = {
56 // For both56 // For both
57 source: 'transformers',57 source: 'transformers',
58 alt_endpoint_url: '',
59 use_alt_endpoint: false,
58 include_wi: false,60 include_wi: false,
59 togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',61 togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
60 openai_model: 'text-embedding-ada-002',62 openai_model: 'text-embedding-ada-002',
@@ -109,6 +111,7 @@ const settings = {
109const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);111const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
110const webllmProvider = new WebLlmVectorProvider();112const webllmProvider = new WebLlmVectorProvider();
111const cachedSummaries = new Map();113const cachedSummaries = new Map();
114const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
112115
113/**116/**
114 * Gets the Collection ID for a file embedded in the chat.117 * Gets the Collection ID for a file embedded in the chat.
@@ -777,14 +780,14 @@ function getVectorsRequestBody(args = {}) {
777 break;780 break;
778 case 'ollama':781 case 'ollama':
779 body.model = extension_settings.vectors.ollama_model;782 body.model = extension_settings.vectors.ollama_model;
780 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];783 body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
781 body.keep = !!extension_settings.vectors.ollama_keep;784 body.keep = !!extension_settings.vectors.ollama_keep;
782 break;785 break;
783 case 'llamacpp':786 case 'llamacpp':
784 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];787 body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
785 break;788 break;
786 case 'vllm':789 case 'vllm':
787 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];790 body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.VLLM];
788 body.model = extension_settings.vectors.vllm_model;791 body.model = extension_settings.vectors.vllm_model;
789 break;792 break;
790 case 'webllm':793 case 'webllm':
@@ -826,11 +829,12 @@ async function getAdditionalArgs(items) {
826* @returns {Promise<number[]>} Saved hashes829* @returns {Promise<number[]>} Saved hashes
827*/830*/
828async function getSavedHashes(collectionId) {831async function getSavedHashes(collectionId) {
832 const args = await getAdditionalArgs([]);
829 const response = await fetch('/api/vector/list', {833 const response = await fetch('/api/vector/list', {
830 method: 'POST',834 method: 'POST',
831 headers: getRequestHeaders(),835 headers: getRequestHeaders(),
832 body: JSON.stringify({836 body: JSON.stringify({
833 ...getVectorsRequestBody(),837 ...getVectorsRequestBody(args),
834 collectionId: collectionId,838 collectionId: collectionId,
835 source: settings.source,839 source: settings.source,
836 }),840 }),
@@ -883,11 +887,18 @@ function throwIfSourceInvalid() {
883 throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });887 throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
884 }888 }
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 }
891 }902 }
892903
893 if (settings.source === 'ollama' && !settings.ollama_model || settings.source === 'vllm' && !settings.vllm_model) {904 if (settings.source === 'ollama' && !settings.ollama_model || settings.source === 'vllm' && !settings.vllm_model) {
@@ -1087,6 +1098,7 @@ function toggleSettings() {
1087 $('#webllm_vectorsModel').toggle(settings.source === 'webllm');1098 $('#webllm_vectorsModel').toggle(settings.source === 'webllm');
1088 $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');1099 $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
1089 $('#google_vectorsModel').toggle(settings.source === 'palm');1100 $('#google_vectorsModel').toggle(settings.source === 'palm');
1101 $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
1090 if (settings.source === 'webllm') {1102 if (settings.source === 'webllm') {
1091 loadWebLlmModels();1103 loadWebLlmModels();
1092 }1104 }
@@ -1144,6 +1156,9 @@ function loadWebLlmModels() {
1144 * @returns {Promise<Record<string, number[]>>} Calculated embeddings1156 * @returns {Promise<Record<string, number[]>>} Calculated embeddings
1145 */1157 */
1146async function createWebLlmEmbeddings(items) {1158async function createWebLlmEmbeddings(items) {
1159 if (items.length === 0) {
1160 return /** @type {Record<string, number[]>} */ ({});
1161 }
1147 return executeWithWebLlmErrorHandling(async () => {1162 return executeWithWebLlmErrorHandling(async () => {
1148 const embeddings = await webllmProvider.embedTexts(items, settings.webllm_model);1163 const embeddings = await webllmProvider.embedTexts(items, settings.webllm_model);
1149 const result = /** @type {Record<string, number[]>} */ ({});1164 const result = /** @type {Record<string, number[]>} */ ({});
@@ -1165,7 +1180,7 @@ async function createKoboldCppEmbeddings(items) {
1165 headers: getRequestHeaders(),1180 headers: getRequestHeaders(),
1166 body: JSON.stringify({1181 body: JSON.stringify({
1167 items: items,1182 items: items,
1168 server: textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP],1183 server: settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP],
1169 }),1184 }),
1170 });1185 });
11711186
@@ -1467,6 +1482,16 @@ jQuery(async () => {
1467 saveSettingsDebounced();1482 saveSettingsDebounced();
1468 toggleSettings();1483 toggleSettings();
1469 });1484 });
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 });
1470 $('#api_key_nomicai').on('click', async () => {1495 $('#api_key_nomicai').on('click', async () => {
1471 const popupText = 'NomicAI API Key:';1496 const popupText = 'NomicAI API Key:';
1472 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {1497 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
public/scripts/extensions/vectors/settings.html+13 -2
@@ -25,6 +25,16 @@
25 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>25 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
26 </select>26 </select>
27 </div>27 </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>
28 <div class="flex-container flexFlowColumn" id="webllm_vectorsModel">38 <div class="flex-container flexFlowColumn" id="webllm_vectorsModel">
29 <label for="vectors_webllm_model" data-i18n="Vectorization Model">39 <label for="vectors_webllm_model" data-i18n="Vectorization Model">
30 Vectorization Model40 Vectorization Model
@@ -87,6 +97,7 @@
87 Vectorization Model97 Vectorization Model
88 </label>98 </label>
89 <select id="vectors_cohere_model" class="text_pole">99 <select id="vectors_cohere_model" class="text_pole">
100 <option value="embed-v4.0">embed-v4.0</option>
90 <option value="embed-english-v3.0">embed-english-v3.0</option>101 <option value="embed-english-v3.0">embed-english-v3.0</option>
91 <option value="embed-multilingual-v3.0">embed-multilingual-v3.0</option>102 <option value="embed-multilingual-v3.0">embed-multilingual-v3.0</option>
92 <option value="embed-english-light-v3.0">embed-english-light-v3.0</option>103 <option value="embed-english-light-v3.0">embed-english-light-v3.0</option>
@@ -310,7 +321,7 @@
310 <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.">321 <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.">
311 <input type="radio" name="vectors_file_position_db" value="1" />322 <input type="radio" name="vectors_file_position_db" value="1" />
312 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>323 <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
313 <input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="999" />324 <input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="9999" />
314 <span>as</span>325 <span>as</span>
315 <select id="vectors_file_depth_role_db" class="text_pole widthNatural">326 <select id="vectors_file_depth_role_db" class="text_pole widthNatural">
316 <option value="0" data-i18n="System">System</option>327 <option value="0" data-i18n="System">System</option>
@@ -362,7 +373,7 @@
362 <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.">373 <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.">
363 <input type="radio" name="vectors_position" value="1" />374 <input type="radio" name="vectors_position" value="1" />
364 <span data-i18n="In-chat @ Depth">In-chat @ Depth </span>375 <span data-i18n="In-chat @ Depth">In-chat @ Depth </span>
365 <input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="999" />376 <input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="9999" />
366 </label>377 </label>
367 </div>378 </div>
368 <div class="flex-container">379 <div class="flex-container">
public/scripts/group-chats.js+19 -7
@@ -13,6 +13,9 @@ import {
13 getBase64Async,13 getBase64Async,
14 resetScrollHeight,14 resetScrollHeight,
15 initScrollHeight,15 initScrollHeight,
16 localizePagination,
17 renderPaginationDropdown,
18 paginationDropdownChangeHandler,
16} from './utils.js';19} from './utils.js';
17import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';20import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
18import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';21import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';
@@ -1374,6 +1377,8 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) {
13741377
1375function printGroupCandidates() {1378function printGroupCandidates() {
1376 const storageKey = 'GroupCandidates_PerPage';1379 const storageKey = 'GroupCandidates_PerPage';
1380 const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
1381 const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
1377 $('#rm_group_add_members_pagination').pagination({1382 $('#rm_group_add_members_pagination').pagination({
1378 dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }),1383 dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }),
1379 pageRange: 1,1384 pageRange: 1,
@@ -1382,18 +1387,20 @@ function printGroupCandidates() {
1382 prevText: '<',1387 prevText: '<',
1383 nextText: '>',1388 nextText: '>',
1384 formatNavigator: PAGINATION_TEMPLATE,1389 formatNavigator: PAGINATION_TEMPLATE,
1390 formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
1385 showNavigator: true,1391 showNavigator: true,
1386 showSizeChanger: true,1392 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) {
1390 accountStorage.setItem(storageKey, e.target.value);1395 accountStorage.setItem(storageKey, e.target.value);
1396 paginationDropdownChangeHandler(e, size);
1391 },1397 },
1392 callback: function (data) {1398 callback: function (data) {
1393 $('#rm_group_add_members').empty();1399 $('#rm_group_add_members').empty();
1394 for (const i of data) {1400 for (const i of data) {
1395 $('#rm_group_add_members').append(getGroupCharacterBlock(i.item));1401 $('#rm_group_add_members').append(getGroupCharacterBlock(i.item));
1396 }1402 }
1403 localizePagination($('#rm_group_add_members_pagination'));
1397 },1404 },
1398 });1405 });
1399}1406}
@@ -1401,6 +1408,9 @@ function printGroupCandidates() {
1401function printGroupMembers() {1408function printGroupMembers() {
1402 const storageKey = 'GroupMembers_PerPage';1409 const storageKey = 'GroupMembers_PerPage';
1403 $('.rm_group_members_pagination').each(function () {1410 $('.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];
1404 $(this).pagination({1414 $(this).pagination({
1405 dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),1415 dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
1406 pageRange: 1,1416 pageRange: 1,
@@ -1411,16 +1421,18 @@ function printGroupMembers() {
1411 formatNavigator: PAGINATION_TEMPLATE,1421 formatNavigator: PAGINATION_TEMPLATE,
1412 showNavigator: true,1422 showNavigator: true,
1413 showSizeChanger: true,1423 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,
1416 afterSizeSelectorChange: function (e) {1426 afterSizeSelectorChange: function (e, size) {
1417 accountStorage.setItem(storageKey, e.target.value);1427 accountStorage.setItem(storageKey, e.target.value);
1428 paginationDropdownChangeHandler(e, size);
1418 },1429 },
1419 callback: function (data) {1430 callback: function (data) {
1420 $('.rm_group_members').empty();1431 $('.rm_group_members').empty();
1421 for (const i of data) {1432 for (const i of data) {
1422 $('.rm_group_members').append(getGroupCharacterBlock(i.item));1433 $('.rm_group_members').append(getGroupCharacterBlock(i.item));
1423 }1434 }
1435 localizePagination($(that));
1424 },1436 },
1425 });1437 });
1426 });1438 });
@@ -1804,7 +1816,7 @@ async function createGroup() {
1804 const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');1816 const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');
18051817
1806 if (!name) {1818 if (!name) {
1807 name = `Group: ${memberNames}`;1819 name = t`Group: ${memberNames}`;
1808 }1820 }
18091821
1810 const avatar_url = $('#group_avatar_preview img').attr('src');1822 const avatar_url = $('#group_avatar_preview img').attr('src');
public/scripts/horde.js+3 -3
@@ -394,7 +394,7 @@ function getHordeModelTemplate(option) {
394 `));394 `));
395}395}
396396
397jQuery(function () {397export function initHorde () {
398 $('#horde_model').on('mousedown change', async function (e) {398 $('#horde_model').on('mousedown change', async function (e) {
399 console.log('Horde model change', e);399 console.log('Horde model change', e);
400 horde_settings.models = $('#horde_model').val();400 horde_settings.models = $('#horde_model').val();
@@ -441,7 +441,7 @@ jQuery(function () {
441 if (!isMobile()) {441 if (!isMobile()) {
442 $('#horde_model').select2({442 $('#horde_model').select2({
443 width: '100%',443 width: '100%',
444 placeholder: 'Select Horde models',444 placeholder: t`Select Horde models`,
445 allowClear: true,445 allowClear: true,
446 closeOnSelect: false,446 closeOnSelect: false,
447 templateSelection: function (data) {447 templateSelection: function (data) {
@@ -451,5 +451,5 @@ jQuery(function () {
451 templateResult: getHordeModelTemplate,451 templateResult: getHordeModelTemplate,
452 });452 });
453 }453 }
454});454}
455455
public/scripts/instruct-mode.js+26 -24
@@ -208,37 +208,39 @@ export function autoSelectInstructPreset(modelId) {
208208
209 // Select matching instruct preset209 // Select matching instruct preset
210 let foundMatch = false;210 let foundMatch = false;
211 for (const instruct_preset of instruct_presets) {211
212 // If instruct preset matches the context template212 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 }
217 }228 }
218 }229 }
230
219 // If no match was found, auto-select instruct preset231 // If no match was found, auto-select instruct preset
220 if (!foundMatch) {232 if (!foundMatch && power_user.instruct.bind_to_context) {
221 for (const preset of instruct_presets) {233 for (const instruct_preset of instruct_presets) {
222 // If activation regex is set, check if it matches the model id234 // If instruct preset matches the context template
223 if (preset.activation_regex) {235 if (instruct_preset.name === power_user.context.preset) {
224 try {236 selectInstructPreset(instruct_preset.name, { isAuto: true });
225 const regex = regexFromString(preset.activation_regex);237 foundMatch = true;
226238 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 }
237 }239 }
238 }240 }
239 }241 }
240242
241 return false;243 return foundMatch;
242}244}
243245
244/**246/**
public/scripts/openai.js+255 -105
@@ -15,12 +15,12 @@ import {
15 extension_prompt_types,15 extension_prompt_types,
16 Generate,16 Generate,
17 getExtensionPrompt,17 getExtensionPrompt,
18 getExtensionPromptMaxDepth,
18 getNextMessageId,19 getNextMessageId,
19 getRequestHeaders,20 getRequestHeaders,
20 getStoppingStrings,21 getStoppingStrings,
21 is_send_press,22 is_send_press,
22 main_api,23 main_api,
23 MAX_INJECTION_DEPTH,
24 name1,24 name1,
25 name2,25 name2,
26 replaceItemizedPromptText,26 replaceItemizedPromptText,
@@ -184,6 +184,7 @@ export const chat_completion_sources = {
184 ZEROONEAI: '01ai',184 ZEROONEAI: '01ai',
185 NANOGPT: 'nanogpt',185 NANOGPT: 'nanogpt',
186 DEEPSEEK: 'deepseek',186 DEEPSEEK: 'deepseek',
187 XAI: 'xai',
187};188};
188189
189const character_names_behavior = {190const character_names_behavior = {
@@ -215,6 +216,15 @@ const openrouter_middleout_types = {
215 OFF: 'off',216 OFF: 'off',
216};217};
217218
219export const reasoning_effort_types = {
220 auto: 'auto',
221 low: 'low',
222 medium: 'medium',
223 high: 'high',
224 min: 'min',
225 max: 'max',
226};
227
218const sensitiveFields = [228const sensitiveFields = [
219 'reverse_proxy',229 'reverse_proxy',
220 'proxy_password',230 'proxy_password',
@@ -257,6 +267,7 @@ export const settingsToUpdate = {
257 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false],267 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false],
258 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false],268 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false],
259 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],269 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],
270 xai_model: ['#model_xai_select', 'xai_model', false],
260 custom_model: ['#custom_model_id', 'custom_model', false],271 custom_model: ['#custom_model_id', 'custom_model', false],
261 custom_url: ['#custom_api_url_text', 'custom_url', false],272 custom_url: ['#custom_api_url_text', 'custom_url', false],
262 custom_include_body: ['#custom_include_body', 'custom_include_body', false],273 custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@@ -345,6 +356,7 @@ const default_settings = {
345 nanogpt_model: 'gpt-4o-mini',356 nanogpt_model: 'gpt-4o-mini',
346 zerooneai_model: 'yi-large',357 zerooneai_model: 'yi-large',
347 deepseek_model: 'deepseek-chat',358 deepseek_model: 'deepseek-chat',
359 xai_model: 'grok-3-beta',
348 custom_model: '',360 custom_model: '',
349 custom_url: '',361 custom_url: '',
350 custom_include_body: '',362 custom_include_body: '',
@@ -379,7 +391,7 @@ const default_settings = {
379 continue_postfix: continue_postfix_types.SPACE,391 continue_postfix: continue_postfix_types.SPACE,
380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,392 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
381 show_thoughts: true,393 show_thoughts: true,
382 reasoning_effort: 'medium',394 reasoning_effort: reasoning_effort_types.auto,
383 enable_web_search: false,395 enable_web_search: false,
384 request_images: false,396 request_images: false,
385 seed: -1,397 seed: -1,
@@ -425,6 +437,7 @@ const oai_settings = {
425 nanogpt_model: 'gpt-4o-mini',437 nanogpt_model: 'gpt-4o-mini',
426 zerooneai_model: 'yi-large',438 zerooneai_model: 'yi-large',
427 deepseek_model: 'deepseek-chat',439 deepseek_model: 'deepseek-chat',
440 xai_model: 'grok-3-beta',
428 custom_model: '',441 custom_model: '',
429 custom_url: '',442 custom_url: '',
430 custom_include_body: '',443 custom_include_body: '',
@@ -459,7 +472,7 @@ const oai_settings = {
459 continue_postfix: continue_postfix_types.SPACE,472 continue_postfix: continue_postfix_types.SPACE,
460 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,473 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
461 show_thoughts: true,474 show_thoughts: true,
462 reasoning_effort: 'medium',475 reasoning_effort: reasoning_effort_types.auto,
463 enable_web_search: false,476 enable_web_search: false,
464 request_images: false,477 request_images: false,
465 seed: -1,478 seed: -1,
@@ -738,7 +751,8 @@ async function populationInjectionPrompts(prompts, messages) {
738 'assistant': extension_prompt_roles.ASSISTANT,751 'assistant': extension_prompt_roles.ASSISTANT,
739 };752 };
740753
741 for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) {754 const maxDepth = getExtensionPromptMaxDepth();
755 for (let i = 0; i <= maxDepth; i++) {
742 // Get prompts for current depth756 // Get prompts for current depth
743 const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content);757 const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content);
744758
@@ -1407,9 +1421,9 @@ export async function prepareOpenAIMessages({
1407 await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });1421 await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });
1408 } catch (error) {1422 } catch (error) {
1409 if (error instanceof TokenBudgetExceededError) {1423 if (error instanceof TokenBudgetExceededError) {
1410 toastr.error(t`An error occurred while counting tokens: Token budget exceeded.`);1424 toastr.error(t`Mandatory prompts exceed the context size.`);
1411 chatCompletion.log('Token budget exceeded.');1425 chatCompletion.log('Mandatory prompts exceed the context size.');
1412 promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.`;1426 promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token limit or disable custom prompts.`;
1413 } else if (error instanceof InvalidCharacterNameError) {1427 } else if (error instanceof InvalidCharacterNameError) {
1414 toastr.warning(t`An error occurred while counting tokens: Invalid character name`);1428 toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
1415 chatCompletion.log('Invalid character name');1429 chatCompletion.log('Invalid character name');
@@ -1644,6 +1658,8 @@ export function getChatCompletionModel(source = null) {
1644 return oai_settings.nanogpt_model;1658 return oai_settings.nanogpt_model;
1645 case chat_completion_sources.DEEPSEEK:1659 case chat_completion_sources.DEEPSEEK:
1646 return oai_settings.deepseek_model;1660 return oai_settings.deepseek_model;
1661 case chat_completion_sources.XAI:
1662 return oai_settings.xai_model;
1647 default:1663 default:
1648 throw new Error(`Unknown chat completion source: ${activeSource}`);1664 throw new Error(`Unknown chat completion source: ${activeSource}`);
1649 }1665 }
@@ -1687,6 +1703,11 @@ function calculateOpenRouterCost() {
1687 }1703 }
1688 }1704 }
16891705
1706 if (oai_settings.enable_web_search) {
1707 const webSearchCost = (0.02).toFixed(2);
1708 cost = t`${cost} + $${webSearchCost}`;
1709 }
1710
1690 $('#openrouter_max_prompt_cost').text(cost);1711 $('#openrouter_max_prompt_cost').text(cost);
1691}1712}
16921713
@@ -1925,6 +1946,31 @@ async function sendAltScaleRequest(messages, logit_bias, signal, type) {
1925 return data.output;1946 return data.output;
1926}1947}
19271948
1949function 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
1928/**1974/**
1929 * Send a chat completion request to backend1975 * Send a chat completion request to backend
1930 * @param {string} type (impersonate, quiet, continue, etc)1976 * @param {string} type (impersonate, quiet, continue, etc)
@@ -1961,13 +2007,14 @@ async function sendOpenAIRequest(type, messages, signal) {
1961 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;2007 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
1962 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;2008 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
1963 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;2009 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
2010 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
1964 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);2011 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
1965 const isQuiet = type === 'quiet';2012 const isQuiet = type === 'quiet';
1966 const isImpersonate = type === 'impersonate';2013 const isImpersonate = type === 'impersonate';
1967 const isContinue = type === 'continue';2014 const isContinue = type === 'continue';
1968 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));2015 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
1969 const useLogprobs = !!power_user.request_token_probabilities;2016 const useLogprobs = !!power_user.request_token_probabilities;
1970 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);2017 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI);
19712018
1972 // If we're using the window.ai extension, use that instead2019 // If we're using the window.ai extension, use that instead
1973 // Doesn't support logit bias yet2020 // Doesn't support logit bias yet
@@ -2010,7 +2057,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2010 'char_name': name2,2057 'char_name': name2,
2011 'group_names': getGroupNames(),2058 'group_names': getGroupNames(),
2012 'include_reasoning': Boolean(oai_settings.show_thoughts),2059 'include_reasoning': Boolean(oai_settings.show_thoughts),
2013 'reasoning_effort': String(oai_settings.reasoning_effort),2060 'reasoning_effort': getReasoningEffort(),
2014 'enable_web_search': Boolean(oai_settings.enable_web_search),2061 'enable_web_search': Boolean(oai_settings.enable_web_search),
2015 'request_images': Boolean(oai_settings.request_images),2062 'request_images': Boolean(oai_settings.request_images),
2016 'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,2063 'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,
@@ -2026,14 +2073,14 @@ async function sendOpenAIRequest(type, messages, signal) {
2026 }2073 }
20272074
2028 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite2075 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
2029 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {2076 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)) {
2030 await validateReverseProxy();2077 await validateReverseProxy();
2031 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;2078 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
2032 generate_data['proxy_password'] = oai_settings.proxy_password;2079 generate_data['proxy_password'] = oai_settings.proxy_password;
2033 }2080 }
20342081
2035 // Add logprobs request (currently OpenAI only, max 5 on their side)2082 // Add logprobs request (currently OpenAI only, max 5 on their side)
2036 if (useLogprobs && (isOAI || isCustom || isDeepSeek)) {2083 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI)) {
2037 generate_data['logprobs'] = 5;2084 generate_data['logprobs'] = 5;
2038 }2085 }
20392086
@@ -2152,29 +2199,42 @@ async function sendOpenAIRequest(type, messages, signal) {
2152 }2199 }
2153 }2200 }
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) {
2156 generate_data['seed'] = oai_settings.seed;2214 generate_data['seed'] = oai_settings.seed;
2157 }2215 }
21582216
2159 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {2217 if (isOAI && /^(o1|o3|o4)/.test(oai_settings.openai_model)) {
2160 generate_data.messages.forEach((msg) => {
2161 if (msg.role === 'system') {
2162 msg.role = 'user';
2163 }
2164 });
2165 generate_data.max_completion_tokens = generate_data.max_tokens;2218 generate_data.max_completion_tokens = generate_data.max_tokens;
2166 delete generate_data.max_tokens;2219 delete generate_data.max_tokens;
2167 delete generate_data.logprobs;2220 delete generate_data.logprobs;
2168 delete generate_data.top_logprobs;2221 delete generate_data.top_logprobs;
2169 delete generate_data.n;2222 delete generate_data.stop;
2223 delete generate_data.logit_bias;
2170 delete generate_data.temperature;2224 delete generate_data.temperature;
2171 delete generate_data.top_p;2225 delete generate_data.top_p;
2172 delete generate_data.frequency_penalty;2226 delete generate_data.frequency_penalty;
2173 delete generate_data.presence_penalty;2227 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 }
2178 }2238 }
21792239
2180 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);2240 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -2210,7 +2270,8 @@ async function sendOpenAIRequest(type, messages, signal) {
22102270
2211 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {2271 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
2212 const swipeIndex = parsed.choices[0].index - 1;2272 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 });
2214 } else {2275 } else {
2215 text += getStreamingReply(parsed, state);2276 text += getStreamingReply(parsed, state);
2216 }2277 }
@@ -2278,6 +2339,11 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
2278 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');2339 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2279 }2340 }
2280 return data.choices?.[0]?.delta?.content || '';2341 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 || '';
2281 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {2347 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
2282 if (show_thoughts) {2348 if (show_thoughts) {
2283 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');2349 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
@@ -2310,6 +2376,7 @@ function parseChatCompletionLogprobs(data) {
2310 switch (oai_settings.chat_completion_source) {2376 switch (oai_settings.chat_completion_source) {
2311 case chat_completion_sources.OPENAI:2377 case chat_completion_sources.OPENAI:
2312 case chat_completion_sources.DEEPSEEK:2378 case chat_completion_sources.DEEPSEEK:
2379 case chat_completion_sources.XAI:
2313 case chat_completion_sources.CUSTOM:2380 case chat_completion_sources.CUSTOM:
2314 if (!data.choices?.length) {2381 if (!data.choices?.length) {
2315 return null;2382 return null;
@@ -3231,6 +3298,7 @@ function loadOpenAISettings(data, settings) {
3231 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;3298 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
3232 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;3299 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
3233 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;3300 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
3301 oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
3234 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;3302 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
3235 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;3303 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
3236 oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;3304 oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@@ -3316,6 +3384,8 @@ function loadOpenAISettings(data, settings) {
3316 $('#model_deepseek_select').val(oai_settings.deepseek_model);3384 $('#model_deepseek_select').val(oai_settings.deepseek_model);
3317 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);3385 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
3318 $('#model_01ai_select').val(oai_settings.zerooneai_model);3386 $('#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);
3319 $('#custom_model_id').val(oai_settings.custom_model);3389 $('#custom_model_id').val(oai_settings.custom_model);
3320 $('#custom_api_url_text').val(oai_settings.custom_url);3390 $('#custom_api_url_text').val(oai_settings.custom_url);
3321 $('#openai_max_context').val(oai_settings.openai_max_context);3391 $('#openai_max_context').val(oai_settings.openai_max_context);
@@ -3512,7 +3582,7 @@ async function getStatusOpen() {
3512 chat_completion_source: oai_settings.chat_completion_source,3582 chat_completion_source: oai_settings.chat_completion_source,
3513 };3583 };
35143584
3515 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {3585 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)) {
3516 await validateReverseProxy();3586 await validateReverseProxy();
3517 }3587 }
35183588
@@ -3781,7 +3851,7 @@ function createLogitBiasListItem(entry) {
3781}3851}
37823852
3783async function createNewLogitBiasPreset() {3853async function createNewLogitBiasPreset() {
3784 const name = await callPopup('Preset name:', 'input');3854 const name = await Popup.show.input(t`Preset name:`, null);
37853855
3786 if (!name) {3856 if (!name) {
3787 return;3857 return;
@@ -4092,9 +4162,15 @@ function getMaxContextOpenAI(value) {
4092 if (oai_settings.max_context_unlocked) {4162 if (oai_settings.max_context_unlocked) {
4093 return unlocked_max;4163 return unlocked_max;
4094 }4164 }
4095 else if (value.startsWith('o1') || value.startsWith('o3')) {4165 else if (value.includes('gpt-4.1')) {
4166 return max_1mil;
4167 }
4168 else if (value.startsWith('o1')) {
4096 return max_128k;4169 return max_128k;
4097 }4170 }
4171 else if (value.startsWith('o4') || value.startsWith('o3')) {
4172 return max_200k;
4173 }
4098 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')) {4174 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')) {
4099 return max_128k;4175 return max_128k;
4100 }4176 }
@@ -4166,6 +4242,80 @@ function getMaxContextWindowAI(value) {
4166}4242}
41674243
4168/**4244/**
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 */
4250function 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/**
4169 * Get the maximum context size for the Groq model4319 * Get the maximum context size for the Groq model
4170 * @param {string} model Model identifier4320 * @param {string} model Model identifier
4171 * @param {boolean} isUnlocked Whether context limits are unlocked4321 * @param {boolean} isUnlocked Whether context limits are unlocked
@@ -4312,6 +4462,11 @@ async function onModelChange() {
4312 $('#custom_model_id').val(value).trigger('input');4462 $('#custom_model_id').val(value).trigger('input');
4313 }4463 }
43144464
4465 if ($(this).is('#model_xai_select')) {
4466 console.log('XAI model changed to', value);
4467 oai_settings.xai_model = value;
4468 }
4469
4315 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {4470 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
4316 if (oai_settings.max_context_unlocked) {4471 if (oai_settings.max_context_unlocked) {
4317 $('#openai_max_context').attr('max', unlocked_max);4472 $('#openai_max_context').attr('max', unlocked_max);
@@ -4326,20 +4481,16 @@ async function onModelChange() {
4326 if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {4481 if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
4327 if (oai_settings.max_context_unlocked) {4482 if (oai_settings.max_context_unlocked) {
4328 $('#openai_max_context').attr('max', max_2mil);4483 $('#openai_max_context').attr('max', max_2mil);
4329 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {4484 } else if (value.includes('gemini-1.5-pro')) {
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')) {
4332 $('#openai_max_context').attr('max', max_2mil);4485 $('#openai_max_context').attr('max', max_2mil);
4333 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash') || value.includes('gemini-2.5-pro-exp-03-25') || value.includes('gemini-2.5-pro-preview-03-25')) {4486 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash') || value.includes('gemini-2.0-pro') || value.includes('gemini-exp') || value.includes('gemini-2.5-flash') || value.includes('gemini-2.5-pro') || value.includes('learnlm-2.0-flash')) {
4334 $('#openai_max_context').attr('max', max_1mil);4487 $('#openai_max_context').attr('max', max_1mil);
4335 } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {4488 } else if (value.includes('gemma-3-27b-it')) {
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')) {
4340 $('#openai_max_context').attr('max', max_128k);4489 $('#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);
4341 } else {4492 } else {
4342 $('#openai_max_context').attr('max', max_4k);4493 $('#openai_max_context').attr('max', max_32k);
4343 }4494 }
4344 let makersuite_max_temp = (value.includes('vision') || value.includes('ultra') || value.includes('gemma')) ? 1.0 : 2.0;4495 let makersuite_max_temp = (value.includes('vision') || value.includes('ultra') || value.includes('gemma')) ? 1.0 : 2.0;
4345 oai_settings.temp_openai = Math.min(makersuite_max_temp, oai_settings.temp_openai);4496 oai_settings.temp_openai = Math.min(makersuite_max_temp, oai_settings.temp_openai);
@@ -4428,27 +4579,10 @@ async function onModelChange() {
4428 }4579 }
44294580
4430 if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {4581 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);
4432 $('#openai_max_context').attr('max', unlocked_max);4583 $('#openai_max_context').attr('max', maxContext);
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 }
4448 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));4584 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
4449 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4585 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4450
4451 //mistral also caps temp at 1.0
4452 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);4586 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
4453 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');4587 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
4454 }4588 }
@@ -4584,6 +4718,22 @@ async function onModelChange() {
4584 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');4718 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4585 }4719 }
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
4587 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {4737 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
4588 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);4738 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
4589 $('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');4739 $('#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) {
4822 }4972 }
4823 }4973 }
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
4825 startStatusLoading();4988 startStatusLoading();
4826 saveSettingsDebounced();4989 saveSettingsDebounced();
4827 await getStatusOpen();4990 await getStatusOpen();
@@ -4878,6 +5041,9 @@ function toggleChatCompletionForms() {
4878 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {5041 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
4879 $('#model_deepseek_select').trigger('change');5042 $('#model_deepseek_select').trigger('change');
4880 }5043 }
5044 else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
5045 $('#model_xai_select').trigger('change');
5046 }
4881 $('[data-source]').each(function () {5047 $('[data-source]').each(function () {
4882 const validSources = $(this).data('source').split(',');5048 const validSources = $(this).data('source').split(',');
4883 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));5049 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -4962,63 +5128,43 @@ export function isImageInliningSupported() {
49625128
4963 // gultra just isn't being offered as multimodal, thanks google.5129 // gultra just isn't being offered as multimodal, thanks google.
4964 const visionSupportedModels = [5130 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',
5000 'gpt-4-turbo',5133 'gpt-4-turbo',
5001 'gpt-4o',5134 'gpt-4-vision',
5002 'gpt-4o-mini',5135 'gpt-4.1',
5003 'gpt-4.5-preview',5136 'gpt-4.5-preview',
5004 'gpt-4.5-preview-2025-02-27',5137 'gpt-4o',
5005 'o1',5138 'o1',
5006 'o1-2024-12-17',5139 'o3',
5007 'chatgpt-4o-latest',5140 'o4-mini',
5141 // 01.AI (Yi)
5008 'yi-vision',5142 'yi-vision',
5009 'pixtral-latest',5143 // Claude
5010 'pixtral-12b-latest',5144 'claude-3',
5011 'pixtral-12b',5145 // Cohere
5012 'pixtral-12b-2409',5146 'c4ai-aya-vision',
5013 'pixtral-large-latest',5147 // Google AI Studio
5014 'pixtral-large-2411',5148 'gemini-1.5',
5015 'c4ai-aya-vision-8b',5149 'gemini-2.0',
5016 'c4ai-aya-vision-32b',5150 'gemini-2.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',
5017 ];5160 ];
50185161
5019 switch (oai_settings.chat_completion_source) {5162 switch (oai_settings.chat_completion_source) {
5020 case chat_completion_sources.OPENAI:5163 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 );
5022 case chat_completion_sources.MAKERSUITE:5168 case chat_completion_sources.MAKERSUITE:
5023 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));5169 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
5024 case chat_completion_sources.CLAUDE:5170 case chat_completion_sources.CLAUDE:
@@ -5033,6 +5179,8 @@ export function isImageInliningSupported() {
5033 return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model));5179 return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model));
5034 case chat_completion_sources.COHERE:5180 case chat_completion_sources.COHERE:
5035 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));5181 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));
5036 default:5184 default:
5037 return false;5185 return false;
5038 }5186 }
@@ -5573,6 +5721,7 @@ export function initOpenAI() {
55735721
5574 $('#openai_enable_web_search').on('input', function () {5722 $('#openai_enable_web_search').on('input', function () {
5575 oai_settings.enable_web_search = !!$(this).prop('checked');5723 oai_settings.enable_web_search = !!$(this).prop('checked');
5724 calculateOpenRouterCost();
5576 saveSettingsDebounced();5725 saveSettingsDebounced();
5577 });5726 });
55785727
@@ -5629,6 +5778,7 @@ export function initOpenAI() {
5629 $('#model_deepseek_select').on('change', onModelChange);5778 $('#model_deepseek_select').on('change', onModelChange);
5630 $('#model_01ai_select').on('change', onModelChange);5779 $('#model_01ai_select').on('change', onModelChange);
5631 $('#model_custom_select').on('change', onModelChange);5780 $('#model_custom_select').on('change', onModelChange);
5781 $('#model_xai_select').on('change', onModelChange);
5632 $('#settings_preset_openai').on('change', onSettingsPresetChange);5782 $('#settings_preset_openai').on('change', onSettingsPresetChange);
5633 $('#new_oai_preset').on('click', onNewPresetClick);5783 $('#new_oai_preset').on('click', onNewPresetClick);
5634 $('#delete_oai_preset').on('click', onDeletePresetClick);5784 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/personas.js+7 -3
@@ -22,7 +22,7 @@ import {
22} from '../script.js';22} from '../script.js';
23import { persona_description_positions, power_user } from './power-user.js';23import { persona_description_positions, power_user } from './power-user.js';
24import { getTokenCountAsync } from './tokenizers.js';24import { getTokenCountAsync } from './tokenizers.js';
25import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';25import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler } from './utils.js';
26import { debounce_timeout } from './constants.js';26import { debounce_timeout } from './constants.js';
27import { FILTER_TYPES, FilterHelper } from './filters.js';27import { FILTER_TYPES, FilterHelper } from './filters.js';
28import { groups, selected_group } from './group-chats.js';28import { groups, selected_group } from './group-chats.js';
@@ -250,16 +250,18 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
250 const storageKey = 'Personas_PerPage';250 const storageKey = 'Personas_PerPage';
251 const listId = '#user_avatar_block';251 const listId = '#user_avatar_block';
252 const perPage = Number(accountStorage.getItem(storageKey)) || 5;252 const perPage = Number(accountStorage.getItem(storageKey)) || 5;
253 const sizeChangerOptions = [5, 10, 25, 50, 100, 250, 500, 1000];
253254
254 $('#persona_pagination_container').pagination({255 $('#persona_pagination_container').pagination({
255 dataSource: entities,256 dataSource: entities,
256 pageSize: perPage,257 pageSize: perPage,
257 sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000],258 sizeChangerOptions,
258 pageRange: 1,259 pageRange: 1,
259 pageNumber: savePersonasPage || 1,260 pageNumber: savePersonasPage || 1,
260 position: 'top',261 position: 'top',
261 showPageNumbers: false,262 showPageNumbers: false,
262 showSizeChanger: true,263 showSizeChanger: true,
264 formatSizeChanger: renderPaginationDropdown(perPage, sizeChangerOptions),
263 prevText: '<',265 prevText: '<',
264 nextText: '>',266 nextText: '>',
265 formatNavigator: PAGINATION_TEMPLATE,267 formatNavigator: PAGINATION_TEMPLATE,
@@ -270,9 +272,11 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
270 $(listId).append(getUserAvatarBlock(item));272 $(listId).append(getUserAvatarBlock(item));
271 }273 }
272 updatePersonaUIStates();274 updatePersonaUIStates();
275 localizePagination($('#persona_pagination_container'));
273 },276 },
274 afterSizeSelectorChange: function (e) {277 afterSizeSelectorChange: function (e, size) {
275 accountStorage.setItem(storageKey, e.target.value);278 accountStorage.setItem(storageKey, e.target.value);
279 paginationDropdownChangeHandler(e, size);
276 },280 },
277 afterPaging: function (e) {281 afterPaging: function (e) {
278 savePersonasPage = e;282 savePersonasPage = e;
public/scripts/popup.js+52 -25
@@ -71,7 +71,8 @@ export const POPUP_RESULT = {
71 * @property {string} id - The id for the html element71 * @property {string} id - The id for the html element
72 * @property {string} label - The label text for the input72 * @property {string} label - The label text for the input
73 * @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label73 * @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label
74 * @property {boolean?} [defaultState=false] - The default state when opening the popup (false if not set)74 * @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)
75 */76 */
7677
77/**78/**
@@ -157,7 +158,7 @@ export class Popup {
157158
158 /** @type {POPUP_RESULT|number} */ result;159 /** @type {POPUP_RESULT|number} */ result;
159 /** @type {any} */ value;160 /** @type {any} */ value;
160 /** @type {Map<string,boolean>?} */ inputResults;161 /** @type {Map<string,string|boolean>?} */ inputResults;
161 /** @type {any} */ cropData;162 /** @type {any} */ cropData;
162163
163 /** @type {HTMLElement} */ lastFocus;164 /** @type {HTMLElement} */ lastFocus;
@@ -260,28 +261,53 @@ export class Popup {
260 return;261 return;
261 }262 }
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');
265 label.setAttribute('for', input.id);266 label.classList.add('checkbox_label', 'justifyCenter');
266 const inputElement = document.createElement('input');267 label.setAttribute('for', input.id);
267 inputElement.type = 'checkbox';268 const inputElement = document.createElement('input');
268 inputElement.id = input.id;269 inputElement.type = 'checkbox';
269 inputElement.checked = input.defaultState ?? false;270 inputElement.id = input.id;
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');
273 labelText.dataset.i18n = input.label;274 labelText.innerText = input.label;
274 label.appendChild(labelText);275 labelText.dataset.i18n = input.label;
275276 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');
280 tooltip.dataset.i18n = '[title]' + input.tooltip;281 tooltip.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
284 this.inputControls.appendChild(label);303 label.appendChild(labelText);
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 }
285 });311 });
286312
287 // Set the default button class313 // Set the default button class
@@ -529,7 +555,8 @@ export class Popup {
529 this.inputResults = new Map(this.customInputs.map(input => {555 this.inputResults = new Map(this.customInputs.map(input => {
530 /** @type {HTMLInputElement} */556 /** @type {HTMLInputElement} */
531 const inputControl = this.dlg.querySelector(`#${input.id}`);557 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];
533 }));560 }));
534 }561 }
535562
@@ -619,7 +646,7 @@ export class Popup {
619 /** @readonly @type {Popup[]} Remember all popups */646 /** @readonly @type {Popup[]} Remember all popups */
620 popups: [],647 popups: [],
621648
622 /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, boolean>?}?} Last popup result */649 /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, string|boolean>?}?} Last popup result */
623 lastResult: null,650 lastResult: null,
624651
625 /** @returns {boolean} Checks if any modal popup dialog is open */652 /** @returns {boolean} Checks if any modal popup dialog is open */
public/scripts/power-user.js+3 -4
@@ -71,8 +71,8 @@ export {
7171
72export const MAX_CONTEXT_DEFAULT = 8192;72export const MAX_CONTEXT_DEFAULT = 8192;
73export const MAX_RESPONSE_DEFAULT = 2048;73export const MAX_RESPONSE_DEFAULT = 2048;
74const MAX_CONTEXT_UNLOCKED = 200 * 1024;74const MAX_CONTEXT_UNLOCKED = 512 * 1024;
75const MAX_RESPONSE_UNLOCKED = 32 * 1024;75const MAX_RESPONSE_UNLOCKED = 64 * 1024;
76const unlockedMaxContextStep = 512;76const unlockedMaxContextStep = 512;
77const maxContextMin = 512;77const maxContextMin = 512;
78const maxContextStep = 64;78const maxContextStep = 64;
@@ -244,7 +244,6 @@ let power_user = {
244 chat_start: defaultChatStart,244 chat_start: defaultChatStart,
245 example_separator: defaultExampleSeparator,245 example_separator: defaultExampleSeparator,
246 use_stop_strings: true,246 use_stop_strings: true,
247 allow_jailbreak: false,
248 names_as_stop_strings: true,247 names_as_stop_strings: true,
249 },248 },
250249
@@ -255,6 +254,7 @@ let power_user = {
255 enabled: true,254 enabled: true,
256 name: 'Neutral - Chat',255 name: 'Neutral - Chat',
257 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',256 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
257 post_history: '',
258 },258 },
259259
260 reasoning: {260 reasoning: {
@@ -334,7 +334,6 @@ const contextControls = [
334 { id: 'context_example_separator', property: 'example_separator', isCheckbox: false, isGlobalSetting: false },334 { id: 'context_example_separator', property: 'example_separator', isCheckbox: false, isGlobalSetting: false },
335 { id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },335 { id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
336 { id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },336 { 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 },
338 { id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },337 { id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
339338
340 // Existing power user settings339 // Existing power user settings
public/scripts/preset-manager.js+6 -0
@@ -902,6 +902,12 @@ export async function initPresetManager() {
902902
903 await presetManager.renamePreset(newName);903 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
905 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;911 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;
906 toastr.success(successToast);912 toastr.success(successToast);
907 });913 });
public/scripts/reasoning.js+2 -0
@@ -109,6 +109,8 @@ export function extractReasoningFromData(data, {
109 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {109 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
110 case chat_completion_sources.DEEPSEEK:110 case chat_completion_sources.DEEPSEEK:
111 return data?.choices?.[0]?.message?.reasoning_content ?? '';111 return data?.choices?.[0]?.message?.reasoning_content ?? '';
112 case chat_completion_sources.XAI:
113 return data?.choices?.[0]?.message?.reasoning_content ?? '';
112 case chat_completion_sources.OPENROUTER:114 case chat_completion_sources.OPENROUTER:
113 return data?.choices?.[0]?.message?.reasoning ?? '';115 return data?.choices?.[0]?.message?.reasoning ?? '';
114 case chat_completion_sources.MAKERSUITE:116 case chat_completion_sources.MAKERSUITE:
public/scripts/secrets.js+2 -0
@@ -42,6 +42,7 @@ export const SECRET_KEYS = {
42 DEEPSEEK: 'api_key_deepseek',42 DEEPSEEK: 'api_key_deepseek',
43 SERPER: 'api_key_serper',43 SERPER: 'api_key_serper',
44 FALAI: 'api_key_falai',44 FALAI: 'api_key_falai',
45 XAI: 'api_key_xai',
45};46};
4647
47const INPUT_MAP = {48const INPUT_MAP = {
@@ -76,6 +77,7 @@ const INPUT_MAP = {
76 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',77 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
77 [SECRET_KEYS.GENERIC]: '#api_key_generic',78 [SECRET_KEYS.GENERIC]: '#api_key_generic',
78 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',79 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
80 [SECRET_KEYS.XAI]: '#api_key_xai',
79};81};
8082
81async function clearSecret() {83async function clearSecret() {
public/scripts/slash-commands.js+162 -7
@@ -1,4 +1,5 @@
1import { Fuse, DOMPurify } from '../lib.js';1import { Fuse, DOMPurify } from '../lib.js';
2import { flashHighlight } from './utils.js';
23
3import {4import {
4 Generate,5 Generate,
@@ -21,6 +22,7 @@ import {
21 extractMessageBias,22 extractMessageBias,
22 generateQuietPrompt,23 generateQuietPrompt,
23 generateRaw,24 generateRaw,
25 getFirstDisplayedMessageId,
24 getThumbnailUrl,26 getThumbnailUrl,
25 is_send_press,27 is_send_press,
26 main_api,28 main_api,
@@ -314,6 +316,11 @@ export function initDefaultSlashCommands() {
314 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),316 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
315 }),317 }),
316 SlashCommandNamedArgument.fromProps({318 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({
317 name: 'return',324 name: 'return',
318 description: 'The way how you want the return value to be provided',325 description: 'The way how you want the return value to be provided',
319 typeList: [ARGUMENT_TYPE.STRING],326 typeList: [ARGUMENT_TYPE.STRING],
@@ -2077,8 +2084,9 @@ export function initDefaultSlashCommands() {
2077 name: 'replace',2084 name: 'replace',
2078 aliases: ['re'],2085 aliases: ['re'],
2079 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {2086 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2080 if (pattern === '')2087 if (!pattern) {
2081 throw new Error('Argument of \'pattern=\' cannot be empty');2088 throw new Error('Argument of \'pattern=\' cannot be empty');
2089 }
2082 switch (mode) {2090 switch (mode) {
2083 case 'literal':2091 case 'literal':
2084 return text.replaceAll(pattern, replacer);2092 return text.replaceAll(pattern, replacer);
@@ -2121,15 +2129,161 @@ export function initDefaultSlashCommands() {
2121 </div>2129 </div>
2122 <div>2130 <div>
2123 <strong>Example:</strong>2131 <strong>Example:</strong>
2124 <pre>/let x Blue house and blue car || </pre>2132 <pre><code class="language-stscript">/let x Blue house and blue car || </code></pre>
2125 <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>2133 <pre><code class="language-stscript">/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</code></pre>
2126 <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>2134 <pre><code class="language-stscript">/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</code></pre>
2127 <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>2135 <pre><code class="language-stscript">/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</code></pre>
2128 <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>2136 <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>
2129 </div>2224 </div>
2130 `,2225 `,
2131 }));2226 }));
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
2133 registerVariableCommands();2287 registerVariableCommands();
2134}2288}
21352289
@@ -3702,7 +3856,7 @@ export async function sendMessageAs(args, text) {
37023856
3703export async function sendNarratorMessage(args, text) {3857export async function sendNarratorMessage(args, text) {
3704 text = String(text ?? '');3858 text = String(text ?? '');
3705 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;3859 const name = args.name ?? (chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT);
3706 // Messages that do nothing but set bias will be hidden from the context3860 // Messages that do nothing but set bias will be hidden from the context
3707 const bias = extractMessageBias(text);3861 const bias = extractMessageBias(text);
3708 const isSystem = bias && !removeMacros(text).length;3862 const isSystem = bias && !removeMacros(text).length;
@@ -3942,6 +4096,7 @@ function getModelOptions(quiet) {
3942 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },4096 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
3943 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },4097 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
3944 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },4098 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
4099 { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
3945 { id: 'model_novel_select', api: 'novel', type: null },4100 { id: 'model_novel_select', api: 'novel', type: null },
3946 { id: 'horde_model', api: 'koboldhorde', type: null },4101 { id: 'horde_model', api: 'koboldhorde', type: null },
3947 ];4102 ];
public/scripts/st-context.js+3 -0
@@ -50,6 +50,8 @@ import {
50 unshallowCharacter,50 unshallowCharacter,
51 deleteLastMessage,51 deleteLastMessage,
52 getCharacterCardFields,52 getCharacterCardFields,
53 swipe_right,
54 swipe_left,
53} from '../script.js';55} from '../script.js';
54import {56import {
55 extension_settings,57 extension_settings,
@@ -196,6 +198,7 @@ export function getContext() {
196 humanizedDateTime,198 humanizedDateTime,
197 updateMessageBlock,199 updateMessageBlock,
198 appendMediaToMessage,200 appendMediaToMessage,
201 swipe: { left: swipe_left, right: swipe_right },
199 variables: {202 variables: {
200 local: {203 local: {
201 get: getLocalVariable,204 get: getLocalVariable,
public/scripts/sysprompt.js+15 -3
@@ -17,6 +17,7 @@ export let system_prompts = [];
17const $enabled = $('#sysprompt_enabled');17const $enabled = $('#sysprompt_enabled');
18const $select = $('#sysprompt_select');18const $select = $('#sysprompt_select');
19const $content = $('#sysprompt_content');19const $content = $('#sysprompt_content');
20const $postHistory = $('#sysprompt_post_history');
20const $contentBlock = $('#SystemPromptBlock');21const $contentBlock = $('#SystemPromptBlock');
2122
22async function migrateSystemPromptFromInstructMode() {23async function migrateSystemPromptFromInstructMode() {
@@ -25,6 +26,7 @@ async function migrateSystemPromptFromInstructMode() {
25 delete power_user.instruct.system_prompt;26 delete power_user.instruct.system_prompt;
26 power_user.sysprompt.enabled = power_user.instruct.enabled;27 power_user.sysprompt.enabled = power_user.instruct.enabled;
27 power_user.sysprompt.content = prompt;28 power_user.sysprompt.content = prompt;
29 power_user.sysprompt.post_history = '';
2830
29 const existingPromptName = system_prompts.find(x => x.content === prompt)?.name;31 const existingPromptName = system_prompts.find(x => x.content === prompt)?.name;
3032
@@ -59,7 +61,8 @@ export async function loadSystemPrompts(data) {
5961
60 $enabled.prop('checked', power_user.sysprompt.enabled);62 $enabled.prop('checked', power_user.sysprompt.enabled);
61 $select.val(power_user.sysprompt.name);63 $select.val(power_user.sysprompt.name);
62 $content.val(power_user.sysprompt.content);64 $content.val(power_user.sysprompt.content || '');
65 $postHistory.val(power_user.sysprompt.post_history || '');
63 if (!CSS.supports('field-sizing', 'content')) {66 if (!CSS.supports('field-sizing', 'content')) {
64 await resetScrollHeight($content);67 await resetScrollHeight($content);
65 }68 }
@@ -165,13 +168,17 @@ export function initSystemPrompts() {
165 const name = String($(this).val());168 const name = String($(this).val());
166 const prompt = system_prompts.find(p => p.name === name);169 const prompt = system_prompts.find(p => p.name === name);
167 if (prompt) {170 if (prompt) {
168 $content.val(prompt.content);171 $content.val(prompt.content || '');
172 $postHistory.val(prompt.post_history || '');
173
169 if (!CSS.supports('field-sizing', 'content')) {174 if (!CSS.supports('field-sizing', 'content')) {
170 await resetScrollHeight($content);175 await resetScrollHeight($content);
176 await resetScrollHeight($postHistory);
171 }177 }
172178
173 power_user.sysprompt.name = name;179 power_user.sysprompt.name = name;
174 power_user.sysprompt.content = prompt.content;180 power_user.sysprompt.content = prompt.content || '';
181 power_user.sysprompt.post_history = prompt.post_history || '';
175 }182 }
176 saveSettingsDebounced();183 saveSettingsDebounced();
177 });184 });
@@ -181,6 +188,11 @@ export function initSystemPrompts() {
181 saveSettingsDebounced();188 saveSettingsDebounced();
182 });189 });
183190
191 $postHistory.on('input', function () {
192 power_user.sysprompt.post_history = String($(this).val());
193 saveSettingsDebounced();
194 });
195
184 SlashCommandParser.addCommandObject(SlashCommand.fromProps({196 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
185 name: 'sysprompt',197 name: 'sysprompt',
186 aliases: ['system-prompt'],198 aliases: ['system-prompt'],
public/scripts/tags.js+5 -11
@@ -27,7 +27,7 @@ import { debounce_timeout } from './constants.js';
27import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';27import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
28import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';28import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
29import { renderTemplateAsync } from './templates.js';29import { renderTemplateAsync } from './templates.js';
30import { t } from './i18n.js';30import { t, translate } from './i18n.js';
3131
32export {32export {
33 TAG_FOLDER_TYPES,33 TAG_FOLDER_TYPES,
@@ -318,7 +318,7 @@ function getTagBlock(tag, entities, hidden = 0, isUseless = false) {
318 template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`);318 template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`);
319 template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`);319 template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`);
320 template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : '');320 template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : '');
321 template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);321 template.find('.bogus_folder_counter').text(`${count} ` + (count != 1 ? t`characters` : t`character`));
322 template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);322 template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);
323 if (isUseless) template.addClass('useless');323 if (isUseless) template.addClass('useless');
324324
@@ -1057,7 +1057,7 @@ function appendTagToList(listElement, tag, { removable = false, isFilter = false
1057 tagElement.attr('title', tag.title);1057 tagElement.attr('title', tag.title);
1058 }1058 }
1059 if (tag.icon) {1059 if (tag.icon) {
1060 tagElement.find('.tag_name').text('').attr('title', `${tag.name} ${tag.title || ''}`.trim()).addClass(tag.icon);1060 tagElement.find('.tag_name').text('').attr('title', `${translate(tag.name)} ${tag.title || ''}`.trim()).addClass(tag.icon);
1061 tagElement.addClass('actionable');1061 tagElement.addClass('actionable');
1062 }1062 }
10631063
@@ -1644,6 +1644,7 @@ function updateDrawTagFolder(element, tag) {
16441644
1645 // Draw/update css attributes for this class1645 // Draw/update css attributes for this class
1646 folderElement.attr('title', tagFolder.tooltip);1646 folderElement.attr('title', tagFolder.tooltip);
1647 folderElement.attr('data-i18n', '[title]' + tagFolder.tooltip);
1647 const indicator = folderElement.find('.tag_folder_indicator');1648 const indicator = folderElement.find('.tag_folder_indicator');
1648 indicator.text(tagFolder.icon);1649 indicator.text(tagFolder.icon);
1649 indicator.css('color', tagFolder.color);1650 indicator.css('color', tagFolder.color);
@@ -1655,14 +1656,7 @@ async function onTagDeleteClick() {
1655 const tag = tags.find(x => x.id === id);1656 const tag = tags.find(x => x.id === id);
1656 const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));1657 const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));
16571658
1658 const popupContent = $(`1659 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
1667 appendTagToList(popupContent.find('#tag_to_delete'), tag);1661 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>
\ No newline at end of file9 \ 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 @@
83 <div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>83 <div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
84 </div>84 </div>
85 <div class="flex-container ">85 <div class="flex-container ">
86 <div class=" flex1 tokenItemizingSubclass">-- Examples:</div>86 <div class=" flex1 tokenItemizingSubclass">
87 <span>-- Examples:</span>
88 {{#if examplesCount}}<small>({{examplesCount}})</small>{{/if}}
89 </div>
87 <div class="tokenItemizingSubclass">{{examplesStringTokens}}</div>90 <div class="tokenItemizingSubclass">{{examplesStringTokens}}</div>
88 </div>91 </div>
89 <div class="flex-container ">92 <div class="flex-container ">
@@ -96,7 +99,10 @@
96 <div class="">{{worldInfoStringTokens}}</div>99 <div class="">{{worldInfoStringTokens}}</div>
97 </div>100 </div>
98 <div class="wide100p flex-container">101 <div class="wide100p flex-container">
99 <div class="flex1" style="color: palegreen;"><span data-i18n="Chat History:">Chat History:</span></div>102 <div class="flex1" style="color: palegreen;">
103 <span data-i18n="Chat History:">Chat History:</span>
104 {{#if messagesCount}}<small>({{messagesCount}})</small>{{/if}}
105 </div>
100 <div class="">{{ActualChatHistoryTokens}}</div>106 <div class="">{{ActualChatHistoryTokens}}</div>
101 </div>107 </div>
102 <div class="wide100p flex-container flexNoGap flexFlowColumn">108 <div class="wide100p flex-container flexNoGap flexFlowColumn">
public/scripts/templates/itemizationText.html+8 -2
@@ -51,7 +51,10 @@
51 <div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>51 <div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
52 </div>52 </div>
53 <div class="flex-container">53 <div class="flex-container">
54 <div class=" flex1 tokenItemizingSubclass">-- Examples:</div>54 <div class=" flex1 tokenItemizingSubclass">
55 <span>-- Examples:</span>
56 {{#if examplesCount}}<small>({{examplesCount}})</small>{{/if}}
57 </div>
55 <div class="tokenItemizingSubclass"> {{examplesStringTokens}}</div>58 <div class="tokenItemizingSubclass"> {{examplesStringTokens}}</div>
56 </div>59 </div>
57 <div class="flex-container">60 <div class="flex-container">
@@ -68,7 +71,10 @@
68 <div class="">{{worldInfoStringTokens}}</div>71 <div class="">{{worldInfoStringTokens}}</div>
69 </div>72 </div>
70 <div class="wide100p flex-container">73 <div class="wide100p flex-container">
71 <div class="flex1" style="color: palegreen;">Chat History:</div>74 <div class="flex1" style="color: palegreen;">
75 <span data-i18n="Chat History:">Chat History:</span>
76 {{#if messagesCount}}<small>({{messagesCount}})</small>{{/if}}
77 </div>
72 <div class=""> {{ActualChatHistoryTokens}}</div>78 <div class=""> {{ActualChatHistoryTokens}}</div>
73 </div>79 </div>
74 <div class="wide100p flex-container flexNoGap flexFlowColumn">80 <div class="wide100p flex-container flexNoGap flexFlowColumn">
public/scripts/textgen-models.js+19 -16
@@ -7,6 +7,7 @@ import { renderTemplateAsync } from './templates.js';
7import { POPUP_TYPE, callGenericPopup } from './popup.js';7import { POPUP_TYPE, callGenericPopup } from './popup.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { accountStorage } from './util/AccountStorage.js';9import { accountStorage } from './util/AccountStorage.js';
10import { localizePagination, PAGINATION_TEMPLATE } from './utils.js';
1011
11let mancerModels = [];12let mancerModels = [];
12let togetherModels = [];13let togetherModels = [];
@@ -41,12 +42,7 @@ const OPENROUTER_PROVIDERS = [
41 'Avian',42 'Avian',
42 'Lambda',43 'Lambda',
43 'Azure',44 'Azure',
44 'Modal',
45 'AnyScale',
46 'Replicate',
47 'Perplexity',45 'Perplexity',
48 'Recursal',
49 'OctoAI',
50 'DeepSeek',46 'DeepSeek',
51 'Infermatic',47 'Infermatic',
52 'AI21',48 'AI21',
@@ -54,10 +50,12 @@ const OPENROUTER_PROVIDERS = [
54 'Inflection',50 'Inflection',
55 'xAI',51 'xAI',
56 'Cloudflare',52 'Cloudflare',
57 'SF Compute',
58 'Minimax',53 'Minimax',
59 'Nineteen',54 'Nineteen',
60 'Liquid',55 'Liquid',
56 'GMICloud',
57 'Stealth',
58 'NCompass',
61 'InferenceNet',59 'InferenceNet',
62 'Friendli',60 'Friendli',
63 'AionLabs',61 'AionLabs',
@@ -69,14 +67,16 @@ const OPENROUTER_PROVIDERS = [
69 'Targon',67 'Targon',
70 'Ubicloud',68 'Ubicloud',
71 'Parasail',69 'Parasail',
72 '01.AI',70 'Phala',
73 'HuggingFace',71 'Cent-ML',
72 'Venice',
73 'OpenInference',
74 'Atoma',
75 'Enfer',
74 'Mancer',76 'Mancer',
75 'Mancer 2',77 'Mancer 2',
76 'Hyperbolic',78 'Hyperbolic',
77 'Hyperbolic 2',79 'Hyperbolic 2',
78 'Lynn 2',
79 'Lynn',
80 'Reflection',80 'Reflection',
81];81];
8282
@@ -362,9 +362,7 @@ export async function loadFeatherlessModels(data) {
362 showSizeChanger: false,362 showSizeChanger: false,
363 prevText: '<',363 prevText: '<',
364 nextText: '>',364 nextText: '>',
365 formatNavigator: function (currentPage, totalPage) {365 formatNavigator: PAGINATION_TEMPLATE,
366 return (currentPage - 1) * perPage + 1 + ' - ' + currentPage * perPage + ' of ' + totalPage * perPage;
367 },
368 showNavigator: true,366 showNavigator: true,
369 callback: function (modelsOnPage, pagination) {367 callback: function (modelsOnPage, pagination) {
370 modelCardBlock.innerHTML = '';368 modelCardBlock.innerHTML = '';
@@ -386,15 +384,15 @@ export async function loadFeatherlessModels(data) {
386384
387 const modelClassDiv = document.createElement('div');385 const modelClassDiv = document.createElement('div');
388 modelClassDiv.classList.add('model-class');386 modelClassDiv.classList.add('model-class');
389 modelClassDiv.textContent = `Class: ${model.model_class || 'N/A'}`;387 modelClassDiv.textContent = t`Class` + `: ${model.model_class || 'N/A'}`;
390388
391 const contextLengthDiv = document.createElement('div');389 const contextLengthDiv = document.createElement('div');
392 contextLengthDiv.classList.add('model-context-length');390 contextLengthDiv.classList.add('model-context-length');
393 contextLengthDiv.textContent = `Context Length: ${model.context_length}`;391 contextLengthDiv.textContent = t`Context Length` + `: ${model.context_length}`;
394392
395 const dateAddedDiv = document.createElement('div');393 const dateAddedDiv = document.createElement('div');
396 dateAddedDiv.classList.add('model-date-added');394 dateAddedDiv.classList.add('model-date-added');
397 dateAddedDiv.textContent = `Added On: ${new Date(model.created * 1000).toLocaleDateString()}`;395 dateAddedDiv.textContent = t`Added On` + `: ${new Date(model.created * 1000).toLocaleDateString()}`;
398396
399 detailsContainer.appendChild(modelClassDiv);397 detailsContainer.appendChild(modelClassDiv);
400 detailsContainer.appendChild(contextLengthDiv);398 detailsContainer.appendChild(contextLengthDiv);
@@ -418,6 +416,7 @@ export async function loadFeatherlessModels(data) {
418416
419 // Update the current page value whenever the page changes417 // Update the current page value whenever the page changes
420 featherlessCurrentPage = pagination.pageNumber;418 featherlessCurrentPage = pagination.pageNumber;
419 localizePagination(paginationContainer);
421 },420 },
422 afterSizeSelectorChange: function (e) {421 afterSizeSelectorChange: function (e) {
423 const newPerPage = e.target.value;422 const newPerPage = e.target.value;
@@ -923,6 +922,10 @@ export function getCurrentDreamGenModelTokenizer() {
923 return tokenizers.YI;922 return tokenizers.YI;
924 } else if (model.id.startsWith('opus-v1-xl')) {923 } else if (model.id.startsWith('opus-v1-xl')) {
925 return tokenizers.LLAMA;924 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;
926 } else {929 } else {
927 return tokenizers.MISTRAL;930 return tokenizers.MISTRAL;
928 }931 }
public/scripts/textgen-settings.js+1 -1
@@ -1146,7 +1146,7 @@ function tryParseStreamingError(response, decoded) {
1146 // No JSON. Do nothing.1146 // No JSON. Do nothing.
1147 }1147 }
11481148
1149 const message = data?.error?.message || data?.message || data?.detail;1149 const message = data?.error?.message || data?.error || data?.message || data?.detail;
11501150
1151 if (message) {1151 if (message) {
1152 toastr.error(message, 'Text Completion API');1152 toastr.error(message, 'Text Completion API');
public/scripts/tool-calling.js+1 -0
@@ -586,6 +586,7 @@ export class ToolManager {
586 chat_completion_sources.DEEPSEEK,586 chat_completion_sources.DEEPSEEK,
587 chat_completion_sources.MAKERSUITE,587 chat_completion_sources.MAKERSUITE,
588 chat_completion_sources.AI21,588 chat_completion_sources.AI21,
589 chat_completion_sources.XAI,
589 ];590 ];
590 return supportedSources.includes(oai_settings.chat_completion_source);591 return supportedSources.includes(oai_settings.chat_completion_source);
591 }592 }
public/scripts/utils.js+41 -2
@@ -20,7 +20,46 @@ import { getCurrentLocale, t } from './i18n.js';
20 * Pagination status string template.20 * Pagination status string template.
21 * @type {string}21 * @type {string}
22 */22 */
23export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>';23export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> .. <%= totalNumber %>';
24
25export 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 */
36export 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
58export 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
25/**64/**
26 * Navigation options for pagination.65 * Navigation options for pagination.
@@ -1047,7 +1086,7 @@ export function getImageSizeFromDataURL(dataUrl) {
10471086
1048/**1087/**
1049 * Gets the filename of the character avatar without extension1088 * Gets the filename of the character avatar without extension
1050 * @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID1089 * @param {string|number?} [chid=null] - Character ID. If not provided, uses the current character ID
1051 * @param {object} [options={}] - Options arguments1090 * @param {object} [options={}] - Options arguments
1052 * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name1091 * @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
1053 * @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid1092 * @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
99// Typedef area99// Typedef area
100/**100/**
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/**
101 * @typedef {object} WIScanEntry The entry that triggered the scan112 * @typedef {object} WIScanEntry The entry that triggered the scan
102 * @property {number} [scanDepth] The depth of the scan113 * @property {number} [scanDepth] The depth of the scan
103 * @property {boolean} [caseSensitive] If the scan is case sensitive114 * @property {boolean} [caseSensitive] If the scan is case sensitive
104 * @property {boolean} [matchWholeWords] If the scan should match whole words115 * @property {boolean} [matchWholeWords] If the scan should match whole words
105 * @property {boolean} [useGroupScoring] If the scan should use group scoring116 * @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
106 * @property {number} [uid] The UID of the entry that triggered the scan123 * @property {number} [uid] The UID of the entry that triggered the scan
107 * @property {string} [world] The world info book of origin of the entry124 * @property {string} [world] The world info book of origin of the entry
108 * @property {string[]} [key] The primary keys to scan for125 * @property {string[]} [key] The primary keys to scan for
@@ -139,6 +156,11 @@ class WorldInfoBuffer {
139 static externalActivations = new Map();156 static externalActivations = new Map();
140157
141 /**158 /**
159 * @type {WIGlobalScanData} Chat independent data to be scanned, such as persona and character descriptions
160 */
161 #globalScanData = null;
162
163 /**
142 * @type {string[]} Array of messages sorted by ascending depth164 * @type {string[]} Array of messages sorted by ascending depth
143 */165 */
144 #depthBuffer = [];166 #depthBuffer = [];
@@ -166,9 +188,11 @@ class WorldInfoBuffer {
166 /**188 /**
167 * Initialize the buffer with the given messages.189 * Initialize the buffer with the given messages.
168 * @param {string[]} messages Array of messages to add to the buffer190 * @param {string[]} messages Array of messages to add to the buffer
191 * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
169 */192 */
170 constructor(messages) {193 constructor(messages, globalScanData) {
171 this.#initDepthBuffer(messages);194 this.#initDepthBuffer(messages);
195 this.#globalScanData = globalScanData;
172 }196 }
173197
174 /**198 /**
@@ -225,6 +249,25 @@ class WorldInfoBuffer {
225 const JOINER = '\n' + MATCHER;249 const JOINER = '\n' + MATCHER;
226 let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);250 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
228 if (this.#injectBuffer.length > 0) {271 if (this.#injectBuffer.length > 0) {
229 result += JOINER + this.#injectBuffer.join(JOINER);272 result += JOINER + this.#injectBuffer.join(JOINER);
230 }273 }
@@ -756,6 +799,7 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
756 * @param {string[]} chat - The chat messages to scan, in reverse order.799 * @param {string[]} chat - The chat messages to scan, in reverse order.
757 * @param {number} maxContext - The maximum context size of the generation.800 * @param {number} maxContext - The maximum context size of the generation.
758 * @param {boolean} isDryRun - If true, the function will not emit any events.801 * @param {boolean} isDryRun - If true, the function will not emit any events.
802 * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
759 * @typedef {object} WIPromptResult803 * @typedef {object} WIPromptResult
760 * @property {string} worldInfoString - Complete world info string804 * @property {string} worldInfoString - Complete world info string
761 * @property {string} worldInfoBefore - World info that goes before the prompt805 * @property {string} worldInfoBefore - World info that goes before the prompt
@@ -766,10 +810,10 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
766 * @property {Array} anAfter - Array of entries after Author's Note810 * @property {Array} anAfter - Array of entries after Author's Note
767 * @returns {Promise<WIPromptResult>} The world info string and depth.811 * @returns {Promise<WIPromptResult>} The world info string and depth.
768 */812 */
769export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {813export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanData) {
770 let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';814 let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
771815
772 const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun);816 const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun, globalScanData);
773 worldInfoBefore = activatedWorldInfo.worldInfoBefore;817 worldInfoBefore = activatedWorldInfo.worldInfoBefore;
774 worldInfoAfter = activatedWorldInfo.worldInfoAfter;818 worldInfoAfter = activatedWorldInfo.worldInfoAfter;
775 worldInfoString = worldInfoBefore + worldInfoAfter;819 worldInfoString = worldInfoBefore + worldInfoAfter;
@@ -966,7 +1010,7 @@ function registerWorldInfoSlashCommands() {
966 /**1010 /**
967 * Gets the name of the character-bound lorebook.1011 * Gets the name of the character-bound lorebook.
968 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments1012 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
969 * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} name Character name1013 * @param {string} name Character name
970 * @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string1014 * @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string
971 */1015 */
972 function getCharBookCallback({ type }, name) {1016 function getCharBookCallback({ type }, name) {
@@ -1338,6 +1382,18 @@ function registerWorldInfoSlashCommands() {
1338 }1382 }
1339 }1383 }
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
1341 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1397 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1342 name: 'world',1398 name: 'world',
1343 callback: onWorldInfoChange,1399 callback: onWorldInfoChange,
@@ -1380,6 +1436,13 @@ function registerWorldInfoSlashCommands() {
1380 aliases: ['getchatlore', 'getchatwi'],1436 aliases: ['getchatlore', 'getchatwi'],
1381 }));1437 }));
1382 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1438 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({
1383 name: 'getpersonabook',1446 name: 'getpersonabook',
1384 callback: getPersonaBookCallback,1447 callback: getPersonaBookCallback,
1385 returns: 'lorebook name',1448 returns: 'lorebook name',
@@ -2172,6 +2235,12 @@ export const originalWIDataKeyMap = {
2172 'matchWholeWords': 'extensions.match_whole_words',2235 'matchWholeWords': 'extensions.match_whole_words',
2173 'useGroupScoring': 'extensions.use_group_scoring',2236 'useGroupScoring': 'extensions.use_group_scoring',
2174 'caseSensitive': 'extensions.case_sensitive',2237 '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',
2175 'scanDepth': 'extensions.scan_depth',2244 'scanDepth': 'extensions.scan_depth',
2176 'automationId': 'extensions.automation_id',2245 'automationId': 'extensions.automation_id',
2177 'vectorized': 'extensions.vectorized',2246 'vectorized': 'extensions.vectorized',
@@ -2589,7 +2658,7 @@ export async function getWorldEntry(name, data, entry) {
2589 if (!isMobile()) {2658 if (!isMobile()) {
2590 $(characterFilter).select2({2659 $(characterFilter).select2({
2591 width: '100%',2660 width: '100%',
2592 placeholder: 'Tie this entry to specific characters or characters with specific tags',2661 placeholder: t`Tie this entry to specific characters or characters with specific tags`,
2593 allowClear: true,2662 allowClear: true,
2594 closeOnSelect: false,2663 closeOnSelect: false,
2595 });2664 });
@@ -3189,7 +3258,7 @@ export async function getWorldEntry(name, data, entry) {
31893258
3190 // Create wrapper div3259 // Create wrapper div
3191 const wrapper = document.createElement('div');3260 const wrapper = document.createElement('div');
3192 wrapper.textContent = t`Move "${sourceName}" to:`;3261 wrapper.textContent = t`Move '${sourceName}' to:`;
31933262
3194 // Create container and append elements3263 // Create container and append elements
3195 const container = document.createElement('div');3264 const container = document.createElement('div');
@@ -3289,6 +3358,28 @@ export async function getWorldEntry(name, data, entry) {
3289 });3358 });
3290 useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');3359 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
3292 // automation id3383 // automation id
3293 const automationIdInput = template.find('input[name="automationId"]');3384 const automationIdInput = template.find('input[name="automationId"]');
3294 automationIdInput.data('uid', entry.uid);3385 automationIdInput.data('uid', entry.uid);
@@ -3424,7 +3515,7 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false }
3424 });3515 });
34253516
3426 $(input).on('focus click', function () {3517 $(input).on('focus click', function () {
3427 $(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : $(input).val());3518 $(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : String($(input).val()));
3428 });3519 });
3429}3520}
34303521
@@ -3495,6 +3586,12 @@ export const newWorldInfoEntryDefinition = {
3495 disable: { default: false, type: 'boolean' },3586 disable: { default: false, type: 'boolean' },
3496 excludeRecursion: { default: false, type: 'boolean' },3587 excludeRecursion: { default: false, type: 'boolean' },
3497 preventRecursion: { default: false, type: 'boolean' },3588 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' },
3498 delayUntilRecursion: { default: 0, type: 'number' },3595 delayUntilRecursion: { default: 0, type: 'number' },
3499 probability: { default: 100, type: 'number' },3596 probability: { default: 100, type: 'number' },
3500 useProbability: { default: true, type: 'boolean' },3597 useProbability: { default: true, type: 'boolean' },
@@ -3959,6 +4056,7 @@ function parseDecorators(content) {
3959 * @param {string[]} chat The chat messages to scan, in reverse order.4056 * @param {string[]} chat The chat messages to scan, in reverse order.
3960 * @param {number} maxContext The maximum context size of the generation.4057 * @param {number} maxContext The maximum context size of the generation.
3961 * @param {boolean} isDryRun Whether to perform a dry run.4058 * @param {boolean} isDryRun Whether to perform a dry run.
4059 * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
3962 * @typedef {object} WIActivated4060 * @typedef {object} WIActivated
3963 * @property {string} worldInfoBefore The world info before the chat.4061 * @property {string} worldInfoBefore The world info before the chat.
3964 * @property {string} worldInfoAfter The world info after the chat.4062 * @property {string} worldInfoAfter The world info after the chat.
@@ -3969,9 +4067,9 @@ function parseDecorators(content) {
3969 * @property {Set<any>} allActivatedEntries All entries.4067 * @property {Set<any>} allActivatedEntries All entries.
3970 * @returns {Promise<WIActivated>} The world info activated.4068 * @returns {Promise<WIActivated>} The world info activated.
3971 */4069 */
3972export async function checkWorldInfo(chat, maxContext, isDryRun) {4070export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) {
3973 const context = getContext();4071 const context = getContext();
3974 const buffer = new WorldInfoBuffer(chat);4072 const buffer = new WorldInfoBuffer(chat, globalScanData);
39754073
3976 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);4074 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
39774075
@@ -4829,6 +4927,12 @@ export function convertCharacterBook(characterBook) {
4829 sticky: entry.extensions?.sticky ?? null,4927 sticky: entry.extensions?.sticky ?? null,
4830 cooldown: entry.extensions?.cooldown ?? null,4928 cooldown: entry.extensions?.cooldown ?? null,
4831 delay: entry.extensions?.delay ?? null,4929 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,
4832 extensions: entry.extensions ?? {},4936 extensions: entry.extensions ?? {},
4833 };4937 };
4834 });4938 });
@@ -5099,7 +5203,7 @@ export function openWorldInfoEditor(worldName) {
50995203
5100/**5204/**
5101 * Assigns a lorebook to the current chat.5205 * Assigns a lorebook to the current chat.
5102 * @param {PointerEvent} event Pointer event5206 * @param {JQuery.ClickEvent<Document, undefined, any, any>} event Pointer event
5103 * @returns {Promise<void>}5207 * @returns {Promise<void>}
5104 */5208 */
5105export async function assignLorebookToChat(event) {5209export async function assignLorebookToChat(event) {
@@ -5138,11 +5242,106 @@ export async function assignLorebookToChat(event) {
5138 saveMetadata();5242 saveMetadata();
5139 });5243 });
51405244
5141 return callGenericPopup(template, POPUP_TYPE.TEXT);5245 await callGenericPopup(template, POPUP_TYPE.TEXT);
5142}5246}
51435247
5144jQuery(() => {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 */
5256export 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
5344export function initWorldInfo() {
5146 $('#world_info').on('mousedown change', async function (e) {5345 $('#world_info').on('mousedown change', async function (e) {
5147 // If there's no world names, don't do anything5346 // If there's no world names, don't do anything
5148 if (world_names.length === 0) {5347 if (world_names.length === 0) {
@@ -5329,7 +5528,7 @@ jQuery(() => {
5329 if (!isMobile()) {5528 if (!isMobile()) {
5330 $('#world_info').select2({5529 $('#world_info').select2({
5331 width: '100%',5530 width: '100%',
5332 placeholder: 'No Worlds active. Click here to select.',5531 placeholder: t`No Worlds active. Click here to select.`,
5333 allowClear: true,5532 allowClear: true,
5334 closeOnSelect: false,5533 closeOnSelect: false,
5335 });5534 });
@@ -5354,100 +5553,4 @@ jQuery(() => {
5354 }5553 }
5355 });5554 });
5356 });5555 });
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 */
5367export 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 }
5453}5556}
public/style.css+19 -3
@@ -157,11 +157,26 @@ body {
157}157}
158158
159::-webkit-scrollbar {159::-webkit-scrollbar {
160 width: 10px;160 width: 0.7rem;
161 height: 10px;
162 scrollbar-gutter: stable;161 scrollbar-gutter: stable;
163}162}
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
165.scrollY {180.scrollY {
166 overflow-y: auto !important;181 overflow-y: auto !important;
167}182}
@@ -5091,7 +5106,7 @@ body:not(.sd) .mes_img_swipes {
5091 display: flex;5106 display: flex;
5092 flex-direction: column;5107 flex-direction: column;
5093 justify-content: center;5108 justify-content: center;
5094 padding: 10px;5109 padding: 0px;
5095 height: 100%;5110 height: 100%;
5096 width: 100%;5111 width: 100%;
5097}5112}
@@ -5700,6 +5715,7 @@ body:not(.movingUI) .drawer-content.maximized {
5700 width: unset;5715 width: unset;
5701 margin: 0;5716 margin: 0;
5702 font-size: calc(var(--mainFontSize) * 0.85);5717 font-size: calc(var(--mainFontSize) * 0.85);
5718 padding-right: 20px;
5703}5719}
57045720
5705.paginationjs-pages ul li a {5721.paginationjs-pages ul li a {
server.js+5 -383
@@ -1,393 +1,15 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2
3// native node modules
4import path from 'node:path';
5import util from 'node:util';
6import net from 'node:net';
7import dns from 'node:dns';
8import process from 'node:process';
9import { fileURLToPath } from 'node:url';
10
11import cors from 'cors';
12import { csrfSync } from 'csrf-sync';
13import express from 'express';
14import compression from 'compression';
15import cookieSession from 'cookie-session';
16import multer from 'multer';
17import responseTime from 'response-time';
18import helmet from 'helmet';
19import bodyParser from 'body-parser';
20import open from 'open';
21
22// local library imports
23import './src/fetch-patch.js';
24import { serverEvents, EVENT_NAMES } from './src/server-events.js';
25import { CommandLineParser } from './src/command-line.js';2import { CommandLineParser } from './src/command-line.js';
26import { loadPlugins } from './src/plugin-loader.js';3import { serverDirectory } from './src/server-directory.js';
27import {
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
44import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
45import basicAuthMiddleware from './src/middleware/basicAuth.js';
46import getWhitelistMiddleware from './src/middleware/whitelist.js';
47import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';
48import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
49import initRequestProxy from './src/request-proxy.js';
50import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
51import corsProxyMiddleware from './src/middleware/corsProxy.js';
52import {
53 getVersion,
54 color,
55 removeColorFormatting,
56 getSeparator,
57 safeReadFileSync,
58 setupLogLevel,
59 setWindowTitle,
60} from './src/util.js';
61import { UPLOADS_DIRECTORY } from './src/constants.js';
62import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
63
64// Routers
65import { router as usersPublicRouter } from './src/endpoints/users-public.js';
66import { init as statsInit, onExit as statsOnExit } from './src/endpoints/stats.js';
67import { checkForNewContent } from './src/endpoints/content-manager.js';
68import { init as settingsInit } from './src/endpoints/settings.js';
69import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './src/server-startup.js';
70import { diskCache } from './src/endpoints/characters.js';
71
72// Unrestrict console logs display limit
73util.inspect.defaultOptions.maxArrayLength = null;
74util.inspect.defaultOptions.maxStringLength = null;
75util.inspect.defaultOptions.depth = 4;
76
77// Set a working directory for the server
78const serverDirectory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
79console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
80process.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.
85if (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
90const cliArgs = new CommandLineParser().parse(process.argv);6const cliArgs = new CommandLineParser().parse(process.argv);
91globalThis.DATA_ROOT = cliArgs.dataRoot;7globalThis.DATA_ROOT = cliArgs.dataRoot;
92globalThis.COMMAND_LINE_ARGS = cliArgs;8globalThis.COMMAND_LINE_ARGS = cliArgs;
939process.chdir(serverDirectory);
94if (!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
99try {11try {
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 }
107} catch (error) {13} catch (error) {
108 console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.');14 console.error('A critical error has occurred while starting the server:', error);
109}
110
111const app = express();
112app.use(helmet({
113 contentSecurityPolicy: false,
114}));
115app.use(compression());
116app.use(responseTime());
117
118app.use(bodyParser.json({ limit: '200mb' }));
119app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
120
121// CORS Settings //
122const CORS = cors({
123 origin: 'null',
124 methods: ['OPTIONS'],
125});
126
127app.use(CORS);
128
129if (cliArgs.listen && cliArgs.basicAuthMode) {
130 app.use(basicAuthMiddleware);
131}
132
133if (cliArgs.whitelistMode) {
134 const whitelistMiddleware = await getWhitelistMiddleware();
135 app.use(whitelistMiddleware);
136}
137
138if (cliArgs.listen) {
139 app.use(accessLoggerMiddleware());
140}
141
142if (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
152app.use(cookieSession({
153 name: getCookieSessionName(),
154 sameSite: 'lax',
155 httpOnly: true,
156 maxAge: getSessionCookieAge(),
157 secret: getCookieSecret(globalThis.DATA_ROOT),
158}));
159
160app.use(setUserDataMiddleware);
161
162// CSRF Protection //
163if (!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
207app.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)
218app.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
229app.get('/login', loginPageMiddleware);
230
231// Host frontend assets
232const webpackMiddleware = getWebpackServeMiddleware();
233app.use(webpackMiddleware);
234app.use(express.static(process.cwd() + '/public', {}));
235
236// Public API
237app.use('/api/users', usersPublicRouter);
238
239// Everything below this line requires authentication
240app.use(requireLoginMiddleware);
241app.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
250const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
251app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
252app.use(multerMonkeyPatch);
253
254app.get('/version', async function (_, response) {
255 const data = await getVersion();
256 response.send(data);
257});
258
259redirectDeprecatedEndpoints(app);
260setupPrivateEndpoints(app);
261
262/**
263 * Tasks that need to be run before the server starts listening.
264 * @returns {Promise<void>}
265 */
266async 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 */
328async 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 */
377function apply404Middleware() {
378 const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? '';
379 app.use((req, res) => {
380 res.status(404).send(notFoundWebpage);
381 });
382}15}
383
384// User storage module needs to be initialized before starting the server
385initUserStorage(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';
2import { hideBin } from 'yargs/helpers';2import { hideBin } from 'yargs/helpers';
3import ipRegex from 'ip-regex';3import ipRegex from 'ip-regex';
4import { canResolve, color, getConfigValue, stringToBool } from './util.js';4import { canResolve, color, getConfigValue, stringToBool } from './util.js';
5import { initConfig } from './config-init.js';
56
6/**7/**
7 * @typedef {object} CommandLineArguments Parsed command line arguments8 * @typedef {object} CommandLineArguments Parsed command line arguments
9 * @property {string} configPath Path to the config file
8 * @property {string} dataRoot Data root directory10 * @property {string} dataRoot Data root directory
9 * @property {number} port Port number11 * @property {number} port Port number
10 * @property {boolean} listen If SillyTavern is listening on all network interfaces12 * @property {boolean} listen If SillyTavern is listening on all network interfaces
@@ -40,6 +42,7 @@ export class CommandLineParser {
40 constructor() {42 constructor() {
41 /** @type {CommandLineArguments} */43 /** @type {CommandLineArguments} */
42 this.default = Object.freeze({44 this.default = Object.freeze({
45 configPath: './config.yaml',
43 dataRoot: './data',46 dataRoot: './data',
44 port: 8000,47 port: 8000,
45 listen: false,48 listen: false,
@@ -88,6 +91,11 @@ export class CommandLineParser {
88 parse(args) {91 parse(args) {
89 const cliArguments = yargs(hideBin(args))92 const cliArguments = yargs(hideBin(args))
90 .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.')93 .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 })
91 .option('enableIPv6', {99 .option('enableIPv6', {
92 type: 'string',100 type: 'string',
93 default: null,101 default: null,
@@ -177,8 +185,11 @@ export class CommandLineParser {
177 describe: 'Request proxy bypass list (space separated list of hosts)',185 describe: 'Request proxy bypass list (space separated list of hosts)',
178 }).parseSync();186 }).parseSync();
179187
188 const configPath = cliArguments.configPath ?? this.default.configPath;
189 initConfig(configPath);
180 /** @type {CommandLineArguments} */190 /** @type {CommandLineArguments} */
181 const result = {191 const result = {
192 configPath: configPath,
182 dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot),193 dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot),
183 port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'),194 port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'),
184 listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'),195 listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'),
src/config-init.js+197 -0
@@ -0,0 +1,197 @@
1import fs from 'node:fs';
2import path from 'node:path';
3import yaml from 'yaml';
4import color from 'chalk';
5import _ from 'lodash';
6import { serverDirectory } from './server-directory.js';
7import { setConfigFilePath } from './util.js';
8
9const 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 */
109function 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 */
128export 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 */
194export function initConfig(configPath) {
195 setConfigFilePath(configPath);
196 addMissingConfigValues(configPath);
197}
src/constants.js+3 -19
@@ -156,7 +156,7 @@ export const GEMINI_SAFETY = [
156 },156 },
157 {157 {
158 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',158 category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
159 threshold: 'BLOCK_NONE',159 threshold: 'OFF',
160 },160 },
161];161];
162162
@@ -176,6 +176,7 @@ export const CHAT_COMPLETION_SOURCES = {
176 ZEROONEAI: '01ai',176 ZEROONEAI: '01ai',
177 NANOGPT: 'nanogpt',177 NANOGPT: 'nanogpt',
178 DEEPSEEK: 'deepseek',178 DEEPSEEK: 'deepseek',
179 XAI: 'xai',
179};180};
180181
181/**182/**
@@ -267,23 +268,6 @@ export const FEATHERLESS_KEYS = [
267 'guided_whitespace_pattern',268 'guided_whitespace_pattern',
268];269];
269270
270// https://dreamgen.com/docs/api#openai-text
271export 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
287// https://docs.together.ai/reference/completions271// https://docs.together.ai/reference/completions
288export const TOGETHERAI_KEYS = [272export const TOGETHERAI_KEYS = [
289 'model',273 'model',
@@ -300,7 +284,7 @@ export const TOGETHERAI_KEYS = [
300 'stop',284 'stop',
301];285];
302286
303// https://github.com/jmorganca/ollama/blob/main/docs/api.md#request-with-options287// https://github.com/ollama/ollama/blob/main/docs/api.md#request-with-options
304export const OLLAMA_KEYS = [288export const OLLAMA_KEYS = [
305 'num_predict',289 'num_predict',
306 'num_ctx',290 'num_ctx',
src/endpoints/assets.js+2 -2
@@ -235,14 +235,14 @@ router.post('/download', async (request, response) => {
235 const contentType = mime.lookup(temp_path) || 'application/octet-stream';235 const contentType = mime.lookup(temp_path) || 'application/octet-stream';
236 response.setHeader('Content-Type', contentType);236 response.setHeader('Content-Type', contentType);
237 response.send(fileContent);237 response.send(fileContent);
238 fs.rmSync(temp_path);238 fs.unlinkSync(temp_path);
239 return;239 return;
240 }240 }
241241
242 // Move into asset place242 // Move into asset place
243 console.info('Download finished, moving file from', temp_path, 'to', file_path);243 console.info('Download finished, moving file from', temp_path, 'to', file_path);
244 fs.copyFileSync(temp_path, file_path);244 fs.copyFileSync(temp_path, file_path);
245 fs.rmSync(temp_path);245 fs.unlinkSync(temp_path);
246 response.sendStatus(200);246 response.sendStatus(200);
247 }247 }
248 catch (error) {248 catch (error) {
src/endpoints/avatars.js+2 -2
@@ -28,7 +28,7 @@ router.post('/delete', getFileNameValidationFunction('avatar'), function (reques
28 const fileName = path.join(request.user.directories.avatars, sanitize(request.body.avatar));28 const fileName = path.join(request.user.directories.avatars, sanitize(request.body.avatar));
2929
30 if (fs.existsSync(fileName)) {30 if (fs.existsSync(fileName)) {
31 fs.rmSync(fileName);31 fs.unlinkSync(fileName);
32 return response.send({ result: 'ok' });32 return response.send({ result: 'ok' });
33 }33 }
3434
@@ -53,7 +53,7 @@ router.post('/upload', async (request, response) => {
53 const filename = request.body.overwrite_name || `${Date.now()}.png`;53 const filename = request.body.overwrite_name || `${Date.now()}.png`;
54 const pathToNewFile = path.join(request.user.directories.avatars, filename);54 const pathToNewFile = path.join(request.user.directories.avatars, filename);
55 writeFileAtomicSync(pathToNewFile, image);55 writeFileAtomicSync(pathToNewFile, image);
56 fs.rmSync(pathToUpload);56 fs.unlinkSync(pathToUpload);
57 return response.send({ path: filename });57 return response.send({ path: filename });
58 } catch (err) {58 } catch (err) {
59 return response.status(400).send('Is not a valid image');59 return response.status(400).send('Is not a valid image');
src/endpoints/backends/chat-completions.js+161 -32
@@ -1,4 +1,5 @@
1import process from 'node:process';1import process from 'node:process';
2import util from 'node:util';
2import express from 'express';3import express from 'express';
3import fetch from 'node-fetch';4import fetch from 'node-fetch';
45
@@ -23,11 +24,13 @@ import {
23 convertCohereMessages,24 convertCohereMessages,
24 convertMistralMessages,25 convertMistralMessages,
25 convertAI21Messages,26 convertAI21Messages,
27 convertXAIMessages,
26 mergeMessages,28 mergeMessages,
27 cachingAtDepthForOpenRouterClaude,29 cachingAtDepthForOpenRouterClaude,
28 cachingAtDepthForClaude,30 cachingAtDepthForClaude,
29 getPromptNames,31 getPromptNames,
30 calculateBudgetTokens,32 calculateClaudeBudgetTokens,
33 calculateGoogleBudgetTokens,
31} from '../../prompt-converters.js';34} from '../../prompt-converters.js';
3235
33import { readSecret, SECRET_KEYS } from '../secrets.js';36import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -53,6 +56,7 @@ const API_01AI = 'https://api.lingyiwanwu.com/v1';
53const API_AI21 = 'https://api.ai21.com/studio/v1';56const API_AI21 = 'https://api.ai21.com/studio/v1';
54const API_NANOGPT = 'https://nano-gpt.com/api/v1';57const API_NANOGPT = 'https://nano-gpt.com/api/v1';
55const API_DEEPSEEK = 'https://api.deepseek.com/beta';58const API_DEEPSEEK = 'https://api.deepseek.com/beta';
59const API_XAI = 'https://api.x.ai/v1';
5660
57/**61/**
58 * Applies a post-processing step to the generated messages.62 * Applies a post-processing step to the generated messages.
@@ -200,7 +204,7 @@ async function sendClaudeRequest(request, response) {
200 // No prefill when thinking204 // No prefill when thinking
201 voidPrefill = true;205 voidPrefill = true;
202 const reasoningEffort = request.body.reasoning_effort;206 const reasoningEffort = request.body.reasoning_effort;
203 const budgetTokens = calculateBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);207 const budgetTokens = calculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);
204 const minThinkTokens = 1024;208 const minThinkTokens = 1024;
205 if (requestBody.max_tokens <= minThinkTokens) {209 if (requestBody.max_tokens <= minThinkTokens) {
206 const newValue = requestBody.max_tokens + minThinkTokens;210 const newValue = requestBody.max_tokens + minThinkTokens;
@@ -248,7 +252,7 @@ async function sendClaudeRequest(request, response) {
248 if (!generateResponse.ok) {252 if (!generateResponse.ok) {
249 const generateResponseText = await generateResponse.text();253 const generateResponseText = await generateResponse.text();
250 console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));254 console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
251 return response.status(generateResponse.status).send({ error: true });255 return response.status(500).send({ error: true });
252 }256 }
253257
254 /** @type {any} */258 /** @type {any} */
@@ -338,8 +342,9 @@ async function sendMakerSuiteRequest(request, response) {
338 const stream = Boolean(request.body.stream);342 const stream = Boolean(request.body.stream);
339 const enableWebSearch = Boolean(request.body.enable_web_search);343 const enableWebSearch = Boolean(request.body.enable_web_search);
340 const requestImages = Boolean(request.body.request_images);344 const requestImages = Boolean(request.body.request_images);
341 const isThinking = model.includes('thinking');345 const reasoningEffort = String(request.body.reasoning_effort);
342 const isGemma = model.includes('gemma');346 const isGemma = model.includes('gemma');
347 const isLearnLM = model.includes('learnlm');
343348
344 const generationConfig = {349 const generationConfig = {
345 stopSequences: request.body.stop,350 stopSequences: request.body.stop,
@@ -353,47 +358,60 @@ async function sendMakerSuiteRequest(request, response) {
353 };358 };
354359
355 function getGeminiBody() {360 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
356 if (!Array.isArray(generationConfig.stopSequences) || !generationConfig.stopSequences.length) {388 if (!Array.isArray(generationConfig.stopSequences) || !generationConfig.stopSequences.length) {
357 delete generationConfig.stopSequences;389 delete generationConfig.stopSequences;
358 }390 }
359391
360 const useMultiModal = requestImages && ['gemini-2.0-flash-exp', 'gemini-2.0-flash-exp-image-generation'].includes(model);392 const enableImageModality = requestImages && imageGenerationModels.includes(model);
361 if (useMultiModal) {393 if (enableImageModality) {
362 generationConfig.responseModalities = ['text', 'image'];394 generationConfig.responseModalities = ['text', 'image'];
363 }395 }
364396
365 const useSystemPrompt = !useMultiModal && (397 const useSystemPrompt = !enableImageModality && !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
375 const tools = [];399 const tools = [];
376 const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request));400 const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request));
377 let safetySettings = GEMINI_SAFETY;401 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)) {
381 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));404 safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' }));
382 }405 }
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
389 if (enableWebSearch && !useMultiModal && !isGemma) {407 if (enableWebSearch && !enableImageModality && !isGemma && !isLearnLM && !noSearchModels.includes(model)) {
390 const searchTool = model.includes('1.5') || model.includes('1.0')408 const searchTool = model.includes('1.5')
391 ? ({ google_search_retrieval: {} })409 ? ({ google_search_retrieval: {} })
392 : ({ google_search: {} });410 : ({ google_search: {} });
393 tools.push(searchTool);411 tools.push(searchTool);
394 }412 }
395413
396 if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !useMultiModal && !isGemma) {414 if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !enableImageModality && !isGemma) {
397 const functionDeclarations = [];415 const functionDeclarations = [];
398 for (const tool of request.body.tools) {416 for (const tool of request.body.tools) {
399 if (tool.type === 'function') {417 if (tool.type === 'function') {
@@ -409,13 +427,21 @@ async function sendMakerSuiteRequest(request, response) {
409 tools.push({ function_declarations: functionDeclarations });427 tools.push({ function_declarations: functionDeclarations });
410 }428 }
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
412 let body = {438 let body = {
413 contents: prompt.contents,439 contents: prompt.contents,
414 safetySettings: safetySettings,440 safetySettings: safetySettings,
415 generationConfig: generationConfig,441 generationConfig: generationConfig,
416 };442 };
417443
418 if (useSystemPrompt) {444 if (useSystemPrompt && Array.isArray(prompt.system_instruction.parts) && prompt.system_instruction.parts.length) {
419 body.systemInstruction = prompt.system_instruction;445 body.systemInstruction = prompt.system_instruction;
420 }446 }
421447
@@ -436,7 +462,7 @@ async function sendMakerSuiteRequest(request, response) {
436 controller.abort();462 controller.abort();
437 });463 });
438464
439 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';465 const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta');
440 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');466 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
441467
442 const generateResponse = await fetch(`${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {468 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) {
447 },473 },
448 signal: controller.signal,474 signal: controller.signal,
449 });475 });
450 // have to do this because of their busted ass streaming endpoint476
451 if (stream) {477 if (stream) {
452 try {478 try {
453 // Pipe remote SSE stream to Express response479 // Pipe remote SSE stream to Express response
@@ -461,7 +487,7 @@ async function sendMakerSuiteRequest(request, response) {
461 } else {487 } else {
462 if (!generateResponse.ok) {488 if (!generateResponse.ok) {
463 console.warn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);489 console.warn(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
464 return response.status(generateResponse.status).send({ error: true });490 return response.status(500).send({ error: true });
465 }491 }
466492
467 /** @type {any} */493 /** @type {any} */
@@ -480,7 +506,7 @@ async function sendMakerSuiteRequest(request, response) {
480 const responseContent = candidates[0].content ?? candidates[0].output;506 const responseContent = candidates[0].content ?? candidates[0].output;
481 const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);507 const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);
482 const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData);508 const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData);
483 console.warn('Google AI Studio response:', responseContent);509 console.debug('Google AI Studio response:', util.inspect(generateResponseJson, { depth: 5, colors: true }));
484510
485 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');511 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
486 if (!responseText && !functionCall && !inlineData) {512 if (!responseText && !functionCall && !inlineData) {
@@ -828,6 +854,100 @@ async function sendDeepSeekRequest(request, response) {
828 }854 }
829}855}
830856
857/**
858 * Sends a request to XAI API.
859 * @param {express.Request} request Express request
860 * @param {express.Response} response Express response
861 */
862async 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
832export const router = express.Router();952export const router = express.Router();
833953
@@ -872,6 +992,10 @@ router.post('/status', async function (request, response_getstatus_openai) {
872 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));992 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
873 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);993 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
874 headers = {};994 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 = {};
875 } else {999 } else {
876 console.warn('This chat completion source is not supported yet.');1000 console.warn('This chat completion source is not supported yet.');
877 return response_getstatus_openai.status(400).send({ error: true });1001 return response_getstatus_openai.status(400).send({ error: true });
@@ -1039,6 +1163,7 @@ router.post('/generate', function (request, response) {
1039 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);1163 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
1040 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);1164 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
1041 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);1165 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
1166 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
1042 }1167 }
10431168
1044 let apiUrl;1169 let apiUrl;
@@ -1108,6 +1233,10 @@ router.post('/generate', function (request, response) {
1108 bodyParams['route'] = 'fallback';1233 bodyParams['route'] = 'fallback';
1109 }1234 }
11101235
1236 if (request.body.reasoning_effort) {
1237 bodyParams['reasoning'] = { effort: request.body.reasoning_effort };
1238 }
1239
1111 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');1240 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
1112 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {1241 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
1113 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);1242 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
@@ -1156,8 +1285,8 @@ router.post('/generate', function (request, response) {
1156 }1285 }
11571286
1158 // A few of OpenAIs reasoning models support reasoning effort1287 // A few of OpenAIs reasoning models support reasoning effort
1159 if ([CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {1288 if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
1160 if (['o1', 'o3-mini', 'o3-mini-2025-01-31'].includes(request.body.model)) {1289 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)) {
1161 bodyParams['reasoning_effort'] = request.body.reasoning_effort;1290 bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1162 }1291 }
1163 }1292 }
src/endpoints/backends/kobold.js+1 -1
@@ -204,7 +204,7 @@ router.post('/transcribe-audio', async function (request, response) {
204 console.debug('Transcribing audio with KoboldCpp', server);204 console.debug('Transcribing audio with KoboldCpp', server);
205205
206 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');206 const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
207 fs.rmSync(request.file.path);207 fs.unlinkSync(request.file.path);
208208
209 const headers = {};209 const headers = {};
210 setAdditionalHeadersByType(headers, TEXTGEN_TYPES.KOBOLDCPP, server, request.user.directories);210 setAdditionalHeadersByType(headers, TEXTGEN_TYPES.KOBOLDCPP, server, request.user.directories);
src/endpoints/backends/text-completions.js+3 -7
@@ -10,7 +10,6 @@ import {
10 INFERMATICAI_KEYS,10 INFERMATICAI_KEYS,
11 OPENROUTER_KEYS,11 OPENROUTER_KEYS,
12 VLLM_KEYS,12 VLLM_KEYS,
13 DREAMGEN_KEYS,
14 FEATHERLESS_KEYS,13 FEATHERLESS_KEYS,
15 OPENAI_KEYS,14 OPENAI_KEYS,
16} from '../../constants.js';15} from '../../constants.js';
@@ -340,9 +339,6 @@ router.post('/generate', async function (request, response) {
340 }339 }
341340
342 if (request.body.api_type === TEXTGEN_TYPES.DREAMGEN) {341 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');
346 args.body = JSON.stringify(request.body);342 args.body = JSON.stringify(request.body);
347 }343 }
348344
@@ -450,7 +446,7 @@ ollama.post('/download', async function (request, response) {
450446
451 if (!fetchResponse.ok) {447 if (!fetchResponse.ok) {
452 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);448 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
453 return response.status(fetchResponse.status).send({ error: true });449 return response.status(500).send({ error: true });
454 }450 }
455451
456 console.debug('Ollama pull response:', await fetchResponse.json());452 console.debug('Ollama pull response:', await fetchResponse.json());
@@ -659,14 +655,14 @@ tabby.post('/download', async function (request, response) {
659 }655 }
660 } else {656 } else {
661 console.error('API Permission error:', permissionResponse.status, permissionResponse.statusText);657 console.error('API Permission error:', permissionResponse.status, permissionResponse.statusText);
662 return response.status(permissionResponse.status).send({ error: true });658 return response.status(500).send({ error: true });
663 }659 }
664660
665 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);661 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
666662
667 if (!fetchResponse.ok) {663 if (!fetchResponse.ok) {
668 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);664 console.error('Download error:', fetchResponse.status, fetchResponse.statusText);
669 return response.status(fetchResponse.status).send({ error: true });665 return response.status(500).send({ error: true });
670 }666 }
671667
672 return response.send({ ok: true });668 return response.send({ ok: true });
src/endpoints/backgrounds.js+3 -3
@@ -30,7 +30,7 @@ router.post('/delete', getFileNameValidationFunction('bg'), function (request, r
30 return response.sendStatus(400);30 return response.sendStatus(400);
31 }31 }
3232
33 fs.rmSync(fileName);33 fs.unlinkSync(fileName);
34 invalidateThumbnail(request.user.directories, 'bg', request.body.bg);34 invalidateThumbnail(request.user.directories, 'bg', request.body.bg);
35 return response.send('ok');35 return response.send('ok');
36});36});
@@ -52,7 +52,7 @@ router.post('/rename', function (request, response) {
52 }52 }
5353
54 fs.copyFileSync(oldFileName, newFileName);54 fs.copyFileSync(oldFileName, newFileName);
55 fs.rmSync(oldFileName);55 fs.unlinkSync(oldFileName);
56 invalidateThumbnail(request.user.directories, 'bg', request.body.old_bg);56 invalidateThumbnail(request.user.directories, 'bg', request.body.old_bg);
57 return response.send('ok');57 return response.send('ok');
58});58});
@@ -65,7 +65,7 @@ router.post('/upload', function (request, response) {
6565
66 try {66 try {
67 fs.copyFileSync(img_path, path.join(request.user.directories.backgrounds, filename));67 fs.copyFileSync(img_path, path.join(request.user.directories.backgrounds, filename));
68 fs.rmSync(img_path);68 fs.unlinkSync(img_path);
69 invalidateThumbnail(request.user.directories, 'bg', filename);69 invalidateThumbnail(request.user.directories, 'bg', filename);
70 response.send(filename);70 response.send(filename);
71 } catch (err) {71 } catch (err) {
src/endpoints/characters.js+10 -4
@@ -702,6 +702,12 @@ function convertWorldInfoToCharacterBook(name, entries) {
702 sticky: entry.sticky ?? null,702 sticky: entry.sticky ?? null,
703 cooldown: entry.cooldown ?? null,703 cooldown: entry.cooldown ?? null,
704 delay: entry.delay ?? null,704 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,
705 },711 },
706 };712 };
707713
@@ -720,7 +726,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
720 */726 */
721async function importFromYaml(uploadPath, context, preservedFileName) {727async function importFromYaml(uploadPath, context, preservedFileName) {
722 const fileText = fs.readFileSync(uploadPath, 'utf8');728 const fileText = fs.readFileSync(uploadPath, 'utf8');
723 fs.rmSync(uploadPath);729 fs.unlinkSync(uploadPath);
724 const yamlData = yaml.parse(fileText);730 const yamlData = yaml.parse(fileText);
725 console.info('Importing from YAML');731 console.info('Importing from YAML');
726 yamlData.name = sanitize(yamlData.name);732 yamlData.name = sanitize(yamlData.name);
@@ -754,7 +760,7 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
754 */760 */
755async function importFromCharX(uploadPath, { request }, preservedFileName) {761async function importFromCharX(uploadPath, { request }, preservedFileName) {
756 const data = fs.readFileSync(uploadPath).buffer;762 const data = fs.readFileSync(uploadPath).buffer;
757 fs.rmSync(uploadPath);763 fs.unlinkSync(uploadPath);
758 console.info('Importing from CharX');764 console.info('Importing from CharX');
759 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');765 const cardBuffer = await extractFileFromZipBuffer(data, 'card.json');
760766
@@ -995,7 +1001,7 @@ router.post('/rename', validateAvatarUrlMiddleware, async function (request, res
995 }1001 }
9961002
997 // Remove the old character file1003 // Remove the old character file
998 fs.rmSync(oldAvatarPath);1004 fs.unlinkSync(oldAvatarPath);
9991005
1000 // Return new avatar name to ST1006 // Return new avatar name to ST
1001 return response.send({ avatar: newAvatarName });1007 return response.send({ avatar: newAvatarName });
@@ -1150,7 +1156,7 @@ router.post('/delete', validateAvatarUrlMiddleware, async function (request, res
1150 return response.sendStatus(400);1156 return response.sendStatus(400);
1151 }1157 }
11521158
1153 fs.rmSync(avatarPath);1159 fs.unlinkSync(avatarPath);
1154 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);1160 invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
1155 let dir_name = (request.body.avatar_url.replace('.png', ''));1161 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
433 }433 }
434434
435 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);435 fs.copyFileSync(pathToOriginalFile, pathToRenamedFile);
436 fs.rmSync(pathToOriginalFile);436 fs.unlinkSync(pathToOriginalFile);
437 console.info('Successfully renamed.');437 console.info('Successfully renamed.');
438 return response.send({ ok: true, sanitizedFileName });438 return response.send({ ok: true, sanitizedFileName });
439});439});
@@ -449,7 +449,7 @@ router.post('/delete', validateAvatarUrlMiddleware, function (request, response)
449 return response.sendStatus(400);449 return response.sendStatus(400);
450 }450 }
451451
452 fs.rmSync(filePath);452 fs.unlinkSync(filePath);
453 console.info(`Deleted chat file: ${filePath}`);453 console.info(`Deleted chat file: ${filePath}`);
454 return response.send('ok');454 return response.send('ok');
455});455});
@@ -665,7 +665,7 @@ router.post('/group/delete', (request, response) => {
665 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);665 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
666666
667 if (fs.existsSync(pathToFile)) {667 if (fs.existsSync(pathToFile)) {
668 fs.rmSync(pathToFile);668 fs.unlinkSync(pathToFile);
669 return response.send({ ok: true });669 return response.send({ ok: true });
670 }670 }
671671
src/endpoints/content-manager.js+20 -6
@@ -1,6 +1,5 @@
1import fs from 'node:fs';1import fs from 'node:fs';
2import path from 'node:path';2import path from 'node:path';
3import process from 'node:process';
4import { Buffer } from 'node:buffer';3import { Buffer } from 'node:buffer';
54
6import express from 'express';5import express from 'express';
@@ -8,11 +7,12 @@ import fetch from 'node-fetch';
8import sanitize from 'sanitize-filename';7import sanitize from 'sanitize-filename';
9import { sync as writeFileAtomicSync } from 'write-file-atomic';8import { sync as writeFileAtomicSync } from 'write-file-atomic';
109
11import { getConfigValue, color } from '../util.js';10import { getConfigValue, color, setPermissionsSync } from '../util.js';
12import { write } from '../character-card-parser.js';11import { write } from '../character-card-parser.js';
12import { serverDirectory } from '../server-directory.js';
1313
14const contentDirectory = path.join(process.cwd(), 'default/content');14const contentDirectory = path.join(serverDirectory, 'default/content');
15const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');15const scaffoldDirectory = path.join(serverDirectory, 'default/scaffold');
16const contentIndexPath = path.join(contentDirectory, 'index.json');16const contentIndexPath = path.join(contentDirectory, 'index.json');
17const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');17const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
1818
@@ -149,6 +149,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
149 }149 }
150150
151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });151 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
152 setPermissionsSync(targetPath);
152 console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);153 console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);
153 anyContentAdded = true;154 anyContentAdded = true;
154 }155 }
@@ -540,9 +541,21 @@ async function downloadGenericPng(url) {
540541
541 if (result.ok) {542 if (result.ok) {
542 const buffer = Buffer.from(await result.arrayBuffer());543 const buffer = Buffer.from(await result.arrayBuffer());
543 const fileName = sanitize(result.url.split('?')[0].split('/').reverse()[0]);544 let fileName = sanitize(result.url.split('?')[0].split('/').reverse()[0]);
544 const contentType = result.headers.get('content-type') || 'image/png'; //yoink it from AICC function lol545 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
546 return {559 return {
547 buffer: buffer,560 buffer: buffer,
548 fileName: fileName,561 fileName: fileName,
@@ -694,10 +707,11 @@ router.post('/importURL', async (request, response) => {
694 type = 'character';707 type = 'character';
695 result = await downloadRisuCharacter(uuid);708 result = await downloadRisuCharacter(uuid);
696 } else if (isGeneric) {709 } else if (isGeneric) {
697 console.info('Downloading from generic url.');710 console.info('Downloading from generic url:', url);
698 type = 'character';711 type = 'character';
699 result = await downloadGenericPng(url);712 result = await downloadGenericPng(url);
700 } else {713 } 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.`);
701 return response.sendStatus(404);715 return response.sendStatus(404);
702 }716 }
703717
src/endpoints/extensions.js+134 -20
@@ -30,17 +30,23 @@ async function getManifest(extensionPath) {
30 * @returns {Promise<Object>} - Returns the extension information as an object30 * @returns {Promise<Object>} - Returns the extension information as an object
31 */31 */
32async function checkIfRepoIsUpToDate(extensionPath) {32async function checkIfRepoIsUpToDate(extensionPath) {
33 const git = simpleGit();33 const git = simpleGit({ baseDir: extensionPath });
34 await git.cwd(extensionPath).fetch('origin');34 await git.fetch('origin');
35 const currentBranch = await git.cwd(extensionPath).branch();35 const currentBranch = await git.branch();
36 const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);36 const currentCommitHash = await git.revparse(['HEAD']);
37 const log = await git.cwd(extensionPath).log({37 const log = await git.log({
38 from: currentCommitHash,38 from: currentCommitHash,
39 to: `origin/${currentBranch.current}`,39 to: `origin/${currentBranch.current}`,
40 });40 });
4141
42 // Fetch remote repository information42 // Fetch remote repository information
43 const remotes = await git.cwd(extensionPath).getRemotes(true);43 const remotes = await git.getRemotes(true);
44 if (remotes.length === 0) {
45 return {
46 isUpToDate: true,
47 remoteUrl: '',
48 };
49 }
4450
45 return {51 return {
46 isUpToDate: log.total === 0,52 isUpToDate: log.total === 0,
@@ -76,7 +82,7 @@ router.post('/install', async (request, response) => {
76 fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);82 fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);
77 }83 }
7884
79 const { url, global } = request.body;85 const { url, global, branch } = request.body;
8086
81 if (global && !request.user.profile.admin) {87 if (global && !request.user.profile.admin) {
82 console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`);88 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) => {
90 return response.status(409).send(`Directory already exists at ${extensionPath}`);96 return response.status(409).send(`Directory already exists at ${extensionPath}`);
91 }97 }
9298
93 await git.clone(url, extensionPath, { '--depth': 1 });99 const cloneOptions = { '--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
96 const { version, author, display_name } = await getManifest(extensionPath);106 const { version, author, display_name } = await getManifest(extensionPath);
97107
@@ -114,7 +124,6 @@ router.post('/install', async (request, response) => {
114 * @returns {void}124 * @returns {void}
115 */125 */
116router.post('/update', async (request, response) => {126router.post('/update', async (request, response) => {
117 const git = simpleGit();
118 if (!request.body.extensionName) {127 if (!request.body.extensionName) {
119 return response.status(400).send('Bad Request: extensionName is required in the request body.');128 return response.status(400).send('Bad Request: extensionName is required in the request body.');
120 }129 }
@@ -128,22 +137,23 @@ router.post('/update', async (request, response) => {
128 }137 }
129138
130 const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;139 const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
131 const extensionPath = path.join(basePath, extensionName);140 const extensionPath = path.join(basePath, sanitize(extensionName));
132141
133 if (!fs.existsSync(extensionPath)) {142 if (!fs.existsSync(extensionPath)) {
134 return response.status(404).send(`Directory does not exist at ${extensionPath}`);143 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
135 }144 }
136145
137 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);146 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
138 const currentBranch = await git.cwd(extensionPath).branch();147 const git = simpleGit({ baseDir: extensionPath });
148 const currentBranch = await git.branch();
139 if (!isUpToDate) {149 if (!isUpToDate) {
140 await git.cwd(extensionPath).pull('origin', currentBranch.current);150 await git.pull('origin', currentBranch.current);
141 console.info(`Extension has been updated at ${extensionPath}`);151 console.info(`Extension has been updated at ${extensionPath}`);
142 } else {152 } else {
143 console.info(`Extension is up to date at ${extensionPath}`);153 console.info(`Extension is up to date at ${extensionPath}`);
144 }154 }
145 await git.cwd(extensionPath).fetch('origin');155 await git.fetch('origin');
146 const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);156 const fullCommitHash = await git.revparse(['HEAD']);
147 const shortCommitHash = fullCommitHash.slice(0, 7);157 const shortCommitHash = fullCommitHash.slice(0, 7);
148158
149 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });159 return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
@@ -154,6 +164,110 @@ router.post('/update', async (request, response) => {
154 }164 }
155});165});
156166
167router.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
212router.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
157router.post('/move', async (request, response) => {271router.post('/move', async (request, response) => {
158 try {272 try {
159 const { extensionName, source, destination } = request.body;273 const { extensionName, source, destination } = request.body;
@@ -194,7 +308,7 @@ router.post('/move', async (request, response) => {
194 return response.sendStatus(204);308 return response.sendStatus(204);
195 } catch (error) {309 } catch (error) {
196 console.error('Moving extension failed', error);310 console.error('Moving extension failed', error);
197 return response.status(500).send('Internal Server Error. Try again later.');311 return response.status(500).send('Internal Server Error. Check the server logs for more details.');
198 }312 }
199});313});
200314
@@ -209,7 +323,6 @@ router.post('/move', async (request, response) => {
209 * @returns {void}323 * @returns {void}
210 */324 */
211router.post('/version', async (request, response) => {325router.post('/version', async (request, response) => {
212 const git = simpleGit();
213 if (!request.body.extensionName) {326 if (!request.body.extensionName) {
214 return response.status(400).send('Bad Request: extensionName is required in the request body.');327 return response.status(400).send('Bad Request: extensionName is required in the request body.');
215 }328 }
@@ -223,19 +336,20 @@ router.post('/version', async (request, response) => {
223 return response.status(404).send(`Directory does not exist at ${extensionPath}`);336 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
224 }337 }
225338
339 const git = simpleGit({ baseDir: extensionPath });
226 let currentCommitHash;340 let currentCommitHash;
227 try {341 try {
228 currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);342 currentCommitHash = await git.revparse(['HEAD']);
229 } catch (error) {343 } catch (error) {
230 // it is not a git repo, or has no commits yet, or is a bare repo344 // it is not a git repo, or has no commits yet, or is a bare repo
231 // not possible to update it, most likely can't get the branch name either345 // not possible to update it, most likely can't get the branch name either
232 return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' });346 return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' });
233 }347 }
234348
235 const currentBranch = await git.cwd(extensionPath).branch();349 const currentBranch = await git.branch();
236 // get only the working branch350 // get only the working branch
237 const currentBranchName = currentBranch.current;351 const currentBranchName = currentBranch.current;
238 await git.cwd(extensionPath).fetch('origin');352 await git.fetch('origin');
239 console.debug(extensionName, currentBranchName, currentCommitHash);353 console.debug(extensionName, currentBranchName, currentCommitHash);
240 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);354 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
241355
src/endpoints/files.js+1 -1
@@ -66,7 +66,7 @@ router.post('/delete', async (request, response) => {
66 return response.status(404).send('File not found');66 return response.status(404).send('File not found');
67 }67 }
6868
69 fs.rmSync(pathToDelete);69 fs.unlinkSync(pathToDelete);
70 console.info(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);70 console.info(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
71 return response.sendStatus(200);71 return response.sendStatus(200);
72 } catch (error) {72 } catch (error) {
src/endpoints/google.js+1 -1
@@ -45,7 +45,7 @@ router.post('/caption-image', async (request, response) => {
45 if (!result.ok) {45 if (!result.ok) {
46 const error = await result.json();46 const error = await result.json();
47 console.error(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);47 console.error(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
48 return response.status(result.status).send({ error: true });48 return response.status(500).send({ error: true });
49 }49 }
5050
51 /** @type {any} */51 /** @type {any} */
src/endpoints/groups.js+2 -2
@@ -117,7 +117,7 @@ router.post('/delete', async (request, response) => {
117 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);117 const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`);
118118
119 if (fs.existsSync(pathToFile)) {119 if (fs.existsSync(pathToFile)) {
120 fs.rmSync(pathToFile);120 fs.unlinkSync(pathToFile);
121 }121 }
122 }122 }
123 }123 }
@@ -126,7 +126,7 @@ router.post('/delete', async (request, response) => {
126 }126 }
127127
128 if (fs.existsSync(pathToGroup)) {128 if (fs.existsSync(pathToGroup)) {
129 fs.rmSync(pathToGroup);129 fs.unlinkSync(pathToGroup);
130 }130 }
131131
132 return response.send({ ok: true });132 return response.send({ ok: true });
src/endpoints/novelai.js+1 -1
@@ -270,7 +270,7 @@ router.post('/generate', async function (req, res) {
270 // ignore270 // ignore
271 }271 }
272272
273 return res.status(response.status).send({ error: { message } });273 return res.status(500).send({ error: { message } });
274 }274 }
275275
276 /** @type {any} */276 /** @type {any} */
src/endpoints/openai.js+9 -1
@@ -65,6 +65,10 @@ router.post('/caption-image', async (request, response) => {
65 key = readSecret(request.user.directories, SECRET_KEYS.COHERE);65 key = readSecret(request.user.directories, SECRET_KEYS.COHERE);
66 }66 }
6767
68 if (request.body.api === 'xai') {
69 key = readSecret(request.user.directories, SECRET_KEYS.XAI);
70 }
71
68 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {72 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
69 console.warn('No key found for API', request.body.api);73 console.warn('No key found for API', request.body.api);
70 return response.sendStatus(400);74 return response.sendStatus(400);
@@ -134,6 +138,10 @@ router.post('/caption-image', async (request, response) => {
134 apiUrl = 'https://api.cohere.ai/v2/chat';138 apiUrl = 'https://api.cohere.ai/v2/chat';
135 }139 }
136140
141 if (request.body.api === 'xai') {
142 apiUrl = 'https://api.x.ai/v1/chat/completions';
143 }
144
137 if (request.body.api === 'ooba') {145 if (request.body.api === 'ooba') {
138 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;146 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
139 const imgMessage = body.messages.pop();147 const imgMessage = body.messages.pop();
@@ -226,7 +234,7 @@ router.post('/transcribe-audio', async (request, response) => {
226 return response.status(500).send(text);234 return response.status(500).send(text);
227 }235 }
228236
229 fs.rmSync(request.file.path);237 fs.unlinkSync(request.file.path);
230 const data = await result.json();238 const data = await result.json();
231 console.debug('OpenAI transcription response', data);239 console.debug('OpenAI transcription response', data);
232 return response.json(data);240 return response.json(data);
src/endpoints/presets.js+1 -1
@@ -124,7 +124,7 @@ router.post('/delete-openai', function (request, response) {
124 const pathToFile = path.join(request.user.directories.openAI_Settings, `${name}.json`);124 const pathToFile = path.join(request.user.directories.openAI_Settings, `${name}.json`);
125125
126 if (fs.existsSync(pathToFile)) {126 if (fs.existsSync(pathToFile)) {
127 fs.rmSync(pathToFile);127 fs.unlinkSync(pathToFile);
128 return response.send({ ok: true });128 return response.send({ ok: true });
129 }129 }
130130
src/endpoints/secrets.js+1 -0
@@ -52,6 +52,7 @@ export const SECRET_KEYS = {
52 GENERIC: 'api_key_generic',52 GENERIC: 'api_key_generic',
53 DEEPSEEK: 'api_key_deepseek',53 DEEPSEEK: 'api_key_deepseek',
54 SERPER: 'api_key_serper',54 SERPER: 'api_key_serper',
55 XAI: 'api_key_xai',
55};56};
5657
57// These are the keys that are safe to expose, even if allowKeysExposure is false58// 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) => {
165 // Remove existing sprite with the same label165 // Remove existing sprite with the same label
166 for (const file of files) {166 for (const file of files) {
167 if (path.parse(file).name === spriteName) {167 if (path.parse(file).name === spriteName) {
168 fs.rmSync(path.join(spritesPath, file));168 fs.unlinkSync(path.join(spritesPath, file));
169 }169 }
170 }170 }
171171
@@ -206,7 +206,7 @@ router.post('/upload-zip', async (request, response) => {
206 const existingFile = files.find(file => path.parse(file).name === path.parse(filename).name);206 const existingFile = files.find(file => path.parse(file).name === path.parse(filename).name);
207207
208 if (existingFile) {208 if (existingFile) {
209 fs.rmSync(path.join(spritesPath, existingFile));209 fs.unlinkSync(path.join(spritesPath, existingFile));
210 }210 }
211211
212 // Write sprite buffer to disk212 // Write sprite buffer to disk
@@ -215,7 +215,7 @@ router.post('/upload-zip', async (request, response) => {
215 }215 }
216216
217 // Remove uploaded ZIP file217 // Remove uploaded ZIP file
218 fs.rmSync(spritePackPath);218 fs.unlinkSync(spritePackPath);
219 return response.send({ count: sprites.length });219 return response.send({ count: sprites.length });
220 } catch (error) {220 } catch (error) {
221 console.error(error);221 console.error(error);
@@ -251,7 +251,7 @@ router.post('/upload', async (request, response) => {
251 // Remove existing sprite with the same label251 // Remove existing sprite with the same label
252 for (const file of files) {252 for (const file of files) {
253 if (path.parse(file).name === spriteName) {253 if (path.parse(file).name === spriteName) {
254 fs.rmSync(path.join(spritesPath, file));254 fs.unlinkSync(path.join(spritesPath, file));
255 }255 }
256 }256 }
257257
@@ -261,7 +261,7 @@ router.post('/upload', async (request, response) => {
261 // Copy uploaded file to sprites folder261 // Copy uploaded file to sprites folder
262 fs.cpSync(spritePath, pathToFile);262 fs.cpSync(spritePath, pathToFile);
263 // Remove uploaded file263 // Remove uploaded file
264 fs.rmSync(spritePath);264 fs.unlinkSync(spritePath);
265 return response.sendStatus(200);265 return response.sendStatus(200);
266 } catch (error) {266 } catch (error) {
267 console.error(error);267 console.error(error);
src/endpoints/stable-diffusion.js+56 -3
@@ -627,8 +627,8 @@ together.post('/models', async (request, response) => {
627 }627 }
628628
629 const models = data629 const models = data
630 .filter(x => x.display_type === 'image')630 .filter(x => x.type === 'image')
631 .map(x => ({ value: x.name, text: x.display_name }));631 .map(x => ({ value: x.id, text: x.display_name }));
632632
633 return response.send(models);633 return response.send(models);
634 } catch (error) {634 } catch (error) {
@@ -1166,7 +1166,8 @@ falai.post('/models', async (_request, response) => {
1166 const models = data1166 const models = data
1167 .filter(x => !x.title.toLowerCase().includes('inpainting') &&1167 .filter(x => !x.title.toLowerCase().includes('inpainting') &&
1168 !x.title.toLowerCase().includes('control') &&1168 !x.title.toLowerCase().includes('control') &&
1169 !x.title.toLowerCase().includes('upscale'))1169 !x.title.toLowerCase().includes('upscale') &&
1170 !x.title.toLowerCase().includes('lora'))
1170 .sort((a, b) => a.title.localeCompare(b.title))1171 .sort((a, b) => a.title.localeCompare(b.title))
1171 .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }));1172 .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }));
1172 return response.send(models);1173 return response.send(models);
@@ -1245,6 +1246,7 @@ falai.post('/generate', async (request, response) => {
1245 'Authorization': `Key ${key}`,1246 'Authorization': `Key ${key}`,
1246 },1247 },
1247 });1248 });
1249 /** @type {any} */
1248 const resultData = await resultFetch.json();1250 const resultData = await resultFetch.json();
12491251
1250 if (resultData.detail !== null && resultData.detail !== undefined) {1252 if (resultData.detail !== null && resultData.detail !== undefined) {
@@ -1270,6 +1272,56 @@ falai.post('/generate', async (request, response) => {
1270 }1272 }
1271});1273});
12721274
1275const xai = express.Router();
1276
1277xai.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
1273router.use('/comfy', comfy);1325router.use('/comfy', comfy);
1274router.use('/together', together);1326router.use('/together', together);
1275router.use('/drawthings', drawthings);1327router.use('/drawthings', drawthings);
@@ -1279,3 +1331,4 @@ router.use('/huggingface', huggingface);
1279router.use('/nanogpt', nanogpt);1331router.use('/nanogpt', nanogpt);
1280router.use('/bfl', bfl);1332router.use('/bfl', bfl);
1281router.use('/falai', falai);1333router.use('/falai', falai);
1334router.use('/xai', xai);
src/endpoints/themes.js+1 -1
@@ -29,7 +29,7 @@ router.post('/delete', function (request, response) {
29 console.error('Theme file not found:', filename);29 console.error('Theme file not found:', filename);
30 return response.sendStatus(404);30 return response.sendStatus(404);
31 }31 }
32 fs.rmSync(filename);32 fs.unlinkSync(filename);
33 return response.sendStatus(200);33 return response.sendStatus(200);
34 } catch (error) {34 } catch (error) {
35 console.error(error);35 console.error(error);
src/endpoints/thumbnails.js+1 -1
@@ -75,7 +75,7 @@ export function invalidateThumbnail(directories, type, file) {
75 const pathToThumbnail = path.join(folder, file);75 const pathToThumbnail = path.join(folder, file);
7676
77 if (fs.existsSync(pathToThumbnail)) {77 if (fs.existsSync(pathToThumbnail)) {
78 fs.rmSync(pathToThumbnail);78 fs.unlinkSync(pathToThumbnail);
79 }79 }
80}80}
8181
src/endpoints/tokenizers.js+5 -1
@@ -407,11 +407,15 @@ export function getTokenizerModel(requestModel) {
407 return 'o1';407 return 'o1';
408 }408 }
409409
410 if (requestModel.includes('o3') || requestModel.includes('o4-mini')) {
411 return 'o1';
412 }
413
410 if (requestModel.includes('gpt-4o') || requestModel.includes('chatgpt-4o-latest')) {414 if (requestModel.includes('gpt-4o') || requestModel.includes('chatgpt-4o-latest')) {
411 return 'gpt-4o';415 return 'gpt-4o';
412 }416 }
413417
414 if (requestModel.includes('gpt-4.5')) {418 if (requestModel.includes('gpt-4.1') || requestModel.includes('gpt-4.5')) {
415 return 'gpt-4o';419 return 'gpt-4o';
416 }420 }
417421
src/endpoints/worldinfo.js+1 -1
@@ -57,7 +57,7 @@ router.post('/delete', (request, response) => {
57 throw new Error(`World info file ${filename} doesn't exist.`);57 throw new Error(`World info file ${filename} doesn't exist.`);
58 }58 }
5959
60 fs.rmSync(pathToWorldInfo);60 fs.unlinkSync(pathToWorldInfo);
6161
62 return response.sendStatus(200);62 return response.sendStatus(200);
63});63});
src/fetch-patch.js+4 -4
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2import path from 'node:path';2import path from 'node:path';
3import { fileURLToPath } from 'node:url';3import { fileURLToPath } from 'node:url';
4import mime from 'mime-types';4import mime from 'mime-types';
5import { serverDirectory } from './server-directory.js';
56
6const originalFetch = globalThis.fetch;7const originalFetch = globalThis.fetch;
78
@@ -67,10 +68,9 @@ globalThis.fetch = async (/** @type {string | URL | Request} */ request, /** @ty
67 }68 }
68 const url = getRequestURL(request);69 const url = getRequestURL(request);
69 const filePath = path.resolve(fileURLToPath(url));70 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.');
74 }74 }
75 const parsedPath = path.parse(filePath);75 const parsedPath = path.parse(filePath);
76 if (!ALLOWED_EXTENSIONS.includes(parsedPath.ext)) {76 if (!ALLOWED_EXTENSIONS.includes(parsedPath.ext)) {
src/prompt-converters.js+106 -53
@@ -3,6 +3,15 @@ import { getConfigValue, tryParse } from './util.js';
33
4const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');4const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
55
6const REASONING_EFFORT = {
7 auto: 'auto',
8 low: 'low',
9 medium: 'medium',
10 high: 'high',
11 min: 'min',
12 max: 'max',
13};
14
6/**15/**
7 * @typedef {object} PromptNames16 * @typedef {object} PromptNames
8 * @property {string} charName Character name17 * @property {string} charName Character name
@@ -342,59 +351,20 @@ export function convertCohereMessages(messages, names) {
342 }351 }
343 });352 });
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
350 return { chatHistory: messages };354 return { chatHistory: messages };
351}355}
352356
353/**357/**
354 * Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.358 * Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.
355 * @param {object[]} messages Array of messages359 * @param {object[]} messages Array of messages
356 * @param {string} model Model name360 * @param {string} _model Model name
357 * @param {boolean} useSysPrompt Use system prompt361 * @param {boolean} useSysPrompt Use system prompt
358 * @param {PromptNames} names Prompt names362 * @param {PromptNames} names Prompt names
359 * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models363 * @returns {{contents: *[], system_instruction: {parts: {text: string}[]}}} Prompt for Google MakerSuite models
360 */364 */
361export function convertGooglePrompt(messages, model, useSysPrompt, names) {365export function convertGooglePrompt(messages, _model, useSysPrompt, names) {
362 const visionSupportedModels = [366 const sysPrompt = [];
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 = '';
398 if (useSysPrompt) {368 if (useSysPrompt) {
399 while (messages.length > 1 && messages[0].role === 'system') {369 while (messages.length > 1 && messages[0].role === 'system') {
400 // Append example names if not already done by the frontend (e.g. for group chats).370 // 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) {
408 messages[0].content = `${names.charName}: ${messages[0].content}`;378 messages[0].content = `${names.charName}: ${messages[0].content}`;
409 }379 }
410 }380 }
411 sys_prompt += `${messages[0].content}\n\n`;381 sysPrompt.push(messages[0].content);
412 messages.shift();382 messages.shift();
413 }383 }
414 }384 }
415385
416 const system_instruction = { parts: [{ text: sys_prompt.trim() }]};386 const system_instruction = { parts: sysPrompt.map(text => ({ text })) };
417 const toolNameMap = {};387 const toolNameMap = {};
418388
419 const contents = [];389 const contents = [];
@@ -492,7 +462,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
492462
493 toolNameMap[toolCall.id] = toolCall.function.name;463 toolNameMap[toolCall.id] = toolCall.function.name;
494 });464 });
495 } else if (part.type === 'image_url' && isMultimodal) {465 } else if (part.type === 'image_url') {
496 const mimeType = part.image_url.url.split(';')[0].split(':')[1];466 const mimeType = part.image_url.url.split(';')[0].split(':')[1];
497 const base64Data = part.image_url.url.split(',')[1];467 const base64Data = part.image_url.url.split(',')[1];
498 parts.push({468 parts.push({
@@ -508,7 +478,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
508 if (index > 0 && message.role === contents[contents.length - 1].role) {478 if (index > 0 && message.role === contents[contents.length - 1].role) {
509 parts.forEach((part) => {479 parts.forEach((part) => {
510 if (part.text) {480 if (part.text) {
511 contents[contents.length - 1].parts[0].text += '\n\n' + part.text;481 const textPart = contents[contents.length - 1].parts.find(p => typeof p.text === 'string');
482 if (textPart) {
483 textPart.text += '\n\n' + part.text;
484 } else {
485 contents[contents.length - 1].parts.push(part);
486 }
512 }487 }
513 if (part.inlineData || part.functionCall || part.functionResponse) {488 if (part.inlineData || part.functionCall || part.functionResponse) {
514 contents[contents.length - 1].parts.push(part);489 contents[contents.length - 1].parts.push(part);
@@ -680,6 +655,43 @@ export function convertMistralMessages(messages, names) {
680}655}
681656
682/**657/**
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 */
663export 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/**
683 * Merge messages with the same consecutive role, removing names if they exist.695 * Merge messages with the same consecutive role, removing names if they exist.
684 * @param {any[]} messages Messages to merge696 * @param {any[]} messages Messages to merge
685 * @param {PromptNames} names Prompt names697 * @param {PromptNames} names Prompt names
@@ -901,25 +913,32 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth) {
901}913}
902914
903/**915/**
904 * Calculate the budget tokens for a given reasoning effort.916 * Calculate the Claude budget tokens for a given reasoning effort.
905 * @param {number} maxTokens Maximum tokens917 * @param {number} maxTokens Maximum tokens
906 * @param {string} reasoningEffort Reasoning effort918 * @param {string} reasoningEffort Reasoning effort
907 * @param {boolean} stream If streaming is enabled919 * @param {boolean} stream If streaming is enabled
908 * @returns {number} Budget tokens920 * @returns {number} Budget tokens
909 */921 */
910export function calculateBudgetTokens(maxTokens, reasoningEffort, stream) {922export function calculateClaudeBudgetTokens(maxTokens, reasoningEffort, stream) {
911 let budgetTokens = 0;923 let budgetTokens = 0;
912924
913 switch (reasoningEffort) {925 switch (reasoningEffort) {
914 case 'low':926 case REASONING_EFFORT.min:
927 budgetTokens = 1024;
928 break;
929 case REASONING_EFFORT.low:
915 budgetTokens = Math.floor(maxTokens * 0.1);930 budgetTokens = Math.floor(maxTokens * 0.1);
916 break;931 break;
917 case 'medium':932 case REASONING_EFFORT.auto:
933 case REASONING_EFFORT.medium:
918 budgetTokens = Math.floor(maxTokens * 0.25);934 budgetTokens = Math.floor(maxTokens * 0.25);
919 break;935 break;
920 case 'high':936 case REASONING_EFFORT.high:
921 budgetTokens = Math.floor(maxTokens * 0.5);937 budgetTokens = Math.floor(maxTokens * 0.5);
922 break;938 break;
939 case REASONING_EFFORT.max:
940 budgetTokens = Math.floor(maxTokens * 0.95);
941 break;
923 }942 }
924943
925 budgetTokens = Math.max(budgetTokens, 1024);944 budgetTokens = Math.max(budgetTokens, 1024);
@@ -930,3 +949,37 @@ export function calculateBudgetTokens(maxTokens, reasoningEffort, stream) {
930949
931 return budgetTokens;950 return budgetTokens;
932}951}
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 */
959export 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 @@
1import path from 'node:path';
2import { fileURLToPath } from 'node:url';
3export 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
2import path from 'node:path';
3import util from 'node:util';
4import net from 'node:net';
5import dns from 'node:dns';
6import process from 'node:process';
7
8import cors from 'cors';
9import { csrfSync } from 'csrf-sync';
10import express from 'express';
11import compression from 'compression';
12import cookieSession from 'cookie-session';
13import multer from 'multer';
14import responseTime from 'response-time';
15import helmet from 'helmet';
16import bodyParser from 'body-parser';
17import open from 'open';
18
19// local library imports
20import './fetch-patch.js';
21import { serverDirectory } from './server-directory.js';
22
23console.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.
28if (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
33import { serverEvents, EVENT_NAMES } from './server-events.js';
34import { loadPlugins } from './plugin-loader.js';
35import {
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
52import getWebpackServeMiddleware from './middleware/webpack-serve.js';
53import basicAuthMiddleware from './middleware/basicAuth.js';
54import getWhitelistMiddleware from './middleware/whitelist.js';
55import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './middleware/accessLogWriter.js';
56import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
57import initRequestProxy from './request-proxy.js';
58import getCacheBusterMiddleware from './middleware/cacheBuster.js';
59import corsProxyMiddleware from './middleware/corsProxy.js';
60import {
61 getVersion,
62 color,
63 removeColorFormatting,
64 getSeparator,
65 safeReadFileSync,
66 setupLogLevel,
67 setWindowTitle,
68} from './util.js';
69import { UPLOADS_DIRECTORY } from './constants.js';
70import { ensureThumbnailCache } from './endpoints/thumbnails.js';
71
72// Routers
73import { router as usersPublicRouter } from './endpoints/users-public.js';
74import { init as statsInit, onExit as statsOnExit } from './endpoints/stats.js';
75import { checkForNewContent } from './endpoints/content-manager.js';
76import { init as settingsInit } from './endpoints/settings.js';
77import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './server-startup.js';
78import { diskCache } from './endpoints/characters.js';
79
80// Unrestrict console logs display limit
81util.inspect.defaultOptions.maxArrayLength = null;
82util.inspect.defaultOptions.maxStringLength = null;
83util.inspect.defaultOptions.depth = 4;
84
85const cliArgs = globalThis.COMMAND_LINE_ARGS;
86
87if (!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
92try {
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
104const app = express();
105app.use(helmet({
106 contentSecurityPolicy: false,
107}));
108app.use(compression());
109app.use(responseTime());
110
111app.use(bodyParser.json({ limit: '200mb' }));
112app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
113
114// CORS Settings //
115const CORS = cors({
116 origin: 'null',
117 methods: ['OPTIONS'],
118});
119
120app.use(CORS);
121
122if (cliArgs.listen && cliArgs.basicAuthMode) {
123 app.use(basicAuthMiddleware);
124}
125
126if (cliArgs.whitelistMode) {
127 const whitelistMiddleware = await getWhitelistMiddleware();
128 app.use(whitelistMiddleware);
129}
130
131if (cliArgs.listen) {
132 app.use(accessLoggerMiddleware());
133}
134
135if (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
145app.use(cookieSession({
146 name: getCookieSessionName(),
147 sameSite: 'lax',
148 httpOnly: true,
149 maxAge: getSessionCookieAge(),
150 secret: getCookieSecret(globalThis.DATA_ROOT),
151}));
152
153app.use(setUserDataMiddleware);
154
155// CSRF Protection //
156if (!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
200app.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)
211app.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
222app.get('/login', loginPageMiddleware);
223
224// Host frontend assets
225const webpackMiddleware = getWebpackServeMiddleware();
226app.use(webpackMiddleware);
227app.use(express.static(path.join(serverDirectory, 'public'), {}));
228
229// Public API
230app.use('/api/users', usersPublicRouter);
231
232// Everything below this line requires authentication
233app.use(requireLoginMiddleware);
234app.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
243const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
244app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
245app.use(multerMonkeyPatch);
246
247app.get('/version', async function (_, response) {
248 const data = await getVersion();
249 response.send(data);
250});
251
252redirectDeprecatedEndpoints(app);
253setupPrivateEndpoints(app);
254
255/**
256 * Tasks that need to be run before the server starts listening.
257 * @returns {Promise<void>}
258 */
259async 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 */
321async 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 */
370function 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
378initUserStorage(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
6import { pipeline, env, RawImage } from 'sillytavern-transformers';6import { pipeline, env, RawImage } from 'sillytavern-transformers';
7import { getConfigValue } from './util.js';7import { getConfigValue } from './util.js';
8import { serverDirectory } from './server-directory.js';
89
9configureTransformers();10configureTransformers();
1011
@@ -12,7 +13,7 @@ function configureTransformers() {
12 // Limit the number of threads to 1 to avoid issues on Android13 // Limit the number of threads to 1 to avoid issues on Android
13 env.backends.onnx.wasm.numThreads = 1;14 env.backends.onnx.wasm.numThreads = 1;
14 // Use WASM from a local folder to avoid CDN connections15 // Use WASM from a local folder to avoid CDN connections
15 env.backends.onnx.wasm.wasmPaths = path.join(process.cwd(), 'dist') + path.sep;16 env.backends.onnx.wasm.wasmPaths = path.join(serverDirectory, 'node_modules', 'sillytavern-transformers', 'dist') + path.sep;
16}17}
1718
18const tasks = {19const tasks = {
src/users.js+2 -1
@@ -18,6 +18,7 @@ import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FIL
18import { getConfigValue, color, delay, generateTimestamp } from './util.js';18import { getConfigValue, color, delay, generateTimestamp } from './util.js';
19import { readSecret, writeSecret } from './endpoints/secrets.js';19import { readSecret, writeSecret } from './endpoints/secrets.js';
20import { getContentOfType } from './endpoints/content-manager.js';20import { getContentOfType } from './endpoints/content-manager.js';
21import { serverDirectory } from './server-directory.js';
2122
22export const KEY_PREFIX = 'user:';23export const KEY_PREFIX = 'user:';
23const AVATAR_PREFIX = 'avatar:';24const AVATAR_PREFIX = 'avatar:';
@@ -905,7 +906,7 @@ export async function loginPageMiddleware(request, response) {
905 console.error('Error during auto-login:', error);906 console.error('Error during auto-login:', error);
906 }907 }
907908
908 return response.sendFile('login.html', { root: path.join(process.cwd(), 'public') });909 return response.sendFile('login.html', { root: path.join(serverDirectory, 'public') });
909}910}
910911
911/**912/**
src/util.js+66 -14
@@ -15,13 +15,15 @@ import yauzl from 'yauzl';
15import mime from 'mime-types';15import mime from 'mime-types';
16import { default as simpleGit } from 'simple-git';16import { default as simpleGit } from 'simple-git';
17import chalk from 'chalk';17import chalk from 'chalk';
18import { LOG_LEVELS } from './constants.js';
19import bytes from 'bytes';18import bytes from 'bytes';
19import { LOG_LEVELS } from './constants.js';
20import { serverDirectory } from './server-directory.js';
2021
21/**22/**
22 * Parsed config object.23 * Parsed config object.
23 */24 */
24let CACHED_CONFIG = null;25let CACHED_CONFIG = null;
26let CONFIG_PATH = null;
2527
26/**28/**
27 * Converts a configuration key to an environment variable key.29 * Converts a configuration key to an environment variable key.
@@ -32,22 +34,37 @@ let CACHED_CONFIG = null;
32export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_');34export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_');
3335
34/**36/**
37 * Set the config file path.
38 * @param {string} configFilePath Path to the config file
39 */
40export 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/**
35 * Returns the config object from the config.yaml file.48 * Returns the config object from the config.yaml file.
36 * @returns {object} Config object49 * @returns {object} Config object
37 */50 */
38export function getConfig() {51export 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 }
39 if (CACHED_CONFIG) {57 if (CACHED_CONFIG) {
40 return CACHED_CONFIG;58 return CACHED_CONFIG;
41 }59 }
4260 if (!fs.existsSync(CONFIG_PATH)) {
43 if (!fs.existsSync('./config.yaml')) {
44 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.'));61 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.'));
45 console.error(color.red('The program will now exit.'));62 console.error(color.red('The program will now exit.'));
46 process.exit(1);63 process.exit(1);
47 }64 }
4865
49 try {66 try {
50 const config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));67 const config = yaml.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
51 CACHED_CONFIG = config;68 CACHED_CONFIG = config;
52 return config;69 return config;
53 } catch (error) {70 } catch (error) {
@@ -121,20 +138,19 @@ export async function getVersion() {
121138
122 try {139 try {
123 const require = createRequire(import.meta.url);140 const require = createRequire(import.meta.url);
124 const pkgJson = require(path.join(process.cwd(), './package.json'));141 const pkgJson = require(path.join(serverDirectory, './package.json'));
125 pkgVersion = pkgJson.version;142 pkgVersion = pkgJson.version;
126 if (commandExistsSync('git')) {143 if (commandExistsSync('git')) {
127 const git = simpleGit();144 const git = simpleGit({ baseDir: serverDirectory });
128 const cwd = process.cwd();145 gitRevision = await git.revparse(['--short', 'HEAD']);
129 gitRevision = await git.cwd(cwd).revparse(['--short', 'HEAD']);146 gitBranch = await git.revparse(['--abbrev-ref', 'HEAD']);
130 gitBranch = await git.cwd(cwd).revparse(['--abbrev-ref', 'HEAD']);147 commitDate = await git.show(['-s', '--format=%ci', gitRevision]);
131 commitDate = await git.cwd(cwd).show(['-s', '--format=%ci', gitRevision]);
132148
133 const trackingBranch = await git.cwd(cwd).revparse(['--abbrev-ref', '@{u}']);149 const trackingBranch = await git.revparse(['--abbrev-ref', '@{u}']);
134150
135 // Might fail, but exception is caught. Just don't run anything relevant after in this block...151 // Might fail, but exception is caught. Just don't run anything relevant after in this block...
136 const localLatest = await git.cwd(cwd).revparse(['HEAD']);152 const localLatest = await git.revparse(['HEAD']);
137 const remoteLatest = await git.cwd(cwd).revparse([trackingBranch]);153 const remoteLatest = await git.revparse([trackingBranch]);
138 isLatest = localLatest === remoteLatest;154 isLatest = localLatest === remoteLatest;
139 }155 }
140 }156 }
@@ -419,7 +435,7 @@ export function removeOldBackups(directory, prefix, limit = null) {
419 break;435 break;
420 }436 }
421437
422 fs.rmSync(oldest);438 fs.unlinkSync(oldest);
423 }439 }
424 }440 }
425}441}
@@ -1071,3 +1087,39 @@ export function mutateJsonString(jsonString, mutation) {
1071 return jsonString;1087 return jsonString;
1072 }1088 }
1073}1089}
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 */
1095export 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 @@
1038 }1038 }
1039 },1039 },
1040 "node_modules/@puppeteer/browsers": {1040 "node_modules/@puppeteer/browsers": {
1041 "version": "2.2.3",1041 "version": "2.10.2",
1042 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz",1042 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.2.tgz",
1043 "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==",1043 "integrity": "sha512-i4Ez+s9oRWQbNjtI/3+jxr7OH508mjAKvza0ekPJem0ZtmsYHP3B5dq62+IaBHKaGCOuqJxXzvFLUhJvQ6jtsQ==",
1044 "license": "Apache-2.0",1044 "license": "Apache-2.0",
1045 "peer": true,1045 "peer": true,
1046 "dependencies": {1046 "dependencies": {
1047 "debug": "4.3.4",1047 "debug": "^4.4.0",
1048 "extract-zip": "2.0.1",1048 "extract-zip": "^2.0.1",
1049 "progress": "2.0.3",1049 "progress": "^2.0.3",
1050 "proxy-agent": "6.4.0",1050 "proxy-agent": "^6.5.0",
1051 "semver": "7.6.0",1051 "semver": "^7.7.1",
1052 "tar-fs": "3.0.5",1052 "tar-fs": "^3.0.8",
1053 "unbzip2-stream": "1.4.3",1053 "yargs": "^17.7.2"
1054 "yargs": "17.7.2"
1055 },1054 },
1056 "bin": {1055 "bin": {
1057 "browsers": "lib/cjs/main-cli.js"1056 "browsers": "lib/cjs/main-cli.js"
@@ -1060,46 +1059,12 @@
1060 "node": ">=18"1059 "node": ">=18"
1061 }1060 }
1062 },1061 },
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 },
1094 "node_modules/@puppeteer/browsers/node_modules/semver": {1062 "node_modules/@puppeteer/browsers/node_modules/semver": {
1095 "version": "7.6.0",1063 "version": "7.7.1",
1096 "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",1064 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
1097 "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",1065 "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
1098 "license": "ISC",1066 "license": "ISC",
1099 "peer": true,1067 "peer": true,
1100 "dependencies": {
1101 "lru-cache": "^6.0.0"
1102 },
1103 "bin": {1068 "bin": {
1104 "semver": "bin/semver.js"1069 "semver": "bin/semver.js"
1105 },1070 },
@@ -1107,13 +1072,6 @@
1107 "node": ">=10"1072 "node": ">=10"
1108 }1073 }
1109 },1074 },
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 },
1117 "node_modules/@sideway/address": {1075 "node_modules/@sideway/address": {
1118 "version": "4.1.5",1076 "version": "4.1.5",
1119 "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",1077 "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -1452,14 +1410,11 @@
1452 }1410 }
1453 },1411 },
1454 "node_modules/agent-base": {1412 "node_modules/agent-base": {
1455 "version": "7.1.1",1413 "version": "7.1.3",
1456 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",1414 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
1457 "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",1415 "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
1458 "license": "MIT",1416 "license": "MIT",
1459 "peer": true,1417 "peer": true,
1460 "dependencies": {
1461 "debug": "^4.3.4"
1462 },
1463 "engines": {1418 "engines": {
1464 "node": ">= 14"1419 "node": ">= 14"
1465 }1420 }
@@ -1581,9 +1536,9 @@
1581 }1536 }
1582 },1537 },
1583 "node_modules/b4a": {1538 "node_modules/b4a": {
1584 "version": "1.6.6",1539 "version": "1.6.7",
1585 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz",1540 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
1586 "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==",1541 "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
1587 "license": "Apache-2.0",1542 "license": "Apache-2.0",
1588 "peer": true1543 "peer": true
1589 },1544 },
@@ -1701,76 +1656,81 @@
1701 "license": "MIT"1656 "license": "MIT"
1702 },1657 },
1703 "node_modules/bare-events": {1658 "node_modules/bare-events": {
1704 "version": "2.4.2",1659 "version": "2.5.4",
1705 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",1660 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
1706 "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",1661 "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
1707 "license": "Apache-2.0",1662 "license": "Apache-2.0",
1708 "optional": true,1663 "optional": true,
1709 "peer": true1664 "peer": true
1710 },1665 },
1711 "node_modules/bare-fs": {1666 "node_modules/bare-fs": {
1712 "version": "2.3.1",1667 "version": "4.1.3",
1713 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz",1668 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.3.tgz",
1714 "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==",1669 "integrity": "sha512-OeEZYIg+2qepaWLyphaOXHAHKo3xkM8y3BeGAvHdMN8GNWvEAU1Yw6rYpGzu/wDDbKxgEjVeVDpgGhDzaeMpjg==",
1715 "license": "Apache-2.0",1670 "license": "Apache-2.0",
1716 "optional": true,1671 "optional": true,
1717 "peer": true,1672 "peer": true,
1718 "dependencies": {1673 "dependencies": {
1719 "bare-events": "^2.0.0",1674 "bare-events": "^2.5.4",
1720 "bare-path": "^2.0.0",1675 "bare-path": "^3.0.0",
1721 "bare-stream": "^2.0.0"1676 "bare-stream": "^2.6.4"
1677 },
1678 "engines": {
1679 "bare": ">=1.16.0"
1680 },
1681 "peerDependencies": {
1682 "bare-buffer": "*"
1683 },
1684 "peerDependenciesMeta": {
1685 "bare-buffer": {
1686 "optional": true
1687 }
1722 }1688 }
1723 },1689 },
1724 "node_modules/bare-os": {1690 "node_modules/bare-os": {
1725 "version": "2.4.0",1691 "version": "3.6.1",
1726 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz",1692 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
1727 "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==",1693 "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
1728 "license": "Apache-2.0",1694 "license": "Apache-2.0",
1729 "optional": true,1695 "optional": true,
1730 "peer": true1696 "peer": true,
1697 "engines": {
1698 "bare": ">=1.14.0"
1699 }
1731 },1700 },
1732 "node_modules/bare-path": {1701 "node_modules/bare-path": {
1733 "version": "2.1.3",1702 "version": "3.0.0",
1734 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",1703 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
1735 "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",1704 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
1736 "license": "Apache-2.0",1705 "license": "Apache-2.0",
1737 "optional": true,1706 "optional": true,
1738 "peer": true,1707 "peer": true,
1739 "dependencies": {1708 "dependencies": {
1740 "bare-os": "^2.1.0"1709 "bare-os": "^3.0.1"
1741 }1710 }
1742 },1711 },
1743 "node_modules/bare-stream": {1712 "node_modules/bare-stream": {
1744 "version": "2.1.3",1713 "version": "2.6.5",
1745 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz",1714 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
1746 "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==",1715 "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
1747 "license": "Apache-2.0",1716 "license": "Apache-2.0",
1748 "optional": true,1717 "optional": true,
1749 "peer": true,1718 "peer": true,
1750 "dependencies": {1719 "dependencies": {
1751 "streamx": "^2.18.0"1720 "streamx": "^2.21.0"
1752 }1721 },
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": {
1758 "funding": [1727 "bare-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"
1766 },1729 },
1767 {1730 "bare-events": {
1768 "type": "consulting",1731 "optional": true
1769 "url": "https://feross.org/support"
1770 }1732 }
1771 ],1733 }
1772 "license": "MIT",
1773 "peer": true
1774 },1734 },
1775 "node_modules/basic-ftp": {1735 "node_modules/basic-ftp": {
1776 "version": "5.0.5",1736 "version": "5.0.5",
@@ -1845,31 +1805,6 @@
1845 "node-int64": "^0.4.0"1805 "node-int64": "^0.4.0"
1846 }1806 }
1847 },1807 },
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 },
1873 "node_modules/buffer-crc32": {1808 "node_modules/buffer-crc32": {
1874 "version": "0.2.13",1809 "version": "0.2.13",
1875 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",1810 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -1950,15 +1885,14 @@
1950 }1885 }
1951 },1886 },
1952 "node_modules/chromium-bidi": {1887 "node_modules/chromium-bidi": {
1953 "version": "0.5.24",1888 "version": "4.1.1",
1954 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.24.tgz",1889 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-4.1.1.tgz",
1955 "integrity": "sha512-5xQNN2SVBdZv4TxeMLaI+PelrnZsHDhn8h2JtyriLr+0qHcZS8BMuo93qN6J1VmtmrgYP+rmcLHcbpnA8QJh+w==",1890 "integrity": "sha512-biR7t4vF3YluE6RlMSk9IWk+b9U+WWyzHp+N2pL9vRTk+UXHYRTVp7jTK58ZNzMLBgoLMHY4QyJMbeuw3eKxqg==",
1956 "license": "Apache-2.0",1891 "license": "Apache-2.0",
1957 "peer": true,1892 "peer": true,
1958 "dependencies": {1893 "dependencies": {
1959 "mitt": "3.0.1",1894 "mitt": "^3.0.1",
1960 "urlpattern-polyfill": "10.0.0",1895 "zod": "^3.24.1"
1961 "zod": "3.23.8"
1962 },1896 },
1963 "peerDependencies": {1897 "peerDependencies": {
1964 "devtools-protocol": "*"1898 "devtools-protocol": "*"
@@ -2169,12 +2103,12 @@
2169 }2103 }
2170 },2104 },
2171 "node_modules/debug": {2105 "node_modules/debug": {
2172 "version": "4.3.5",2106 "version": "4.4.0",
2173 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",2107 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
2174 "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",2108 "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
2175 "license": "MIT",2109 "license": "MIT",
2176 "dependencies": {2110 "dependencies": {
2177 "ms": "2.1.2"2111 "ms": "^2.1.3"
2178 },2112 },
2179 "engines": {2113 "engines": {
2180 "node": ">=6.0"2114 "node": ">=6.0"
@@ -2248,9 +2182,9 @@
2248 }2182 }
2249 },2183 },
2250 "node_modules/devtools-protocol": {2184 "node_modules/devtools-protocol": {
2251 "version": "0.0.1299070",2185 "version": "0.0.1425554",
2252 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz",2186 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1425554.tgz",
2253 "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==",2187 "integrity": "sha512-uRfxR6Nlzdzt0ihVIkV+sLztKgs7rgquY/Mhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw==",
2254 "license": "BSD-3-Clause",2188 "license": "BSD-3-Clause",
2255 "peer": true2189 "peer": true
2256 },2190 },
@@ -2987,21 +2921,6 @@
2987 "node": ">=0.10.0"2921 "node": ">=0.10.0"
2988 }2922 }
2989 },2923 },
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 },
3005 "node_modules/fs.realpath": {2924 "node_modules/fs.realpath": {
3006 "version": "1.0.0",2925 "version": "1.0.0",
3007 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",2926 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3071,16 +2990,15 @@
3071 }2990 }
3072 },2991 },
3073 "node_modules/get-uri": {2992 "node_modules/get-uri": {
3074 "version": "6.0.3",2993 "version": "6.0.4",
3075 "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz",2994 "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
3076 "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==",2995 "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
3077 "license": "MIT",2996 "license": "MIT",
3078 "peer": true,2997 "peer": true,
3079 "dependencies": {2998 "dependencies": {
3080 "basic-ftp": "^5.0.2",2999 "basic-ftp": "^5.0.2",
3081 "data-uri-to-buffer": "^6.0.2",3000 "data-uri-to-buffer": "^6.0.2",
3082 "debug": "^4.3.4",3001 "debug": "^4.3.4"
3083 "fs-extra": "^11.2.0"
3084 },3002 },
3085 "engines": {3003 "engines": {
3086 "node": ">= 14"3004 "node": ">= 14"
@@ -3254,13 +3172,13 @@
3254 }3172 }
3255 },3173 },
3256 "node_modules/https-proxy-agent": {3174 "node_modules/https-proxy-agent": {
3257 "version": "7.0.5",3175 "version": "7.0.6",
3258 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",3176 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
3259 "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",3177 "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
3260 "license": "MIT",3178 "license": "MIT",
3261 "peer": true,3179 "peer": true,
3262 "dependencies": {3180 "dependencies": {
3263 "agent-base": "^7.0.2",3181 "agent-base": "^7.1.2",
3264 "debug": "4"3182 "debug": "4"
3265 },3183 },
3266 "engines": {3184 "engines": {
@@ -3276,27 +3194,6 @@
3276 "node": ">=10.17.0"3194 "node": ">=10.17.0"
3277 }3195 }
3278 },3196 },
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 },
3300 "node_modules/ignore": {3197 "node_modules/ignore": {
3301 "version": "5.3.1",3198 "version": "5.3.1",
3302 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",3199 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -4287,19 +4184,6 @@
4287 "node": ">=6"4184 "node": ">=6"
4288 }4185 }
4289 },4186 },
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 },
4303 "node_modules/keyv": {4187 "node_modules/keyv": {
4304 "version": "4.5.4",4188 "version": "4.5.4",
4305 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",4189 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4501,9 +4385,9 @@
4501 "peer": true4385 "peer": true
4502 },4386 },
4503 "node_modules/ms": {4387 "node_modules/ms": {
4504 "version": "2.1.2",4388 "version": "2.1.3",
4505 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",4389 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
4506 "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",4390 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
4507 "license": "MIT"4391 "license": "MIT"
4508 },4392 },
4509 "node_modules/natural-compare": {4393 "node_modules/natural-compare": {
@@ -4657,20 +4541,20 @@
4657 }4541 }
4658 },4542 },
4659 "node_modules/pac-proxy-agent": {4543 "node_modules/pac-proxy-agent": {
4660 "version": "7.0.2",4544 "version": "7.2.0",
4661 "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",4545 "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
4662 "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",4546 "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
4663 "license": "MIT",4547 "license": "MIT",
4664 "peer": true,4548 "peer": true,
4665 "dependencies": {4549 "dependencies": {
4666 "@tootallnate/quickjs-emscripten": "^0.23.0",4550 "@tootallnate/quickjs-emscripten": "^0.23.0",
4667 "agent-base": "^7.0.2",4551 "agent-base": "^7.1.2",
4668 "debug": "^4.3.4",4552 "debug": "^4.3.4",
4669 "get-uri": "^6.0.1",4553 "get-uri": "^6.0.1",
4670 "http-proxy-agent": "^7.0.0",4554 "http-proxy-agent": "^7.0.0",
4671 "https-proxy-agent": "^7.0.5",4555 "https-proxy-agent": "^7.0.6",
4672 "pac-resolver": "^7.0.1",4556 "pac-resolver": "^7.0.1",
4673 "socks-proxy-agent": "^8.0.4"4557 "socks-proxy-agent": "^8.0.5"
4674 },4558 },
4675 "engines": {4559 "engines": {
4676 "node": ">= 14"4560 "node": ">= 14"
@@ -4876,20 +4760,20 @@
4876 }4760 }
4877 },4761 },
4878 "node_modules/proxy-agent": {4762 "node_modules/proxy-agent": {
4879 "version": "6.4.0",4763 "version": "6.5.0",
4880 "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",4764 "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
4881 "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",4765 "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
4882 "license": "MIT",4766 "license": "MIT",
4883 "peer": true,4767 "peer": true,
4884 "dependencies": {4768 "dependencies": {
4885 "agent-base": "^7.0.2",4769 "agent-base": "^7.1.2",
4886 "debug": "^4.3.4",4770 "debug": "^4.3.4",
4887 "http-proxy-agent": "^7.0.1",4771 "http-proxy-agent": "^7.0.1",
4888 "https-proxy-agent": "^7.0.3",4772 "https-proxy-agent": "^7.0.6",
4889 "lru-cache": "^7.14.1",4773 "lru-cache": "^7.14.1",
4890 "pac-proxy-agent": "^7.0.1",4774 "pac-proxy-agent": "^7.1.0",
4891 "proxy-from-env": "^1.1.0",4775 "proxy-from-env": "^1.1.0",
4892 "socks-proxy-agent": "^8.0.2"4776 "socks-proxy-agent": "^8.0.5"
4893 },4777 },
4894 "engines": {4778 "engines": {
4895 "node": ">= 14"4779 "node": ">= 14"
@@ -4912,9 +4796,9 @@
4912 "license": "MIT"4796 "license": "MIT"
4913 },4797 },
4914 "node_modules/pump": {4798 "node_modules/pump": {
4915 "version": "3.0.0",4799 "version": "3.0.2",
4916 "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",4800 "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
4917 "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",4801 "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
4918 "license": "MIT",4802 "license": "MIT",
4919 "peer": true,4803 "peer": true,
4920 "dependencies": {4804 "dependencies": {
@@ -4932,37 +4816,40 @@
4932 }4816 }
4933 },4817 },
4934 "node_modules/puppeteer": {4818 "node_modules/puppeteer": {
4935 "version": "22.12.1",4819 "version": "24.7.2",
4936 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.12.1.tgz",4820 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.7.2.tgz",
4937 "integrity": "sha512-1GxY8dnEnHr1SLzdSDr0FCjM6JQfAh2E2I/EqzeF8a58DbGVk9oVjj4lFdqNoVbpgFSpAbz7VER9St7S1wDpNg==",4821 "integrity": "sha512-ifYqoY6wGs0yZeFuFPn8BE9FhuveXkarF+eO18I2e/axdoCh4Qh1AE+qXdJBhdaeoPt6eRNTY4Dih29Jbq8wow==",
4938 "hasInstallScript": true,4822 "hasInstallScript": true,
4939 "license": "Apache-2.0",4823 "license": "Apache-2.0",
4940 "peer": true,4824 "peer": true,
4941 "dependencies": {4825 "dependencies": {
4942 "@puppeteer/browsers": "2.2.3",4826 "@puppeteer/browsers": "2.10.2",
4827 "chromium-bidi": "4.1.1",
4943 "cosmiconfig": "^9.0.0",4828 "cosmiconfig": "^9.0.0",
4944 "devtools-protocol": "0.0.1299070",4829 "devtools-protocol": "0.0.1425554",
4945 "puppeteer-core": "22.12.1"4830 "puppeteer-core": "24.7.2",
4831 "typed-query-selector": "^2.12.0"
4946 },4832 },
4947 "bin": {4833 "bin": {
4948 "puppeteer": "lib/esm/puppeteer/node/cli.js"4834 "puppeteer": "lib/cjs/puppeteer/node/cli.js"
4949 },4835 },
4950 "engines": {4836 "engines": {
4951 "node": ">=18"4837 "node": ">=18"
4952 }4838 }
4953 },4839 },
4954 "node_modules/puppeteer-core": {4840 "node_modules/puppeteer-core": {
4955 "version": "22.12.1",4841 "version": "24.7.2",
4956 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.12.1.tgz",4842 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.7.2.tgz",
4957 "integrity": "sha512-XmqeDPVdC5/3nGJys1jbgeoZ02wP0WV1GBlPtr/ULRbGXJFuqgXMcKQ3eeNtFpBzGRbpeoCGWHge1ZWKWl0Exw==",4843 "integrity": "sha512-P9pZyTmJqKODFCnkZgemCpoFA4LbAa8+NumHVQKyP5X9IgdNS1ZnAnIh1sMAwhF8/xEUGf7jt+qmNLlKieFw1Q==",
4958 "license": "Apache-2.0",4844 "license": "Apache-2.0",
4959 "peer": true,4845 "peer": true,
4960 "dependencies": {4846 "dependencies": {
4961 "@puppeteer/browsers": "2.2.3",4847 "@puppeteer/browsers": "2.10.2",
4962 "chromium-bidi": "0.5.24",4848 "chromium-bidi": "4.1.1",
4963 "debug": "^4.3.5",4849 "debug": "^4.4.0",
4964 "devtools-protocol": "0.0.1299070",4850 "devtools-protocol": "0.0.1425554",
4965 "ws": "^8.17.1"4851 "typed-query-selector": "^2.12.0",
4852 "ws": "^8.18.1"
4966 },4853 },
4967 "engines": {4854 "engines": {
4968 "node": ">=18"4855 "node": ">=18"
@@ -5051,13 +4938,6 @@
5051 ],4938 ],
5052 "license": "MIT"4939 "license": "MIT"
5053 },4940 },
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 },
5061 "node_modules/react-is": {4941 "node_modules/react-is": {
5062 "version": "18.3.1",4942 "version": "18.3.1",
5063 "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",4943 "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -5254,9 +5134,9 @@
5254 }5134 }
5255 },5135 },
5256 "node_modules/socks": {5136 "node_modules/socks": {
5257 "version": "2.8.3",5137 "version": "2.8.4",
5258 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",5138 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
5259 "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",5139 "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
5260 "license": "MIT",5140 "license": "MIT",
5261 "peer": true,5141 "peer": true,
5262 "dependencies": {5142 "dependencies": {
@@ -5269,13 +5149,13 @@
5269 }5149 }
5270 },5150 },
5271 "node_modules/socks-proxy-agent": {5151 "node_modules/socks-proxy-agent": {
5272 "version": "8.0.4",5152 "version": "8.0.5",
5273 "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",5153 "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
5274 "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",5154 "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
5275 "license": "MIT",5155 "license": "MIT",
5276 "peer": true,5156 "peer": true,
5277 "dependencies": {5157 "dependencies": {
5278 "agent-base": "^7.1.1",5158 "agent-base": "^7.1.2",
5279 "debug": "^4.3.4",5159 "debug": "^4.3.4",
5280 "socks": "^2.8.3"5160 "socks": "^2.8.3"
5281 },5161 },
@@ -5345,14 +5225,13 @@
5345 }5225 }
5346 },5226 },
5347 "node_modules/streamx": {5227 "node_modules/streamx": {
5348 "version": "2.18.0",5228 "version": "2.22.0",
5349 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz",5229 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
5350 "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==",5230 "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
5351 "license": "MIT",5231 "license": "MIT",
5352 "peer": true,5232 "peer": true,
5353 "dependencies": {5233 "dependencies": {
5354 "fast-fifo": "^1.3.2",5234 "fast-fifo": "^1.3.2",
5355 "queue-tick": "^1.0.1",
5356 "text-decoder": "^1.1.0"5235 "text-decoder": "^1.1.0"
5357 },5236 },
5358 "optionalDependencies": {5237 "optionalDependencies": {
@@ -5453,9 +5332,9 @@
5453 }5332 }
5454 },5333 },
5455 "node_modules/tar-fs": {5334 "node_modules/tar-fs": {
5456 "version": "3.0.5",5335 "version": "3.0.8",
5457 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz",5336 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
5458 "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==",5337 "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
5459 "license": "MIT",5338 "license": "MIT",
5460 "peer": true,5339 "peer": true,
5461 "dependencies": {5340 "dependencies": {
@@ -5463,8 +5342,8 @@
5463 "tar-stream": "^3.1.5"5342 "tar-stream": "^3.1.5"
5464 },5343 },
5465 "optionalDependencies": {5344 "optionalDependencies": {
5466 "bare-fs": "^2.1.1",5345 "bare-fs": "^4.0.1",
5467 "bare-path": "^2.1.0"5346 "bare-path": "^3.0.0"
5468 }5347 }
5469 },5348 },
5470 "node_modules/tar-stream": {5349 "node_modules/tar-stream": {
@@ -5494,9 +5373,9 @@
5494 }5373 }
5495 },5374 },
5496 "node_modules/text-decoder": {5375 "node_modules/text-decoder": {
5497 "version": "1.1.1",5376 "version": "1.2.3",
5498 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz",5377 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
5499 "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==",5378 "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
5500 "license": "Apache-2.0",5379 "license": "Apache-2.0",
5501 "peer": true,5380 "peer": true,
5502 "dependencies": {5381 "dependencies": {
@@ -5509,13 +5388,6 @@
5509 "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",5388 "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
5510 "license": "MIT"5389 "license": "MIT"
5511 },5390 },
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 },
5519 "node_modules/tmpl": {5391 "node_modules/tmpl": {
5520 "version": "1.0.5",5392 "version": "1.0.5",
5521 "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",5393 "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -5594,6 +5466,13 @@
5594 "url": "https://github.com/sponsors/sindresorhus"5466 "url": "https://github.com/sponsors/sindresorhus"
5595 }5467 }
5596 },5468 },
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 },
5597 "node_modules/typescript": {5476 "node_modules/typescript": {
5598 "version": "5.5.3",5477 "version": "5.5.3",
5599 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",5478 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
@@ -5608,33 +5487,12 @@
5608 "node": ">=14.17"5487 "node": ">=14.17"
5609 }5488 }
5610 },5489 },
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 },
5622 "node_modules/undici-types": {5490 "node_modules/undici-types": {
5623 "version": "5.26.5",5491 "version": "5.26.5",
5624 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",5492 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
5625 "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",5493 "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
5626 "license": "MIT"5494 "license": "MIT"
5627 },5495 },
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 },
5638 "node_modules/update-browserslist-db": {5496 "node_modules/update-browserslist-db": {
5639 "version": "1.1.0",5497 "version": "1.1.0",
5640 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",5498 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -5674,13 +5532,6 @@
5674 "punycode": "^2.1.0"5532 "punycode": "^2.1.0"
5675 }5533 }
5676 },5534 },
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 },
5684 "node_modules/v8-to-istanbul": {5535 "node_modules/v8-to-istanbul": {
5685 "version": "9.3.0",5536 "version": "9.3.0",
5686 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",5537 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -5784,9 +5635,9 @@
5784 }5635 }
5785 },5636 },
5786 "node_modules/ws": {5637 "node_modules/ws": {
5787 "version": "8.18.0",5638 "version": "8.18.1",
5788 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",5639 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
5789 "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",5640 "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
5790 "license": "MIT",5641 "license": "MIT",
5791 "peer": true,5642 "peer": true,
5792 "engines": {5643 "engines": {
@@ -5871,9 +5722,9 @@
5871 }5722 }
5872 },5723 },
5873 "node_modules/zod": {5724 "node_modules/zod": {
5874 "version": "3.23.8",5725 "version": "3.24.3",
5875 "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",5726 "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
5876 "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",5727 "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
5877 "license": "MIT",5728 "license": "MIT",
5878 "peer": true,5729 "peer": true,
5879 "funding": {5730 "funding": {
webpack.config.js+2 -1
@@ -1,6 +1,7 @@
1import process from 'node:process';1import process from 'node:process';
2import path from 'node:path';2import path from 'node:path';
3import isDocker from 'is-docker';3import isDocker from 'is-docker';
4import { serverDirectory } from './src/server-directory.js';
45
5/**6/**
6 * Get the Webpack configuration for the public/lib.js file.7 * Get the Webpack configuration for the public/lib.js file.
@@ -40,7 +41,7 @@ export default function getPublicLibConfig(forceDist = false) {
4041
41 return {42 return {
42 mode: 'production',43 mode: 'production',
43 entry: './public/lib.js',44 entry: path.join(serverDirectory, 'public/lib.js'),
44 cache: {45 cache: {
45 type: 'filesystem',46 type: 'filesystem',
46 cacheDirectory: cacheDirectory,47 cacheDirectory: cacheDirectory,