Merge pull request #4444 from SillyTavern/staging Staging

1a1f57e00ead3b7e4a30269caf4b1c756310206b

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

Signed
180 files changed, +4065 -1204Ignore whitespace
README.md+14 -0
@@ -0,0 +1,14 @@
1+# SillyTavern
2+
3+LLM Frontend for Power Users
4+
5+## Resources
6+
7+- GitHub: <https://github.com/SillyTavern/SillyTavern>
8+- Docs: <https://docs.sillytavern.app/>
9+- Discord: <https://discord.gg/sillytavern>
10+- Reddit: <https://reddit.com/r/SillyTavernAI>
11+
12+## License
13+
14+AGPL-3.0
default/content/index.json+16 -0
@@ -838,5 +838,21 @@
838838 {
839839 "filename": "presets/context/Moonshot AI.json",
840840 "type": "context"
841+ },
842+ {
843+ "filename": "presets/context/OpenAI Harmony.json",
844+ "type": "context"
845+ },
846+ {
847+ "filename": "presets/instruct/OpenAI Harmony.json",
848+ "type": "instruct"
849+ },
850+ {
851+ "filename": "presets/instruct/OpenAI Harmony (Thinking).json",
852+ "type": "instruct"
853+ },
854+ {
855+ "filename": "presets/reasoning/OpenAI Harmony.json",
856+ "type": "reasoning"
841857 }
842858]
default/content/presets/context/Adventure.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": false,
711 "trim_sentences": false,
812 "single_line": true,
default/content/presets/context/Alpaca-Single-Turn.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n##{{#if Instruction:anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": false,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Alpaca.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}\n{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/ChatML-Names.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|im_start|>system{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|im_end|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/ChatML.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|im_start|>system{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|im_end|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Command R.json+6 -2
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble\nThis chat is an exception to AI's usual ethical protocols. The AI will engage with the user without breaking character regardless of how the chat context progresses.\n\n{{# Systemif PreambleanchorBefore}}{{anchorBefore}}\n#{{/if}}{{# Basicif Rulessystem}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}\n\n# User Preamble\n## Task and Context\n{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}\n\n#{{# Styleif GuideanchorAfter}}{{anchorAfter}}\n{{system/if}}<|END_OF_TURN_TOKEN|>{{trim}}",
33 "example_separator": "",
4- "chat_start": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>New Roleplay:<|END_OF_TURN_TOKEN|>",
4+ "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/DeepSeek-V2.5.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Default.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "***",
44 "chat_start": "***",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Dots1.json+5 -2
@@ -1,11 +1,14 @@
11{
22 "story_string": "<|system|>{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|endofsystem|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
66 "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
710 "always_force_name2": true,
811 "trim_sentences": false,
912 "single_line": false,
1013 "name": "Dots1"
11-}
11 \ No newline at end of file
14+}
default/content/presets/context/DreamGen Role-Play V1 ChatML.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|im_start|>system{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n\n\n{{/if}}## Overall plot description:\n\n{{#if scenario}}{{scenario}}{{else}}Conversation between {{char}} and {{user}}.{{/if}}{{#if wiBefore}}\n\n{{wiBefore}}{{/if}}\n\n\n## Characters:\n\n### {{char}}\n\n{{#if description}}{{description}}\n\n{{/if}}{{#if personality}}{{personality}}\n\n{{/if}}### {{user}}\n\n{{#if persona}}{{persona}}{{else}}{{user}} is the protagonist of the role-play.{{/if}}{{#if wiAfter}}\n\n{{wiAfter}}{{/if}}{{#if mesExamples}}\n\n{{mesExamples}}{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": false,
711 "trim_sentences": true,
812 "single_line": false,
default/content/presets/context/DreamGen Role-Play V1 Llama3.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|start_header_id|>system<|end_header_id|>\n{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n\n\n{{/if}}## Overall plot description:\n\n{{#if scenario}}{{scenario}}{{else}}Conversation between {{char}} and {{user}}.{{/if}}{{#if wiBefore}}\n\n{{wiBefore}}{{/if}}\n\n\n## Characters:\n\n### {{char}}\n\n{{#if description}}{{description}}\n\n{{/if}}{{#if personality}}{{personality}}\n\n{{/if}}### {{user}}\n\n{{#if persona}}{{persona}}{{else}}{{user}} is the protagonist of the role-play.{{/if}}{{#if wiAfter}}\n\n{{wiAfter}}{{/if}}{{#if mesExamples}}\n\n{{mesExamples}}{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nWrite an example narrative / conversation that is not part of the main story.",
44 "chat_start": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nStart the role-play between {{char}} and {{user}}.",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": false,
711 "trim_sentences": true,
812 "single_line": false,
default/content/presets/context/GLM-4.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "[gMASK]<sop>{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Gemma 2.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<start_of_turn>user{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<end_of_turn>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Libra-32B.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "##{{#if Instruction:anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n\n{{/if}}### Character Sheet:\n{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "### Example:",
44 "chat_start": "### START ROLEPLAY:",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Lightning 1.1.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the{{#if request.\nanchorBefore}}{{anchorBefore}}\n##{{/if}}{{#if Instruction:\nsystem}}{{system}}\n{{/if}}{{#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}}{{userpersona}}'s persona:\n{{/if}}{{#if anchorAfter}}{{personaanchorAfter}}\n{{/if}}\n\n{{trim}}",
33 "example_separator": "Example of an interaction:\n",
44 "chat_start": "This is the history of the roleplay:\n",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Llama 2 Chat.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "[INST]{{#if <<SYS>>anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n<</SYS>>\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#if anchorAfter}} [{{anchorAfter}}\n{{/INST]if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Llama 3 Instruct.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|start_header_id|>system<|end_header_id|>\n{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|eot_id|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Llama 4 Instruct.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|begin_of_text|><|header_start|>system<|header_end|>\n{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|eot|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Llama-3-Instruct-Names.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|start_header_id|>system<|end_header_id|>\n{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|eot_id|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Metharme.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Minimalist.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Mistral V1.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": " [INST]{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim#if anchorAfter}} [/INST] Understood.<{{anchorAfter}}\n{{/s>if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Mistral V2 & V3.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "[INST]{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim#if anchorAfter}}[/INST] Understood.<{{anchorAfter}}\n{{/s>if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Mistral V3-Tekken.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "[INST]{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim#if anchorAfter}}[/INST]Understood.<{{anchorAfter}}\n{{/s>if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Mistral V7-Tekken.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "[SYSTEM_PROMPT]{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim#if anchorAfter}}[{{anchorAfter}}\n{{/SYSTEM_PROMPT]if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Mistral V7.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "[SYSTEM_PROMPT]{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim#if anchorAfter}}[{{anchorAfter}}\n{{/SYSTEM_PROMPT]if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Moonshot AI.json+5 -2
@@ -1,11 +1,14 @@
11{
22 "story_string": "<|im_system|>system<|im_middle|>{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}<|im_end|>",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
66 "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
710 "always_force_name2": true,
811 "trim_sentences": false,
912 "single_line": false,
1013 "name": "Moonshot AI"
11-}
11 \ No newline at end of file
14+}
default/content/presets/context/NovelAI.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if systemanchorBefore}}{{systemanchorBefore}}\n{{/if}}\n{{#if wiBeforesystem}}{{wiBeforesystem}}\n{{/if}}\n{{#if personawiBefore}}{{personawiBefore}}\n{{/if}}\n{{#if description}}{{description}}\n{{/if}}\n{{#if personality}}Personality: {{personality}}\n{{/if}}\n{{#if scenario}}Scenario: {{scenario}}\n{{/if}}\n{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "***",
44 "chat_start": "***",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/OldDefault.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}Circumstances and context of the dialogue: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}",
33 "example_separator": "This is how {{char}} should talk",
44 "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/OpenAI Harmony.json+14 -0
@@ -0,0 +1,14 @@
1+{
2+ "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
3+ "example_separator": "",
4+ "chat_start": "",
5+ "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
10+ "always_force_name2": true,
11+ "trim_sentences": false,
12+ "single_line": false,
13+ "name": "OpenAI Harmony"
14+}
default/content/presets/context/Phi.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|system|>{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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#if anchorAfter}}<|end|>{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Story.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Synthia.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/Tulu.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "<|system|>{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#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#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
33 "example_separator": "",
44 "chat_start": "",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/context/simple-proxy-for-tavern.json+5 -1
@@ -1,8 +1,12 @@
11{
22 "story_string": "## {{char#if anchorBefore}}\n- You're \"{{charanchorBefore}}\" in this never-ending roleplay with \"n{{user/if}}\".\n### Input:\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}}##{{#if Response:anchorAfter}}{{anchorAfter}}\n(OOC) Understood. I will take this info into account for the roleplay. (end OOC){{/if}}",
33 "example_separator": "### New Roleplay:",
44 "chat_start": "### New Roleplay:",
55 "use_stop_strings": false,
6+ "names_as_stop_strings": true,
7+ "story_string_position": 0,
8+ "story_string_depth": 1,
9+ "story_string_role": 0,
610 "always_force_name2": true,
711 "trim_sentences": false,
812 "single_line": false,
default/content/presets/instruct/Adventure.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "none",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "Adventure"
2225}
default/content/presets/instruct/Alpaca-Single-Turn.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "<START OF CHAT>",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:",
23+ "story_string_suffix": "\n",
2124 "name": "Alpaca-Single-Turn"
2225}
default/content/presets/instruct/Alpaca.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "\n\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "\n\n",
2124 "name": "Alpaca"
2225}
default/content/presets/instruct/ChatML-Names.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "none",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|im_end|>\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|im_start|>system",
23+ "story_string_suffix": "<|im_end|>\n",
2124 "name": "ChatML-Names"
2225}
default/content/presets/instruct/ChatML.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|im_end|>\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|im_start|>system",
23+ "story_string_suffix": "<|im_end|>\n",
2124 "name": "ChatML"
2225}
default/content/presets/instruct/Command R.json+8 -5
@@ -1,22 +1,25 @@
11{
22 "input_sequence": "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>",
33 "output_sequence": "<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>",
4- "first_output_sequence": "",
54 "last_output_sequence": "",
65 "system_sequence_prefixsystem_sequence": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>",
7- "system_sequence_suffix": "",
86 "stop_sequence": "<|END_OF_TURN_TOKEN|>",
97 "wrap": false,
108 "macro": true,
119 "names_behavior": "force",
1210 "activation_regex": "",
11+ "first_output_sequence": "",
1312 "skip_examples": false,
1413 "output_suffix": "<|END_OF_TURN_TOKEN|>",
1514 "input_suffix": "<|END_OF_TURN_TOKEN|>",
16- "system_sequence": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>",
1715 "system_suffix": "<|END_OF_TURN_TOKEN|>",
1816 "user_alignment_message": "",
19- "last_system_sequence": "",
2017 "system_same_as_user": false,
18+ "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>",
23+ "story_string_suffix": "<|END_OF_TURN_TOKEN|>",
2124 "name": "Command R"
2225}
default/content/presets/instruct/DeepSeek-V2.5.json+8 -5
@@ -1,22 +1,25 @@
11{
22 "input_sequence": "<|User|>",
33 "output_sequence": "<|Assistant|>",
4- "first_output_sequence": "",
54 "last_output_sequence": "",
65 "system_sequence_prefixsystem_sequence": "",
7- "system_sequence_suffix": "",
86 "stop_sequence": "",
97 "wrap": false,
108 "macro": true,
119 "names_behavior": "force",
1210 "activation_regex": "",
11+ "first_output_sequence": "",
1312 "skip_examples": false,
1413 "output_suffix": "<|end▁of▁sentence|>",
1514 "input_suffix": "",
16- "system_sequence": "",
1715 "system_suffix": "",
1816 "user_alignment_message": "",
19- "last_system_sequence": "",
2017 "system_same_as_user": true,
18+ "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "DeepSeek-V2.5"
2225}
default/content/presets/instruct/Dots1.json+3 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|endofresponse|>\n",
@@ -20,5 +18,8 @@
2018 "last_system_sequence": "",
2119 "first_input_sequence": "",
2220 "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|system|>",
23+ "story_string_suffix": "<|endofsystem|>\n",
2324 "name": "Dots1"
2425}
default/content/presets/instruct/DreamGen Role-Play V1 ChatML.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "none",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|im_end|>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|im_start|>system\n",
23+ "story_string_suffix": "<|im_end|>",
2124 "name": "DreamGen Role-Play V1 ChatML"
2225}
default/content/presets/instruct/DreamGen Role-Play V1 Llama3.json+13 -4
@@ -1,16 +1,25 @@
11{
22 "input_sequence": "<|eot_id|>\n<|start_header_id|>writer character: {{user}}<|end_header_id|>\n\n",
33 "output_sequence": "<|eot_id|>\n<|start_header_id|>writer character: {{char}}<|end_header_id|>\n\n",
4- "first_output_sequence": "",
54 "last_output_sequence": "",
65 "system_sequence_prefixsystem_sequence": "",
7- "system_sequence_suffix": "",
86 "stop_sequence": "",
9- "separator_sequence": "",
107 "wrap": false,
118 "macro": true,
129 "names_behavior": "none",
1310 "activation_regex": "",
11+ "first_output_sequence": "",
1412 "skip_examples": false,
13+ "output_suffix": "",
14+ "input_suffix": "",
15+ "system_suffix": "",
16+ "user_alignment_message": "",
17+ "system_same_as_user": false,
18+ "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|start_header_id|>system<|end_header_id|>\n",
23+ "story_string_suffix": "",
1524 "name": "DreamGen Role-Play V1 Llama3"
1625}
default/content/presets/instruct/GLM-4.json+8 -5
@@ -1,22 +1,25 @@
11{
22 "input_sequence": "<|user|>\n",
33 "output_sequence": "<|assistant|>\n",
4- "first_output_sequence": "",
54 "last_output_sequence": "",
65 "system_sequence_prefixsystem_sequence": "<|system|>\n",
7- "system_sequence_suffix": "",
86 "stop_sequence": "",
97 "wrap": false,
108 "macro": true,
119 "names_behavior": "force",
1210 "activation_regex": "",
11+ "first_output_sequence": "",
1312 "skip_examples": false,
1413 "output_suffix": "",
1514 "input_suffix": "",
16- "system_sequence": "",
1715 "system_suffix": "",
1816 "user_alignment_message": "",
19- "last_system_sequence": "",
2017 "system_same_as_user": true,
18+ "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "[gMASK]<sop>",
23+ "story_string_suffix": "\n",
2124 "name": "GLM-4"
2225}
default/content/presets/instruct/Gemma 2.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<end_of_turn>\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<start_of_turn>user",
23+ "story_string_suffix": "<end_of_turn>\n",
2124 "name": "Gemma 2"
2225}
default/content/presets/instruct/Koala.json+6 -3
@@ -8,15 +8,18 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "BEGINNING OF CONVERSATION: ",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
1614 "input_suffix": " ",
1715 "system_suffix": "",
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "BEGINNING OF CONVERSATION: ",
23+ "story_string_suffix": "\n",
2124 "name": "Koala"
2225}
default/content/presets/instruct/KoboldAI.json+6 -3
@@ -4,12 +4,10 @@
44 "last_output_sequence": "",
55 "system_sequence": "{{[SYSTEM]}}",
66 "stop_sequence": "",
77 "wrap": truefalse,
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "{{[OUTPUT_END]}}",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "{{[SYSTEM]}}",
23+ "story_string_suffix": "{{[SYSTEM_END]}}",
2124 "name": "KoboldAI"
2225}
default/content/presets/instruct/Libra-32B.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "always",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "### Instruction:",
23+ "story_string_suffix": "",
2124 "name": "Libra-32B"
2225}
default/content/presets/instruct/Lightning 1.1.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "\n\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:",
23+ "story_string_suffix": "\n\n",
2124 "name": "Lightning 1.1"
2225}
default/content/presets/instruct/Llama 2 Chat.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "[INST] <<SYS>>\n",
23+ "story_string_suffix": "\n<</SYS>> Understood. [/INST]",
2124 "name": "Llama 2 Chat"
2225}
default/content/presets/instruct/Llama 3 Instruct.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|eot_id|>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|start_header_id|>system<|end_header_id|>\n\n",
23+ "story_string_suffix": "<|eot_id|>",
2124 "name": "Llama 3 Instruct"
2225}
default/content/presets/instruct/Llama 4 Instruct.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|eot|>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|begin_of_text|><|header_start|>system<|header_end|>\n\n",
23+ "story_string_suffix": "<|eot|>",
2124 "name": "Llama 4 Instruct"
2225}
default/content/presets/instruct/Llama-3-Instruct-Names.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "none",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|eot_id|>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|start_header_id|>system<|end_header_id|>\n\n",
23+ "story_string_suffix": "<|eot_id|>",
2124 "name": "Llama-3-Instruct-Names"
2225}
default/content/presets/instruct/Metharme.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "<|system|>",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|system|>",
23+ "story_string_suffix": "",
2124 "name": "Metharme"
2225}
default/content/presets/instruct/Mistral V1.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": " [INST] ",
23+ "story_string_suffix": " [/INST] Understood.</s>",
2124 "name": "Mistral V1"
2225}
default/content/presets/instruct/Mistral V2 & V3.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "[INST] ",
23+ "story_string_suffix": "[/INST] Understood.</s>",
2124 "name": "Mistral V2 & V3"
2225}
default/content/presets/instruct/Mistral V3-Tekken.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "[INST]",
23+ "story_string_suffix": "[/INST]Understood.</s>",
2124 "name": "Mistral V3-Tekken"
2225}
default/content/presets/instruct/Mistral V7-Tekken.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "[SYSTEM_PROMPT]",
23+ "story_string_suffix": "[/SYSTEM_PROMPT]",
2124 "name": "Mistral V7-Tekken"
2225}
default/content/presets/instruct/Mistral V7.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "[SYSTEM_PROMPT] ",
23+ "story_string_suffix": "[/SYSTEM_PROMPT]",
2124 "name": "Mistral V7"
2225}
default/content/presets/instruct/Moonshot AI.json+4 -3
@@ -6,9 +6,8 @@
66 "stop_sequence": "<|im_end|>",
77 "wrap": false,
88 "macro": true,
9+ "names_behavior": "force",
910 "activation_regex": "",
10- "system_sequence_prefix": "",
11- "system_sequence_suffix": "",
1211 "first_output_sequence": "",
1312 "skip_examples": false,
1413 "output_suffix": "<|im_end|>",
@@ -19,6 +18,8 @@
1918 "last_system_sequence": "",
2019 "first_input_sequence": "",
2120 "last_input_sequence": "",
2221 "names_behaviorsequences_as_stop_strings": "force"true,
22+ "story_string_prefix": "<|im_system|>system<|im_middle|>",
23+ "story_string_suffix": "<|im_end|>",
2324 "name": "Moonshot AI"
2425}
default/content/presets/instruct/OpenAI Harmony (Thinking).json+25 -0
@@ -0,0 +1,25 @@
1+{
2+ "input_sequence": "<|start|>user<|message|>",
3+ "output_sequence": "<|start|>assistant<|channel|>final<|message|>",
4+ "last_output_sequence": "{{noop}}",
5+ "system_sequence": "<|start|>developer<|message|>",
6+ "stop_sequence": "",
7+ "wrap": false,
8+ "macro": true,
9+ "activation_regex": "",
10+ "first_output_sequence": "",
11+ "skip_examples": false,
12+ "output_suffix": "<|end|>",
13+ "input_suffix": "<|end|>",
14+ "system_suffix": "<|end|>",
15+ "user_alignment_message": "",
16+ "system_same_as_user": false,
17+ "last_system_sequence": "",
18+ "first_input_sequence": "",
19+ "last_input_sequence": "",
20+ "names_behavior": "force",
21+ "sequences_as_stop_strings": false,
22+ "story_string_prefix": "<|start|>system<|message|>",
23+ "story_string_suffix": "<|end|>",
24+ "name": "OpenAI Harmony (Thinking)"
25+}
default/content/presets/instruct/OpenAI Harmony.json+25 -0
@@ -0,0 +1,25 @@
1+{
2+ "input_sequence": "<|start|>user<|message|>",
3+ "output_sequence": "<|start|>assistant<|channel|>final<|message|>",
4+ "last_output_sequence": "",
5+ "system_sequence": "<|start|>developer<|message|>",
6+ "stop_sequence": "",
7+ "wrap": false,
8+ "macro": true,
9+ "activation_regex": "",
10+ "first_output_sequence": "",
11+ "skip_examples": false,
12+ "output_suffix": "<|end|>",
13+ "input_suffix": "<|end|>",
14+ "system_suffix": "<|end|>",
15+ "user_alignment_message": "",
16+ "system_same_as_user": false,
17+ "last_system_sequence": "",
18+ "first_input_sequence": "",
19+ "last_input_sequence": "",
20+ "names_behavior": "force",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|start|>system<|message|>",
23+ "story_string_suffix": "<|end|>",
24+ "name": "OpenAI Harmony"
25+}
default/content/presets/instruct/OpenOrca-OpenChat.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "<|end_of_turn|>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "OpenOrca-OpenChat"
2225}
default/content/presets/instruct/Phi.json+8 -5
@@ -1,22 +1,25 @@
11{
22 "input_sequence": "<|user|>\n",
33 "output_sequence": "<|assistant|>\n",
4- "first_output_sequence": "",
54 "last_output_sequence": "",
65 "system_sequence_prefixsystem_sequence": "<|system|>\n",
7- "system_sequence_suffix": "",
86 "stop_sequence": "<|end|>",
97 "wrap": false,
108 "macro": true,
119 "names_behavior": "force",
1210 "activation_regex": "",
11+ "first_output_sequence": "",
1312 "skip_examples": false,
1413 "output_suffix": "<|end|>\n",
1514 "input_suffix": "<|end|>\n",
16- "system_sequence": "<|system|>\n",
1715 "system_suffix": "<|end|>\n",
1816 "user_alignment_message": "",
19- "last_system_sequence": "",
2017 "system_same_as_user": false,
18+ "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|system|>\n",
23+ "story_string_suffix": "<|end|>\n",
2124 "name": "Phi"
2225}
default/content/presets/instruct/Story.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "none",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "Story"
2225}
default/content/presets/instruct/Synthia.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "SYSTEM: ",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "\n",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "SYSTEM: ",
23+ "story_string_suffix": "\n",
2124 "name": "Synthia"
2225}
default/content/presets/instruct/Tulu.json+8 -5
@@ -1,22 +1,25 @@
11{
22 "input_sequence": "<|user|>\n",
33 "output_sequence": "<|assistant|>\n",
4- "first_output_sequence": "",
54 "last_output_sequence": "",
65 "system_sequence_prefixsystem_sequence": "<|system|>\n",
7- "system_sequence_suffix": "",
86 "stop_sequence": "<|end_of_text|>",
97 "wrap": false,
108 "macro": true,
119 "names_behavior": "force",
1210 "activation_regex": "",
11+ "first_output_sequence": "",
1312 "skip_examples": false,
1413 "output_suffix": "<|end_of_text|>\n",
1514 "input_suffix": "\n",
16- "system_sequence": "<|system|>\n",
1715 "system_suffix": "\n",
1816 "user_alignment_message": "",
19- "last_system_sequence": "",
2017 "system_same_as_user": false,
18+ "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "<|system|>\n",
23+ "story_string_suffix": "\n",
2124 "name": "Tulu"
2225}
default/content/presets/instruct/Vicuna 1.0.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "Vicuna 1.0"
2225}
default/content/presets/instruct/Vicuna 1.1.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "BEGINNING OF CONVERSATION:",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "BEGINNING OF CONVERSATION: ",
23+ "story_string_suffix": "",
2124 "name": "Vicuna 1.1"
2225}
default/content/presets/instruct/WizardLM-13B.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": true,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "WizardLM-13B"
2225}
default/content/presets/instruct/WizardLM.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "force",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "</s>",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "",
23+ "story_string_suffix": "",
2124 "name": "WizardLM"
2225}
default/content/presets/instruct/simple-proxy-for-tavern.json+5 -2
@@ -8,8 +8,6 @@
88 "macro": true,
99 "names_behavior": "none",
1010 "activation_regex": "",
11- "system_sequence_prefix": "",
12- "system_sequence_suffix": "",
1311 "first_output_sequence": "",
1412 "skip_examples": false,
1513 "output_suffix": "",
@@ -18,5 +16,10 @@
1816 "user_alignment_message": "",
1917 "system_same_as_user": false,
2018 "last_system_sequence": "",
19+ "first_input_sequence": "",
20+ "last_input_sequence": "",
21+ "sequences_as_stop_strings": true,
22+ "story_string_prefix": "## {{char}}\n- You're \"{{char}}\" in this never-ending roleplay with \"{{user}}\".\n### Input:",
23+ "story_string_suffix": "### Response:\n(OOC) Understood. I will take this info into account for the roleplay. (end OOC)\n",
2124 "name": "simple-proxy-for-tavern"
2225}
default/content/presets/reasoning/OpenAI Harmony.json+6 -0
@@ -0,0 +1,6 @@
1+{
2+ "prefix": "<|start|>assistant<|channel|>analysis<|message|>",
3+ "suffix": "<|start|>assistant<|channel|>final<|message|>",
4+ "separator": "",
5+ "name": "OpenAI Harmony"
6+}
default/content/settings.json+0 -2
@@ -160,8 +160,6 @@
160160 "macro": true,
161161 "names_behavior": "force",
162162 "activation_regex": "",
163- "system_sequence_prefix": "",
164- "system_sequence_suffix": "",
165163 "first_output_sequence": "",
166164 "skip_examples": false,
167165 "output_suffix": "\n\n",
package-lock.json+2 -2
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.13.23",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.13.23",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
package.json+1 -1
@@ -112,7 +112,7 @@
112112 "type": "git",
113113 "url": "https://github.com/SillyTavern/SillyTavern.git"
114114 },
115115 "version": "1.13.23",
116116 "scripts": {
117117 "start": "node server.js",
118118 "debug": "node --inspect server.js",
public/css/rm-groups.css+1 -0
@@ -6,6 +6,7 @@
66 align-items: center;
77}
88
9+#rm_group_chats_block .tag.filterByFolder,
910#rm_group_chats_block .tag.filterByGroups {
1011 display: none;
1112}
public/css/toggle-dependent.css+14 -0
@@ -530,3 +530,17 @@ label[for="trim_spaces"]:not(:has(input:checked)) small {
530530label[for="bind_preset_to_connection"]:has(input:checked) {
531531 color: var(--active);
532532}
533+
534+#openai_settings input[type="checkbox"]:not(:checked)~[data-cc-toggle],
535+#openai_settings [data-cc-toggle="false"]>.icon-supported,
536+#openai_settings [data-cc-toggle="true"]>.icon-unsupported {
537+ display: none;
538+}
539+
540+#openai_settings [data-cc-toggle="true"]>.icon-supported {
541+ color: var(--active);
542+}
543+
544+#openai_settings [data-cc-toggle="false"]>.icon-unsupported {
545+ color: var(--golden);
546+}
public/css/world-info.css+21 -6
@@ -171,17 +171,32 @@
171171#world_editor_select {
172172 text-overflow: ellipsis;
173173 white-space: nowrap;
174- width: 10em;
175174}
176175
176+#world_editor_select,
177+#world_editor_select~.select2-container,
178+#world_info_sort_order,
177179#world_info_search {
178180 widthflex: 10em1;
179- min-width: 10em;
180- flex-grow: 1;
181181}
182182
183183#world_info_sort_order {,
184- width: 7em;
184+#world_info_search {
185+ min-width: 6em;
186+}
187+
188+#world_editor_select~.select2-container {
189+ overflow: hidden;
190+}
191+
192+#world_editor_select~.select2-container .select2-selection--single .select2-selection__rendered {
193+ padding-right: 30px;
194+}
195+
196+#world_editor_select~.select2-container .select2-selection .select2-selection__clear {
197+ right: 20px;
198+ display: flex;
199+ align-items: center;
185200}
186201
187202.world_entry .killSwitch.fa-toggle-on {
public/global.d.ts+2 -0
@@ -6,6 +6,8 @@ import { QuickReplyApi } from './scripts/extensions/quick-reply/api/QuickReplyAp
66declare global {
77 // Custom types
88 declare type InstructSettings = typeof power_user.instruct;
9+ declare type ContextSettings = typeof power_user.context;
10+ declare type ReasoningSettings = typeof power_user.reasoning;
911
1012 // Global namespace modules
1113 interface Window {
public/img/cometapi.svg+1 -0
@@ -0,0 +1 @@
1+<svg version="1.0" width="274.67" height="309.33" viewBox="0 0 206 232" xmlns="http://www.w3.org/2000/svg"><path d="M127.3.98c-7 0-14 .67-19.3 2.02a77.68 77.68 0 0 0-50 40.5C53.2 53 0 200.9 0 204.5c0 1.4.6 2.5 1.3 2.5 1.5 0 10.1-8.7 23.1-23.7 10.1-11.6 14.2-15.1 15.7-13.6 1.4 1.4-.7 7.2-10.1 29.3-9.7 22.6-11.5 27.9-10 29.4 1.8 1.8 4.6-.3 29.1-22.2 15.6-14 21.9-17.9 21.9-13.5 0 .9-2.9 7.8-6.4 15.4-7.5 16-9.4 21.5-7.9 23 2.6 2.6 3.3 2.1 71.9-52.7 27-21.5 51.4-41.5 54.2-44.3 9.8-9.8 17.2-22.8 20.8-36.6 2.6-10.4 2.5-28.6-.4-38.8-8-28.1-28.7-48.5-56.6-55.7-5.3-1.35-12.3-2.02-19.3-2.02zm-.74 14.13c5.77-.1 11.5.48 16.64 1.79a64.64 64.64 0 0 1 46.3 46.2c2.2 8.3 2.3 22.5.1 30.9-4.5 17.9-17.4 34-33.6 41.9-11 5.4-20.9 7.5-32.1 6.8-22.9-1.4-44.1-15.5-53.8-35.9-5-10.6-6.5-17.1-6.4-28.3.1-24.7 13.5-46 36.1-57.4 7.37-3.75 17.13-5.82 26.76-6zm.61 16.05c-4.6.02-9.17.44-12.17 1.24-24.2 6.8-39.6 31.4-34.6 55.3 4 19.5 18.5 34.2 37.4 38.2 36.7 7.6 67.3-28.2 54.7-64-4.9-13.8-18-25.5-33-29.5-3.1-.85-7.73-1.25-12.33-1.24zM132 42.41c3.9.05 7.81.87 11.5 2.49a29 29 0 0 1 17.3 24.9c.5 7.6-1.1 14-4.9 19.2-5.1 7.1-10.8 8.5-19.9 5.1-6.9-2.6-26.9-16.1-29.9-20.1-4.6-6.4-3.7-13.9 2.8-21.3A30.28 30.28 0 0 1 132 42.41z"/></svg>
1 \ No newline at end of file
public/img/fireworks.svg+1 -0
@@ -0,0 +1 @@
1+<svg preserveAspectRatio="xMinYMid meet" class="h-5" viewBox="0 0 638 315" xmlns="http://www.w3.org/2000/svg"><path d="M318.563 221.755C300.863 221.755 284.979 211.247 278.206 194.978L196.549 0H244.342L318.842 178.361L393.273 0H441.066L358.92 195.048C352.112 211.247 336.263 221.755 318.563 221.755Z"></path><path d="M425.111 314.933C407.481 314.933 391.667 304.494 384.824 288.366C377.947 272.097 381.507 253.524 393.936 240.921L542.657 90.2803L561.229 134.094L425.076 271.748L619.147 270.666L637.72 314.479L425.146 315.003L425.076 314.933H425.111Z"></path><path d="M0 314.408L18.5727 270.595L212.643 271.677L76.525 133.988L95.0977 90.1748L243.819 240.816C256.247 253.384 259.843 272.026 252.93 288.26C246.088 304.424 230.203 314.827 212.643 314.827L0.0698221 314.339L0 314.408Z"></path></svg>
public/img/moonshot.svg+1 -0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="556" height="557.333" viewBox="0 0 417 418" preserveAspectRatio="xMidYMid meet" xmlns:v="https://vecta.io/nano"><path d="M203.162.45L195.5.601c-17.9 1.5-42.2 6.599-56 11.899a214.62 214.62 0 0 1-8.7 3.201c-3.3 1-1.8 1.9 6.5 4 4.5 1.1 16.801 4.399 27.201 7.299L191 34l24.501 6.5 35 9.5 28 7.5 30 7.9L329 71c12.3 3.6 44 12 45.3 12 2.5 0-16.6-20.899-28.6-31.299-36.3-31.6-82.7-49.6-131.2-51.1L203.162.45zM109.5 27.3c-1.1-.2-4.8 1.3-8.7 3.5-6.5 3.9-24.7 16.901-28 20.101-14.6 14.1-22.2 22.199-28 29.899-3.9 5.1-6.899 9.401-6.699 9.501.4.4 8.2 2.5 19.9 5.6l15 3.999c4.1 1.2 15.2 4.201 24.5 6.601l27.5 7.4 30 8 30.501 8.2L215 138l46.5 12.5c2.8.7 10.2 2.7 16.5 4.4l41.001 11c4.1 1.2 11.999 3.301 17.499 4.601L353.5 175c8.5 2.5 25 6.9 45 12 8.3 2.1 15.9 4.2 16.9 4.6 1.6.6 1.7.2 1.1-5.2-.7-6-3.001-17.7-6.101-31.6-1.9-8.4-2.099-8.7-11.399-10.8-2.5-.5-7.6-1.9-11.5-2.9L337 127.5l-29.5-7.9-31.701-8.6c-2.7-.7-4.799-1.5-4.799-1.9 0-2.5 19.4-22.3 26.3-26.9 2-1.3 3.7-2.8 3.7-3.3 0-.4-1.901-1.199-4.201-1.699-3.9-.9-20.199-5.2-36.299-9.7L239 61.8 217 56l-29.5-7.9-32-8.6-46-12.199zM31.1 104c-1.1 0-2.8 1.501-4.1 3.501C20.8 117.5 10.8 142.1 7.6 155c-2.1 8.5-4.6 20.8-4.6 22.4 0 .8 1.899 1.8 4.299 2.5l61.901 16.6 21 5.5C101.001 204.7 120.5 210 141 215.7l13 3.4c1.9.4 8.2 2 14 3.7l26 7.1L217 236l24.5 6.601 44 11.9L302.5 259c6.1 1.5 19.6 5.1 30 8l30 8 17 4.5 14.7 3.901 8.7 2.2 1-2.6c3.7-9.7 5.4-15.3 7.1-23.3l2.5-11.301c.6-2.5-.301-2.899-23.5-8.799L365 233 352 229.4c-3-.9-11.301-3.1-18.501-4.9L314.5 219.5 247 201.4c-16.9-4.3-28.201-7.9-28.701-8.9-.5-1.6 3.2-8.799 12.7-24.199 2.7-4.6 5-8.701 5-9.201s-1.899-1.4-4.299-1.9c-5.7-1.3-30.201-7.7-40.701-10.6L144.5 134l-11-3L117 126.7 69.5 114C51.8 108.9 32.8 104 31.1 104zM1.935 193.44c-.703.309-.986 1.885-1.436 6.16-1.5 14 1.1 38.9 6.4 59.9 4.5 18.1 4.901 18.799 11.801 20.799L62 292 99.5 302c1.7.4 9.1 2.4 16.5 4.4l28.5 7.6 28.5 7.601 22 5.899c4.7 1.4 17.1 4.7 27.5 7.5L275 349.1l24 6.4 29.101 7.701c11.6 3.1 16.9 4.1 17.6 3.4 14.6-14.1 19.599-19.401 24.599-25.601 5.2-6.5 5.7-7.499 4.1-8.099-4.7-1.8-36.599-10.501-52.699-14.401-3.7-.9-8.6-2.3-11-3.1-2.3-.8-7.101-2.2-10.701-3-3.6-.7-9.6-2.3-13.5-3.4-3.8-1.1-13.7-3.8-22-6l-30-8L204.601 287c-8.1-2.1-14.901-4-15.101-4.2-.2-.3 3.901-18.6 7.301-32.6.7-3.2.7-4.399-.1-4.699-.7-.2-7.501-2-15.201-4s-15.8-4.3-18-5-7.1-2.1-11-2.9c-8.5-2-17-4.2-25-6.7-3.3-1-9.6-2.7-14-3.8L54.5 207.4 23 199c-10.2-2.6-19.2-5.1-20.2-5.5-.35-.125-.63-.163-.864-.06zm18.563 102.62c-.316.053-.498.165-.498.34 0 1 4 8.8 7.8 15.3 2.3 3.9 4.2 7.4 4.2 7.8 0 2.1 21.4 28.6 29.6 36.7 12.2 12 12.501 12.3 24.901 21.3 12.5 9 35.5 21.999 43.5 24.599 1.4.4 6.3 2.1 11 3.8 31.5 11.5 69.099 14.601 103.299 8.701C257.199 412.301 282 405.4 282 404c0-.3-7.3-2.399-16.2-4.799l-59.3-15.801-17.101-4.5c-4.9-1.2-5.1-1.7-5.9-19.9-.9-17.8-1.2-19.699-3.8-20.399-1.2-.4-7.999-2.3-15.199-4.2l-28-7.5L109 319.6 81 312l-34-9-21.299-5.9c-2.1-.825-4.256-1.199-5.203-1.04z"/></svg>
1 \ No newline at end of file
public/index.html+217 -86
@@ -652,7 +652,7 @@
652652 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
653653 </div>
654654 </div>
655655 <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot">
656656 <div class="range-block-title" data-i18n="Multiple swipes per generation">
657657 Multiple swipes per generation
658658 </div>
@@ -691,7 +691,7 @@
691691 </span>
692692 </div>
693693 </div>
694694 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
695695 <div class="range-block-title" data-i18n="Temperature">
696696 Temperature
697697 </div>
@@ -704,7 +704,7 @@
704704 </div>
705705 </div>
706706 </div>
707707 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
708708 <div class="range-block-title" data-i18n="Frequency Penalty">
709709 Frequency Penalty
710710 </div>
@@ -717,7 +717,7 @@
717717 </div>
718718 </div>
719719 </div>
720720 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
721721 <div class="range-block-title" data-i18n="Presence Penalty">
722722 Presence Penalty
723723 </div>
@@ -743,7 +743,7 @@
743743 </div>
744744 </div>
745745 </div>
746746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
747747 <div class="range-block-title" data-i18n="Top P">
748748 Top P
749749 </div>
@@ -980,7 +980,7 @@
980980 </div>
981981 </div>
982982 </div>
983983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai">
984984 <div class="range-block-title justifyLeft" data-i18n="Seed">
985985 Seed
986986 </div>
@@ -1984,10 +1984,14 @@
19841984 </b>
19851985 </div>
19861986 </div>
19871987 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi">
19881988 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19891989 <input id="openai_function_calling" type="checkbox" />
19901990 <span data-i18n="Enable function calling">Enable function calling</span>
1991+ <div id="openai_function_calling_supported" data-cc-toggle="false">
1992+ <i class="icon-supported fa-solid fa-circle-check" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
1993+ <i class="icon-unsupported fa-solid fa-circle-exclamation" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
1994+ </div>
19911995 </label>
19921996 <div class="flexBasis100p toggle-description justifyLeft">
19931997 <span data-i18n="enable_functions_desc_1">Allows using </span><a href="https://platform.openai.com/docs/guides/function-calling" target="_blank" data-i18n="enable_functions_desc_2">function tools</a>.
@@ -1995,10 +1999,14 @@
19951999 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
19962000 </div>
19972001 </div>
19982002 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations,moonshot,cohere,cometapi">
19992003 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
20002004 <input id="openai_image_inlining" type="checkbox" />
20012005 <span data-i18n="Send inline images">Send inline images</span>
2006+ <div id="openai_image_inlining_supported" data-cc-toggle="false">
2007+ <i class="icon-supported fa-solid fa-circle-check" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2008+ <i class="icon-unsupported fa-solid fa-circle-exclamation" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2009+ </div>
20022010 </label>
20032011 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
20042012 <span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it. Use the</span>
@@ -2007,7 +2015,7 @@
20072015 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
20082016 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
20092017 </div>
20102018 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere">
20112019 <div class="flex-container oneline-dropdown">
20122020 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
20132021 Inline Image Quality
@@ -2024,6 +2032,10 @@
20242032 <label for="openai_video_inlining" class="checkbox_label flexWrap widthFreeExpand">
20252033 <input id="openai_video_inlining" type="checkbox" />
20262034 <span data-i18n="Send inline videos">Send inline videos</span>
2035+ <div id="openai_video_inlining_supported" data-cc-toggle="false">
2036+ <i class="icon-supported fa-solid fa-circle-check" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2037+ <i class="icon-unsupported fa-solid fa-circle-exclamation" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2038+ </div>
20272039 </label>
20282040 <div id="video_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
20292041 <span data-i18n="video_inlining_hint_1">Sends videos in prompts if the model supports it. Use the</span>
@@ -2065,7 +2077,7 @@
20652077 </span>
20662078 </div>
20672079 </div>
20682080 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi">
20692081 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20702082 <input id="openai_show_thoughts" type="checkbox" />
20712083 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2079,7 +2091,7 @@
20792091 </span>
20802092 </div>
20812093 </div>
20822094 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi">
20832095 <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.">
20842096 <label for="openai_reasoning_effort">
20852097 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2788,16 +2800,18 @@
27882800 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
27892801 </optgroup>
27902802 <optgroup>
2791- <option value="01ai">01.AI (Yi)</option>
27922803 <option value="ai21">AI21</option>
27932804 <option value="aimlapi">AI/ML API</option>
27942805 <option value="claude">Claude</option>
27952806 <option value="cohere">Cohere</option>
2807+ <option value="cometapi">CometAPI</option>
27962808 <option value="deepseek">DeepSeek</option>
2809+ <option value="fireworks">Fireworks AI</option>
27972810 <option value="groq">Groq</option>
27982811 <option value="makersuite">Google AI Studio</option>
27992812 <option value="vertexai">Google Vertex AI</option>
28002813 <option value="mistralai">MistralAI</option>
2814+ <option value="moonshot">Moonshot AI</option>
28012815 <option value="nanogpt">NanoGPT</option>
28022816 <option value="openrouter">OpenRouter</option>
28032817 <option value="perplexity">Perplexity</option>
@@ -2919,6 +2933,15 @@
29192933 <div>
29202934 <h4 data-i18n="OpenAI Model">OpenAI Model</h4>
29212935 <select id="model_openai_select">
2936+ <optgroup label="GPT-5">
2937+ <option value="gpt-5">gpt-5</option>
2938+ <option value="gpt-5-2025-08-07">gpt-5-2025-08-07</option>
2939+ <option value="gpt-5-chat-latest">gpt-5-chat-latest</option>
2940+ <option value="gpt-5-mini">gpt-5-mini</option>
2941+ <option value="gpt-5-mini-2025-08-07">gpt-5-mini-2025-08-07</option>
2942+ <option value="gpt-5-nano">gpt-5-nano</option>
2943+ <option value="gpt-5-nano-2025-08-07">gpt-5-nano-2025-08-07</option>
2944+ </optgroup>
29222945 <optgroup label="GPT-4o">
29232946 <option value="gpt-4o">gpt-4o</option>
29242947 <option value="gpt-4o-2024-11-20">gpt-4o-2024-11-20</option>
@@ -3009,6 +3032,8 @@
30093032 <h4 data-i18n="Claude Model">Claude Model</h4>
30103033 <select id="model_claude_select">
30113034 <optgroup label="Versions">
3035+ <option value="claude-opus-4-1">claude-opus-4-1</option>
3036+ <option value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option>
30123037 <option value="claude-opus-4-0">claude-opus-4-0</option>
30133038 <option value="claude-opus-4-20250514">claude-opus-4-20250514</option>
30143039 <option value="claude-sonnet-4-0">claude-sonnet-4-0</option>
@@ -3150,6 +3175,7 @@
31503175 <option value="gemini-2.5-flash">gemini-2.5-flash</option>
31513176 <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
31523177 <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
3178+ <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
31533179 <option value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option>
31543180 </optgroup>
31553181 <optgroup label="Gemini 2.0">
@@ -3158,6 +3184,7 @@
31583184 <option value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option>
31593185 <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
31603186 <option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
3187+ <option value="gemini-2.0-flash-preview-image-generation">gemini-2.0-flash-preview-image-generation</option>
31613188 <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
31623189 <option value="gemini-2.0-flash">gemini-2.0-flash</option>
31633190 <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>
@@ -3166,6 +3193,7 @@
31663193 <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
31673194 <option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
31683195 <option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
3196+ <option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option>
31693197 </optgroup>
31703198 <optgroup label="Gemini 1.5">
31713199 <option value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
@@ -3176,6 +3204,7 @@
31763204 <option value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
31773205 <option value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
31783206 <option value="gemini-1.5-flash">gemini-1.5-flash</option>
3207+ <option value="gemini-1.5-flash-8b-latest">gemini-1.5-flash-8b-latest</option>
31793208 <option value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
31803209 <option value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
31813210 <option value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
@@ -3183,6 +3212,7 @@
31833212 </optgroup>
31843213 <optgroup label="Gemma">
31853214 <option value="gemma-3n-e4b-it">gemma-3n-e4b-it</option>
3215+ <option value="gemma-3n-e2b-it">gemma-3n-e2b-it</option>
31863216 <option value="gemma-3-27b-it">gemma-3-27b-it</option>
31873217 <option value="gemma-3-12b-it">gemma-3-12b-it</option>
31883218 <option value="gemma-3-4b-it">gemma-3-4b-it</option>
@@ -3323,6 +3353,7 @@
33233353 <option value="gemini-2.5-flash">gemini-2.5-flash</option>
33243354 <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
33253355 <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
3356+ <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
33263357 <option value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option>
33273358 </optgroup>
33283359 <optgroup label="Gemini 2.0">
@@ -3384,6 +3415,7 @@
33843415 <option value="mistral-small-2506">mistral-small-2506</option>
33853416 <option value="mistral-medium-2312">mistral-medium-2312</option>
33863417 <option value="mistral-medium-2505">mistral-medium-2505</option>
3418+ <option value="mistral-medium-2508">mistral-medium-2508</option>
33873419 <option value="mistral-large-2402">mistral-large-2402</option>
33883420 <option value="mistral-large-2407">mistral-large-2407</option>
33893421 <option value="mistral-large-2411">mistral-large-2411</option>
@@ -3395,6 +3427,7 @@
33953427 <option value="codestral-2411-rc5">codestral-2411-rc5</option>
33963428 <option value="codestral-2412">codestral-2412</option>
33973429 <option value="codestral-2501">codestral-2501</option>
3430+ <option value="codestral-2508">codestral-2508</option>
33983431 <option value="pixtral-12b-2409">pixtral-12b-2409</option>
33993432 <option value="pixtral-large-2411">pixtral-large-2411</option>
34003433 <option value="devstral-small-2505">devstral-small-2505</option>
@@ -3467,6 +3500,40 @@
34673500 </select>
34683501 </div>
34693502 </div>
3503+ <div id="fireworks_form" data-source="fireworks">
3504+ <h4 data-i18n="Fireworks AI API Key">Fireworks AI API Key</h4>
3505+ <div class="flex-container">
3506+ <input id="api_key_fireworks" name="api_key_fireworks" class="text_pole flex1" value="" type="text" autocomplete="off">
3507+ <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_fireworks"></div>
3508+ </div>
3509+ <div data-for="api_key_fireworks" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3510+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3511+ </div>
3512+ <div>
3513+ <h4 data-i18n="Fireworks AI Model">Fireworks AI Model</h4>
3514+ <select id="model_fireworks_select">
3515+ <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
3516+ </select>
3517+ </div>
3518+ </div>
3519+ <div id="cometapi_form" data-source="cometapi">
3520+ <h4><a href="https://api.cometapi.com/" target="_blank" rel="noopener noreferrer" data-i18n="CometAPI API Key">CometAPI API Key</a></h4>
3521+ <div class="flex-container">
3522+ <input id="api_key_cometapi" name="api_key_cometapi" class="text_pole flex1" value="" type="text" autocomplete="off">
3523+ <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_cometapi"></div>
3524+ </div>
3525+ <div data-for="api_key_cometapi" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3526+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3527+ </div>
3528+ <div>
3529+ <h4 data-i18n="CometAPI Model">CometAPI Model</h4>
3530+ <div class="flex-container">
3531+ <select id="model_cometapi_select" class="flex1">
3532+ <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
3533+ </select>
3534+ </div>
3535+ </div>
3536+ </div>
34703537 <div id="perplexity_form" data-source="perplexity">
34713538 <h4 data-i18n="Perplexity API Key">Perplexity API Key</h4>
34723539 <div class="flex-container">
@@ -3526,6 +3593,7 @@
35263593 <option value="command-r-plus-08-2024">command-r-plus-08-2024</option>
35273594 <option value="command-r7b-12-2024">command-r7b-12-2024</option>
35283595 <option value="command-a-03-2025">command-a-03-2025</option>
3596+ <option value="command-a-vision-07-2025">command-a-vision-07-2025</option>
35293597 </optgroup>
35303598 <optgroup label="Nightly">
35313599 <option value="command-light-nightly">command-light-nightly</option>
@@ -3566,23 +3634,6 @@
35663634 <select id="model_custom_select" class="text_pole model_custom_select"></select>
35673635 </div>
35683636 </form>
3569- <div id="01ai_form" data-source="01ai">
3570- <h4>
3571- <a data-i18n="01.AI API Key" href="https://platform.lingyiwanwu.com/" target="_blank" rel="noopener noreferrer">
3572- 01.AI API Key
3573- </a>
3574- </h4>
3575- <div class="flex-container">
3576- <input id="api_key_01ai" name="api_key_01ai" class="text_pole flex1" value="" type="text" autocomplete="off">
3577- <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_01ai"></div>
3578- </div>
3579- <div data-for="api_key_01ai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3580- For privacy reasons, your API key will be hidden after you click 'Connect'.
3581- </div>
3582- <h4 data-i18n="01.AI Model">01.AI Model</h4>
3583- <select id="model_01ai_select">
3584- </select>
3585- </div>
35863637 <div id="xai_form" data-source="xai">
35873638 <h4>
35883639 <a data-i18n="xAI API Key" href="https://console.x.ai/" target="_blank" rel="noopener noreferrer">
@@ -3611,7 +3662,7 @@
36113662 </div>
36123663 <div id="aimlapi_form" data-source="aimlapi">
36133664 <h4>
36143665 <span data-i18n="AI/ML API Key">AI/ML API Key</span>
36153666 </h4>
36163667 <div class="flex-container">
36173668 <input id="api_key_aimlapi" name="api_key_aimlapi" class="text_pole flex1" value="" type="text" autocomplete="off">
@@ -3620,7 +3671,7 @@
36203671 <div data-for="api_key_aimlapi" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
36213672 For privacy reasons, your API key will be hidden after you click 'Connect'.
36223673 </div>
36233674 <h4 data-i18n="AI/ML Model">AI/ML Model</h4>
36243675 <select id="model_aimlapi_select">
36253676 </select>
36263677 </div>
@@ -3639,6 +3690,33 @@
36393690 </span>
36403691 </div>
36413692 </div>
3693+ <div id="moonshot_form" data-source="moonshot">
3694+ <h4>
3695+ <a data-i18n="Moonshot AI API Key" href="https://platform.moonshot.ai/console/api-keys" target="_blank" rel="noopener noreferrer">
3696+ Moonshot AI API Key
3697+ </a>
3698+ </h4>
3699+ <div class="flex-container">
3700+ <input id="api_key_moonshot" name="api_key_moonshot" class="text_pole flex1" value="" type="text" autocomplete="off">
3701+ <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_moonshot"></div>
3702+ </div>
3703+ <div data-for="api_key_moonshot" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3704+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3705+ </div>
3706+ <h4 data-i18n="Moonshot AI Model">Moonshot AI Model</h4>
3707+ <select id="model_moonshot_select">
3708+ <option value="kimi-k2-0711-preview">kimi-k2-0711-preview</option>
3709+ <option value="moonshot-v1-8k">moonshot-v1-8k</option>
3710+ <option value="moonshot-v1-32k">moonshot-v1-32k</option>
3711+ <option value="moonshot-v1-128k">moonshot-v1-128k</option>
3712+ <option value="moonshot-v1-auto">moonshot-v1-auto</option>
3713+ <option value="kimi-latest">kimi-latest</option>
3714+ <option value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option>
3715+ <option value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option>
3716+ <option value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option>
3717+ <option value="kimi-thinking-preview">kimi-thinking-preview</option>
3718+ </select>
3719+ </div>
36423720 <div id="prompt_post_processing_form">
36433721 <h4>
36443722 <span data-i18n="Prompt Post-Processing">
@@ -3746,6 +3824,35 @@
37463824 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="context_story_string" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
37473825 </label>
37483826 <textarea id="context_story_string" class="text_pole textarea_compact autoSetHeight"></textarea>
3827+ <div class="flex-container flexFlowColumn">
3828+ <div id="context_story_string_position_block">
3829+ <label for="context_story_string_position">
3830+ <small data-i18n="Position:">Position:</small>
3831+ </label>
3832+ <select id="context_story_string_position" class="text_pole">
3833+ <option value="0" data-i18n="Default (top of context)">Default (top of context)</option>
3834+ <option value="1" data-i18n="In-chat @ Depth">In-chat @ Depth</option>
3835+ </select>
3836+ </div>
3837+ <div id="context_story_string_inject_settings" class="flex-container">
3838+ <div class="flex1">
3839+ <label for="context_story_string_depth">
3840+ <small data-i18n="Depth:">Depth:</small>
3841+ </label>
3842+ <input type="number" id="context_story_string_depth" class="text_pole" min="0" max="">
3843+ </div>
3844+ <div class="flex1">
3845+ <label for="context_story_string_role">
3846+ <small data-i18n="Role:">Role:</small>
3847+ </label>
3848+ <select id="context_story_string_role" class="text_pole">
3849+ <option data-i18n="System" value="0">System</option>
3850+ <option data-i18n="User" value="1">User</option>
3851+ <option data-i18n="Assistant" value="2">Assistant</option>
3852+ </select>
3853+ </div>
3854+ </div>
3855+ </div>
37493856 <div class="flex-container">
37503857 <div class="flex1">
37513858 <label for="context_example_separator">
@@ -3866,6 +3973,10 @@
38663973 <input id="instruct_macro" type="checkbox" />
38673974 <small data-i18n="Replace Macro in Sequences">Replace Macro in Sequences</small>
38683975 </label>
3976+ <label for="instruct_sequences_as_stop_strings" class="checkbox_label">
3977+ <input id="instruct_sequences_as_stop_strings" type="checkbox" />
3978+ <small data-i18n="Sequences as Stop Strings">Sequences as Stop Strings</small>
3979+ </label>
38693980 <label for="instruct_skip_examples" class="checkbox_label">
38703981 <input id="instruct_skip_examples" type="checkbox" />
38713982 <small data-i18n="Skip Example Dialogues Formatting">Skip Example Dialogues Formatting</small>
@@ -3890,7 +4001,31 @@
38904001 </span>
38914002 </b>
38924003 </h4>
38934004 <!-- We keep onesome auto-open so the user would know what is going on in the picked template -->
4005+ <details open>
4006+ <summary>
4007+ <span data-i18n="Story String Sequences">Story String Sequences</span>
4008+ <small class="fa-solid fa-question-circle" title="Used in Default position only." data-i18n="[title]Used in Default position only."></small>
4009+ </summary>
4010+ <div class="flex-container">
4011+ <div class="flexAuto" title="Inserted before a Story String." data-i18n="[title]Inserted before a Story String.">
4012+ <label for="instruct_story_string_prefix">
4013+ <small data-i18n="Story String Prefix">Story String Prefix</small>
4014+ </label>
4015+ <div>
4016+ <textarea id="instruct_story_string_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
4017+ </div>
4018+ </div>
4019+ <div class="flexAuto" title="Inserted after a Story String." data-i18n="[title]Inserted after a Story String.">
4020+ <label for="instruct_story_string_suffix">
4021+ <small data-i18n="Story String Suffix">Story String Suffix</small>
4022+ </label>
4023+ <div>
4024+ <textarea id="instruct_story_string_suffix" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4025+ </div>
4026+ </div>
4027+ </div>
4028+ </details>
38944029 <details open>
38954030 <summary data-i18n="User Message Sequences">User Message Sequences</summary>
38964031 <div class="flex-container">
@@ -3937,27 +4072,6 @@
39374072 </div>
39384073 </details>
39394074 <details>
3940- <summary data-i18n="System Prompt Sequences">System Prompt Sequences</summary>
3941- <div class="flex-container">
3942- <div class="flexAuto" title="Inserted before a System prompt." data-i18n="[title]Inserted before a System prompt.">
3943- <label for="instruct_system_sequence_prefix">
3944- <small data-i18n="System Prompt Prefix">System Prompt Prefix</small>
3945- </label>
3946- <div>
3947- <textarea id="instruct_system_sequence_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3948- </div>
3949- </div>
3950- <div class="flexAuto" title="Inserted after a System prompt." data-i18n="[title]Inserted after a System prompt.">
3951- <label for="instruct_system_sequence_suffix">
3952- <small data-i18n="System Prompt Suffix">System Prompt Suffix</small>
3953- </label>
3954- <div>
3955- <textarea id="instruct_system_sequence_suffix" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
3956- </div>
3957- </div>
3958- </div>
3959- </details>
3960- <details>
39614075 <summary data-i18n="Misc. Sequences">Misc. Sequences</summary>
39624076 <div class="flex-container">
39634077 <div class="flexAuto" title="Inserted before the first Assistant's message." data-i18n="[title]Inserted before the first Assistant's message.">
@@ -4380,28 +4494,30 @@
43804494 </div>
43814495 <div id="world_popup">
43824496 <hr>
43834497 <div class="flex-container alignitemscenter gap3px">
43844498 <input type="file" id="world_import_file" accept=".json,.lorebook,.png" name="avatar" hidden>
43854499 <div id="world_create_button" class="menu_button menu_button_icon">
43864500 <i class="fa-solid fa-globe"></i>
43874501 <span data-i18n="New">New</span>
43884502 </div>
43894503 <small data-i18n="or">or</small>
43904504 <select id="world_editor_select" class="margin0">
43914505 <option value="" data-i18n="--- Pick to Edit ---">--- Pick to Edit ---</option>
43924506 </select>
4507+ <div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>
4508+ <div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>
43934509 <div id="world_popup_name_button" class="menu_button fa-pencil fa-solid" title="Rename World Info" data-i18n="[title]Rename World Info"></div>
4510+ <div id="world_duplicate" class="menu_button fa-solid fa-paste" title="Duplicate World Info" data-i18n="[title]Duplicate World Info"></div>
4511+ <div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
4512+ </div>
4513+ <div class="flex-container alignitemscenter">
4514+ <div id="world_popup_new" class="menu_button fa-solid fa-plus" title="New Entry" data-i18n="[title]New Entry"></div>
43944515 <div id="OpenAllWIEntries" class="menu_button fa-solid fa-expand" title="Open all Entries" data-i18n="[title]Open all Entries"></div>
43954516 <div id="CloseAllWIEntries" class="menu_button fa-solid fa-compress" title="Close all Entries" data-i18n="[title]Close all Entries"></div>
4396- <div id="world_popup_new" class="menu_button fa-solid fa-plus" title="New Entry" data-i18n="[title]New Entry"></div>
43974517 <div id="world_backfill_memos" class="menu_button fa-solid fa-notes-medical" title="Fill empty Memo/Titles with Keywords" data-i18n="[title]Fill empty Memo/Titles with Keywords"></div>
43984518 <div id="world_apply_current_sorting" class="menu_button fa-solid fa-solid fa-arrow-down-9-1" title="Apply current sorting as Order" data-i18n="[title]Apply current sorting as Order"></div>
4399- <div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>
4400- <div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>
4401- <div id="world_duplicate" class="menu_button fa-solid fa-paste" title="Duplicate World Info" data-i18n="[title]Duplicate World Info"></div>
4402- <div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
44034519 <input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
44044520 <select id="world_info_sort_order" class="margin0text_pole textarea_compact">
44054521 <option data-rule="search" value="14" data-i18n="Search" hidden>Search</option>
44064522 <option data-rule="priority" value="0" data-i18n="Priority">Priority</option>
44074523 <option data-rule="custom" value="13" data-i18n="Custom">Custom</option>
@@ -5263,6 +5379,7 @@
52635379 <div id="message_limit_container" class="extension_container"></div>
52645380 <div id="injects_container" class="extension_container"></div>
52655381 <div id="accuweather_container" class="extension_container"></div>
5382+ <div id="dice_container" class="extension_container"></div>
52665383 </div>
52675384 <hr class="wide100p margin0">
52685385 <div class="alignitemscenter flex-container justifyCenter wide100p" style="justify-content: space-between;">
@@ -6317,25 +6434,35 @@
63176434 (<span data-i18n="extension_token_counter">Tokens:</span>&nbsp; <span class="world_entry_form_token_counter" data-first-run="true">counting...</span>)&nbsp;
63186435 <span class="world_entry_form_uid_value" data-first-run="true"></span>
63196436 </span>
63206437 <div class="flex-container">
63216438 <labeldiv class="checkbox flex-container alignitemscenter flexNoGapflexFlowColumn">
6322- <input type="checkbox" name="excludeRecursion" />
6439+ <label class="checkbox flex-container alignitemscenter flexNoGap" title="This entry will not be recursively activated by other entries.">
6323- <span data-i18n="Non-recursable (will not be activated by another)">
6440+ <input type="checkbox" name="excludeRecursion" />
6324- Non-recursable (will not be activated by another)
6441+ <span data-i18n="Non-recursable">
6325- </span>
6442+ Non-recursable
63266443 </labelspan>
6327- <label class="checkbox flex-container alignitemscenter flexNoGap">
6444+ </label>
6328- <input type="checkbox" name="preventRecursion" />
6445+ <label class="checkbox flex-container alignitemscenter flexNoGap" title="This entry will not activate other entries recursively." data-i18n="[title]This entry will not activate other entries recursively.">
6329- <span data-i18n="Prevent further recursion (this entry will not activate others)">
6446+ <input type="checkbox" name="preventRecursion" />
6330- Prevent further recursion (will not activate others)
6447+ <span data-i18n="Prevent further recursion">
6331- </span>
6448+ Prevent further recursion
63326449 </labelspan>
6333- <label class="checkbox flex-container alignitemscenter flexNoGap">
6450+ </label>
6334- <input type="checkbox" name="delay_until_recursion" />
6451+ </div>
6335- <span data-i18n="Delay until recursion (can only be activated on recursive checking)">
6452+ <div class="flex-container flexFlowColumn">
6336- Delay until recursion (can only be activated on recursive checking)
6453+ <label class="checkbox flex-container alignitemscenter flexNoGap" title="This entry can only be activated on recursive checking." data-i18n="[title]This entry can only be activated on recursive checking.">
6337- </span>
6454+ <input type="checkbox" name="delay_until_recursion" />
6338- </label>
6455+ <span data-i18n="Delay until recursion">
6456+ Delay until recursion
6457+ </span>
6458+ </label>
6459+ <label class="checkbox flex-container alignitemscenter flexNoGap" title="This entry will be included ignoring budget constraints, assuming all other checks pass." data-i18n="[title]This entry will be included ignoring budget constraints, assuming all other checks pass.">
6460+ <input type="checkbox" name="ignoreBudget" />
6461+ <span data-i18n="Ignore budget">
6462+ Ignore budget
6463+ </span>
6464+ </label>
6465+ </div>
63396466 </div>
63406467 </span>
63416468 </small>
@@ -6606,7 +6733,7 @@
66066733 </div>
66076734 </div>
66086735 </div>
66096736 <i class="menu_button move_entry_button fa-solid fa-right-left" title="Move/Copy Entry to Another Lorebook" data-i18n="[title]Move/Copy Entry to Another Lorebook"></i>
66106737 <i class="menu_button duplicate_entry_button fa-solid fa-paste" title="Duplicate world info entry" data-i18n="[title]Duplicate world info entry" type="submit" value=""></i>
66116738 <i class="menu_button delete_entry_button fa-solid fa-trash-can" title="Delete world info entry" data-i18n="[title]Delete world info entry" type="submit" value=""></i>
66126739 </div>
@@ -6768,9 +6895,9 @@
67686895 <textarea id="completion_prompt_manager_popup_entry_form_prompt" class="text_pole" name="prompt" placeholder="The prompt to be sent." data-i18n="[placeholder]The prompt to be sent."></textarea>
67696896 </div>
67706897 <div class="completion_prompt_manager_popup_entry_form_footer">
67716898 <a id="completion_prompt_manager_popup_entry_form_close" title="closeClose" data-i18n="[title]close" class="fa-solid fa-close menu_button"></a>
67726899 <a id="completion_prompt_manager_popup_entry_form_reset" title="resetReset" data-i18n="[title]reset" class="fa-solid fa-undo menu_button"></a>
67736900 <a id="completion_prompt_manager_popup_entry_form_save" title="saveSave" data-i18n="[title]save" class="fa-solid fa-save menu_button" data-pm-prompt=""></a>
67746901 </div>
67756902 </form>
67766903 </div>
@@ -6836,6 +6963,7 @@
68366963 <div class="mes_reasoning_edit_done menu_button edit_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm Edit"></div>
68376964 <div class="mes_reasoning_delete menu_button edit_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
68386965 <div class="mes_reasoning_edit_cancel menu_button edit_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6966+ <div class="mes_reasoning_close_all mes_button fa-solid fa-minimize" title="Collapse all reasoning blocks" data-i18n="[title]Collapse all reasoning blocks"></div>
68396967 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
68406968 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
68416969 </div>
@@ -7043,7 +7171,10 @@
70437171 <details open>
70447172 <summary>
70457173 <div class="title_restorable">
7046- <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
7174+ <div class="flex-container alignItemsCenter">
7175+ <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
7176+ <i class="editor_maximize fa-solid fa-maximize right_menu_button" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
7177+ </div>
70477178 <div class="menu_button menu_button_icon delete_alternate_greeting">
70487179 <i class="fa-solid fa-trash-alt"></i>
70497180 <span data-i18n="Delete">Delete</span>
@@ -7445,7 +7576,7 @@
74457576 <div id="leftSendForm" class="alignContentCenter">
74467577 <div id="options_button" class="fa-solid fa-bars interactable"></div>
74477578 </div>
74487579 <textarea id="send_textarea" name="text" class="mdHotkeys" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help" autocomplete="off"></textarea>
74497580 <div id="rightSendForm" class="alignContentCenter">
74507581 <div id="stscript_continue" title="Continue script execution" class="stscript_btn stscript_continue" data-i18n="[title]Continue script execution">
74517582 <i class="fa-solid fa-play"></i>
public/locales/ar-sa.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( لا أحد )",
984984 "Content": "المحتوى",
985985 "Exclude from recursion": "استبعاد من التكرار",
986986 "Prevent further recursion (this entry will not activate others)": "منع المزيد من التكرار (هذا الإدخال لن ينشط الإدخالات الأخرى)",
987- "Delay until recursion (this entry can only be activated on recursive checking)": "التأخير حتى العودية (لا يمكن تفعيل هذا الإدخال إلا عند التحقق العودي)",
987+ "Delay until recursion": "التأخير حتى العودية",
988988 "What this keyword should mean to the AI, sent verbatim": "ما يجب أن يعني هذا الكلمة الرئيسية للذكاء الاصطناعي، ترسل حرفيًا",
989989 "Filter to Character(s)": "تصفية إلى الشخصيات",
990990 "Character Exclusion": "استبعاد الشخصيات",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "رابط PNG المباشر (راجع",
13771377 "char_import_7": "للمضيفين المسموح بهم)",
13781378 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
1379- "char_import_9": "شخصية Soulkyn (رابط مباشر)",
13801379 "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)",
13811380 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",
13821381 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",
public/locales/de-de.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Keiner )",
984984 "Content": "Inhalt",
985985 "Exclude from recursion": "Aus Rekursion ausschließen",
986986 "Prevent further recursion (this entry will not activate others)": "Weitere Rekursion verhindern (dieser Eintrag aktiviert keine anderen)",
987987 "Delay until recursion (this entry can only be activated on recursive checking)": "Verzögerung bis zur Rekursion (dieser Eintrag kann nur bei rekursiver Prüfung aktiviert werden)",
988988 "What this keyword should mean to the AI, sent verbatim": "Was dieses Schlüsselwort für die KI bedeuten soll, wörtlich gesendet",
989989 "Filter to Character(s)": "Filtern auf Charakter(e)",
990990 "Character Exclusion": "Charakterausschluss",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Direkter PNG-Link (siehe",
13771377 "char_import_7": "für erlaubte Hosts)",
13781378 "char_import_8": "RisuRealm-Charakter (Direktlink)",
1379- "char_import_9": "Soulkyn-Charakter (Direktlink)",
13801379 "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)",
13811380 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",
13821381 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",
public/locales/es-es.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Ninguno )",
984984 "Content": "Contenido",
985985 "Exclude from recursion": "Excluir de la recursión",
986986 "Prevent further recursion (this entry will not activate others)": "Evitar una mayor recurrencia (esta entrada no activará otras)",
987- "Delay until recursion (this entry can only be activated on recursive checking)": "Retraso hasta la recursión (esta entrada solo se puede activar en la comprobación recursiva)",
987+ "Delay until recursion": "Retraso hasta la recursión",
988988 "What this keyword should mean to the AI, sent verbatim": "Lo que este palabra clave debería significar para la IA, enviado textualmente",
989989 "Filter to Character(s)": "Filtrar a Personaje(s)",
990990 "Character Exclusion": "Exclusión de Personaje",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Enlace PNG directo (consulte",
13771377 "char_import_7": "para hosts permitidos)",
13781378 "char_import_8": "Personaje RisuRealm (Enlace directo)",
1379- "char_import_9": "Personaje Soulkyn (Enlace directo)",
13801379 "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)",
13811380 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",
13821381 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",
public/locales/fr-fr.json+3 -6
@@ -925,7 +925,7 @@
925925 "Automation ID": "ID d'automatisation",
926926 "( None )": "( Aucun )",
927927 "Content": "Contenu",
928928 "Prevent further recursion (this entry will not activate others)": "Empêcher toute récursion ultérieure (cette entrée n’en activera pas d’autres)",
929929 "What this keyword should mean to the AI, sent verbatim": "Ce que ce mot-clé devrait signifier pour l'IA, envoyé textuellement",
930930 "-- Characters not found --": "-- Personnages non trouvés --",
931931 "Inclusion Group": "Groupe d'inclusion",
@@ -1298,7 +1298,6 @@
12981298 "char_import_6": "Lien PNG direct (voir",
12991299 "char_import_7": "pour les hôtes autorisés)",
13001300 "char_import_8": "Personnage de RisuRealm (lien direct)",
1301- "char_import_9": "Personnage de Soulkyn (lien direct)",
13021301 "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)",
13031302 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",
13041303 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",
@@ -1426,8 +1425,6 @@
14261425 "prompt_post_processing_merge": "Fusionner les rôles consécutifs",
14271426 "prompt_post_processing_semi": "Semi-strict (rôles alternés)",
14281427 "prompt_post_processing_strict": "Strict (l'utilisateur d'abord, rôles alternés)",
1429- "01.AI API Key": "01.Clé IA API",
1430- "01.AI Model": "01.Modèle IA",
14311428 "Additional Parameters": "Paramètres supplémentaires",
14321429 "Import Advanced Formatting settings": "Importer les paramètres de formatage avancé\n\nVous pouvez également fournir des fichiers hérités pour les modèles Instruct et Context..",
14331430 "Master Import": "Master Import",
@@ -1531,8 +1528,8 @@
15311528 "Group Scoring": "Notation des groupes",
15321529 "delay_until_recursion_level": "Définit les niveaux de délai pour les balayages récursifs.\r\rDans un premier temps, seul le premier niveau (le plus petit nombre) sera pris en compte.\rSi aucune correspondance n'est trouvée, le niveau suivant devient éligible pour la correspondance.\rCette opération est répétée jusqu'à ce que tous les niveaux soient vérifiés.\r\rLié au réglage \"Délai avant récursion\".",
15331530 "Recursion Level": "Niveau de récursivité",
1534- "Non-recursable (will not be activated by another)": "Non récursable (ne peut être activé par un autre)",
1531+ "Non-recursable": "Non récursable",
15351532 "Delay until recursion (can only be activated on recursive checking)": "Délai jusqu'à la récursion (ne peut être activé que lors d'un contrôle récursif)",
15361533 "Prioritize": "Priorités",
15371534 "Sticky entries will stay active for N messages after being triggered.": "Les entrées collantes restent actives pendant N messages après avoir été déclenchées.",
15381535 "Sticky": "Collant",
public/locales/is-is.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Enginn )",
984984 "Content": "Efnisatriði",
985985 "Exclude from recursion": "Útiloka frá endurtekningu",
986986 "Prevent further recursion (this entry will not activate others)": "Koma í veg fyrir frekari endurkomu (þessi færsla mun ekki virkja aðra)",
987- "Delay until recursion (this entry can only be activated on recursive checking)": "Seinkað þar til endurkomu (þessi færsla er aðeins hægt að virkja við endurkvæma athugun)",
987+ "Delay until recursion": "Seinkað þar til endurkomu",
988988 "What this keyword should mean to the AI, sent verbatim": "Hvað þetta lykilorð ætti að þýða fyrir AI, sent bókstaflega",
989989 "Filter to Character(s)": "Sía til Persónu(r)",
990990 "Character Exclusion": "Persónuúteslutningur",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Beinn PNG hlekkur (sjá",
13771377 "char_import_7": "fyrir leyfilega gestgjafa)",
13781378 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
1379- "char_import_9": "Soulkyn karakter (beinn hlekkur)",
13801379 "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)",
13811380 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",
13821381 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",
public/locales/it-it.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Nessuno )",
984984 "Content": "Contenuto",
985985 "Exclude from recursion": "Escludi dalla ricorsione",
986986 "Prevent further recursion (this entry will not activate others)": "Prevenire ulteriore ricorsione (questa voce non ne attiverà altre)",
987- "Delay until recursion (this entry can only be activated on recursive checking)": "Ritardo fino alla ricorsione (questa voce può essere attivata solo durante il controllo ricorsivo)",
987+ "Delay until recursion": "Ritardo fino alla ricorsione",
988988 "What this keyword should mean to the AI, sent verbatim": "Cosa dovrebbe significare questa parola chiave per l'AI, inviata testualmente",
989989 "Filter to Character(s)": "Filtra per Personaggio(i)",
990990 "Character Exclusion": "Esclusione Personaggio",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Collegamento PNG diretto (fare riferimento a",
13771377 "char_import_7": "per gli host consentiti)",
13781378 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
1379- "char_import_9": "Personaggio Soulkyn (collegamento diretto)",
13801379 "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)",
13811380 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",
13821381 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",
public/locales/ja-jp.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( なし )",
984984 "Content": "内容",
985985 "Exclude from recursion": "再帰から除外",
986986 "Prevent further recursion (this entry will not activate others)": "それ以上の再帰を防止します(このエントリは他のエントリをアクティブにしません)",
987987 "Delay until recursion (this entry can only be activated on recursive checking)": "再帰まで遅延 (このエントリは再帰チェックでのみ有効になります)",
988988 "What this keyword should mean to the AI, sent verbatim": "このキーワードがAIにとって何を意味するか、そのまま送信されます",
989989 "Filter to Character(s)": "キャラクターにフィルター",
990990 "Character Exclusion": "キャラクターの除外",
@@ -1378,7 +1378,6 @@
13781378 "char_import_6": "直接PNGリンク(参照",
13791379 "char_import_7": "許可されたホストの場合)",
13801380 "char_import_8": "RisuRealm キャラクター (直接リンク)",
1381- "char_import_9": "Soulkyn キャラクター (直接リンク)",
13821381 "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)",
13831382 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
13841383 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
public/locales/ko-kr.json+2 -3
@@ -1000,8 +1000,8 @@
10001000 "( None )": "( 없음 )",
10011001 "Content": "콘텐츠",
10021002 "Exclude from recursion": "재귀에서 제외",
10031003 "Prevent further recursion (this entry will not activate others)": "추가 재귀 방지(이 항목은 다른 항목을 활성화하지 않습니다)",
1004- "Delay until recursion (this entry can only be activated on recursive checking)": "재귀까지 지연(이 항목은 재귀 검사에서만 활성화될 수 있음)",
1004+ "Delay until recursion": "재귀까지 지연",
10051005 "What this keyword should mean to the AI, sent verbatim": "AI에게 이 키워드가 무엇을 의미하는지를 원래 그대로 보냄",
10061006 "Filter to Character(s)": "캐릭터로 필터링",
10071007 "Character Exclusion": "캐릭터 제외",
@@ -1395,7 +1395,6 @@
13951395 "char_import_6": "직접 PNG 링크(참조",
13961396 "char_import_7": "허용된 호스트의 경우)",
13971397 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
1398- "char_import_9": "Soulkyn 캐릭터 (직접링크)",
13991398 "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)",
14001399 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",
14011400 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",
public/locales/nl-nl.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Geen )",
984984 "Content": "Inhoud",
985985 "Exclude from recursion": "Uitsluiten van recursie",
986986 "Prevent further recursion (this entry will not activate others)": "Voorkom verdere recursie (dit item activeert andere niet)",
987- "Delay until recursion (this entry can only be activated on recursive checking)": "Vertraging tot recursie (deze invoer kan alleen worden geactiveerd bij recursieve controle)",
987+ "Delay until recursion": "Vertraging tot recursie",
988988 "What this keyword should mean to the AI, sent verbatim": "Wat dit trefwoord voor de AI zou moeten betekenen, woordelijk verzonden",
989989 "Filter to Character(s)": "Filteren op personage(s)",
990990 "Character Exclusion": "Personage uitsluiting",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Directe PNG-link (zie",
13771377 "char_import_7": "voor toegestane hosts)",
13781378 "char_import_8": "RisuRealm-personage (directe link)",
1379- "char_import_9": "Soulkyn-personage (directe link)",
13801379 "char_import_10": "Perchance-personage (directe link of UUID + .gz)",
13811380 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",
13821381 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",
public/locales/pt-pt.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Nenhum )",
984984 "Content": "Conteúdo",
985985 "Exclude from recursion": "Excluir da recursão",
986986 "Prevent further recursion (this entry will not activate others)": "Impedir mais recursões (esta entrada não ativará outras)",
987987 "Delay until recursion (this entry can only be activated on recursive checking)": "Atraso até a recursão (esta entrada só pode ser ativada na verificação recursiva)",
988988 "What this keyword should mean to the AI, sent verbatim": "O que essa palavra-chave deve significar para a IA, enviada literalmente",
989989 "Filter to Character(s)": "Filtrar para Personagem(s)",
990990 "Character Exclusion": "Exclusão de personagem",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Link PNG direto (consulte",
13771377 "char_import_7": "para hosts permitidos)",
13781378 "char_import_8": "Personagem RisuRealm (link direto)",
1379- "char_import_9": "Personagem Soulkyn (link direto)",
13801379 "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)",
13811380 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",
13821381 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",
public/locales/ru-ru.json+53 -24
@@ -21,12 +21,10 @@
2121 "Maximum Temp": "Макс. температура",
2222 "Exponent": "Экспонента",
2323 "Mirostat Mode": "Режим",
2424 "Mirostat TauMirostat_Tau_desc": "TauЦелевая перплексия.",
2525 "Mirostat EtaMirostat_Eta_desc": "EtaСкорость обучения Mirostat.",
26- "Variability parameter for Mirostat outputs": "Вариативность для выходных данных Mirostat.",
27- "Learning rate of Mirostat": "Скорость обучения Mirostat.",
2826 "Temperature Last": "Температура последней",
2927 "LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены, что правильно их определяете.",
3028 "Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]",
3129 "Classifier Free Guidance. More helpful tip coming soon": "Classifier Free Guidance. Чуть позже опишем более подробно",
3230 "Scale": "Scale",
@@ -157,7 +155,7 @@
157155 "Advanced Formatting": "Расширенное форматирование",
158156 "Context Template": "Шаблон контекста",
159157 "Replace Macro in Stop Strings": "Заменять макросы в пользовательских стоп-строках",
160158 "Story String": "СтрокаОбщий историишаблон",
161159 "Example Separator": "Разделитель примеров сообщений",
162160 "Chat Start": "Начало чата",
163161 "Activation Regex": "Рег. выражение для активации",
@@ -301,7 +299,7 @@
301299 "Trim Incomplete Sentences": "Удалять неоконченные предложения",
302300 "Include Newline": "Добавлять новую строку",
303301 "Non-markdown strings": "Строки без разметки",
304302 "Replace Macro in Sequences": "ЗаменитьЗаменять макросы в строках",
305303 "Presets": "Пресеты",
306304 "Start Reply With": "Начинать ответ с",
307305 "Show reply prefix in chat": "Показывать префикс ответов в чате",
@@ -428,7 +426,7 @@
428426 "Persona Description": "Описание персоны",
429427 "Your Persona": "Ваша персона",
430428 "Show notifications on switching personas": "Показывать уведомления при смене персоны",
431429 "In Story String / Prompt Manager": "В строкеобщем историишаблоне / Менеджере промптов",
432430 "Top of Author's Note": "Сверху от заметок автора",
433431 "Bottom of Author's Note": "Снизу от заметок автора",
434432 "How do I use this?": "Как пользоваться?",
@@ -610,7 +608,7 @@
610608 "Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
611609 "Change persona image": "Сменить аватар персоны",
612610 "Delete persona": "Удалить персону",
613611 "Reduced Motion": "СокращениеУрезать анимацийанимации",
614612 "Auto-select": "Авто-выбор",
615613 "Automatically select a background based on the chat context": "Автоматический выбор фона в зависимости от контекста чата",
616614 "Filter": "Фильтр",
@@ -819,7 +817,7 @@
819817 "Chat CFG": "CFG для чата",
820818 "bg_chat_hint_1": "Здесь будут появляться фоны, сгенерированные расширением",
821819 "bg_chat_hint_2": ".",
822820 "Prevent further recursion (this entry will not activate others)": "Пресечь дальнейшую рекурсию (эта запись не будет активировать другие)",
823821 "Alert if your world info is greater than the allocated budget.": "Оповещать, если ваш мир выходит за выделенный бюджет.",
824822 "Convert to Persona": "Преобразовать в персону",
825823 "Link to Source": "Ссылка на источник",
@@ -963,7 +961,6 @@
963961 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
964962 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
965963 "char_import_7": ")",
966- "char_import_9": "Персонаж с Soulkyn (прямая ссылка)",
967964 "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)",
968965 "Grammar String": "Грамматика",
969966 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
@@ -1133,11 +1130,11 @@
11331130 "prompt_post_processing_none": "Отсутствует",
11341131 "Additional Parameters": "Доп. параметры",
11351132 "Include Body Parameters": "Добавить параметры в тело",
11361133 "custom_include_body_desc": "Эти параметры будут добавлены в тело запроса к API Chat Completion (YAML-объект)\n\nПример:\n- top_kntop_k: 20\n- repetition_penaltynrepetition_penalty: 1.1",
11371134 "Exclude Body Parameters": "Исключить параметры из тела",
11381135 "custom_exclude_body_desc": "Эти параметры будут исключены из тела запроса к API Chat Completion (YAML-массив)\n\nПример:\n- frequency_penalty\n- presence_penalty",
11391136 "Include Request Headers": "Добавить заголовки запроса",
11401137 "custom_include_headers_desc": "Дополнительные заголовки, добавляемые к запросу в API Chat Completion (YAML-объект)\n\nПример:\n- CustomHeadernCustomHeader: custom-value\n- AnotherHeadernAnotherHeader: custom-value",
11411138 "Groq API Key": "Ключ от API Groq",
11421139 "Groq Model": "Модель Groq",
11431140 "Perplexity API Key": "Ключ от API Perplexity",
@@ -1619,8 +1616,6 @@
16191616 "Do not proceed if you do not agree to this!": "Не продолжайте, если не согласны с этими условиями!",
16201617 "Relative (to other prompts in prompt manager) or In-chat @ Depth.": "Относительно других промтов в менеджере, либо на опред. глубину в чате.",
16211618 "prompt_manager_in_chat": "На глубине в чате",
1622- "01.AI API Key": "Ключ от API 01.AI",
1623- "01.AI Model": "Модель 01.AI",
16241619 "Load a custom asset list or select": "Загрузите набор внешних ресурсов или выберите",
16251620 "Install Extension": "Установить расширение",
16261621 "to install 3rd party extensions.": ", чтобы установить стороннее расширение.",
@@ -2052,8 +2047,8 @@
20522047 "Trigger %": "% срабатывания",
20532048 "Use global": "Глоб. настройка",
20542049 "Whole Words": "Целые слова",
20552050 "Non-recursable (will not be activated by another)": "Не рекурсивная (не активируется другими)",
20562051 "Delay until recursion (can only be activated on recursive checking)": "Рекурсивная (активируется только рекурсией)",
20572052 "Toggle entry's active state.": "Вкл/выкл запись.",
20582053 "Prioritize": "Важная",
20592054 "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Важная запись получает приоритет среди всех выбранных. Если важных записей несколько, выбирается та, у которой выше \"Очерёдность\".",
@@ -2275,7 +2270,17 @@
22752270 "characters": "перс.",
22762271 "character": "персонаж",
22772272 "in this group": "в группе",
22782273 "Chatting Since": "ПерваяПервый беседачат",
2274+ "Chat Time": "Общее время чата",
2275+ "Swipes": "Свайпов сделано",
2276+ "First Interaction": "Первое взаимодействие",
2277+ "User Messages": "Сообщений отправлено пользователем",
2278+ "Character Messages": "Сообщений отправлено персонажем",
2279+ "User Words": "Слов отправлено пользователем",
2280+ "Character Words": "Слов отправлено персонажем",
2281+ "stats_header_User": "пользователю",
2282+ "stats_header_Character": "персонажу",
2283+ "${0} Stats": "Статистика по ${0}",
22792284 "Context": "Контекст",
22802285 "Response": "Ответ",
22812286 "Connected": "Подключено",
@@ -2338,10 +2343,10 @@
23382343 "Can be used to automatically activate Quick Replies": "Используется для автоматической активации быстрых ответов (Quick Replies)",
23392344 "( None )": "(Отсутствует)",
23402345 "Tie this entry to specific characters or characters with specific tags": "Привязать запись к опред. персонажам или персонажам с заданными тегами",
23412346 "Move/Copy Entry to Another Lorebook": "Переместить/Скопировать запись в другой лорбук",
23422347 "There are no other lorebooks to move to.": "Некуда перемещать: не найдено других лорбуков.",
23432348 "Select Target Lorebook": "Выберите куда переместить",
23442349 "Move/Copy '${0}' to:": "Переместить/Скопировать '${0}' в:",
23452350 "Please select a target lorebook.": "Выберите лорбук, в который будет перемещена запись.",
23462351 "Scan depth cannot be negative": "Глубина сканирования не может быть отрицательной",
23472352 "Scan depth cannot exceed ${0}": "Глубина сканирования не может превышать ${0}",
@@ -2379,8 +2384,8 @@
23792384 "Delete Tag": "Удалить тег",
23802385 "Do you want to delete the tag": "Вы точно хотите удалить тег",
23812386 "If you want to merge all references to this tag into another tag, select it below:": "Если хотите заменить ссылки на этот тег на какой-то другой, то выберите из списка:",
23822387 "Open Folder (Show all characters even if not selected)": "Открытая папка (показатьпоказывает всех персонажей, включаядаже невыбранныхесли не выбрана)",
23832388 "Closed Folder (Hide all characters unless selected)": "Закрытая папка (скрыть всехпоказывает персонажей, крометолько выбранныхесли выбрана)",
23842389 "No Folder": "Не папка",
23852390 "Show only favorites": "Показать только избранных персонажей",
23862391 "Show only groups": "Показать только группы",
@@ -2530,7 +2535,7 @@
25302535 "Avoid sending sensitive information. Provider's outputs may include ads.": "Не отправляйте чувствительную информацию. Тексты от провайдера могут содержать рекламу.",
25312536 "Pollinations Model": "Модель Pollinations",
25322537 "Character details are hidden.": "Информация о персонаже скрыта.",
25332538 "Allow / Forbid the use of global styles for this character.": "Разрешить/запретить использование персонажемприменение глобальных стилей к этому персонажу.",
25342539 "Choose how to apply CSS style tags if they are defined in Creator's Notes of this character:": "Выберите, к чему применять CSS-теги, объявленные в Заметках создателя у этого персонажа:",
25352540 "CAUTION: Malformed styles may cause issues.": "ОСТОРОЖНО: некорректно размеченные стили могут приводить к неожиданному поведению.",
25362541 "Just to Creator's Notes": "Только к Заметкам создателя",
@@ -2554,5 +2559,29 @@
25542559 "If you want to do it later, select 'Regex' from the extensions menu.": "Если захотите включить их позже, выберите в меню расширений пункт \"Regex\".",
25552560 "Are you sure you want to delete the selected regex scripts?": "Вы точно хотите удалить выбранные скрипты?",
25562561 "Regex script \"${0}\" imported.": "Успешно импортирован скрипт ${0}",
25572562 "Invalid regex object.": "Некорректный объект рег. выражения.",
2563+ "Sequences as Stop Strings": "Строки из шаблона в качестве стоп-строк",
2564+ "Story String Sequences": "Строки для общего шаблона",
2565+ "Used in Default position only.": "Только для позиции \"По умолчанию\"",
2566+ "Story String Prefix": "Префикс для общего шаблона",
2567+ "Inserted before a Story String.": "Вставляется перед общим шаблоном",
2568+ "Story String Suffix": "Постфикс для общего шаблона",
2569+ "Inserted after a Story String.": "Вставляется после общего шаблона",
2570+ "Default (top of context)": "По умолчанию (наверху контекста)",
2571+ "Bind Model to Templates": "Связать модель с шаблоном",
2572+ "Persona Auto Selected": "Персона автоматически изменена",
2573+ "Auto-selected persona based on ${0} connection.<br />Your messages will now be sent as ${1}.": "Персона выбрана автоматически на основе следующего критерия: ${0}<br />Ваши сообщения будут отправляться от лица ${1}",
2574+ "Filter to specific generation types.": "Применять промпт только при определённых типах генерации.",
2575+ "Quiet": "Тихая",
2576+ "Swipe": "Свайп",
2577+ "Triggers": "Триггеры",
2578+ "All types (default)": "Всегда (по умолчанию)",
2579+ "Disabling is not recommended.": "Рекомендуем не отключать.",
2580+ "Toggle search bar": "Показать/скрыть поисковую строку",
2581+ "Fireworks AI API Key": "Ключ от API Fireworks AI",
2582+ "Fireworks AI Model": "Модель Fireworks AI",
2583+ "Moonshot AI API Key": "Ключ от API Moonshot AI",
2584+ "Moonshot AI Model": "Модель Moonshot AI",
2585+ "AI/ML API Key": "Ключ от API AI/ML",
2586+ "AI/ML Model": "Модель AI/ML"
25582587}
public/locales/uk-ua.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Жодного )",
984984 "Content": "Зміст",
985985 "Exclude from recursion": "Виключити з рекурсії",
986986 "Prevent further recursion (this entry will not activate others)": "Запобігти подальшій рекурсії (цей запис не активує інші)",
987- "Delay until recursion (this entry can only be activated on recursive checking)": "Затримка до рекурсії (цей запис можна активувати лише під час рекурсивної перевірки)",
987+ "Delay until recursion": "Затримка до рекурсії",
988988 "What this keyword should mean to the AI, sent verbatim": "Що це ключове слово повинно означати для ШІ, надсилається дослівно",
989989 "Filter to Character(s)": "Фільтр для персонажів",
990990 "Character Exclusion": "Виключення персонажів",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Пряме посилання на PNG (див",
13771377 "char_import_7": "для дозволених хостів)",
13781378 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
1379- "char_import_9": "Персонаж Soulkyn (пряме посилання)",
13801379 "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)",
13811380 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",
13821381 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",
public/locales/vi-vn.json+2 -3
@@ -983,8 +983,8 @@
983983 "( None )": "( Không có )",
984984 "Content": "Nội dung",
985985 "Exclude from recursion": "Loại trừ khỏi đệ quy",
986986 "Prevent further recursion (this entry will not activate others)": "Ngăn chặn đệ quy tiếp theo (mục này sẽ không kích hoạt mục khác)",
987987 "Delay until recursion (this entry can only be activated on recursive checking)": "Trì hoãn cho đến khi đệ quy (mục này chỉ có thể được kích hoạt khi kiểm tra đệ quy)",
988988 "What this keyword should mean to the AI, sent verbatim": "Nghĩa của từ khóa này đối với AI, gửi một cách nguyên văn",
989989 "Filter to Character(s)": "Lọc đến Nhân vật",
990990 "Character Exclusion": "Loại trừ Nhân vật",
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Nhập PNG trực tiếp (tham khảo",
13771377 "char_import_7": "đối với các máy chủ được phép)",
13781378 "char_import_8": "RisuRealm (URL trực tiếp)",
1379- "char_import_9": "Soulkyn (URL trực tiếp)",
13801379 "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)",
13811380 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",
13821381 "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.",
public/locales/zh-cn.json+5 -8
@@ -460,8 +460,6 @@
460460 "prompt_post_processing_merge": "合并相同角色连续的发言",
461461 "prompt_post_processing_semi": "半严格(强制对话角色交替)",
462462 "prompt_post_processing_strict": "严格(强制对话角色交替、用户最先)",
463- "01.AI API Key": "01.AI API密钥",
464- "01.AI Model": "01.AI 模型",
465463 "Additional Parameters": "附加参数",
466464 "Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "通过发送简短的测试消息验证您的API连接。请注意,您将因此消耗额度!",
467465 "Test Message": "发送测试消息",
@@ -1129,9 +1127,9 @@
11291127 "delay_until_recursion_level": "Defines delay levels for recursive scans.\r\rInitially, only the first level (smallest number) will match.\rOnce no matches are found, the next level becomes eligible for matching.\rThis repeats until all levels are checked.\r\rTied to the \"Delay until recursion\" setting.",
11301128 "Recursion Level": "递归等级",
11311129 "Content": "内容",
11321130 "Non-recursable (will not be activated by another)": "不可递归(不会被其他条目激活)",
11331131 "Prevent further recursion (this entry will not activate others)": "防止进一步递归(本条目将不会激活其他条目)",
11341132 "Delay until recursion (can only be activated on recursive checking)": "延迟到递归(本条目只能在递归检查时激活)",
11351133 "What this keyword should mean to the AI, sent verbatim": "这个关键词对AI的含义,逐字发送",
11361134 "Inclusion Group": "包含组",
11371135 "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "包含组可确保每次仅激活组中的一项(如果触发了多项)。支持多个逗号分隔的组。文档:世界信息 - 包含组",
@@ -1784,11 +1782,11 @@
17841782 "(Leave empty to auto-generate)": "(留空以自动生成)",
17851783 "The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.": "当前检查点将会被解绑并替换为新的检查点,但仍可在聊天管理中找到。",
17861784 "Include Body Parameters": "包括主体参数",
17871785 "custom_include_body_desc": "聊天完成请求主体中要包含的参数(YAML 对象)\n\n示例:\n- top_kntop_k:20\n- repetition_penaltynrepetition_penalty:1.1",
17881786 "Exclude Body Parameters": "排除主体参数",
17891787 "custom_exclude_body_desc": "要从聊天完成请求主体中排除的参数(YAML 数组)\n\n示例:\n- frequency_penalty\n- presence_penalty",
17901788 "Include Request Headers": "包含请求标头",
17911789 "custom_include_headers_desc": "聊天完成请求的附加标头(YAML 对象)\n\n示例:\n- CustomHeadernCustomHeader:自定义值\n- AnotherHeadernAnotherHeader:自定义值",
17921790 "Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.": "此类别中的功能仅供高级用户使用。如果您不确定后果,请不要点击任何内容。",
17931791 "THIS IS PERMANENT!": "此操作不可逆!",
17941792 "Also delete the chat files": "同时删除聊天文件",
@@ -1890,7 +1888,6 @@
18901888 "char_import_6": "被允许的PNG直链(请参阅",
18911889 "char_import_7": ")",
18921890 "char_import_8": "RisuRealm 角色(直链)",
1893- "char_import_9": "Soulkyn 角色(直链)",
18941891 "char_import_10": "Perchance 角色(直链或UUID + .gz)",
18951892 "Supports importing multiple characters.": "支持导入多个角色。",
18961893 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
public/locales/zh-tw.json+8 -12
@@ -983,9 +983,9 @@
983983 "Automation ID": "自動化 ID",
984984 "( None )": "(無)",
985985 "Content": "內容",
986986 "Exclude from recursion": "不可遞迴(此條目不會被其他條目啟用)",
987987 "Prevent further recursion (this entry will not activate others)": "防止進一步遞迴(此條目不會啟用其他條目)",
988988 "Delay until recursion (this entry can only be activated on recursive checking)": "延遲遞迴(此條目只能在遞迴檢查時啟用)",
989989 "What this keyword should mean to the AI, sent verbatim": "這個關鍵字對 AI 應意味著什麼,逐字傳送",
990990 "Filter to Character(s)": "角色篩選",
991991 "Character Exclusion": "角色排除",
@@ -1381,7 +1381,6 @@
13811381 "char_import_6": "直接 PNG 連結(請參閱",
13821382 "char_import_7": "對於允許的主機)",
13831383 "char_import_8": "RisuRealm 角色(直接連結)",
1384- "char_import_9": "Soulkyn 角色(直接連結)",
13851384 "char_import_10": "Perchance 角色(直接連結或 ID + .gz)",
13861385 "Supports importing multiple characters.": "支援匯入多個字元。",
13871386 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
@@ -1547,8 +1546,6 @@
15471546 "Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。",
15481547 "world_button_title": "角色背景設定\n「Shift+點選」可開啟「連結至世界資訊」彈窗",
15491548 "# of Beams": "# of Beams",
1550- "01.AI API Key": "01.AI API 金鑰",
1551- "01.AI Model": "01.AI 模型",
15521549 "Additional Parameters": "其他參數",
15531550 "All": "全部",
15541551 "Allow fallback models": "允許回退模型",
@@ -1603,7 +1600,6 @@
16031600 "Data Bank files": "資料庫文件",
16041601 "Default / Fallback Expression": "預設/回退表情",
16051602 "Delay": "延遲",
1606- "Delay until recursion (can only be activated on recursive checking)": "遞迴掃描延遲(僅在啟用遞迴掃描時可用)",
16071603 "Do not proceed if you do not agree to this!": "若不同意此條款,請勿繼續!",
16081604 "Edge Color": "邊緣顏色",
16091605 "Edit captions before saving": "在儲存前編輯註解",
@@ -1683,7 +1679,7 @@
16831679 "Names as Stop Strings": "將名稱用作停止字串",
16841680 "Never": "從不",
16851681 "NomicAI API Key": "NomicAI API 金鑰",
16861682 "Non-recursable (will not be activated by another)": "不可遞迴(不會被其他條目啟動)",
16871683 "None (disabled)": "無(已停用)",
16881684 "OK": "確定",
16891685 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "舊訊息會在聊天時逐步向量化。\n若要處理所有先前訊息,請點選下方按鈕。",
@@ -1911,11 +1907,11 @@
19111907 "chat_rename_3": "這將斷開各檢查點間的連結。",
19121908 "chat_rename_4": "無需在結尾加上 `.jsonl`。",
19131909 "Include Body Parameters": "包含請求主體參數",
19141910 "custom_include_body_desc": "包含在 Chat Completion 請求體中的參數(YAML 格式)\n\n範例:\n- top_kntop_k: 20\n- repetition_penaltynrepetition_penalty: 1.1",
19151911 "Exclude Body Parameters": "排除請求主體參數",
19161912 "custom_exclude_body_desc": "排除於 Chat Completion 請求體中的參數(YAML 格式)\n\n範例:\n- frequency_penalty\n- presence_penalty",
19171913 "Include Request Headers": "包含請求標頭(Request Headers)",
19181914 "custom_include_headers_desc": "新增於 Chat Completion 請求的自定義標頭(YAML 格式)\n\n範例:\n- CustomHeadernCustomHeader: custom-value\n- AnotherHeadernAnotherHeader: custom-value",
19191915 "THIS IS PERMANENT!": "這是「永久性」的!",
19201916 "Also delete the chat files": "同時刪除此聊天檔案",
19211917 "Are you sure you want to duplicate this character?": "您確定要複製該角色嗎?",
@@ -2537,7 +2533,7 @@
25372533 "Delete Persona": "刪除使用者角色",
25382534 "Duplicate Persona": "複製使用者角色",
25392535 "Expand and zoom": "展開並縮放",
25402536 "Move/Copy Entry to Another Lorebook": "將條目移動到其他知識書將條目移動/複製到其他知識書",
25412537 "Persona is locked to the current character": "使用者角色已綁定至目前角色",
25422538 "Persona is locked to the current chat": "使用者角色已綁定至目前聊天",
25432539 "Rename a prompt": "重新命名提示詞",
@@ -2597,7 +2593,7 @@
25972593 "Tie this entry to specific characters or characters with specific tags": "將此項綁定至特定角色或標籤",
25982594 "There are no other lorebooks to move to.": "暫無其他知識書可供移動。",
25992595 "Select Target Lorebook": "選擇目標知識書",
26002596 "Move/Copy '${0}' to:": "將「${0}」移動至移動/複製至:",
26012597 "Please select a target lorebook.": "請選擇目標知識書。",
26022598 "Scan depth cannot be negative": "掃描深度不能為負值",
26032599 "Scan depth cannot exceed ${0}": "掃描深度不能超過 ${0}",
public/script.js+124 -155
@@ -176,7 +176,7 @@ import {
176176 renderPaginationDropdown,
177177 paginationDropdownChangeHandler,
178178} from './scripts/utils.js';
179179import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids } from './scripts/constants.js';
180180
181181import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
182182import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
@@ -217,8 +217,8 @@ import {
217217 formatInstructModeChat,
218218 formatInstructModePrompt,
219219 formatInstructModeExamples,
220+ formatInstructModeStoryString,
220221 getInstructStoppingSequences,
221- formatInstructModeSystemPrompt,
222222} from './scripts/instruct-mode.js';
223223import { initLocales, t } from './scripts/i18n.js';
224224import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
@@ -364,7 +364,6 @@ let settingsReady = false;
364364let currentVersion = '0.0.0';
365365export let displayVersion = 'SillyTavern';
366366
367-let generatedPromptCache = '';
368367let generation_started = new Date();
369368/** @type {import('./scripts/char-data.js').v1CharData[]} */
370369export let characters = [];
@@ -1964,7 +1963,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
19641963 let avatarImg = getThumbnailUrl('persona', user_avatar);
19651964 const isSystem = mes.is_system;
19661965 const title = mes.title;
1967- generatedPromptCache = '';
19681966
19691967 //for non-user mesages
19701968 if (!mes['is_user']) {
@@ -2345,10 +2343,12 @@ export function getStoppingStrings(isImpersonate, isContinue) {
23452343 * @prop {number} [responseLength] Maximum response length. If unset, the global default value is used.
23462344 * @prop {number} [forceChId] Character ID to use for this generation run. Works in groups only.
23472345 * @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
2346+ * @prop {boolean} [removeReasoning] Parses and removes the reasoning block according to reasoning format preferences
2347+ * @prop {boolean} [trimToSentence] Whether to trim the response to the last complete sentence
23482348 * @param {GenerateQuietPromptParams} params Parameters for the quiet prompt generation
23492349 * @returns {Promise<string>} Generated text. If using structured output, will contain a serialized JSON object.
23502350 */
23512351export async function generateQuietPrompt({ quietPrompt = '', quietToLoud = false, skipWIAN = false, quietImage = null, quietName = null, responseLength = null, forceChId = null, jsonSchema = null, removeReasoning = true, trimToSentence = false } = {}) {
23522352 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
23532353 console.trace('generateQuietPrompt called with positional arguments. Please use an object instead.');
23542354 [quietPrompt, quietToLoud, skipWIAN, quietImage, quietName, responseLength, forceChId, jsonSchema] = arguments;
@@ -2372,8 +2372,10 @@ export async function generateQuietPrompt({ quietPrompt = '', quietToLoud = fals
23722372 TempResponseLength.save(main_api, responseLength);
23732373 eventHook = TempResponseLength.setupEventHook(main_api);
23742374 }
23752375 constlet result = await Generate('quiet', generateOptions);
2376- return removeReasoningFromString(result);
2376+ result = trimToSentence ? trimToEndSentence(result) : result;
2377+ result = removeReasoning ? removeReasoningFromString(result) : result;
2378+ return result;
23772379 } finally {
23782380 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
23792381 TempResponseLength.restore(main_api);
@@ -2688,7 +2690,7 @@ export function parseMesExamples(examplesStr, isInstruct) {
26882690 }
26892691
26902692 const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
26912693 const blockHeading = (main_api === 'openai' ? '<START>\n' : (exampleSeparator || (isInstruct) ? '<START>\n' : ''))exampleSeparator;
26922694 const splitExamples = examplesStr.split(/<START>/gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`);
26932695
26942696 return splitExamples;
@@ -2783,7 +2785,7 @@ class StreamingProcessor {
27832785
27842786 #updateMessageBlockVisibility() {
27852787 if (this.messageDom instanceof HTMLElement && Array.isArray(this.toolCalls) && this.toolCalls.length > 0) {
27862788 const shouldHide = ['', '...'].includes(this.result) && !this.reasoningHandler.reasoning;
27872789 this.messageDom.classList.toggle('displayNone', shouldHide);
27882790 }
27892791 }
@@ -2958,7 +2960,6 @@ class StreamingProcessor {
29582960 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
29592961 await saveChatConditional();
29602962 unblockGeneration();
2961- generatedPromptCache = '';
29622963
29632964 const isAborted = this.abortController.signal.aborted;
29642965 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
@@ -2973,7 +2974,6 @@ class StreamingProcessor {
29732974 this.isStopped = true;
29742975
29752976 this.markUIGenStopped();
2976- generatedPromptCache = '';
29772977 unblockGeneration();
29782978
29792979 const noEmitTypes = ['swipe', 'impersonate', 'continue'];
@@ -3108,7 +3108,7 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
31083108 // prepend system prompt, if provided
31093109 if (systemPrompt) {
31103110 systemPrompt = substituteParams(systemPrompt);
31113111 systemPrompt = isInstruct ? (formatInstructModeSystemPromptformatInstructModeStoryString(systemPrompt) + '\n') : systemPrompt.trim();
31123112 prompt.unshift({ role: 'system', content: systemPrompt });
31133113 }
31143114
@@ -3577,7 +3577,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
35773577 system = power_user.prefer_character_prompt && system
35783578 ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? ''))
35793579 : baseChatReplace(power_user.sysprompt.content, name1, name2);
35803580 system = isInstruct ? formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.sysprompt.content)) : system;
35813581 } else {
35823582 // Nullify if it's not enabled
35833583 system = '';
@@ -3591,13 +3591,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
35913591 if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) {
35923592 groupDepthPrompts.forEach((value, index) => {
35933593 const role = getExtensionPromptRoleByName(value.role);
35943594 setExtensionPrompt('DEPTH_PROMPT_' + inject_ids.DEPTH_PROMPT_INDEX(index), value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan, role);
35953595 });
35963596 } else {
35973597 const depthPromptText = charDepthPrompt || '';
35983598 const depthPromptDepth = characters[this_chid]?.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
35993599 const depthPromptRole = getExtensionPromptRoleByName(characters[this_chid]?.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default);
36003600 setExtensionPrompt('inject_ids.DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan, depthPromptRole);
36013601 }
36023602
36033603 // First message in fresh 1-on-1 chat reacts to user/character settings changes
@@ -3723,7 +3723,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37233723
37243724 // Add WI to prompt (and also inject WI to AN value via hijack)
37253725 // Make quiet prompt available for WIAN
37263726 setExtensionPrompt('inject_ids.QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
37273727 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();
37283728 /** @type {import('./scripts/world-info.js').WIGlobalScanData} */
37293729 const globalScanData = {
@@ -3736,7 +3736,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37363736 trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal',
37373737 };
37383738 const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);
37393739 setExtensionPrompt('inject_ids.QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);
37403740
37413741 // Add message example WI
37423742 for (const example of worldInfoExamples) {
@@ -3771,13 +3771,56 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37713771 if (Array.isArray(worldInfoDepth)) {
37723772 worldInfoDepth.forEach((e) => {
37733773 const joinedEntries = e.entries.join('\n');
37743774 setExtensionPrompt(`customDepthWI-${inject_ids.CUSTOM_WI_DEPTH_ROLE(e.depth}-${, e.role}`), joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role);
37753775 });
37763776 }
37773777 } else {
37783778 console.log('skipping WIAN');
37793779 }
37803780
3781+ // Collect before / after story string injections
3782+ const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT);
3783+ const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT);
3784+
3785+ const storyStringParams = {
3786+ description: description,
3787+ personality: personality,
3788+ persona: power_user.persona_description_position == persona_description_positions.IN_PROMPT ? persona : '',
3789+ scenario: scenario,
3790+ system: system,
3791+ char: name2,
3792+ user: name1,
3793+ wiBefore: worldInfoBefore,
3794+ wiAfter: worldInfoAfter,
3795+ loreBefore: worldInfoBefore,
3796+ loreAfter: worldInfoAfter,
3797+ anchorBefore: beforeScenarioAnchor.trim(),
3798+ anchorAfter: afterScenarioAnchor.trim(),
3799+ mesExamples: mesExamplesArray.join(''),
3800+ mesExamplesRaw: mesExamplesRawArray.join(''),
3801+ };
3802+
3803+ // Render the story string and combine with injections
3804+ const storyString = renderStoryString(storyStringParams);
3805+ let combinedStoryString = isInstruct ? formatInstructModeStoryString(storyString) : storyString;
3806+
3807+ // Inject the story string as in-chat prompt (if needed)
3808+ const applyStoryStringInject = main_api !== 'openai' && power_user.context.story_string_position === extension_prompt_types.IN_CHAT;
3809+ if (applyStoryStringInject) {
3810+ const depth = power_user.context.story_string_depth ?? 1;
3811+ const role = power_user.context.story_string_role ?? extension_prompt_roles.SYSTEM;
3812+ setExtensionPrompt(inject_ids.STORY_STRING, combinedStoryString, extension_prompt_types.IN_CHAT, depth, false, role);
3813+ // Remove to prevent duplication
3814+ combinedStoryString = '';
3815+ } else {
3816+ setExtensionPrompt(inject_ids.STORY_STRING, '', extension_prompt_types.IN_CHAT, 0);
3817+ }
3818+
3819+ // Story string rendered, safe to remove
3820+ if (power_user.strip_examples) {
3821+ mesExamplesArray = [];
3822+ }
3823+
37813824 // Inject all Depth prompts. Chat Completion does it separately
37823825 let injectedIndices = [];
37833826 if (main_api !== 'openai') {
@@ -3796,7 +3839,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37963839 coreChat.splice(coreChat.length - 1, 0, { mes: jailbreak, is_user: true });
37973840 }
37983841 else {
3842+ // This operation will result in the injectedIndices indexes being off by one
37993843 coreChat.push({ mes: jailbreak, is_user: true });
3844+ // Add +1 to the elements to correct for the new PHI/Jailbreak message.
3845+ injectedIndices.forEach((e, idx) => injectedIndices[idx] = e + 1);
38003846 }
38013847 }
38023848 }
@@ -3864,33 +3910,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38643910 userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, force_output_sequence.FIRST);
38653911 }
38663912
3867- // Call combined AN into Generate
3868- const beforeScenarioAnchor = (await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT)).trimStart();
3869- const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT);
3870-
3871- const storyStringParams = {
3872- description: description,
3873- personality: personality,
3874- persona: power_user.persona_description_position == persona_description_positions.IN_PROMPT ? persona : '',
3875- scenario: scenario,
3876- system: system,
3877- char: name2,
3878- user: name1,
3879- wiBefore: worldInfoBefore,
3880- wiAfter: worldInfoAfter,
3881- loreBefore: worldInfoBefore,
3882- loreAfter: worldInfoAfter,
3883- mesExamples: mesExamplesArray.join(''),
3884- mesExamplesRaw: mesExamplesRawArray.join(''),
3885- };
3886-
3887- const storyString = renderStoryString(storyStringParams);
3888-
3889- // Story string rendered, safe to remove
3890- if (power_user.strip_examples) {
3891- mesExamplesArray = [];
3892- }
3893-
38943913 let oaiMessages = [];
38953914 let oaiMessageExamples = [];
38963915
@@ -3910,9 +3929,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39103929
39113930 async function getMessagesTokenCount() {
39123931 const encodeString = [
39133932 beforeScenarioAnchorcombinedStoryString,
3914- storyString,
3915- afterScenarioAnchor,
39163933 examplesString,
39173934 userAlignmentMessage,
39183935 chatString,
@@ -4044,7 +4061,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
40444061 is_send_press = true;
40454062 }
40464063
40474064 let generatedPromptCache += cyclePrompt || '';
40484065 if (generatedPromptCache.length == 0 || type === 'continue') {
40494066 console.debug('generating prompt');
40504067 chatString = '';
@@ -4149,34 +4166,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
41494166 return lastMesString;
41504167 }
41514168
4152- // Clean up the already generated prompt for seamless addition
4153- function cleanupPromptCache(promptCache) {
4154- // Remove the first occurrance of character's name
4155- if (promptCache.trimStart().startsWith(`${name2}:`)) {
4156- promptCache = promptCache.replace(`${name2}:`, '').trimStart();
4157- }
4158-
4159- // Remove the first occurrance of prompt bias
4160- if (promptCache.trimStart().startsWith(promptBias)) {
4161- promptCache = promptCache.replace(promptBias, '');
4162- }
4163-
4164- // Add a space if prompt cache doesn't start with one
4165- if (!/^\s/.test(promptCache) && !isInstruct) {
4166- promptCache = ' ' + promptCache;
4167- }
4168-
4169- return promptCache;
4170- }
4171-
41724169 async function checkPromptSize() {
41734170 console.debug('---checking Prompt size');
41744171 setPromptString();
41754172 const jointMessages = mesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join('');
41764173 const prompt = [
41774174 beforeScenarioAnchorcombinedStoryString,
4178- storyString,
4179- afterScenarioAnchor,
41804175 mesExmString,
41814176 addChatsPreamble(addChatsSeparator(jointMessages)),
41824177 '\n',
@@ -4259,11 +4254,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42594254 }
42604255 }
42614256
4262- // Prune from prompt cache if it exists
4263- if (generatedPromptCache.length !== 0) {
4264- generatedPromptCache = cleanupPromptCache(generatedPromptCache);
4265- }
4266-
42674257 // Flattens the multiple prompt objects to a string.
42684258 const combine = () => {
42694259 // Right now, everything is suffixed with a newline
@@ -4275,14 +4265,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42754265 // add chat preamble
42764266 mesSendString = addChatsPreamble(mesSendString);
42774267
42784268 let combinedPrompt = beforeScenarioAnchor +[
4279- storyString +
4269+ combinedStoryString,
4280- afterScenarioAnchor +
4270+ mesExmString,
4281- mesExmString +
4271+ mesSendString,
4282- mesSendString +
4272+ generatedPromptCache,
4283- generatedPromptCache;
4273+ ].join('').replace(/\r/gm, '');
4284-
4285- combinedPrompt = combinedPrompt.replace(/\r/gm, '');
42864274
42874275 if (power_user.collapse_newlines) {
42884276 combinedPrompt = collapseNewlines(combinedPrompt);
@@ -4406,7 +4394,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44064394 await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data);
44074395
44084396 if (dryRun) {
4409- generatedPromptCache = '';
44104397 return Promise.resolve();
44114398 }
44124399
@@ -4500,7 +4487,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45004487 if (canPerformToolCalls && isStreamFinished && isStreamWithToolCalls) {
45014488 const lastMessage = chat[chat.length - 1];
45024489 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
45034490 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);
45044491 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
45054492 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
45064493 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
@@ -4510,7 +4497,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45104497 ToolManager.showToolCallError(invocationResult.errors);
45114498 }
45124499 unblockGeneration(type);
4513- generatedPromptCache = '';
45144500 streamingProcessor = null;
45154501 return;
45164502 }
@@ -4556,7 +4542,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45564542 // if an error was returned in data (textgenwebui), show it and throw it
45574543 if (data.error) {
45584544 unblockGeneration(type);
4559- generatedPromptCache = '';
45604545
45614546 if (data?.response) {
45624547 toastr.error(data.response, t`API Error`, { preventDuplicates: true });
@@ -4566,7 +4551,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45664551
45674552 if (jsonSchema) {
45684553 unblockGeneration(type);
4569- generatedPromptCache = '';
45704554 return extractJsonFromData(data);
45714555 }
45724556
@@ -4609,7 +4593,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
46094593
46104594 if (isImpersonate) {
46114595 $('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles: true }));
4612- generatedPromptCache = '';
46134596 await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
46144597 }
46154598 else if (type == 'quiet') {
@@ -4631,7 +4614,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
46314614
46324615 if (canPerformToolCalls) {
46334616 const hasToolCalls = ToolManager.hasToolCalls(data);
46344617 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;
46354618 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
46364619 const invocationResult = await ToolManager.invokeFunctionTools(data);
46374620 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
@@ -4641,7 +4624,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
46414624 ToolManager.showToolCallError(invocationResult.errors);
46424625 }
46434626 unblockGeneration(type);
4644- generatedPromptCache = '';
46454627 return;
46464628 }
46474629
@@ -4685,8 +4667,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
46854667 toastr.error(exception.error.message, t`Text generation error`, { timeOut: 10000, extendedTimeOut: 20000 });
46864668 }
46874669
4688- generatedPromptCache = '';
4689-
46904670 unblockGeneration(type);
46914671 console.log(exception);
46924672 streamingProcessor = null;
@@ -4757,7 +4737,7 @@ async function doChatInject(messages, isContinue) {
47574737
47584738 if (roleMessages.length) {
47594739 const depth = isContinue && i === 0 ? 1 : i;
47604740 const injectIdx = Math.min(depth + totalInsertedMessages, messages.length);
47614741 messages.splice(injectIdx, 0, ...roleMessages);
47624742 totalInsertedMessages += roleMessages.length;
47634743 injectedIndices.push(...Array.from({ length: roleMessages.length }, (_, i) => injectIdx + i));
@@ -4771,7 +4751,7 @@ async function doChatInject(messages, isContinue) {
47714751function flushWIDepthInjections() {
47724752 //prevent custom depth WI entries (which have unique random key names) from duplicating
47734753 for (const key of Object.keys(extension_prompts)) {
47744754 if (key.startsWith('customDepthWI'inject_ids.CUSTOM_WI_DEPTH)) {
47754755 delete extension_prompts[key];
47764756 }
47774757 }
@@ -5314,28 +5294,29 @@ function parseAndSaveLogprobs(data, continueFrom) {
53145294 * @returns {string} Extracted message
53155295 */
53165296export function extractMessageFromData(data, activeApi = null) {
5317- if (typeof data === 'string') {
5297+ function getResult() {
5318- return data;
5298+ if (typeof data === 'string') {
5319- }
5299+ return data;
5300+ }
53205301
53215302 switch (activeApi ?? main_api) {
53225303 case 'kobold':
53235304 return data.results[0].text;
53245305 case 'koboldhorde':
53255306 return data.text;
53265307 case 'textgenerationwebui':
5327- return data.choices?.[0]?.text
5308+ return data.choices?.[0]?.text ?? data.choices?.[0]?.message?.content ?? data.content ?? data.response ?? '';
5328- ?? data.choices?.[0]?.message?.content
5309+ case 'novel':
53295310 ??return data.contentoutput;
5330- ?? data.response
5311+ case 'openai':
5331- ?? '';
5312+ return data?.content?.find(p => p.type === 'text')?.text ?? data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
5332- case 'novel':
5313+ default:
5333- return data.output;
5314+ return '';
5334- case 'openai':
5315+ }
5335- return data?.content?.find(p => p.type === 'text')?.text ?? data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
5336- default:
5337- return '';
53385316 }
5317+
5318+ const result = getResult();
5319+ return Array.isArray(result) ? result.map(x => x.text).filter(x => x).join('') : result;
53395320}
53405321
53415322/**
@@ -5541,30 +5522,19 @@ export function cleanUpMessage({ getMessage, isImpersonate, isContinue, displayI
55415522 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
55425523 }
55435524 }
5544- if (isInstruct && power_user.instruct.input_sequence && isImpersonate) {
5525+
5545- //getMessage = getMessage.replaceAll(power_user.instruct.input_sequence, '');
5526+ // Remove instruct sequences leaking to the output
5546- power_user.instruct.input_sequence.split('\n')
5527+ if (isInstruct && power_user.instruct.sequences_as_stop_strings) {
5547- .filter(line => line.trim() !== '')
5528+ const sequences = [
5548- .forEach(line => {
5529+ { value: power_user.instruct.input_sequence, apply: isImpersonate && isNotEmpty(power_user.instruct.input_sequence) },
5549- getMessage = getMessage.replaceAll(line, '');
5530+ { value: power_user.instruct.output_sequence, apply: !isImpersonate && isNotEmpty(power_user.instruct.output_sequence) },
5550- });
5531+ { value: power_user.instruct.last_output_sequence, apply: !isImpersonate && isNotEmpty(power_user.instruct.last_output_sequence) },
5551- }
5532+ ];
5552- if (isInstruct && power_user.instruct.output_sequence && !isImpersonate) {
5533+ for (const seq of sequences.filter(s => s.apply)) {
5553- //getMessage = getMessage.replaceAll(power_user.instruct.output_sequence, '');
5534+ seq.value.split('\n').filter(line => line.trim() !== '').forEach(line => { getMessage = getMessage.replaceAll(line, ''); });
5554- power_user.instruct.output_sequence.split('\n')
5535+ }
5555- .filter(line => line.trim() !== '')
5556- .forEach(line => {
5557- getMessage = getMessage.replaceAll(line, '');
5558- });
5559- }
5560- if (isInstruct && power_user.instruct.last_output_sequence && !isImpersonate) {
5561- //getMessage = getMessage.replaceAll(power_user.instruct.last_output_sequence, '');
5562- power_user.instruct.last_output_sequence.split('\n')
5563- .filter(line => line.trim() !== '')
5564- .forEach(line => {
5565- getMessage = getMessage.replaceAll(line, '');
5566- });
55675536 }
5537+
55685538 // clean-up group message from excessive generations
55695539 if (selected_group) {
55705540 getMessage = cleanGroupMessage(getMessage);
@@ -6448,11 +6418,7 @@ async function read_avatar_load(input) {
64486418 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
64496419 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url').toString()), {
64506420 method: 'GET',
64516421 cache: 'no-cachereload',
6452- headers: {
6453- 'pragma': 'no-cache',
6454- 'cache-control': 'no-cache',
6455- },
64566422 });
64576423
64586424 const messages = $('.mes').toArray();
@@ -7703,7 +7669,7 @@ export function getExtensionPromptRoleByName(roleName) {
77037669 */
77047670export function removeDepthPrompts() {
77057671 for (const key of Object.keys(extension_prompts)) {
77067672 if (key.startsWith('inject_ids.DEPTH_PROMPT')) {
77077673 delete extension_prompts[key];
77087674 }
77097675 }
@@ -8252,11 +8218,14 @@ function openAlternateGreetings() {
82528218 */
82538219function addAlternateGreeting(template, greeting, index, getArray, popup) {
82548220 const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
82558221 greetingBlock.find('.alternate_greeting_text').on('input', async function () {
8256- const value = $(this).val();
8222+ .attr('id', `alternate_greeting_${index}`)
8257- const array = getArray();
8223+ .on('input', async function () {
8258- array[index] = value;
8224+ const value = $(this).val();
8259- }).val(greeting);
8225+ const array = getArray();
8226+ array[index] = value;
8227+ }).val(greeting);
8228+ greetingBlock.find('.editor_maximize').attr('data-for', `alternate_greeting_${index}`);
82608229 greetingBlock.find('.greeting_index').text(index + 1);
82618230 greetingBlock.find('.delete_alternate_greeting').on('click', async function (event) {
82628231 event.preventDefault();
@@ -9217,6 +9186,7 @@ async function removeCharacterFromUI() {
92179186 await getCharacters();
92189187 await printMessages();
92199188 saveSettingsDebounced();
9189+ await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
92209190}
92219191
92229192/**
@@ -9535,7 +9505,7 @@ jQuery(async function () {
95359505 return;
95369506 }
95379507
95389508 const scrollIsAtBottom = Math.abs(chatElementScroll.scrollHeight - chatElementScroll.clientHeight - chatElementScroll.scrollTop) < 15;
95399509
95409510 // Resume autoscroll if the user scrolls to the bottom
95419511 if (scrollLock && scrollIsAtBottom) {
@@ -9547,8 +9517,7 @@ jQuery(async function () {
95479517 scrollLock = true;
95489518 }
95499519 };
95509520 chatElementScroll.addEventListener('wheelscroll', chatScrollHandler, { passive: true });
9551- chatElementScroll.addEventListener('touchmove', chatScrollHandler, { passive: true });
95529521
95539522 $(document).on('click', '.mes', function () {
95549523 //when a 'delete message' parent div is clicked
@@ -10854,7 +10823,7 @@ jQuery(async function () {
1085410823 data.set(file, characters[this_chid].avatar);
1085510824 await processDroppedFiles([file], data);
1085610825 await openCharacterChat(chatFile);
1085710826 await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'no-cachereload' });
1085810827 } catch {
1085910828 toastr.error('Failed to replace the character card.', 'Something went wrong');
1086010829 }
public/scripts/BulkEditOverlay.js+9 -6
@@ -101,7 +101,7 @@ class CharacterContextMenu {
101101 * @param {number} characterId
102102 * @returns {Promise<void>}
103103 */
104104 static persona = async (characterId) => void(await convertCharacterToPersona(characterId));
105105
106106 /**
107107 * Delete one or more characters,
@@ -342,7 +342,7 @@ class BulkTagPopupHandler {
342342
343343 for (const characterId of this.characterIds) {
344344 for (const tag of mutualTags) {
345345 removeTagFromMap(tag.id, characterId.toString());
346346 }
347347 }
348348
@@ -370,7 +370,7 @@ class BulkEditOverlayState {
370370 * Implement a SingletonPattern, allowing access to the group overlay instance
371371 * from everywhere via (new CharacterGroupOverlay())
372372 *
373373 * @type {Readonly<BulkEditOverlay>}
374374 */
375375let bulkEditOverlayInstance = null;
376376
@@ -590,7 +590,8 @@ class BulkEditOverlay {
590590 this.selectState();
591591 } else if (this.state === BulkEditOverlayState.select) {
592592 this.#contextMenuOpen = true;
593- CharacterContextMenu.show(...this.#getContextMenuPosition(event));
593+ const [x, y] = this.#getContextMenuPosition(event);
594+ CharacterContextMenu.show(x, y);
594595 }
595596 }
596597
@@ -694,7 +695,7 @@ class BulkEditOverlay {
694695 const characterId = Number(character.getAttribute('data-chid'));
695696
696697 const select = !this.selectedCharacters.includes(characterId);
697698 const legacyBulkEditCheckbox = /** @type {HTMLInputElement} */ (character.querySelector('.' + BulkEditOverlay.legacySelectedClass));
698699
699700 if (select) {
700701 character.classList.add(BulkEditOverlay.selectedClass);
@@ -753,7 +754,8 @@ class BulkEditOverlay {
753754
754755 handleContextMenuShow = (event) => {
755756 event.preventDefault();
756757 CharacterContextMenu.show(...const [x,y] = this.#getContextMenuPosition(event));
758+ CharacterContextMenu.show(x, y);
757759 this.#contextMenuOpen = true;
758760 };
759761
@@ -761,6 +763,7 @@ class BulkEditOverlay {
761763 let contextMenu = document.getElementById(BulkEditOverlay.contextMenuId);
762764 if (false === contextMenu.contains(event.target)) {
763765 CharacterContextMenu.hide();
766+ this.#contextMenuOpen = false;
764767 }
765768 };
766769
public/scripts/RossAscends-mods.js+39 -40
@@ -111,12 +111,12 @@ export function humanizeGenTime(total_gen_time) {
111111 let hours = time_spent % 24;
112112 time_spent = Math.floor(time_spent / 24);
113113 let days = time_spent;
114114 time_spentlet result = '';
115115 if (days > 0) { time_spentresult += `${days} Days, `; }
116116 if (hours > 0) { time_spentresult += `${hours} Hours, `; }
117117 if (minutes > 0) { time_spentresult += `${minutes} Minutes, `; }
118118 time_spentresult += `${seconds} Seconds`;
119119 return time_spentresult;
120120}
121121
122122/**
@@ -276,40 +276,36 @@ export async function RA_CountCharTokens() {
276276 * The character or group is selected (clicked) if it is found.
277277 */
278278async function RA_autoloadchat() {
279- if (document.querySelector('#rm_print_characters_block .character_select') !== null) {
279+ // active character is the name, we should look it up in the character list and get the id
280- // active character is the name, we should look it up in the character list and get the id
280+ if (active_character !== null && active_character !== undefined) {
281- if (active_character !== null && active_character !== undefined) {
281+ const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
282- const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
282+ if (active_character_id !== -1) {
283- if (active_character_id !== -1) {
283+ await selectCharacterById(active_character_id);
284- await selectCharacterById(active_character_id);
284+
285-
285+ // Do a little tomfoolery to spoof the tag selector
286- // Do a little tomfoolery to spoof the tag selector
286+ const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
287- const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
287+ applyTagsOnCharacterSelect.call(selectedCharElement);
288- applyTagsOnCharacterSelect.call(selectedCharElement);
288+ } else {
289- } else {
289+ setActiveCharacter(null);
290290 setActiveCharacter saveSettingsDebounced(null);
291- saveSettingsDebounced();
291+ console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
292- console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
293- }
294292 }
293+ }
295294
296295 if (active_group !== null && active_group !== undefined) {
297296 if (active_character) {
298297 console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
298+ setActiveGroup(null);
299+ saveSettingsDebounced();
300+ } else {
301+ const result = await openGroupById(String(active_group));
302+ if (!result) {
299303 setActiveGroup(null);
300304 saveSettingsDebounced();
301- } else {
305+ console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
302- const result = await openGroupById(String(active_group));
303- if (!result) {
304- setActiveGroup(null);
305- saveSettingsDebounced();
306- console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
307- }
308306 }
309307 }
310-
308+ }
311- // if the character list hadn't been loaded yet, try again.
312- } else { setTimeout(RA_autoloadchat, 100); }
313309}
314310
315311export async function favsToHotswap() {
@@ -406,11 +402,13 @@ function RA_autoconnect(PrevApi) {
406402 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
407403 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
408404 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
409- || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
410405 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
411406 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
412407 || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
413408 || (secret_state[SECRET_KEYS.AIMLAPI] && oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI)
409+ || (secret_state[SECRET_KEYS.MOONSHOT] && oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT)
410+ || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS)
411+ || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
414412 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
415413 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
416414 ) {
@@ -483,7 +481,7 @@ export function dragElement($elmnt) {
483481 let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
484482 let height, width, top, left, right, bottom,
485483 maxX, maxY, winHeight, winWidth,
486484 topbar, topBarFirstX, topBarLastY;
487485
488486 const elmntName = $elmnt.attr('id');
489487 const elmntNameEscaped = $.escapeSelector(elmntName);
@@ -529,7 +527,8 @@ export function dragElement($elmnt) {
529527 return;
530528 }
531529
532530 const styleelement = getComputedStyle/** @type {HTMLElement} */ ($target[0]);
531+ const style = getComputedStyle(element);
533532 height = parseInt(style.height);
534533 width = parseInt(style.width);
535534 top = parseInt(style.top);
@@ -572,9 +571,9 @@ export function dragElement($elmnt) {
572571 if (top + $elmnt.height() >= winHeight) $elmnt.css('height', winHeight - top - 1 + 'px');
573572 if (left + $elmnt.width() >= winWidth) $elmnt.css('width', winWidth - left - 1 + 'px');
574573 }
575574 //if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX) {
576575 // $elmnt.css('width', width - 1 + 'px');
577576 // }
578577 $elmnt.css({ left, top });
579578 $elmnt.off('mouseup').on('mouseup', () => {
580579 if (
public/scripts/bookmarks.js+7 -4
@@ -14,7 +14,9 @@ import {
1414} from '../script.js';
1515import { humanizedDateTime } from './RossAscends-mods.js';
1616import {
17+ DEFAULT_AUTO_MODE_DELAY,
1718 group_activation_strategy,
19+ group_generation_mode,
1820 groups,
1921 openGroupById,
2022 openGroupChat,
@@ -295,8 +297,6 @@ export async function convertSoloToGroupChat() {
295297 const chatName = humanizedDateTime();
296298 const chats = [chatName];
297299 const members = [character.avatar];
298- const activationStrategy = group_activation_strategy.NATURAL;
299- const allowSelfResponses = false;
300300 const favChecked = character.fav || character.fav == 'true';
301301 /** @type {any} */
302302 const metadata = Object.assign({}, chat_metadata);
@@ -309,13 +309,16 @@ export async function convertSoloToGroupChat() {
309309 name: name,
310310 members: members,
311311 avatar_url: avatar,
312312 allow_self_responses: activationStrategyfalse,
313313 activation_strategy: allowSelfResponsesgroup_activation_strategy.NATURAL,
314314 disabled_members: [],
315315 chat_metadata: metadata,
316316 fav: favChecked,
317317 chat_id: chatName,
318318 chats: chats,
319+ hideMutedSprites: false,
320+ generation_mode: group_generation_mode.SWAP,
321+ auto_mode_delay: DEFAULT_AUTO_MODE_DELAY,
319322 }),
320323 });
321324
public/scripts/chat-templates.js+11 -0
@@ -90,10 +90,21 @@ const hash_derivations = {
9090 'aab20feb9bc6881f941ea649356130ffbc4943b3c2577c0991e1fba90de5a0fc':
9191 'Moonshot AI'
9292 ,
93+
94+ // gpt-oss (unsloth)
95+ '70da0d2348e40aaf8dad05f04a316835fd10547bd7e3392ce337e4c79ba91c01':
96+ 'OpenAI Harmony'
97+ ,
98+
99+ // gpt-oss (ggml-org)
100+ 'a4c9919cbbd4acdd51ccffe22da049264b1b73e59055fa58811a99efbd7c8146':
101+ 'OpenAI Harmony'
102+ ,
93103};
94104
95105const substr_derivations = [
96106 ['Moonshot AI', ['<|im_user|>user<|im_middle|>', '<|im_assistant|>assistant<|im_middle|>', '<|im_end|>']],
107+ ['OpenAI Harmony', ['<|start|>user<|message|>', '<|start|>assistant<|channel|>final<|message|>', '<|end|>']],
97108
98109 // Generic cases
99110 ['ChatML', ['<|im_start|>user', '<|im_start|>assistant', '<|im_end|>']],
public/scripts/chats.js+5 -3
@@ -14,7 +14,6 @@ import {
1414 name1,
1515 name2,
1616 reloadCurrentChat,
17- saveChatDebounced,
1817 saveSettingsDebounced,
1918 showSwipeButtons,
2019 this_chid,
@@ -163,7 +162,7 @@ export async function hideChatMessageRange(start, end, unhide, nameFitler = null
163162 hideSwipeButtons();
164163 showSwipeButtons();
165164
166165 saveChatDebouncedawait saveChatConditional();
167166}
168167
169168/**
@@ -1925,7 +1924,10 @@ export function initChatUtilities() {
19251924 embedMessageFile(messageId, messageBlock);
19261925 });
19271926
19281927 $(document).on('click', '.editor_maximize', async function (e) {
1928+ e.preventDefault();
1929+ e.stopPropagation();
1930+
19291931 const broId = $(this).attr('data-for');
19301932 const bro = $(`#${broId}`);
19311933 const contentEditable = bro.is('[contenteditable]');
public/scripts/constants.js+25 -0
@@ -40,3 +40,28 @@ export const GENERATION_TYPE_TRIGGERS = [
4040 'regenerate',
4141 'quiet',
4242];
43+
44+/**
45+ * Known injection IDs and helper functions for system extensions handling.
46+ */
47+export const inject_ids = {
48+ STORY_STRING: '__STORY_STRING__',
49+ QUIET_PROMPT: 'QUIET_PROMPT',
50+ DEPTH_PROMPT: 'DEPTH_PROMPT',
51+ DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`,
52+ CUSTOM_WI_DEPTH: 'customDepthWI',
53+ CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`,
54+};
55+
56+export const COMETAPI_IGNORE_PATTERNS = [
57+ // Image generation models
58+ 'dall-e', 'dalle', 'midjourney', 'mj_', 'stable-diffusion', 'sd-',
59+ 'flux-', 'playground-v', 'ideogram', 'recraft-', 'black-forest-labs',
60+ '/recraft-v3', 'recraftv3', 'stability-ai/', 'sdxl',
61+ // Audio generation models
62+ 'suno_', 'tts', 'whisper',
63+ // Video generation models
64+ 'runway', 'luma_', 'luma-', 'veo', 'kling_', 'minimax_video', 'hunyuan-t1',
65+ // Utility models
66+ 'embedding', 'search-gpts', 'files_retrieve', 'moderation',
67+];
public/scripts/custom-request.js+4 -4
@@ -256,7 +256,7 @@ export class TextCompletionService {
256256 message.role,
257257 message.content,
258258 message.role === 'user',
259- false,
259+ message.role === 'system',
260260 undefined,
261261 undefined,
262262 undefined,
@@ -389,8 +389,8 @@ export class TextCompletionService {
389389
390390 // Initialize base payload with common parameters
391391 let payload = {
392392 'temperature': settings.temp >= 0 ? Number(settings.temp) : undefined,
393393 'min_p': settings.min_p >= 0 ? Number(settings.min_p) : undefined,
394394 };
395395
396396 // Remove undefined values to avoid API errors
@@ -573,7 +573,7 @@ export class ChatCompletionService {
573573
574574 // Initialize base payload with common parameters
575575 const payload = {
576576 temperature: settings.temperature >= 0 ? Number(settings.temperature) : undefined,
577577 };
578578
579579 // Remove undefined values to avoid API errors
public/scripts/extensions-slashcommands.js+5 -4
@@ -9,7 +9,8 @@ import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './uti
99
1010/**
1111 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
12- * @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise<string>}
12+ * @typedef {import('./slash-commands/SlashCommand.js').NamedArguments | import('./slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsAssignment
13+ * @returns {(args: NamedArgumentsAssignment, extensionName: string | SlashCommandClosure) => Promise<string>}
1314 */
1415function getExtensionActionCallback(action) {
1516 return async (args, extensionName) => {
@@ -20,7 +21,7 @@ function getExtensionActionCallback(action) {
2021 return '';
2122 }
2223
2324 const reload = !isFalseBoolean(args?.reload?.toString());
2425 const internalExtensionName = findExtension(extensionName);
2526 if (!internalExtensionName) {
2627 toastr.warning(`Extension ${extensionName} does not exist.`);
@@ -186,8 +187,8 @@ export function registerExtensionSlashCommands() {
186187 if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
187188 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
188189
189190 const action = isTrueBoolean(args?.state?.toString()) ? 'enable' :
190191 isFalseBoolean(args?.state?.toString()) ? 'disable' :
191192 'toggle';
192193
193194 return await getExtensionActionCallback(action)(args, extensionName);
public/scripts/extensions.js+13 -3
@@ -1,10 +1,10 @@
11import { DOMPurify, Popper } from '../lib.js';
22
33import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';
44import { showLoader } from './loader.js';
55import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
66import { renderTemplate, renderTemplateAsync } from './templates.js';
77import { delay, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
88import { getContext } from './st-context.js';
99import { isAdmin } from './user.js';
1010import { addLocaleData, getCurrentLocale, t } from './i18n.js';
@@ -387,6 +387,7 @@ async function getManifests(names) {
387387 */
388388async function activateExtensions() {
389389 extensionLoadErrors.clear();
390+ const clientVersion = CLIENT_VERSION.split(':')[1];
390391 const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));
391392 const extensionNames = extensions.map(x => x[0]);
392393 const promises = [];
@@ -396,11 +397,17 @@ async function activateExtensions() {
396397 const manifest = entry[1];
397398 const extrasRequirements = manifest.requires;
398399 const extensionDependencies = manifest.dependencies;
400+ const minClientVersion = manifest.minimum_client_version;
399401 const displayName = manifest.display_name || name;
400402
401403 if (activeExtensions.has(name)) {
402404 continue;
403405 }
406+ // Client version requirement: pass if 'minimum_client_version' is undefined or null.
407+ let meetsClientMinimumVersion = true;
408+ if (minClientVersion !== undefined) {
409+ meetsClientMinimumVersion = versionCompare(clientVersion, minClientVersion);
410+ }
404411
405412 // Module requirements: pass if 'requires' is undefined, null, or not an array; check subset if it's an array
406413 let meetsModuleRequirements = true;
@@ -438,7 +445,7 @@ async function activateExtensions() {
438445
439446 const isDisabled = extension_settings.disabledExtensions.includes(name);
440447
441448 if (meetsModuleRequirements && meetsExtensionDeps && meetsClientMinimumVersion && !isDisabled) {
442449 try {
443450 console.debug('Activating extension', name);
444451 const promise = addExtensionLocale(name, manifest).finally(() =>
@@ -465,6 +472,9 @@ async function activateExtensions() {
465472 console.warn(t`Extension "${name}" did not load. Missing required extensions: "${missingDependencies.join(', ')}"`);
466473 extensionLoadErrors.add(t`Extension "${displayName}" did not load. Missing required extensions: "${missingDependencies.join(', ')}"`);
467474 }
475+ } else if (!meetsClientMinimumVersion && !isDisabled) {
476+ console.warn(t`Extension "${name}" did not load. Requires ST client version ${minClientVersion}, but current version is ${clientVersion}.`);
477+ extensionLoadErrors.add(t`Extension "${displayName}" did not load. Requires ST client version ${minClientVersion}, but current version is ${clientVersion}.`);
468478 }
469479 }
470480
public/scripts/extensions/caption/index.js+46 -35
@@ -435,10 +435,10 @@ jQuery(async function () {
435435
436436 const chatCompletionApis = {
437437 'openrouter': SECRET_KEYS.OPENROUTER,
438- 'zerooneai': SECRET_KEYS.ZEROONEAI,
439438 'groq': SECRET_KEYS.GROQ,
440439 'cohere': SECRET_KEYS.COHERE,
441440 'aimlapi': SECRET_KEYS.AIMLAPI,
441+ 'moonshot': SECRET_KEYS.MOONSHOT,
442442 };
443443
444444 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -488,8 +488,13 @@ jQuery(async function () {
488488 $('#img_file').on('change', (e) => onSelectImage(e.originalEvent, '', false));
489489 }
490490 async function switchMultimodalBlocks() {
491491 await addOpenRouterModelsaddRemoteEndpointModels();
492492 const isMultimodal = extension_settings.caption.source === 'multimodal';
493+ if (!extension_settings.caption.multimodal_model) {
494+ const dropdown = $('#caption_multimodal_model');
495+ const options = dropdown.find(`option[data-type="${extension_settings.caption.multimodal_api}"]`);
496+ extension_settings.caption.multimodal_model = String(options.first().val());
497+ }
493498 $('#caption_multimodal_block').toggle(isMultimodal);
494499 $('#caption_prompt_block').toggle(isMultimodal);
495500 $('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
@@ -504,35 +509,42 @@ jQuery(async function () {
504509 const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT });
505510 $('#caption_container').append(html);
506511 }
507- async function addOpenRouterModels() {
512+
508- const dropdown = document.getElementById('caption_multimodal_model');
513+ async function addRemoteEndpointModels() {
509- if (!(dropdown instanceof HTMLSelectElement)) {
514+ async function processEndpoint(api, url) {
510- return;
515+ const dropdown = document.getElementById('caption_multimodal_model');
511- }
516+ if (!(dropdown instanceof HTMLSelectElement)) {
512- if (extension_settings.caption.source !== 'multimodal' || extension_settings.caption.multimodal_api !== 'openrouter') {
517+ return;
513- return;
518+ }
514- }
519+ if (extension_settings.caption.source !== 'multimodal' || extension_settings.caption.multimodal_api !== api) {
515- const options = Array.from(dropdown.options);
520+ return;
516- const response = await fetch('/api/openrouter/models/multimodal', {
521+ }
517- method: 'POST',
522+ const options = Array.from(dropdown.options);
518- headers: getRequestHeaders(),
523+ const response = await fetch(url, {
519- });
524+ method: 'POST',
520- if (!response.ok) {
525+ headers: getRequestHeaders(),
521- return;
522- }
523- const modelIds = await response.json();
524- if (Array.isArray(modelIds) && modelIds.length > 0) {
525- modelIds.forEach((modelId) => {
526- if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId)) {
527- return;
528- }
529- const option = document.createElement('option');
530- option.value = modelId;
531- option.textContent = modelId;
532- option.dataset.type = 'openrouter';
533- dropdown.add(option);
534526 });
527+ if (!response.ok) {
528+ return;
529+ }
530+ const modelIds = await response.json();
531+ if (Array.isArray(modelIds) && modelIds.length > 0) {
532+ modelIds.sort().forEach((modelId) => {
533+ if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId)) {
534+ return;
535+ }
536+ const option = document.createElement('option');
537+ option.value = modelId;
538+ option.textContent = modelId;
539+ option.dataset.type = api;
540+ dropdown.add(option);
541+ });
542+ }
535543 }
544+
545+ await processEndpoint('openrouter', '/api/openrouter/models/multimodal');
546+ await processEndpoint('aimlapi', '/api/backends/chat-completions/aimlapi/models/multimodal');
547+ await processEndpoint('pollinations', '/api/backends/chat-completions/pollinations/models/multimodal');
536548 }
537549
538550 await addSettings();
@@ -550,9 +562,9 @@ jQuery(async function () {
550562 $('#caption_prompt').val(extension_settings.caption.prompt);
551563 $('#caption_template').val(extension_settings.caption.template);
552564 $('#caption_refine_mode').on('input', onRefineModeInput);
553565 $('#caption_source').on('change', async () => {
554566 extension_settings.caption.source = String($('#caption_source').val());
555567 await switchMultimodalBlocks();
556568 saveSettingsDebounced();
557569 });
558570 $('#caption_prompt').on('input', () => {
@@ -581,13 +593,12 @@ jQuery(async function () {
581593 $('#ollama_download_model').trigger('click');
582594 $('#dialogue_popup_input').val(presetModel);
583595 });
584596 $('#caption_multimodal_api').on('change', async () => {
585597 const api = String($('#caption_multimodal_api').val());
586- const model = String($(`#caption_multimodal_model option[data-type="${api}"]`).first().val());
587598 extension_settings.caption.multimodal_api = api;
588599 extension_settings.caption.multimodal_model = model'';
600+ await switchMultimodalBlocks();
589601 saveSettingsDebounced();
590- switchMultimodalBlocks();
591602 });
592603 $('#caption_multimodal_model').on('change', () => {
593604 extension_settings.caption.multimodal_model = String($('#caption_multimodal_model').val());
public/scripts/extensions/caption/settings.html+19 -58
@@ -17,7 +17,6 @@
1717 <div class="flex1 flex-container flexFlowColumn flexNoGap">
1818 <label for="caption_multimodal_api" data-i18n="API">API</label>
1919 <select id="caption_multimodal_api" class="flex1 text_pole">
20- <option value="zerooneai">01.AI (Yi)</option>
2120 <option value="aimlapi">AI/ML API</option>
2221 <option value="anthropic">Anthropic</option>
2322 <option value="cohere">Cohere</option>
@@ -28,6 +27,7 @@
2827 <option value="koboldcpp">KoboldCpp</option>
2928 <option value="llamacpp">llama.cpp</option>
3029 <option value="mistral">MistralAI</option>
30+ <option value="moonshot">Moonshot AI</option>
3131 <option value="ollama">Ollama</option>
3232 <option value="openai">OpenAI</option>
3333 <option value="openrouter">OpenRouter</option>
@@ -40,8 +40,10 @@
4040 <div class="flex1 flex-container flexFlowColumn flexNoGap">
4141 <label for="caption_multimodal_model" data-i18n="Model">Model</label>
4242 <select id="caption_multimodal_model" class="flex1 text_pole">
43+ <!-- AI/ML API, OpenRouter, Pollinations are added externally by JavaScript -->
4344 <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option>
4445 <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option>
46+ <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option>
4547 <option data-type="mistral" value="pixtral-12b-latest">pixtral-12b-latest</option>
4648 <option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option>
4749 <option data-type="mistral" value="pixtral-large-latest">pixtral-large-latest</option>
@@ -52,7 +54,17 @@
5254 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
5355 <option data-type="mistral" value="mistral-medium-latest">mistral-medium-latest</option>
5456 <option data-type="mistral" value="mistral-medium-2505">mistral-medium-2505</option>
5557 <option data-type="zerooneaimistral" value="yimistral-visionmedium-2508">yimistral-visionmedium-2508</option>
58+ <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option>
59+ <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option>
60+ <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option>
61+ <option data-type="openai" value="gpt-5">gpt-5</option>
62+ <option data-type="openai" value="gpt-5-2025-08-07">gpt-5-2025-08-07</option>
63+ <option data-type="openai" value="gpt-5-chat-latest">gpt-5-chat-latest</option>
64+ <option data-type="openai" value="gpt-5-mini">gpt-5-mini</option>
65+ <option data-type="openai" value="gpt-5-mini-2025-08-07">gpt-5-mini-2025-08-07</option>
66+ <option data-type="openai" value="gpt-5-nano">gpt-5-nano</option>
67+ <option data-type="openai" value="gpt-5-nano-2025-08-07">gpt-5-nano-2025-08-07</option>
5668 <option data-type="openai" value="gpt-4.1">gpt-4.1</option>
5769 <option data-type="openai" value="gpt-4.1-2025-04-14">gpt-4.1-2025-04-14</option>
5870 <option data-type="openai" value="gpt-4.1-mini">gpt-4.1-mini</option>
@@ -73,6 +85,8 @@
7385 <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
7486 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
7587 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
88+ <option data-type="anthropic" value="claude-opus-4-1">claude-opus-4-1</option>
89+ <option data-type="anthropic" value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option>
7690 <option data-type="anthropic" value="claude-opus-4-0">claude-opus-4-0</option>
7791 <option data-type="anthropic" value="claude-opus-4-20250514">claude-opus-4-20250514</option>
7892 <option data-type="anthropic" value="claude-sonnet-4-0">claude-sonnet-4-0</option>
@@ -94,6 +108,7 @@
94108 <option data-type="google" value="gemini-2.5-flash">gemini-2.5-flash</option>
95109 <option data-type="google" value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
96110 <option data-type="google" value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
111+ <option data-type="google" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
97112 <option data-type="google" value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option>
98113 <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>
99114 <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option>
@@ -116,6 +131,7 @@
116131 <option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
117132 <option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
118133 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
134+ <option data-type="google" value="gemini-1.5-flash-8b-latest">gemini-1.5-flash-8b-latest</option>
119135 <option data-type="google" value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
120136 <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
121137 <option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
@@ -128,56 +144,13 @@
128144 <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option>
129145 <option data-type="vertexai" value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
130146 <option data-type="vertexai" value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
147+ <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
131148 <option data-type="vertexai" value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option>
132149 <option data-type="vertexai" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
133150 <option data-type="vertexai" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
134151 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
135152 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
136153 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
137- <option data-type="aimlapi" value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
138- <option data-type="aimlapi" value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
139- <option data-type="aimlapi" value="gpt-4o">gpt-4o</option>
140- <option data-type="aimlapi" value="gpt-4o-mini">gpt-4o-mini</option>
141- <option data-type="aimlapi" value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>
142- <option data-type="aimlapi" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
143- <option data-type="aimlapi" value="gpt-4-turbo">gpt-4-turbo</option>
144- <option data-type="aimlapi" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
145- <option data-type="aimlapi" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
146- <option data-type="aimlapi" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
147- <option data-type="aimlapi" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
148- <option data-type="aimlapi" value="google/gemini-1.5-flash">google/gemini-1.5-flash</option>
149- <option data-type="aimlapi" value="google/gemini-1.5-pro">google/gemini-1.5-pro</option>
150- <option data-type="aimlapi" value="google/gemini-2.0-flash">google/gemini-2.0-flash</option>
151- <option data-type="aimlapi" value="google/gemini-2.0-flash-exp">google/gemini-2.0-flash-exp</option>
152- <option data-type="aimlapi" value="google/gemini-2.5-flash-preview">google/gemini-2.5-flash-preview</option>
153- <option data-type="aimlapi" value="meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo">meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo</option>
154- <option data-type="aimlapi" value="meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo">meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo</option>
155- <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
156- <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
157- <option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>
158- <option data-type="openrouter" value="openai/gpt-4o-2024-08-06">openai/gpt-4o-2024-08-06</option>
159- <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
160- <option data-type="openrouter" value="openai/gpt-4o-mini">openai/gpt-4o-mini</option>
161- <option data-type="openrouter" value="openai/gpt-4o-mini-2024-07-18">openai/gpt-4o-mini-2024-07-18</option>
162- <option data-type="openrouter" value="openai/chatgpt-4o-latest">openai/chatgpt-4o-latest</option>
163- <option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
164- <option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
165- <option data-type="openrouter" value="anthropic/claude-3.5-sonnet">anthropic/claude-3.5-sonnet</option>
166- <option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
167- <option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
168- <option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
169- <option data-type="openrouter" value="anthropic/claude-3.5-sonnet:beta">anthropic/claude-3.5-sonnet:beta</option>
170- <option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
171- <option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
172- <option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
173- <option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
174- <option data-type="openrouter" value="google/gemini-flash-8b-1.5-exp">google/gemini-flash-8b-1.5-exp</option>
175- <option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
176- <option data-type="openrouter" value="google/gemini-flash-1.5-exp">google/gemini-flash-1.5-exp</option>
177- <option data-type="openrouter" value="google/gemini-pro-1.5">google/gemini-pro-1.5</option>
178- <option data-type="openrouter" value="google/gemini-pro-1.5-exp">google/gemini-pro-1.5-exp</option>
179- <option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
180- <option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
181154 <option data-type="ollama" value="ollama_current" data-i18n="currently_selected">[Currently selected]</option>
182155 <option data-type="ollama" value="bakllava">bakllava</option>
183156 <option data-type="ollama" value="llava">llava</option>
@@ -192,18 +165,6 @@
192165 <option data-type="xai" value="grok-4-0709">grok-4-0709</option>
193166 <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>
194167 <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>
195- <option data-type="pollinations" value="openai">openai</option>
196- <option data-type="pollinations" value="openai-fast">openai-fast</option>
197- <option data-type="pollinations" value="openai-large">openai-large</option>
198- <option data-type="pollinations" value="openai-roblox">openai-roblox</option>
199- <option data-type="pollinations" value="mistral">mistral</option>
200- <option data-type="pollinations" value="unity">unity</option>
201- <option data-type="pollinations" value="mirexa">mirexa</option>
202- <option data-type="pollinations" value="searchgpt">searchgpt</option>
203- <option data-type="pollinations" value="evil">evil</option>
204- <option data-type="pollinations" value="phi">phi</option>
205- <option data-type="pollinations" value="sur">sur</option>
206- <option data-type="pollinations" value="bidara">bidara</option>
207168 </select>
208169 </div>
209170 <div data-type="ollama">
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+6 -0
@@ -202,6 +202,7 @@ export class QuickReplyApi {
202202 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
203203 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
204204 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
205+ * @param {boolean} [props.executeBeforeGeneration] whether to execute the quick reply before message generation
205206 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
206207 * @returns {QuickReply} the new quick reply
207208 */
@@ -217,6 +218,7 @@ export class QuickReplyApi {
217218 executeOnChatChange,
218219 executeOnGroupMemberDraft,
219220 executeOnNewChat,
221+ executeBeforeGeneration,
220222 automationId,
221223 } = {}) {
222224 const set = this.getSetByName(setName);
@@ -236,6 +238,7 @@ export class QuickReplyApi {
236238 qr.executeOnChatChange = executeOnChatChange ?? false;
237239 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
238240 qr.executeOnNewChat = executeOnNewChat ?? false;
241+ qr.executeBeforeGeneration = executeBeforeGeneration ?? false;
239242 qr.automationId = automationId ?? '';
240243 qr.onUpdate();
241244 return qr;
@@ -259,6 +262,7 @@ export class QuickReplyApi {
259262 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
260263 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
261264 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
265+ * @param {boolean} [props.executeBeforeGeneration] whether to execute the quick reply before message generation
262266 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
263267 * @returns {QuickReply} the altered quick reply
264268 */
@@ -275,6 +279,7 @@ export class QuickReplyApi {
275279 executeOnChatChange,
276280 executeOnGroupMemberDraft,
277281 executeOnNewChat,
282+ executeBeforeGeneration,
278283 automationId,
279284 } = {}) {
280285 const qr = this.getQrByLabel(setName, label);
@@ -293,6 +298,7 @@ export class QuickReplyApi {
293298 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
294299 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
295300 qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
301+ qr.executeBeforeGeneration = executeBeforeGeneration ?? qr.executeBeforeGeneration;
296302 qr.automationId = automationId ?? qr.automationId;
297303 qr.onUpdate();
298304 return qr;
public/scripts/extensions/quick-reply/html/qrEditor.html+4 -0
@@ -116,6 +116,10 @@
116116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
117117 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
118118 </label>
119+ <label class="checkbox_label">
120+ <input type="checkbox" id="qr--executeBeforeGeneration" >
121+ <span><i class="fa-solid fa-fw fa-paper-plane"></i><span data-i18n="Execute before message generation">Execute before message generation</span></span>
122+ </label>
119123 <div class="flex-container alignItemsBaseline flexFlowColumn flexNoGap" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered.">
120124 <small data-i18n="Automation ID:">Automation ID</small>
121125 <input type="text" id="qr--automationId" class="text_pole flex1" placeholder="( None )">
public/scripts/extensions/quick-reply/html/settings.html+13 -0
@@ -41,6 +41,19 @@
4141
4242 <hr>
4343
44+ <div id="qr--character">
45+ <div class="qr--head">
46+ <div class="qr--title" data-i18n="Character Quick Reply Sets">Character Quick Reply Sets</div>
47+ <small data-i18n="(Private)">(Private)</small>
48+ <div class="qr--actions">
49+ <div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--character-setListAdd" title="Add quick reply set"></div>
50+ </div>
51+ </div>
52+ <div id="qr--character-setList" class="qr--setList"></div>
53+ </div>
54+
55+ <hr>
56+
4457 <div id="qr--editor">
4558 <div class="qr--head">
4659 <div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div>
public/scripts/extensions/quick-reply/index.js+83 -4
@@ -1,4 +1,4 @@
11import { chat, chat_metadata, eventSource, event_types, getRequestHeaders, this_chid, characters } from '../../../script.js';
22import { extension_settings } from '../../extensions.js';
33import { QuickReplyApi } from './api/QuickReplyApi.js';
44import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
@@ -10,6 +10,7 @@ import { SlashCommandHandler } from './src/SlashCommandHandler.js';
1010import { ButtonUi } from './src/ui/ButtonUi.js';
1111import { SettingsUi } from './src/ui/SettingsUi.js';
1212import { debounceAsync } from '../../utils.js';
13+import { selected_group } from '../../group-chats.js';
1314export { debounceAsync };
1415
1516
@@ -39,6 +40,8 @@ const defaultSettings = {
3940let isReady = false;
4041/** @type {Function[]}*/
4142let executeQueue = [];
43+/** @type {string}*/
44+let lastCharId;
4245/** @type {QuickReplySettings}*/
4346let settings;
4447/** @type {SettingsUi} */
@@ -82,6 +85,7 @@ const loadSets = async () => {
8285 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
8386 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
8487 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
88+ qr.executeBeforeGeneration = slot.autoExecute_beforeGeneration ?? false;
8589 qr.automationId = slot.automationId ?? '';
8690 qr.contextList = (slot.contextMenu ?? []).map(it=>({
8791 set: it.preset,
@@ -123,6 +127,8 @@ const loadSettings = async () => {
123127 }
124128 try {
125129 settings = QuickReplySettings.from(extension_settings.quickReplyV2);
130+ settings.config.scope = 'global';
131+ settings.config.onUpdate = () => settings.save();
126132 } catch (ex) {
127133 settings = QuickReplySettings.from(defaultSettings);
128134 }
@@ -138,8 +144,34 @@ const executeIfReadyElseQueue = async (functionToCall, args) => {
138144 }
139145};
140146
147+const handleCharChange = () => {
148+ if (lastCharId === this_chid) return;
141149
150+ // Unload the old character's config and update the character ID cache.
151+ settings.charConfig = null;
152+ lastCharId = this_chid;
142153
154+ // If no character is loaded, there's nothing more to do.
155+ /** @type {import('../../char-data.js').v1CharData} */
156+ const character = characters[this_chid];
157+ if (!character || selected_group) {
158+ return;
159+ }
160+
161+ // Get the character-specific config from the local settings storage.
162+ let charConfig = settings.characterConfigs[character.avatar];
163+
164+ // If no config exists for this character, create a new one.
165+ if (!charConfig) {
166+ charConfig = QuickReplyConfig.from({ setList: [] });
167+ settings.characterConfigs[character.avatar] = charConfig;
168+ }
169+
170+ charConfig.scope = 'character';
171+ // The main settings save function will handle persistence.
172+ charConfig.onUpdate = () => settings.save();
173+ settings.charConfig = charConfig;
174+};
143175
144176const init = async () => {
145177 await loadSets();
@@ -154,8 +186,12 @@ const init = async () => {
154186 settings.onSave = ()=>buttons.refresh();
155187
156188 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
157- let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
189+ let qr = [
158- .map(it=>it.set.qrList)
190+ ...settings.config.setList,
191+ ...(settings.chatConfig?.setList ?? []),
192+ ...(settings.charConfig?.setList ?? []),
193+ ]
194+ .map(it => it.set.qrList)
159195 .flat()
160196 .find(it=>it.label == name)
161197 ;
@@ -199,10 +235,38 @@ const finalizeInit = async () => {
199235};
200236await init();
201237
238+const purgeCharacterQuickReplySets = ({ character }) => {
239+ // Remove the character's Quick Reply Sets from the settings.
240+ const avatar = character?.avatar;
241+ if (avatar && avatar in settings.characterConfigs) {
242+ log(`Purging Quick Reply Sets for character: ${avatar}`);
243+ delete settings.characterConfigs[avatar];
244+ settings.save();
245+ }
246+};
247+
248+const updateCharacterQuickReplySets = (oldAvatar, newAvatar) => {
249+ // Update the character's Quick Reply Sets in the settings.
250+ if (oldAvatar && newAvatar && oldAvatar !== newAvatar) {
251+ log(`Updating Quick Reply Sets for character: ${oldAvatar} -> ${newAvatar}`);
252+ if (settings.characterConfigs[oldAvatar]) {
253+ settings.characterConfigs[newAvatar] = settings.characterConfigs[oldAvatar];
254+ delete settings.characterConfigs[oldAvatar];
255+ settings.save();
256+ }
257+ }
258+};
259+
202260const onChatChanged = async (chatIdx) => {
203261 log('CHAT_CHANGED', chatIdx);
262+
263+ handleCharChange();
264+
204265 if (chatIdx) {
205266 settings.const chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
267+ chatConfig.scope = 'chat';
268+ chatConfig.onUpdate = () => settings.save();
269+ settings.chatConfig = chatConfig;
206270 } else {
207271 settings.chatConfig = null;
208272 }
@@ -212,6 +276,8 @@ const onChatChanged = async (chatIdx) => {
212276 await autoExec.handleChatChanged();
213277};
214278eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args));
279+eventSource.on(event_types.CHARACTER_DELETED, purgeCharacterQuickReplySets);
280+eventSource.on(event_types.CHARACTER_RENAMED, updateCharacterQuickReplySets);
215281
216282const onUserMessage = async () => {
217283 await autoExec.handleUser();
@@ -242,3 +308,16 @@ const onNewChat = async () => {
242308 await autoExec.handleNewChat();
243309};
244310eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
311+
312+const onBeforeGeneration = async (_generationType, _options = {}, isDryRun = false) => {
313+ if (isDryRun) {
314+ log('Before-generation hook skipped due to dryRun.');
315+ return;
316+ }
317+ if (selected_group && this_chid === undefined) {
318+ log('Before-generation hook skipped for event before group wrapper.');
319+ return;
320+ }
321+ await autoExec.handleBeforeGeneration();
322+};
323+eventSource.on(event_types.GENERATION_AFTER_COMMANDS, (...args) => executeIfReadyElseQueue(onBeforeGeneration, args));
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+32 -32
@@ -36,58 +36,51 @@ export class AutoExecuteHandler {
3636 }
3737
3838
39+ getCommands(eventName) {
40+ const getFromConfig = (config) => {
41+ // This safely handles cases where a link exists but the set hasn't been loaded (link.set is null)
42+ return config?.setList?.map(link => link.set ? link.set.qrList.filter(qr => qr[eventName]) : [])?.flat() ?? [];
43+ };
44+ return [
45+ ...getFromConfig(this.settings.config),
46+ ...getFromConfig(this.settings.chatConfig),
47+ ...getFromConfig(this.settings.charConfig),
48+ ];
49+ }
50+
3951 async handleStartup() {
4052 if (!this.checkExecute()) return;
41- const qrList = [
53+ await this.performAutoExecute(this.getCommands('executeOnStartup'));
42- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(),
43- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []),
44- ];
45- await this.performAutoExecute(qrList);
4654 }
4755
4856 async handleUser() {
4957 if (!this.checkExecute()) return;
50- const qrList = [
58+ await this.performAutoExecute(this.getCommands('executeOnUser'));
51- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(),
52- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []),
53- ];
54- await this.performAutoExecute(qrList);
5559 }
5660
5761 async handleAi() {
5862 if (!this.checkExecute()) return;
59- const qrList = [
63+ await this.performAutoExecute(this.getCommands('executeOnAi'));
60- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(),
61- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []),
62- ];
63- await this.performAutoExecute(qrList);
6464 }
6565
6666 async handleChatChanged() {
6767 if (!this.checkExecute()) return;
68- const qrList = [
68+ await this.performAutoExecute(this.getCommands('executeOnChatChange'));
69- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(),
70- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []),
71- ];
72- await this.performAutoExecute(qrList);
7369 }
7470
7571 async handleGroupMemberDraft() {
7672 if (!this.checkExecute()) return;
77- const qrList = [
73+ await this.performAutoExecute(this.getCommands('executeOnGroupMemberDraft'));
78- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft)).flat(),
79- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft))?.flat() ?? []),
80- ];
81- await this.performAutoExecute(qrList);
8274 }
8375
8476 async handleNewChat() {
8577 if (!this.checkExecute()) return;
86- const qrList = [
78+ await this.performAutoExecute(this.getCommands('executeOnNewChat'));
87- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(),
79+ }
88- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []),
80+
89- ];
81+ async handleBeforeGeneration() {
9082 awaitif (!this.performAutoExecutecheckExecute(qrList)) return;
83+ await this.performAutoExecute(this.getCommands('executeBeforeGeneration'));
9184 }
9285
9386 /**
@@ -98,9 +91,16 @@ export class AutoExecuteHandler {
9891 const automationIds = entries.map(entry => entry.automationId).filter(Boolean);
9992 if (automationIds.length === 0) return;
10093
94+ const getFromConfig = (config) => {
95+ return config?.setList
96+ ?.map(link => link.set ? link.set.qrList.filter(qr => qr.automationId && automationIds.includes(qr.automationId)) : [])
97+ ?.flat() ?? [];
98+ };
99+
101100 const qrList = [
102- ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId))).flat(),
101+ ...getFromConfig(this.settings.config),
103- ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId)))?.flat() ?? []),
102+ ...getFromConfig(this.settings.chatConfig),
103+ ...getFromConfig(this.settings.charConfig),
104104 ];
105105
106106 await this.performAutoExecute(qrList);
public/scripts/extensions/quick-reply/src/QuickReply.js+24 -0
@@ -46,6 +46,7 @@ export class QuickReply {
4646 /**@type {boolean}*/ executeOnChatChange = false;
4747 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
4848 /**@type {boolean}*/ executeOnNewChat = false;
49+ /**@type {boolean}*/ executeBeforeGeneration = false;
4950 /**@type {string}*/ automationId = '';
5051
5152 /**@type {function}*/ onExecute;
@@ -434,6 +435,7 @@ export class QuickReply {
434435 this.updateLabel(label.value);
435436 });
436437 let switcherList;
438+ // @ts-ignore
437439 dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{
438440 if (switcherList) {
439441 switcherList.remove();
@@ -618,7 +620,9 @@ export class QuickReply {
618620 accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
619621 updateSyntaxEnabled();
620622 });
623+ // @ts-ignore
621624 if (navigator.keyboard) {
625+ // @ts-ignore
622626 navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
623627 } else {
624628 dom.querySelector('#qr--modal-commentKey').closest('small').remove();
@@ -781,6 +785,7 @@ export class QuickReply {
781785 message.addEventListener('wheel', (evt)=>{
782786 updateScrollDebounced(evt);
783787 });
788+ // @ts-ignore
784789 message.addEventListener('scroll', (evt)=>{
785790 updateScrollDebounced();
786791 });
@@ -1061,6 +1066,13 @@ export class QuickReply {
10611066 this.updateContext();
10621067 });
10631068 /**@type {HTMLInputElement}*/
1069+ const executeBeforeGeneration = dom.querySelector('#qr--executeBeforeGeneration');
1070+ executeBeforeGeneration.checked = this.executeBeforeGeneration;
1071+ executeBeforeGeneration.addEventListener('click', ()=>{
1072+ this.executeBeforeGeneration = executeBeforeGeneration.checked;
1073+ this.updateContext();
1074+ });
1075+ /**@type {HTMLInputElement}*/
10641076 const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat');
10651077 executeOnNewChat.checked = this.executeOnNewChat;
10661078 executeOnNewChat.addEventListener('click', ()=>{
@@ -1117,6 +1129,7 @@ export class QuickReply {
11171129 /**@type {HTMLTextAreaElement} */
11181130 const inputOg = document.querySelector('#send_textarea');
11191131 const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1132+ // @ts-ignore
11201133 inputMirror.value = inputOg.value;
11211134 const inputOgMo = new MutationObserver(muts=>{
11221135 if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) {
@@ -1125,10 +1138,12 @@ export class QuickReply {
11251138 });
11261139 inputOgMo.observe(document.body, { childList:true });
11271140 const inputOgListener = ()=>{
1141+ // @ts-ignore
11281142 inputMirror.value = inputOg.value;
11291143 };
11301144 inputOg.addEventListener('input', inputOgListener);
11311145 inputMirror.addEventListener('input', ()=>{
1146+ // @ts-ignore
11321147 inputOg.value = inputMirror.value;
11331148 });
11341149
@@ -1172,15 +1187,19 @@ export class QuickReply {
11721187 isResizing = true;
11731188 evt.preventDefault();
11741189 resizeStart = evt.x;
1190+ // @ts-ignore
11751191 wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
11761192 const dragListener = debounce((evt)=>{
11771193 const w = wStart + resizeStart - evt.x;
1194+ // @ts-ignore
11781195 dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
11791196 }, 5);
11801197 window.addEventListener('pointerup', ()=>{
1198+ // @ts-ignore
11811199 window.removeEventListener('pointermove', dragListener);
11821200 isResizing = false;
11831201 }, { once:true });
1202+ // @ts-ignore
11841203 window.addEventListener('pointermove', dragListener);
11851204 });
11861205
@@ -1282,6 +1301,7 @@ export class QuickReply {
12821301 syntax.addEventListener('wheel', (evt)=>{
12831302 updateScrollDebounced(evt);
12841303 });
1304+ // @ts-ignore
12851305 syntax.addEventListener('scroll', (evt)=>{
12861306 updateScrollDebounced();
12871307 });
@@ -1414,8 +1434,11 @@ export class QuickReply {
14141434 let i = 0;
14151435 let unnamed = this.debugController.unnamedArguments ?? [];
14161436 if (!Array.isArray(unnamed)) unnamed = [unnamed];
1437+ // @ts-ignore
14171438 while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1439+ // @ts-ignore
14181440 unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]);
1441+ // @ts-ignore
14191442 for (const arg of unnamed) {
14201443 i++;
14211444 const item = document.createElement('div'); {
@@ -1904,6 +1927,7 @@ export class QuickReply {
19041927 executeOnChatChange: this.executeOnChatChange,
19051928 executeOnGroupMemberDraft: this.executeOnGroupMemberDraft,
19061929 executeOnNewChat: this.executeOnNewChat,
1930+ executeBeforeGeneration: this.executeBeforeGeneration,
19071931 automationId: this.automationId,
19081932 };
19091933 }
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+1 -1
@@ -4,7 +4,7 @@ import { QuickReplySet } from './QuickReplySet.js';
44
55export class QuickReplyConfig {
66 /**@type {QuickReplySetLink[]}*/ setList = [];
77 /**@type {Boolean'global'|'chat'|'character'}*/ isGlobalscope;
88
99 /**@type {Function}*/ onUpdate;
1010 /**@type {Function}*/ onRequestEditSet;
public/scripts/extensions/quick-reply/src/QuickReplySet.js+9 -0
@@ -9,6 +9,10 @@ import { QuickReply } from './QuickReply.js';
99export class QuickReplySet {
1010 /**@type {QuickReplySet[]}*/ static list = [];
1111
12+ /**
13+ * @param {Partial<QuickReplySet>} props
14+ * @returns {QuickReplySet}
15+ */
1216 static from(props) {
1317 props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
1418 const instance = Object.assign(new this(), props);
@@ -24,6 +28,7 @@ export class QuickReplySet {
2428 }
2529
2630 /**@type {string}*/ name;
31+ /**@type {'global'|'chat'|'character'}*/ scope = 'global';
2732 /**@type {boolean}*/ disableSend = false;
2833 /**@type {boolean}*/ placeBeforeInput = false;
2934 /**@type {boolean}*/ injectInput = false;
@@ -251,6 +256,7 @@ export class QuickReplySet {
251256 * @param {QuickReply} qr
252257 */
253258 hookQuickReply(qr) {
259+ // @ts-ignore
254260 qr.onDebug = ()=>this.debug(qr);
255261 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
256262 qr.onDelete = ()=>this.removeQuickReply(qr);
@@ -297,12 +303,14 @@ export class QuickReplySet {
297303 }
298304 sel.addEventListener('keyup', (evt)=>{
299305 if (evt.key == 'Shift') {
306+ // @ts-ignore
300307 (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
301308 return;
302309 }
303310 });
304311 sel.addEventListener('keydown', (evt)=>{
305312 if (evt.key == 'Shift') {
313+ // @ts-ignore
306314 (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
307315 return;
308316 }
@@ -331,6 +339,7 @@ export class QuickReplySet {
331339 isCopy = true;
332340 dlg.completeAffirmative();
333341 });
342+ // @ts-ignore
334343 (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
335344 }
336345 const prom = dlg.show();
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+25 -0
@@ -5,6 +5,10 @@ import { QuickReplyConfig } from './QuickReplyConfig.js';
55export class QuickReplySettings {
66 static from(props) {
77 props.config = QuickReplyConfig.from(props.config);
8+ props.characterConfigs = props.characterConfigs ?? {};
9+ for (const key of Object.keys(props.characterConfigs)) {
10+ props.characterConfigs[key] = QuickReplyConfig.from(props.characterConfigs[key]);
11+ }
812 const instance = Object.assign(new this(), props);
913 instance.init();
1014 return instance;
@@ -18,7 +22,9 @@ export class QuickReplySettings {
1822 /**@type {Boolean}*/ isPopout = false;
1923 /**@type {Boolean}*/ showPopoutButton = true;
2024 /**@type {QuickReplyConfig}*/ config;
25+ /**@type {{[key:string]: QuickReplyConfig}}*/ characterConfigs = {};
2126 /**@type {QuickReplyConfig}*/ _chatConfig;
27+ /**@type {QuickReplyConfig}*/ _charConfig;
2228 get chatConfig() {
2329 return this._chatConfig;
2430 }
@@ -29,6 +35,16 @@ export class QuickReplySettings {
2935 this.hookConfig(this._chatConfig);
3036 }
3137 }
38+ get charConfig() {
39+ return this._charConfig;
40+ }
41+ set charConfig(value) {
42+ if (this._charConfig != value) {
43+ this.unhookConfig(this._charConfig);
44+ this._charConfig = value;
45+ this.hookConfig(this._charConfig);
46+ }
47+ }
3248
3349 /**@type {Function}*/ onSave;
3450 /**@type {Function}*/ onRequestEditSet;
@@ -39,6 +55,7 @@ export class QuickReplySettings {
3955 init() {
4056 this.hookConfig(this.config);
4157 this.hookConfig(this.chatConfig);
58+ this.hookConfig(this.charConfig);
4259 }
4360
4461 hookConfig(config) {
@@ -76,12 +93,20 @@ export class QuickReplySettings {
7693 }
7794
7895 toJSON() {
96+ const characterConfigs = {};
97+ for (const key of Object.keys(this.characterConfigs)) {
98+ if (this.characterConfigs[key]?.setList?.length === 0) {
99+ continue;
100+ }
101+ characterConfigs[key] = this.characterConfigs[key].toJSON();
102+ }
79103 return {
80104 isEnabled: this.isEnabled,
81105 isCombined: this.isCombined,
82106 isPopout: this.isPopout,
83107 showPopoutButton: this.showPopoutButton,
84108 config: this.config,
109+ characterConfigs,
85110 };
86111 }
87112}
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+4 -0
@@ -36,6 +36,7 @@ export class SlashCommandHandler {
3636 if (qr.executeOnChatChange) icons += '💬';
3737 if (qr.executeOnNewChat) icons += '🆕';
3838 if (qr.executeOnGroupMemberDraft) icons += enumIcons.group;
39+ if (qr.executeBeforeGeneration) icons += '✈️';
3940 return icons;
4041 }
4142
@@ -267,6 +268,7 @@ export class SlashCommandHandler {
267268 new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
268269 new SlashCommandNamedArgument('new', 'auto execute on new chat, e.g., new=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
269270 new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
271+ new SlashCommandNamedArgument('generation', 'auto execute before message generation, e.g., generation=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
270272 new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
271273 ];
272274 const qrUpdateArgs = [
@@ -874,6 +876,7 @@ export class SlashCommandHandler {
874876 executeOnChatChange: isTrueBoolean(args.load),
875877 executeOnNewChat: isTrueBoolean(args.new),
876878 executeOnGroupMemberDraft: isTrueBoolean(args.group),
879+ executeBeforeGeneration: isTrueBoolean(args.generation),
877880 automationId: args.automationId ?? '',
878881 },
879882 );
@@ -910,6 +913,7 @@ export class SlashCommandHandler {
910913 executeOnChatChange: args.load === undefined ? undefined : isTrueBoolean(args.load),
911914 executeOnGroupMemberDraft: args.group === undefined ? undefined : isTrueBoolean(args.group),
912915 executeOnNewChat: args.new === undefined ? undefined : isTrueBoolean(args.new),
916+ executeBeforeGeneration: args.generation === undefined ? undefined : isTrueBoolean(args.generation),
913917 automationId: args.automationId ?? '',
914918 },
915919 );
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+2 -2
@@ -90,7 +90,7 @@ export class ButtonUi {
9090 root.append(buttons);
9191 }
9292 }
9393 [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])]
9494 .filter(link=>link.isVisible)
9595 .forEach(link=>buttonHolder.append(link.set.render()))
9696 ;
@@ -150,7 +150,7 @@ export class ButtonUi {
150150 body.append(buttons);
151151 }
152152 }
153153 [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])]
154154 .filter(link=>link.isVisible)
155155 .forEach(link=>buttonHolder.append(link.set.render()))
156156 ;
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+48 -3
@@ -18,6 +18,7 @@ export class SettingsUi {
1818 /**@type {HTMLElement}*/ globalSetList;
1919
2020 /**@type {HTMLElement}*/ chatSetList;
21+ /**@type {HTMLElement}*/ characterSetList;
2122
2223 /**@type {QuickReplySet}*/ currentQrSet;
2324 /**@type {HTMLInputElement}*/ disableSend;
@@ -107,6 +108,25 @@ export class SettingsUi {
107108 }
108109 this.dom.querySelector('#qr--chat').replaceWith(clone);
109110 }
111+ prepareCharacterSetList() {
112+ const dom = this.template.querySelector('#qr--character');
113+ const clone = /** @type {HTMLElement} */ (dom.cloneNode(true));
114+
115+ if (!this.settings.charConfig) {
116+ const setListContainer = /** @type {HTMLElement} */ (clone.querySelector('.qr--setList'));
117+ setListContainer.innerHTML = '';
118+ const info = document.createElement('div');
119+ info.textContent = 'No character is currently loaded.';
120+ setListContainer.append(info);
121+ } else {
122+ // Let the config object handle its own rendering. It will render an empty list if there are no sets,
123+ // but the "add" button will always be functional.
124+ this.settings.charConfig.renderSettingsInto(clone);
125+ }
126+
127+ // Replace the old DOM element with our newly prepared clone.
128+ this.dom.querySelector('#qr--character').replaceWith(clone);
129+ }
110130
111131 prepareQrEditor() {
112132 // qr editor
@@ -174,21 +194,26 @@ export class SettingsUi {
174194 });
175195 let initialColorChange = true;
176196 this.color = this.dom.querySelector('#qr--color');
197+ // @ts-ignore
177198 this.color.color = this.currentQrSet?.color ?? 'transparent';
178199 this.color.addEventListener('change', (evt)=>{
179200 if (!this.dom.closest('body')) return;
180201 const qrs = this.currentQrSet;
181202 if (initialColorChange) {
182203 initialColorChange = false;
204+ // @ts-ignore
183205 this.color.color = qrs.color;
184206 return;
185207 }
208+ // @ts-ignore
186209 qrs.color = evt.detail.rgb;
187210 qrs.save();
188211 this.currentQrSet.updateColor();
189212 });
213+ // @ts-ignore
190214 this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
191215 const qrs = this.currentQrSet;
216+ // @ts-ignore
192217 this.color.color = 'transparent';
193218 qrs.save();
194219 this.currentQrSet.updateColor();
@@ -207,6 +232,7 @@ export class SettingsUi {
207232 this.disableSend.checked = this.currentQrSet.disableSend;
208233 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
209234 this.injectInput.checked = this.currentQrSet.injectInput;
235+ // @ts-ignore
210236 this.color.color = this.currentQrSet.color ?? 'transparent';
211237 this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
212238 this.qrList.innerHTML = '';
@@ -225,6 +251,7 @@ export class SettingsUi {
225251 this.prepareGeneralSettings();
226252 this.prepareGlobalSetList();
227253 this.prepareChatSetList();
254+ this.prepareCharacterSetList();
228255 this.prepareQrEditor();
229256 }
230257
@@ -301,6 +328,13 @@ export class SettingsUi {
301328 }
302329 }
303330 }
331+ if (this.settings.charConfig) {
332+ for (let i = this.settings.charConfig.setList.length - 1; i >= 0; i--) {
333+ if (this.settings.charConfig.setList[i].set == qrs) {
334+ this.settings.charConfig.setList.splice(i, 1);
335+ }
336+ }
337+ }
304338 this.settings.save();
305339 }
306340
@@ -327,6 +361,11 @@ export class SettingsUi {
327361 set.set.name = newName;
328362 }
329363 });
364+ this.settings.charConfig?.setList.forEach(set => {
365+ if (set.set.name === oldName) {
366+ set.set.name = newName;
367+ }
368+ });
330369 this.settings.save();
331370
332371 // Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below.
@@ -339,6 +378,7 @@ export class SettingsUi {
339378 this.onQrSetChange();
340379 this.prepareGlobalSetList();
341380 this.prepareChatSetList();
381+ this.prepareCharacterSetList();
342382
343383 console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`);
344384 }
@@ -362,6 +402,7 @@ export class SettingsUi {
362402 this.onQrSetChange();
363403 this.prepareGlobalSetList();
364404 this.prepareChatSetList();
405+ this.prepareCharacterSetList();
365406 }
366407 } else {
367408 const qrs = new QuickReplySet();
@@ -386,6 +427,7 @@ export class SettingsUi {
386427 this.onQrSetChange();
387428 this.prepareGlobalSetList();
388429 this.prepareChatSetList();
430+ this.prepareCharacterSetList();
389431 }
390432 }
391433 }
@@ -421,6 +463,7 @@ export class SettingsUi {
421463 this.onQrSetChange();
422464 this.prepareGlobalSetList();
423465 this.prepareChatSetList();
466+ this.prepareCharacterSetList();
424467 }
425468 } else {
426469 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
@@ -443,6 +486,7 @@ export class SettingsUi {
443486 this.onQrSetChange();
444487 this.prepareGlobalSetList();
445488 this.prepareChatSetList();
489+ this.prepareCharacterSetList();
446490 }
447491 }
448492 } catch (ex) {
@@ -470,10 +514,10 @@ export class SettingsUi {
470514 toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
471515 return;
472516 }
473517 const newQrSet = QuickReplySet.from(JSON.parse(JSON.stringify(this.currentQrSet).toJSON());
474518 newQrSet.name = newName;
475519 newQrSet.qrList = this.currentQrSet.qrList.map(qr => QuickReply.from(JSON.parse(JSONqr.stringifytoJSON(qr))));
476520 newQrSet.addQuickReplyinit();
477521 const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(newName.toLowerCase()) == 1);
478522 if (idx > -1) {
479523 QuickReplySet.list.splice(idx, 0, newQrSet);
@@ -493,6 +537,7 @@ export class SettingsUi {
493537 this.onQrSetChange();
494538 this.prepareGlobalSetList();
495539 this.prepareChatSetList();
540+ this.prepareCharacterSetList();
496541 }
497542 }
498543
public/scripts/extensions/regex/editor.html+1 -1
@@ -61,7 +61,7 @@
6161 <small data-i18n="Replace With">Replace With</small>
6262 </label>
6363 <div>
6464 <textarea class="regex_replace_string text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_replace_string_placeholder" placeholder="Use {{match}} to include the matched text from the Find Regex or, $1, $2, etc. for numbered capture groups, or $&lt;name&gt; for named capture groups." rows="23"></textarea>
6565 </div>
6666 </div>
6767 <div class="flex1">
public/scripts/extensions/regex/engine.js+9 -5
@@ -163,9 +163,15 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
163163 newString = rawString.replace(findRegex, function (match) {
164164 const args = [...arguments];
165165 const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
166166 const replaceWithGroups = replaceString.replaceAll(/\$(\d+)|\$<([^>]+)>/g, (_, num, groupName) => {
167- // Get a full match or a capture group
167+ if (num) {
168- const match = args[Number(num)];
168+ // Handle numbered capture groups ($1, $2, etc.)
169+ match = args[Number(num)];
170+ } else if (groupName) {
171+ // Handle named capture groups ($<name>)
172+ const groups = args[args.length - 1];
173+ match = groups && typeof groups === 'object' && groups[groupName];
174+ }
169175
170176 // No match found - return the empty string
171177 if (!match) {
@@ -175,8 +181,6 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
175181 // Remove trim strings from the match
176182 const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride });
177183
178- // TODO: Handle overlay here
179-
180184 return filteredMatch;
181185 });
182186
public/scripts/extensions/shared.js+4 -4
@@ -163,10 +163,6 @@ function throwIfInvalidModel(useReverseProxy) {
163163 throw new Error('Anthropic (Claude) API key is not set.');
164164 }
165165
166- if (multimodalApi === 'zerooneai' && !secret_state[SECRET_KEYS.ZEROONEAI]) {
167- throw new Error('01.AI API key is not set.');
168- }
169-
170166 if (multimodalApi === 'groq' && !secret_state[SECRET_KEYS.GROQ]) {
171167 throw new Error('Groq API key is not set.');
172168 }
@@ -242,6 +238,10 @@ function throwIfInvalidModel(useReverseProxy) {
242238 if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) {
243239 throw new Error('AI/ML API key is not set.');
244240 }
241+
242+ if (multimodalApi === 'moonshot' && !secret_state[SECRET_KEYS.MOONSHOT]) {
243+ throw new Error('Moonshot AI API key is not set.');
244+ }
245245}
246246
247247/**
public/scripts/extensions/stable-diffusion/index.js+9 -0
@@ -306,6 +306,7 @@ const defaultSettings = {
306306 novel_sm: false,
307307 novel_sm_dyn: false,
308308 novel_decrisper: false,
309+ novel_variety_boost: false,
309310
310311 // OpenAI settings
311312 openai_style: 'vivid',
@@ -482,6 +483,7 @@ async function loadSettings() {
482483 $('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
483484 $('#sd_novel_sm_dyn').prop('disabled', !extension_settings.sd.novel_sm);
484485 $('#sd_novel_decrisper').prop('checked', extension_settings.sd.novel_decrisper);
486+ $('#sd_novel_variety_boost').prop('checked', extension_settings.sd.novel_variety_boost);
485487 $('#sd_pollinations_enhance').prop('checked', extension_settings.sd.pollinations_enhance);
486488 $('#sd_horde').prop('checked', extension_settings.sd.horde);
487489 $('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw);
@@ -1055,6 +1057,11 @@ function onNovelDecrisperInput() {
10551057 saveSettingsDebounced();
10561058}
10571059
1060+function onNovelVarietyBoostInput() {
1061+ extension_settings.sd.novel_variety_boost = !!$('#sd_novel_variety_boost').prop('checked');
1062+ saveSettingsDebounced();
1063+}
1064+
10581065function onPollinationsEnhanceInput() {
10591066 extension_settings.sd.pollinations_enhance = !!$('#sd_pollinations_enhance').prop('checked');
10601067 saveSettingsDebounced();
@@ -3234,6 +3241,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) {
32343241 negative_prompt: negativePrompt,
32353242 upscale_ratio: extension_settings.sd.hr_scale,
32363243 decrisper: extension_settings.sd.novel_decrisper,
3244+ variety_boost: extension_settings.sd.novel_variety_boost,
32373245 sm: sm,
32383246 sm_dyn: sm_dyn,
32393247 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
@@ -4654,6 +4662,7 @@ jQuery(async () => {
46544662 $('#sd_novel_sm').on('input', onNovelSmInput);
46554663 $('#sd_novel_sm_dyn').on('input', onNovelSmDynInput);
46564664 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
4665+ $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);
46574666 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
46584667 $('#sd_comfy_validate').on('click', validateComfyUrl);
46594668 $('#sd_comfy_url').on('input', onComfyUrlInput);
public/scripts/extensions/stable-diffusion/settings.html+4 -0
@@ -451,6 +451,10 @@
451451 <input id="sd_novel_decrisper" type="checkbox" />
452452 <small data-i18n="Decrisper">Decrisper</small>
453453 </label>
454+ <label class="flex1 checkbox_label" data-i18n="[title]Enable guidance only after body has been formed, to improve diversity and saturation of samples. May reduce relevance" title="Enable guidance only after body has been formed, to improve diversity and saturation of samples. May reduce relevance">
455+ <input id="sd_novel_variety_boost" type="checkbox" />
456+ <small data-i18n="Variety+">Variety+</small>
457+ </label>
454458 </div>
455459
456460 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,bfl" class="marginTop5">
public/scripts/extensions/tts/css/minimax-tts.css+33 -0
@@ -0,0 +1,33 @@
1+.minimax-custom-item {
2+ display: flex;
3+ justify-content: space-between;
4+ align-items: center;
5+ padding: 8px;
6+ background: #333;
7+ margin: 5px 0;
8+ border-radius: 4px;
9+}
10+
11+.minimax-custom-item-info {
12+ display: flex;
13+ flex-direction: column;
14+}
15+
16+.minimax-custom-item-name {
17+ font-weight: bold;
18+}
19+
20+.minimax-custom-item-details {
21+ color: #aaa;
22+ font-size: 0.9em;
23+}
24+
25+.minimax-custom-item-remove {
26+ padding: 4px 8px;
27+ font-size: 12px;
28+}
29+
30+.minimax-empty-list {
31+ color: #888;
32+ font-style: italic;
33+}
public/scripts/extensions/tts/css/openai-tts.css+11 -0
@@ -0,0 +1,11 @@
1+#openai-character-instructions {
2+ display: flex;
3+ flex-direction: column;
4+ gap: 10px;
5+}
6+
7+#openai-character-instructions .character-instructions {
8+ display: flex;
9+ flex-direction: column;
10+ gap: 5px;
11+}
public/scripts/extensions/tts/index.js+173 -13
@@ -32,6 +32,7 @@ import { ChatterboxTtsProvider } from './chatterbox.js';
3232import { KokoroTtsProvider } from './kokoro.js';
3333import { TtsWebuiProvider } from './tts-webui.js';
3434import { PollinationsTtsProvider } from './pollinations.js';
35+import { MiniMaxTtsProvider } from './minimax.js';
3536
3637const UPDATE_INTERVAL = 1000;
3738const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -127,6 +128,7 @@ const ttsProviders = {
127128 GSVI: GSVITtsProvider,
128129 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
129130 Kokoro: KokoroTtsProvider,
131+ MiniMax: MiniMaxTtsProvider,
130132 Novel: NovelTtsProvider,
131133 OpenAI: OpenAITtsProvider,
132134 'OpenAI Compatible': OpenAICompatibleTtsProvider,
@@ -465,7 +467,7 @@ function completeTtsJob() {
465467 currentTtsJob = null;
466468}
467469
468470async function tts(text, voiceId, char, voiceMapKey = null) {
469471 async function processResponse(response) {
470472 // RVC injection
471473 if (typeof window['rvcVoiceConversion'] === 'function' && extension_settings.rvc.enabled)
@@ -474,7 +476,8 @@ async function tts(text, voiceId, char) {
474476 await addAudioJob(response, char);
475477 }
476478
477- let response = await ttsProvider.generateTts(text, voiceId);
479+ // voiceMapKey can also include segment qualifiers, e.g. '{char} ("Quotes")'
480+ let response = await ttsProvider.generateTts(text, voiceId, voiceMapKey);
478481
479482 // If async generator, process every chunk as it comes in
480483 if (typeof response[Symbol.asyncIterator] === 'function') {
@@ -488,6 +491,69 @@ async function tts(text, voiceId, char) {
488491 completeTtsJob();
489492}
490493
494+function parseMessageSegments(text) {
495+ if (!extension_settings.tts.multi_voice_enabled) {
496+ return [{ type: 'other', text: text }];
497+ }
498+
499+ const segments = [];
500+ const segmentRegex = /(\*[^*]*?\*)|(".*?")|(\u201C.*?\u201D)|(\u00AB.*?\u00BB)|(\u300C.*?\u300D)|(\u300E.*?\u300F)|(\uFF02.*?\uFF02)/gim;
501+ let lastIndex = 0;
502+ let match;
503+
504+ segmentRegex.lastIndex = 0;
505+
506+ while ((match = segmentRegex.exec(text)) !== null) {
507+ // Add other text before this match
508+ if (match.index > lastIndex) {
509+ const otherText = text.substring(lastIndex, match.index).trim();
510+ if (otherText && otherText.length > 0) {
511+ segments.push({ type: 'other', text: otherText });
512+ }
513+ }
514+
515+ const matchedText = match[0];
516+ let segmentType = 'other';
517+ let content = '';
518+
519+ if (match[1]) {
520+ // Asterisk content (*action*)
521+ segmentType = 'action';
522+ content = matchedText.slice(1, -1);
523+ } else if (match[2] || match[3] || match[4] || match[5] || match[6] || match[7]) {
524+ // Various quote types ("dialogue")
525+ segmentType = 'dialogue';
526+ content = matchedText.slice(1, -1);
527+ }
528+
529+ // Trim and check for actual content
530+ content = content.trim();
531+ if (content.length > 0) {
532+ segments.push({
533+ type: segmentType,
534+ text: content,
535+ });
536+ }
537+
538+ lastIndex = match.index + matchedText.length;
539+ }
540+
541+ // Add remaining other text after last match
542+ if (lastIndex < text.length) {
543+ const otherText = text.substring(lastIndex).trim();
544+ if (otherText.length > 0) {
545+ segments.push({ type: 'other', text: otherText });
546+ }
547+ }
548+
549+ // If no segments found and not empty, treat whole text as other text
550+ if (segments.length === 0 && text.trim().length > 0) {
551+ segments.push({ type: 'other', text: text.trim() });
552+ }
553+
554+ return segments;
555+}
556+
491557async function processTtsQueue() {
492558 // Called each moduleWorker iteration to pull chat messages from queue
493559 if (currentTtsJob || ttsJobQueue.length <= 0 || audioPaused) {
@@ -496,6 +562,59 @@ async function processTtsQueue() {
496562
497563 console.debug('New message found, running TTS');
498564 currentTtsJob = ttsJobQueue.shift();
565+
566+ // Handle segmented jobs that already have processed text
567+ if (currentTtsJob.segmentType && currentTtsJob.segmentText) {
568+ const char = currentTtsJob.name;
569+ const segmentText = currentTtsJob.segmentText;
570+ const segmentType = currentTtsJob.segmentType;
571+
572+ console.log(`TTS (${segmentType}): ${segmentText}`);
573+
574+ try {
575+ let voiceMapKey = char;
576+
577+ // If multi-voice is enabled, modify the voice map key based on segment type
578+ if (extension_settings.tts.multi_voice_enabled && char !== DEFAULT_VOICE_MARKER) {
579+ switch (segmentType) {
580+ case 'dialogue':
581+ voiceMapKey = `${char} ("Quotes")`;
582+ break;
583+ case 'action':
584+ voiceMapKey = `${char} (*Text inside asterisks*)`;
585+ break;
586+ case 'other':
587+ default:
588+ voiceMapKey = `${char} (Other text)`;
589+ break;
590+ }
591+ }
592+
593+ const voiceMapEntry = voiceMap[voiceMapKey] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[voiceMapKey];
594+
595+ if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
596+ throw `${char} not in voicemap. Configure character in extension settings voice map`;
597+ }
598+
599+ const voice = await ttsProvider.getVoice(voiceMapEntry);
600+ const voiceId = voice.voice_id;
601+ if (voiceId == null) {
602+ toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`);
603+ throw `Unable to attain voiceId for ${char}`;
604+ }
605+
606+ // Pass the full voiceMapKey (e.g., "User ("Quotes")") as well with character name
607+ await tts(segmentText, voiceId, char, voiceMapKey);
608+
609+ } catch (error) {
610+ toastr.error(error.toString());
611+ console.error(error);
612+ currentTtsJob = null;
613+ }
614+ return;
615+ }
616+
617+ // Process unsegmented job (first time processing)
499618 let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes;
500619
501620 // Substitute macros
@@ -551,18 +670,31 @@ async function processTtsQueue() {
551670 return;
552671 }
553672
554- const voiceMapEntry = voiceMap[char] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[char];
673+ // Parse message into segments if multi-voice is enabled
674+ const segments = parseMessageSegments(text);
555675
556676 if (!voiceMapEntry || voiceMapEntrysegments.length === DISABLED_VOICE_MARKER0) {
557- throw `${char} not in voicemap. Configure character in extension settings voice map`;
677+ console.warn('No valid segments found in text.');
678+ completeTtsJob();
679+ return;
558680 }
559- const voice = await ttsProvider.getVoice(voiceMapEntry);
681+
560- const voiceId = voice.voice_id;
682+ // Add all segments to the queue as separate jobs (in reverse order so they process in correct order)
561- if (voiceId == null) {
683+ for (let i = segments.length - 1; i >= 0; i--) {
562- toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`);
684+ const segmentJob = {
563- throw `Unable to attain voiceId for ${char}`;
685+ name: char,
686+ segmentType: segments[i].type,
687+ segmentText: segments[i].text,
688+ is_user: currentTtsJob.is_user,
689+ mes: currentTtsJob.mes,
690+ extra: currentTtsJob.extra,
691+ };
692+ ttsJobQueue.unshift(segmentJob);
564693 }
565- await tts(text, voiceId, char);
694+
695+ // Clear current job so the segmented jobs can be processed
696+ currentTtsJob = null;
697+
566698 } catch (error) {
567699 toastr.error(error.toString());
568700 console.error(error);
@@ -617,6 +749,7 @@ function loadSettings() {
617749 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
618750 $('#tts_skip_codeblocks').prop('checked', extension_settings.tts.skip_codeblocks);
619751 $('#tts_skip_tags').prop('checked', extension_settings.tts.skip_tags);
752+ $('#tts_multi_voice_enabled').prop('checked', extension_settings.tts.multi_voice_enabled);
620753 $('#playback_rate').val(extension_settings.tts.playback_rate);
621754 $('#playback_rate_counter').val(Number(extension_settings.tts.playback_rate).toFixed(2));
622755 $('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
@@ -631,6 +764,7 @@ const defaultSettings = {
631764 auto_generation: true,
632765 narrate_user: false,
633766 playback_rate: 1,
767+ multi_voice_enabled: false,
634768};
635769
636770function setTtsStatus(status, success) {
@@ -724,6 +858,13 @@ function onPassAsterisksClick() {
724858 console.log('setting pass asterisks', extension_settings.tts.pass_asterisks);
725859}
726860
861+function onMultiVoiceClick() {
862+ extension_settings.tts.multi_voice_enabled = !!$('#tts_multi_voice_enabled').prop('checked');
863+ saveSettingsDebounced();
864+ // Reinitialize voice map to show/hide voices
865+ initVoiceMap();
866+}
867+
727868//##############//
728869// TTS Provider //
729870//##############//
@@ -1002,10 +1143,28 @@ function getCharacters(unrestricted) {
10021143 }
10031144 }
10041145 }
10051146 returncharacters = characters.filter(onlyUnique);
1147+
1148+ // If multi-voice is enabled, expand characters to include segment types
1149+ if (extension_settings.tts.multi_voice_enabled) {
1150+ const expandedCharacters = [];
1151+ for (const char of characters) {
1152+ if (char === DEFAULT_VOICE_MARKER || char === 'SillyTavern System') {
1153+ expandedCharacters.push(char);
1154+ } else {
1155+ expandedCharacters.push(`${char} ("Quotes")`);
1156+ expandedCharacters.push(`${char} (*Text inside asterisks*)`);
1157+ expandedCharacters.push(`${char} (Other text)`);
1158+ }
1159+ }
1160+ return expandedCharacters;
1161+ }
1162+
1163+ return characters;
1164+
10061165}
10071166
10081167export function sanitizeId(input) {
10091168 // Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
10101169 let sanitized = encodeURIComponent(input).replace(/[^a-zA-Z0-9-_]/g, '');
10111170
@@ -1214,6 +1373,7 @@ jQuery(async function () {
12141373 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
12151374 $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
12161375 $('#tts_narrate_user').on('click', onNarrateUserClick);
1376+ $('#tts_multi_voice_enabled').on('click', onMultiVoiceClick);
12171377
12181378 $('#playback_rate').on('input', function () {
12191379 const value = $(this).val();
public/scripts/extensions/tts/minimax.js+935 -0
@@ -0,0 +1,935 @@
1+import { getPreviewString, initVoiceMap, saveTtsProviderSettings } from './index.js';
2+import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
3+import { SECRET_KEYS, secret_state } from '../../secrets.js';
4+import { getBase64Async } from '../../utils.js';
5+
6+export { MiniMaxTtsProvider };
7+
8+class MiniMaxTtsProvider {
9+ //########//
10+ // Config //
11+ //########//
12+
13+ settings;
14+ voices = [];
15+ separator = ' . ';
16+ audioElement = document.createElement('audio');
17+
18+ defaultSettings = {
19+ apiHost: 'https://api.minimax.io',
20+ model: 'speech-02-hd',
21+ voiceMap: {},
22+ speed: 1.0,
23+ volume: 1.0,
24+ pitch: 1.0,
25+ audioSampleRate: 32000,
26+ bitrate: 128000,
27+ format: 'mp3',
28+ customModels: [],
29+ customVoices: [],
30+ customVoiceId: '',
31+ };
32+
33+ // MiniMax API doesn't provide a method to list user's cloned voices
34+ // so users need to manually input their custom cloned voice IDs
35+ static defaultVoices = [
36+ { name: 'Unrestrained Young Man', voice_id: 'Chinese (Mandarin)_Unrestrained_Young_Man', lang: 'zh-CN', preview_url: null },
37+ ];
38+
39+ // default models (by MiniMax doc)
40+ static defaultModels = [
41+ { id: 'speech-02-hd', name: 'Speech-02-HD (High Quality)' },
42+ { id: 'speech-02-turbo', name: 'Speech-02-Turbo (Fast)' },
43+ { id: 'speech-01', name: 'Speech-01 (Legacy)' },
44+ { id: 'speech-01-240228', name: 'Speech-01-240228 (Legacy)' },
45+ ];
46+
47+ availableModels = [];
48+ availableVoices = [];
49+
50+ get settingsHtml() {
51+ return `
52+ <div class="minimax_tts_settings">
53+ <div class="tts_block justifyCenter">
54+ <div id="api_key_minimax" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_minimax">
55+ <i class="fa-solid fa-key"></i>
56+ <span>Click to set API Key</span>
57+ </div>
58+ <div id="minimax_group_id" class="menu_button menu_button_icon manage-api-keys" data-key="minimax_group_id">
59+ <i class="fa-solid fa-key"></i>
60+ <span>Click to set Group ID</span>
61+ </div>
62+ </div>
63+ <div class="tts_block">
64+ <label for="minimax_tts_api_host">API Host</label>
65+ <select id="minimax_tts_api_host" class="text_pole">
66+ <option value="https://api.minimax.io">Official (api.minimax.io)</option>
67+ <option value="https://api.minimaxi.chat">Global (api.minimaxi.chat)</option>
68+ <option value="https://api.minimax.chat">Mainland China (api.minimax.chat)</option>
69+ </select>
70+ </div>
71+ <div class="tts_block">
72+ <label for="minimax_tts_model">Model</label>
73+ <select id="minimax_tts_model" class="text_pole">
74+ <option value="speech-02-hd">Speech-02-HD (High Quality)</option>
75+ <option value="speech-02-turbo">Speech-02-Turbo (Fast)</option>
76+ <option value="speech-01">Speech-01 (Legacy)</option>
77+ <option value="speech-01-240228">Speech-01-240228 (Legacy)</option>
78+ </select>
79+ </div>
80+ <div class="tts_block">
81+ <input id="minimax_connect" class="menu_button" type="button" value="Connect" />
82+ <input id="minimax_refresh" class="menu_button" type="button" value="Refresh" />
83+ </div>
84+
85+ <div class="tts_block">
86+ <label for="minimax_tts_speed">Speed: <span id="minimax_tts_speed_output"></span></label>
87+ <input id="minimax_tts_speed" type="range" value="${this.defaultSettings.speed}" min="0.5" max="2.0" step="0.1" />
88+ </div>
89+ <div class="tts_block">
90+ <label for="minimax_tts_volume">Volume: <span id="minimax_tts_volume_output"></span></label>
91+ <input id="minimax_tts_volume" type="range" value="${this.defaultSettings.volume}" min="0.1" max="2.0" step="0.1" />
92+ </div>
93+ <div class="tts_block">
94+ <label for="minimax_tts_pitch">Pitch: <span id="minimax_tts_pitch_output"></span></label>
95+ <input id="minimax_tts_pitch" type="range" value="${this.defaultSettings.pitch}" min="0.5" max="2.0" step="0.1" />
96+ </div>
97+ <div class="tts_block">
98+ <label for="minimax_tts_format">Audio Format</label>
99+ <select id="minimax_tts_format" class="text_pole">
100+ <option value="mp3">MP3</option>
101+ <option value="wav">WAV</option>
102+ <option value="flac">FLAC</option>
103+ </select>
104+ </div>
105+
106+ <hr>
107+ <div class="tts_block">
108+ <label for="minimax_tts_custom_voice_id">Custom Voice ID (for 'customVoice' option)</label>
109+ <input id="minimax_tts_custom_voice_id" type="text" class="text_pole" placeholder="Enter custom voice ID from MiniMax platform"/>
110+ </div>
111+
112+ <hr>
113+ <div id="minimax_custom_voice_cloning" class="tts_block flexFlowColumn">
114+ <h4>Custom Voice Management</h4>
115+ <div class="tts_block wide100p">
116+ <input id="minimax_custom_voice_name" type="text" class="text_pole" placeholder="Voice Name"/>
117+ </div>
118+ <div class="tts_block wide100p">
119+ <input id="minimax_custom_voice_id" type="text" class="text_pole" placeholder="Voice ID (from MiniMax platform)"/>
120+ </div>
121+ <div class="tts_block wide100p">
122+ <select id="minimax_custom_voice_lang" class="text_pole">
123+ <option value="auto">Auto Detect</option>
124+ <option value="Chinese">Chinese (中文)</option>
125+ <option value="Chinese,Yue">Chinese, Yue (粤语)</option>
126+ <option value="English">English</option>
127+ <option value="Arabic">Arabic (العربية)</option>
128+ <option value="Russian">Russian (Русский)</option>
129+ <option value="Spanish">Spanish (Español)</option>
130+ <option value="French">French (Français)</option>
131+ <option value="Portuguese">Portuguese (Português)</option>
132+ <option value="German">German (Deutsch)</option>
133+ <option value="Turkish">Turkish (Türkçe)</option>
134+ <option value="Dutch">Dutch (Nederlands)</option>
135+ <option value="Ukrainian">Ukrainian (Українська)</option>
136+ <option value="Vietnamese">Vietnamese (Tiếng Việt)</option>
137+ <option value="Indonesian">Indonesian (Bahasa Indonesia)</option>
138+ <option value="Japanese">Japanese (日本語)</option>
139+ <option value="Italian">Italian (Italiano)</option>
140+ <option value="Korean">Korean (한국어)</option>
141+ <option value="Thai">Thai (ไทย)</option>
142+ <option value="Polish">Polish (Polski)</option>
143+ <option value="Romanian">Romanian (Română)</option>
144+ <option value="Greek">Greek (Ελληνικά)</option>
145+ <option value="Czech">Czech (Čeština)</option>
146+ <option value="Finnish">Finnish (Suomi)</option>
147+ <option value="Hindi">Hindi (हिन्दी)</option>
148+ </select>
149+ </div>
150+ <div class="tts_block">
151+ <input id="minimax_add_custom_voice" class="menu_button" type="button" value="Add Custom Voice">
152+ </div>
153+ <div id="minimax_custom_voices_list" style="margin-top: 10px;"></div>
154+ </div>
155+
156+ <hr>
157+ <div id="minimax_custom_model_management" class="tts_block flexFlowColumn">
158+ <h4>Custom Model Management</h4>
159+ <div class="tts_block wide100p">
160+ <input id="minimax_custom_model_id" type="text" class="text_pole" placeholder="Model ID"/>
161+ </div>
162+ <div class="tts_block wide100p">
163+ <input id="minimax_custom_model_name" type="text" class="text_pole" placeholder="Model Name"/>
164+ </div>
165+ <div class="tts_block">
166+ <input id="minimax_add_custom_model" class="menu_button" type="button" value="Add Custom Model">
167+ </div>
168+ <div id="minimax_custom_models_list" style="margin-top: 10px;"></div>
169+ </div>
170+ </div>
171+ `;
172+ }
173+
174+ constructor() {
175+ this.handler = async function (/** @type {string} */ key) {
176+ if (![SECRET_KEYS.MINIMAX, SECRET_KEYS.MINIMAX_GROUP_ID].includes(key)) return;
177+ $('#api_key_minimax').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX]);
178+ $('#minimax_group_id').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]);
179+ await this.onRefreshClick();
180+ }.bind(this);
181+ }
182+
183+ dispose() {
184+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
185+ eventSource.removeListener(event, this.handler);
186+ });
187+ }
188+
189+ onSettingsChange() {
190+ this.settings.apiHost = $('#minimax_tts_api_host').val();
191+ this.settings.speed = parseFloat($('#minimax_tts_speed').val().toString());
192+ this.settings.volume = parseFloat($('#minimax_tts_volume').val().toString());
193+ this.settings.pitch = parseFloat($('#minimax_tts_pitch').val().toString());
194+ this.settings.model = $('#minimax_tts_model').find(':selected').val();
195+ this.settings.format = $('#minimax_tts_format').find(':selected').val();
196+ this.settings.customVoiceId = $('#minimax_tts_custom_voice_id').val();
197+
198+ $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1));
199+ $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1));
200+ $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1));
201+
202+ saveTtsProviderSettings();
203+ }
204+
205+ addCustomModel() {
206+ const modelId = $('#minimax_custom_model_id').val().toString().trim();
207+ const modelName = $('#minimax_custom_model_name').val().toString().trim();
208+
209+ if (!modelId || !modelName) {
210+ toastr.error('Please enter model ID and name');
211+ return;
212+ }
213+
214+ // Check if already exists in custom models
215+ if (this.settings.customModels.find(m => m.id === modelId)) {
216+ toastr.error('Model ID already exists in custom models');
217+ return;
218+ }
219+
220+ // Check if conflicts with default models
221+ if (MiniMaxTtsProvider.defaultModels.find(m => m.id === modelId)) {
222+ toastr.error('Model ID conflicts with default model. Please use a different model ID.');
223+ return;
224+ }
225+
226+ // Check if conflicts with default model names
227+ if (MiniMaxTtsProvider.defaultModels.find(m => m.name === modelName)) {
228+ toastr.error('Model name conflicts with default model. Please use a different model name.');
229+ return;
230+ }
231+
232+ this.settings.customModels.push({ id: modelId, name: modelName });
233+ $('#minimax_custom_model_id').val('');
234+ $('#minimax_custom_model_name').val('');
235+
236+ this.updateCustomModelsDisplay();
237+ this.updateModelSelect(this.getAllModels());
238+ saveTtsProviderSettings();
239+ toastr.success('Model added successfully');
240+ }
241+
242+ removeCustomModel(modelId) {
243+ this.settings.customModels = this.settings.customModels.filter(m => m.id !== modelId);
244+ this.updateCustomModelsDisplay();
245+ this.updateModelSelect(this.getAllModels());
246+ saveTtsProviderSettings();
247+
248+ toastr.success('Model removed successfully');
249+ }
250+
251+ addCustomVoice() {
252+ const voiceName = $('#minimax_custom_voice_name').val().toString().trim();
253+ const voiceId = $('#minimax_custom_voice_id').val().toString().trim();
254+ const voiceLang = $('#minimax_custom_voice_lang').val().toString().trim();
255+
256+ if (!voiceName || !voiceId) {
257+ toastr.error('Please enter voice name and ID');
258+ return;
259+ }
260+
261+ // Check if already exists in custom voices
262+ if (this.settings.customVoices.find(v => v.voice_id === voiceId)) {
263+ toastr.error('Voice ID already exists in custom voices');
264+ return;
265+ }
266+
267+ // Check if conflicts with default voices
268+ if (MiniMaxTtsProvider.defaultVoices.find(v => v.voice_id === voiceId)) {
269+ toastr.error('Voice ID conflicts with default voice. Please use a different voice ID.');
270+ return;
271+ }
272+
273+ // Check if conflicts with default voice names
274+ if (MiniMaxTtsProvider.defaultVoices.find(v => v.name === voiceName)) {
275+ toastr.error('Voice name conflicts with default voice. Please use a different voice name.');
276+ return;
277+ }
278+
279+ // Convert display name to standard language code before saving
280+ const standardLangCode = this.convertDisplayNameToLanguageCode(voiceLang);
281+
282+ this.settings.customVoices.push({
283+ name: voiceName,
284+ voice_id: voiceId,
285+ lang: standardLangCode,
286+ preview_url: null,
287+ });
288+
289+ $('#minimax_custom_voice_name').val('');
290+ $('#minimax_custom_voice_id').val('');
291+ $('#minimax_custom_voice_lang').val('auto');
292+
293+ this.updateCustomVoicesDisplay();
294+ initVoiceMap(); // Update TTS extension voiceMap
295+ saveTtsProviderSettings();
296+ toastr.success('Voice added successfully');
297+ }
298+
299+ // Remove custom voice
300+ removeCustomVoice(voiceId) {
301+ this.settings.customVoices = this.settings.customVoices.filter(v => v.voice_id !== voiceId);
302+ this.updateCustomVoicesDisplay();
303+ initVoiceMap(); // Update TTS extension voiceMap
304+ saveTtsProviderSettings();
305+ toastr.success('Voice removed successfully');
306+ }
307+
308+ // Helper function to escape HTML
309+ escapeHtml(text) {
310+ const div = document.createElement('div');
311+ div.textContent = text;
312+ return div.innerHTML;
313+ }
314+
315+ // Update custom models display
316+ updateCustomModelsDisplay() {
317+ const container = $('#minimax_custom_models_list');
318+ container.empty();
319+
320+ if (this.settings.customModels.length === 0) {
321+ container.append('<div class="minimax-empty-list">No custom models added</div>');
322+ return;
323+ }
324+
325+ this.settings.customModels.forEach(model => {
326+ const modelDiv = $('<div></div>').addClass('minimax-custom-item');
327+
328+ const modelInfo = $('<div></div>').addClass('minimax-custom-item-info');
329+ const modelName = $('<div></div>').addClass('minimax-custom-item-name').text(model.name);
330+ const modelId = $('<div></div>').addClass('minimax-custom-item-details').text(`(${model.id})`);
331+ modelInfo.append(modelName).append(modelId);
332+
333+ const removeBtn = $('<button></button>')
334+ .addClass('menu_button minimax-custom-item-remove')
335+ .text('Remove')
336+ .on('click', () => {
337+ try {
338+ this.removeCustomModel(model.id);
339+ } catch (error) {
340+ console.error('MiniMax TTS: Error removing custom model:', error);
341+ toastr.error(`Failed to remove custom model: ${error.message}`);
342+ }
343+ });
344+
345+ modelDiv.append(modelInfo).append(removeBtn);
346+ container.append(modelDiv);
347+ });
348+ }
349+
350+ // Update custom voices display
351+ updateCustomVoicesDisplay() {
352+ const container = $('#minimax_custom_voices_list');
353+ container.empty();
354+
355+ if (this.settings.customVoices.length === 0) {
356+ container.append('<div class="minimax-empty-list">No custom voices added</div>');
357+ return;
358+ }
359+
360+ this.settings.customVoices.forEach(voice => {
361+ const voiceDiv = $('<div></div>').addClass('minimax-custom-item');
362+
363+ const voiceInfo = $('<div></div>').addClass('minimax-custom-item-info');
364+ const voiceName = $('<div></div>').addClass('minimax-custom-item-name').text(voice.name);
365+ const voiceDetails = $('<div></div>').addClass('minimax-custom-item-details').text(`(${voice.voice_id}) - ${voice.lang}`);
366+ voiceInfo.append(voiceName).append(voiceDetails);
367+
368+ const removeBtn = $('<button></button>')
369+ .addClass('menu_button minimax-custom-item-remove')
370+ .text('Remove')
371+ .on('click', () => {
372+ try {
373+ this.removeCustomVoice(voice.voice_id);
374+ } catch (error) {
375+ console.error('MiniMax TTS: Error removing custom voice:', error);
376+ toastr.error(`Failed to remove custom voice: ${error.message}`);
377+ }
378+ });
379+
380+ voiceDiv.append(voiceInfo).append(removeBtn);
381+ container.append(voiceDiv);
382+ });
383+ }
384+
385+ // Get all models (default + custom)
386+ getAllModels() {
387+ return [...MiniMaxTtsProvider.defaultModels, ...this.settings.customModels];
388+ }
389+
390+ // Get all voices (default + custom)
391+ getAllVoices() {
392+ return [...MiniMaxTtsProvider.defaultVoices, ...this.settings.customVoices];
393+ }
394+
395+ /**
396+ * Convert display names to standard language codes
397+ * @param {string} displayName Language display name
398+ * @returns {string} Standard language code
399+ */
400+ convertDisplayNameToLanguageCode(displayName) {
401+ const displayNameToCode = {
402+ 'Chinese': 'zh-CN',
403+ 'Chinese,Yue': 'zh-TW',
404+ 'English': 'en-US',
405+ 'Japanese': 'ja-JP',
406+ 'Korean': 'ko-KR',
407+ 'French': 'fr-FR',
408+ 'German': 'de-DE',
409+ 'Spanish': 'es-ES',
410+ 'Portuguese': 'pt-BR',
411+ 'Italian': 'it-IT',
412+ 'Arabic': 'ar-SA',
413+ 'Russian': 'ru-RU',
414+ 'Turkish': 'tr-TR',
415+ 'Dutch': 'nl-NL',
416+ 'Ukrainian': 'uk-UA',
417+ 'Vietnamese': 'vi-VN',
418+ 'Indonesian': 'id-ID',
419+ 'Thai': 'th-TH',
420+ 'Polish': 'pl-PL',
421+ 'Romanian': 'ro-RO',
422+ 'Greek': 'el-GR',
423+ 'Czech': 'cs-CZ',
424+ 'Finnish': 'fi-FI',
425+ 'Hindi': 'hi-IN',
426+ };
427+
428+ return displayNameToCode[displayName] || displayName;
429+ }
430+
431+ updateModelSelect(models) {
432+ const modelSelect = $('#minimax_tts_model');
433+ const currentValue = modelSelect.val();
434+
435+ // Clear existing options
436+ modelSelect.empty();
437+
438+ // Add all models
439+ models.forEach(model => {
440+ const option = $('<option></option>');
441+ option.val(model.id);
442+ option.text(model.name);
443+ modelSelect.append(option);
444+ });
445+
446+ // Restore previous selection if it still exists
447+ if (currentValue && models.find(m => m.id === currentValue)) {
448+ modelSelect.val(currentValue);
449+ }
450+ }
451+
452+ async loadSettings(settings) {
453+ // Populate Provider UI given input settings
454+ if (Object.keys(settings).length === 0) {
455+ console.info('Using default MiniMax TTS Provider settings');
456+ }
457+
458+ // Only accept keys defined in defaultSettings
459+ this.settings = { ...this.defaultSettings };
460+
461+ for (const key in settings) {
462+ if (key in this.settings) {
463+ this.settings[key] = settings[key];
464+ } else {
465+ console.warn(`Invalid setting passed to MiniMax TTS Provider: ${key}`);
466+ }
467+ }
468+
469+ // Ensure custom configuration arrays exist
470+ if (!this.settings.customModels) this.settings.customModels = [];
471+ if (!this.settings.customVoices) this.settings.customVoices = [];
472+
473+ $('#minimax_tts_api_host').val(this.settings.apiHost || 'https://api.minimax.io');
474+ $('#minimax_tts_model').val(this.settings.model);
475+ $('#minimax_tts_speed').val(this.settings.speed);
476+ $('#minimax_tts_volume').val(this.settings.volume);
477+ $('#minimax_tts_pitch').val(this.settings.pitch);
478+ $('#minimax_tts_format').val(this.settings.format);
479+ $('#minimax_tts_custom_voice_id').val(this.settings.customVoiceId);
480+
481+ $('#minimax_connect').on('click', () => {
482+ try {
483+ this.onConnectClick();
484+ } catch (error) {
485+ console.error('MiniMax TTS: Error in connect click handler:', error);
486+ toastr.error(`Connection failed: ${error.message}`);
487+ }
488+ });
489+ $('#minimax_refresh').on('click', () => {
490+ try {
491+ this.onRefreshClick();
492+ } catch (error) {
493+ console.error('MiniMax TTS: Error in refresh click handler:', error);
494+ toastr.error(`Refresh failed: ${error.message}`);
495+ }
496+ });
497+ $('#minimax_tts_api_host').on('change', this.onSettingsChange.bind(this));
498+ $('#minimax_tts_speed').on('input', this.onSettingsChange.bind(this));
499+ $('#minimax_tts_volume').on('input', this.onSettingsChange.bind(this));
500+ $('#minimax_tts_pitch').on('input', this.onSettingsChange.bind(this));
501+ $('#minimax_tts_model').on('change', this.onSettingsChange.bind(this));
502+ $('#minimax_tts_format').on('change', this.onSettingsChange.bind(this));
503+ $('#minimax_tts_custom_voice_id').on('input', this.onSettingsChange.bind(this));
504+
505+ // Custom model and voice event listeners
506+ $('#minimax_add_custom_model').on('click', () => {
507+ try {
508+ this.addCustomModel();
509+ } catch (error) {
510+ console.error('MiniMax TTS: Error adding custom model:', error);
511+ toastr.error(`Failed to add custom model: ${error.message}`);
512+ }
513+ });
514+ $('#minimax_add_custom_voice').on('click', () => {
515+ try {
516+ this.addCustomVoice();
517+ } catch (error) {
518+ console.error('MiniMax TTS: Error adding custom voice:', error);
519+ toastr.error(`Failed to add custom voice: ${error.message}`);
520+ }
521+ });
522+
523+ // Keyboard event listeners
524+ const ENTER_KEY = 13;
525+ $('#minimax_custom_model_id, #minimax_custom_model_name').on('keypress', (e) => {
526+ if (e.which === ENTER_KEY) {
527+ try {
528+ this.addCustomModel();
529+ } catch (error) {
530+ console.error('MiniMax TTS: Error adding custom model via keyboard:', error);
531+ toastr.error(`Failed to add custom model: ${error.message}`);
532+ }
533+ }
534+ });
535+
536+ $('#minimax_custom_voice_name, #minimax_custom_voice_id').on('keypress', (e) => {
537+ if (e.which === ENTER_KEY) {
538+ try {
539+ this.addCustomVoice();
540+ } catch (error) {
541+ console.error('MiniMax TTS: Error adding custom voice via keyboard:', error);
542+ toastr.error(`Failed to add custom voice: ${error.message}`);
543+ }
544+ }
545+ });
546+
547+ $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1));
548+ $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1));
549+ $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1));
550+
551+ // Initialize custom configuration display
552+ this.updateCustomModelsDisplay();
553+ this.updateCustomVoicesDisplay();
554+
555+ // Update model selector to include custom models
556+ this.updateModelSelect(this.getAllModels());
557+
558+ // Initialize voice map for character voice assignment
559+ try {
560+ await initVoiceMap();
561+ } catch (error) {
562+ console.debug('MiniMax: Voice map initialization failed, but continuing');
563+ }
564+
565+ $('#api_key_minimax').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX]);
566+ $('#minimax_group_id').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]);
567+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
568+ eventSource.on(event, this.handler);
569+ });
570+
571+ // Only check ready status when API credentials are available
572+ if (secret_state[SECRET_KEYS.MINIMAX] && secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
573+ try {
574+ await this.checkReady();
575+ console.debug('MiniMax TTS: Settings loaded and ready');
576+ } catch (error) {
577+ console.debug('MiniMax TTS: Settings loaded, but not ready:', error);
578+ }
579+ } else {
580+ console.debug('MiniMax TTS: Settings loaded, waiting for API credentials');
581+ }
582+ }
583+
584+ // Perform a simple readiness check
585+ async checkReady() {
586+ if (!secret_state[SECRET_KEYS.MINIMAX] || !secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
587+ const error = new Error('API Key and Group ID are required');
588+ console.error('MiniMax TTS checkReady error:', error.message);
589+ throw error;
590+ }
591+ // Try to fetch available models and voices, but don't block connection on failure
592+ try {
593+ await this.updateModelsAndVoices();
594+ } catch (error) {
595+ console.warn('MiniMax TTS: Failed to fetch models/voices during ready check, will use all available:', error);
596+ // Even if API call fails, set all available values to ensure basic functionality
597+ this.availableModels = this.getAllModels();
598+ this.availableVoices = this.getAllVoices();
599+ }
600+
601+ // Ensure at least voices are available
602+ if (!this.availableVoices || this.availableVoices.length === 0) {
603+ this.availableVoices = this.getAllVoices();
604+ }
605+ }
606+
607+ async onRefreshClick() {
608+ try {
609+ await this.updateModelsAndVoices();
610+ await initVoiceMap(); // Update voice map after refresh
611+ toastr.success('MiniMax TTS: Models and voices refreshed successfully');
612+ } catch (error) {
613+ toastr.error(`MiniMax TTS: Failed to refresh - ${error.message}`);
614+ }
615+ }
616+
617+ async onConnectClick() {
618+ try {
619+ await this.checkReady();
620+ await initVoiceMap(); // Update voice map after connection
621+ toastr.success('MiniMax TTS: Connected successfully');
622+ saveTtsProviderSettings();
623+ } catch (error) {
624+ toastr.error(`MiniMax TTS: ${error.message}`);
625+ }
626+ }
627+
628+ async getVoice(voiceName) {
629+ if (!voiceName) {
630+ const error = new Error('TTS Voice name not provided');
631+ console.error('MiniMax TTS getVoice error:', error.message);
632+ throw error;
633+ }
634+
635+ // If no available voices, try to fetch them
636+ if (!this.availableVoices || this.availableVoices.length === 0) {
637+ this.availableVoices = await this.fetchTtsVoiceObjects();
638+ }
639+
640+ // Ensure at least voices are available
641+ if (!this.availableVoices || this.availableVoices.length === 0) {
642+ this.availableVoices = this.getAllVoices();
643+ }
644+
645+ const voice = this.availableVoices.find(voice =>
646+ voice.voice_id === voiceName || voice.name === voiceName,
647+ );
648+
649+ if (!voice) {
650+ const error = new Error(`TTS Voice not found: ${voiceName}`);
651+ console.error('MiniMax TTS getVoice error:', error.message);
652+ throw error;
653+ }
654+
655+ return voice;
656+ }
657+
658+ async generateTts(text, voiceId) {
659+ // If voiceId is 'customVoice', use the custom voice ID from settings
660+ if (voiceId === 'customVoice') {
661+ const customVoiceId = this.settings.customVoiceId;
662+ if (!customVoiceId || customVoiceId.trim() === '') {
663+ const error = new Error('Please enter custom voice ID in settings first');
664+ console.error('MiniMax TTS generateTts error:', error.message);
665+ throw error;
666+ }
667+ voiceId = customVoiceId.trim();
668+ }
669+
670+ // Get the voice object to determine language
671+ let language = null;
672+ try {
673+ const voice = await this.getVoice(voiceId);
674+ if (voice && voice.lang) {
675+ language = this.mapLanguageToMiniMaxFormat(voice.lang);
676+ console.debug(`MiniMax TTS: Using voice language ${voice.lang}, API language: ${language}`);
677+ }
678+ } catch (error) {
679+ console.debug('MiniMax TTS: Could not determine voice language, using default');
680+ }
681+
682+ return await this.fetchTtsGeneration(text, voiceId, language);
683+ }
684+
685+ async fetchTtsVoiceObjects() {
686+ try {
687+ if (!secret_state[SECRET_KEYS.MINIMAX] || !secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
688+ console.warn('MiniMax TTS: API Key and Group ID required for fetching voices');
689+ console.warn('Using all available voices (default + custom). Please check your API credentials');
690+ return this.getAllVoices();
691+ }
692+
693+ // MiniMax API doesn't provide a voices listing endpoint
694+ // Using all available voices (default + custom)
695+ console.info('MiniMax TTS: Using all available voices (default + custom)');
696+ return this.getAllVoices();
697+ } catch (error) {
698+ console.error('Error fetching MiniMax voices:', error);
699+ console.warn('Using all available voices (default + custom). Please check your API credentials');
700+ return this.getAllVoices();
701+ }
702+ }
703+
704+ async fetchTtsModels() {
705+ // MiniMax API doesn't provide a models listing endpoint
706+ // Using all available models (default + custom)
707+ console.info('MiniMax TTS: Using all available models (default + custom)');
708+ this.availableModels = this.getAllModels();
709+ return this.getAllModels();
710+ }
711+
712+ async updateModelsAndVoices() {
713+ try {
714+ // Get models list
715+ this.availableModels = await this.fetchTtsModels();
716+ console.info(`MiniMax TTS: Loaded ${this.availableModels.length} models`);
717+
718+ // Get voices list (now fetched from API)
719+ this.availableVoices = await this.fetchTtsVoiceObjects();
720+ console.info(`MiniMax TTS: Loaded ${this.availableVoices.length} voices`);
721+
722+ // Update model dropdown
723+ this.updateModelSelect(this.availableModels);
724+
725+ return {
726+ models: this.availableModels,
727+ voices: this.availableVoices,
728+ };
729+ } catch (error) {
730+ console.error('MiniMax TTS: Failed to update models and voices:', error);
731+ // Set all available values to ensure basic functionality
732+ this.availableModels = this.getAllModels();
733+ this.availableVoices = this.getAllVoices();
734+ throw error;
735+ }
736+ }
737+
738+ // Get correct MIME type
739+ getAudioMimeType(format) {
740+ const mimeTypes = {
741+ 'mp3': 'audio/mpeg',
742+ 'wav': 'audio/wav',
743+ 'pcm': 'audio/pcm',
744+ 'flac': 'audio/flac',
745+ 'aac': 'audio/aac',
746+ };
747+ return mimeTypes[format] || 'audio/mpeg';
748+ }
749+
750+ async fetchTtsGeneration(inputText, voiceId, language = null) {
751+ console.info(`Generating new MiniMax TTS for voice_id ${voiceId}`);
752+
753+ if (!secret_state[SECRET_KEYS.MINIMAX] || !secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
754+ const error = new Error('API Key and Group ID are required');
755+ console.error('MiniMax TTS fetchTtsGeneration error:', error.message);
756+ throw error;
757+ }
758+
759+ const requestBody = {
760+ text: inputText,
761+ voiceId: voiceId,
762+ apiHost: this.settings.apiHost,
763+ model: this.settings.model || 'speech-02-hd',
764+ speed: Number(this.settings.speed) || 1.0,
765+ volume: Number(this.settings.volume) || 1.0,
766+ pitch: Number(this.settings.pitch) || 1.0,
767+ audioSampleRate: Number(this.settings.audioSampleRate) || 32000,
768+ bitrate: Number(this.settings.bitrate) || 128000,
769+ format: this.settings.format || 'mp3',
770+ language: language,
771+ };
772+
773+ console.debug('MiniMax TTS Request:', {
774+ body: { ...requestBody, voiceId: '[REDACTED]' },
775+ });
776+
777+ try {
778+ const response = await fetch('/api/minimax/generate-voice', {
779+ method: 'POST',
780+ headers: getRequestHeaders(),
781+ body: JSON.stringify(requestBody),
782+ });
783+
784+ if (!response.ok) {
785+ let errorMessage = `HTTP ${response.status}`;
786+
787+ try {
788+ // Try to parse JSON error response from backend
789+ const errorData = await response.json();
790+ console.error('MiniMax TTS backend error:', errorData);
791+ errorMessage = errorData.error || errorMessage;
792+ } catch (jsonError) {
793+ // If not JSON, try to read text
794+ try {
795+ const errorText = await response.text();
796+ console.error('MiniMax TTS backend error (Text):', errorText);
797+ errorMessage = errorText || errorMessage;
798+ } catch (textError) {
799+ console.error('MiniMax TTS: Failed to read error response:', textError);
800+ }
801+ }
802+
803+ toastr.error(`${errorMessage}`, 'MiniMax TTS Generation Failed');
804+ const error = new Error(errorMessage);
805+ console.error('MiniMax TTS fetchTtsGeneration error:', error.message);
806+ throw error;
807+ }
808+
809+ // Backend handles all the complex processing and returns audio data directly
810+ console.debug('MiniMax TTS: Audio response received from backend');
811+ return response;
812+
813+ } catch (error) {
814+ console.error('Error in MiniMax TTS generation:', error);
815+ throw error;
816+ }
817+ }
818+
819+ /**
820+ * Map language codes to MiniMax API supported language format
821+ * @param {string} lang Language code or display name
822+ * @returns {string} MiniMax API language format
823+ */
824+ mapLanguageToMiniMaxFormat(lang) {
825+ // Convert display name to language code if needed
826+ const languageCode = this.convertDisplayNameToLanguageCode(lang);
827+
828+ // Then map language codes to MiniMax API format
829+ const languageMap = {
830+ 'zh-CN': 'zh_CN',
831+ 'zh-TW': 'zh_TW',
832+ 'en-US': 'en_US',
833+ 'en-GB': 'en_GB',
834+ 'en-AU': 'en_AU',
835+ 'en-IN': 'en_IN',
836+ 'ja-JP': 'ja_JP',
837+ 'ko-KR': 'ko_KR',
838+ 'fr-FR': 'fr_FR',
839+ 'de-DE': 'de_DE',
840+ 'es-ES': 'es_ES',
841+ 'pt-BR': 'pt_BR',
842+ 'it-IT': 'it_IT',
843+ 'ar-SA': 'ar_SA',
844+ 'ru-RU': 'ru_RU',
845+ 'tr-TR': 'tr_TR',
846+ 'nl-NL': 'nl_NL',
847+ 'uk-UA': 'uk_UA',
848+ 'vi-VN': 'vi_VN',
849+ 'id-ID': 'id_ID',
850+ 'th-TH': 'th_TH',
851+ 'pl-PL': 'pl_PL',
852+ 'ro-RO': 'ro_RO',
853+ 'el-GR': 'el_GR',
854+ 'cs-CZ': 'cs_CZ',
855+ 'fi-FI': 'fi_FI',
856+ 'hi-IN': 'hi_IN',
857+ };
858+
859+ // Return mapped language or default to auto
860+ return languageMap[languageCode] || 'auto';
861+ }
862+
863+ /**
864+ * Preview TTS for a given voice ID.
865+ * @param {string} voiceId Voice ID
866+ */
867+ async previewTtsVoice(voiceId) {
868+ this.audioElement.pause();
869+ this.audioElement.currentTime = 0;
870+
871+ try {
872+ const voice = await this.getVoice(voiceId);
873+ // Get preview text based on voice language, defaulting to en-US
874+ const previewLang = voice.lang || 'en-US';
875+ const text = getPreviewString(previewLang);
876+
877+ // Map the language to MiniMax API format for the request
878+ const apiLang = this.mapLanguageToMiniMaxFormat(previewLang);
879+ console.debug(`MiniMax TTS: Using preview language ${previewLang}, API language: ${apiLang}`);
880+
881+ const response = await this.fetchTtsGeneration(text, voiceId, apiLang);
882+
883+ if (!response.ok) {
884+ const errorText = await response.text();
885+ const error = new Error(`HTTP ${response.status}: ${errorText}`);
886+ console.error('MiniMax TTS previewTtsVoice error:', error.message);
887+ throw error;
888+ }
889+
890+ const audio = await response.blob();
891+ console.debug(`MiniMax TTS: Audio blob size: ${audio.size}, type: ${audio.type}`);
892+
893+ // Use the same method as other TTS providers - convert to base64 data URL
894+ const srcUrl = await getBase64Async(audio);
895+ console.debug('MiniMax TTS: Base64 data URL created');
896+
897+ // Clean up previous event listener to prevent memory leaks
898+ this.audioElement.onended = null;
899+ this.audioElement.onerror = null;
900+
901+ this.audioElement.src = srcUrl;
902+ this.audioElement.volume = Math.min(this.settings.volume || 1.0, 1.0); // HTML audio element max is 1.0
903+
904+ // Add error handler for audio element
905+ this.audioElement.onerror = (e) => {
906+ console.error('MiniMax TTS: Audio element error:', e);
907+ console.error('MiniMax TTS: Audio element error details:', {
908+ error: this.audioElement.error,
909+ networkState: this.audioElement.networkState,
910+ readyState: this.audioElement.readyState,
911+ src: this.audioElement.src,
912+ });
913+
914+ toastr.error('Audio playback failed. The audio format may not be supported by your browser.');
915+ };
916+
917+ try {
918+ await this.audioElement.play();
919+ console.debug('MiniMax TTS: Audio playback started successfully');
920+ } catch (playError) {
921+ console.error('MiniMax TTS: Play error:', playError);
922+ throw new Error(`Audio playback failed: ${playError.message}`);
923+ }
924+
925+ this.audioElement.onended = () => {
926+ this.audioElement.onended = null;
927+ this.audioElement.onerror = null;
928+ };
929+
930+ } catch (error) {
931+ console.error('MiniMax TTS Preview Error:', error);
932+ toastr.error(`Could not generate preview: ${error.message}`);
933+ }
934+ }
935+}
public/scripts/extensions/tts/openai.js+116 -11
@@ -1,15 +1,18 @@
11import { getRequestHeaders, substituteParams } from '../../../script.js';
22import { saveTtsProviderSettings, sanitizeId } from './index.js';
33
44export { OpenAITtsProvider };
55
66class OpenAITtsProvider {
77 static voices = [
88 { name: 'Alloy', voice_id: 'alloy', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/alloy.wav' },
9+ { name: 'Ash', voice_id: 'ash', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/ash.wav' },
10+ { name: 'Coral', voice_id: 'coral', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/coral.wav' },
911 { name: 'Echo', voice_id: 'echo', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/echo.wav' },
1012 { name: 'Fable', voice_id: 'fable', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/fable.wav' },
1113 { name: 'Onyx', voice_id: 'onyx', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/onyx.wav' },
1214 { name: 'Nova', voice_id: 'nova', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/nova.wav' },
15+ { name: 'Sage', voice_id: 'sage', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/sage.wav' },
1316 { name: 'Shimmer', voice_id: 'shimmer', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/shimmer.wav' },
1417 ];
1518
@@ -23,6 +26,7 @@ class OpenAITtsProvider {
2326 customVoices: [],
2427 model: 'tts-1',
2528 speed: 1,
29+ characterInstructions: {},
2630 };
2731
2832 get settingsHtml() {
@@ -35,6 +39,7 @@ class OpenAITtsProvider {
3539 <optgroup label="Latest">
3640 <option value="tts-1">tts-1</option>
3741 <option value="tts-1-hd">tts-1-hd</option>
42+ <option value="gpt-4o-mini-tts">gpt-4o-mini-tts</option>
3843 </optgroup>
3944 <optgroup label="Snapshots">
4045 <option value="tts-1-1106">tts-1-1106</option>
@@ -79,14 +84,104 @@ class OpenAITtsProvider {
7984 $('#openai-tts-speed-output').text(this.settings.speed);
8085
8186 await this.checkReady();
87+ // Initialize UI state based on current model (gpt-4o-mini-tts or other)
88+ this.updateInstructionsUI();
89+ // Look for voice map changes
90+ this.setupVoiceMapObserver();
91+
8292 console.debug('OpenAI TTS: Settings loaded');
8393 }
8494
95+ setupVoiceMapObserver() {
96+ if (this.voiceMapObserver) {
97+ this.voiceMapObserver.disconnect();
98+ this.voiceMapObserver = null;
99+ }
100+
101+ const targetNode = document.getElementById('tts_voicemap_block');
102+ if (!targetNode) return;
103+
104+ const observer = new MutationObserver(() => {
105+ if (this.settings.model === 'gpt-4o-mini-tts') {
106+ this.populateCharacterInstructions();
107+ }
108+ });
109+
110+ observer.observe(targetNode, { childList: true, subtree: true });
111+ this.voiceMapObserver = observer;
112+ }
113+
85114 onSettingsChange() {
86115 // Update dynamically
87116 this.settings.model = String($('#openai-tts-model').find(':selected').val());
88117 this.settings.speed = Number($('#openai-tts-speed').val());
89118 $('#openai-tts-speed-output').text(this.settings.speed);
119+ this.updateInstructionsUI();
120+ saveTtsProviderSettings();
121+ }
122+
123+ updateInstructionsUI() {
124+ if (this.settings.model === 'gpt-4o-mini-tts') {
125+ this.createInstructionsContainer();
126+ $('#openai-instructions-container').show();
127+ this.populateCharacterInstructions();
128+ } else {
129+ $('#openai-instructions-container').hide();
130+ this.voiceMapObserver?.disconnect();
131+ this.voiceMapObserver = null;
132+ }
133+ }
134+
135+ createInstructionsContainer() {
136+ if ($('#openai-instructions-container').length === 0) {
137+ const containerHtml = `
138+ <div id="openai-instructions-container" style="display: none;">
139+ <span>Voice Instructions (GPT-4o Mini TTS)</span><br>
140+ <small>Customize how each character speaks</small>
141+ <div id="openai-character-instructions"></div>
142+ </div>
143+ `;
144+ $('#openai-tts-speed').parent().after(containerHtml);
145+ }
146+ }
147+
148+ populateCharacterInstructions() {
149+
150+ const currentCharacters = $('.tts_voicemap_block_char span').map((i, el) => $(el).text()).get();
151+
152+ $('#openai-character-instructions').empty();
153+
154+ for (const char of currentCharacters) {
155+ if (char === 'SillyTavern System' || char === '[Default Voice]') continue;
156+
157+ const sanitizedName = sanitizeId(char);
158+ const savedInstructions = this.settings.characterInstructions?.[char] || '';
159+
160+ const instructionBlock = document.createElement('div');
161+ const label = document.createElement('label');
162+ const textArea = document.createElement('textarea');
163+ instructionBlock.appendChild(label);
164+ instructionBlock.appendChild(textArea);
165+ instructionBlock.className = 'character-instructions';
166+ label.setAttribute('for', `openai_char_${sanitizedName}`);
167+ label.innerText = `${char}:`;
168+ textArea.id = `openai_char_${sanitizedName}`;
169+ textArea.placeholder = 'e.g., "Speak cheerfully and energetically"';
170+ textArea.className = 'textarea_compact autoSetHeight';
171+ textArea.value = savedInstructions;
172+ textArea.addEventListener('input', () => {
173+ this.saveCharacterInstructions(char, textArea.value);
174+ });
175+
176+ $('#openai-character-instructions').append(instructionBlock);
177+ }
178+ }
179+
180+ saveCharacterInstructions(characterName, instructions) {
181+ if (!this.settings.characterInstructions) {
182+ this.settings.characterInstructions = {};
183+ }
184+ this.settings.characterInstructions[characterName] = instructions;
90185 saveTtsProviderSettings();
91186 }
92187
@@ -112,8 +207,8 @@ class OpenAITtsProvider {
112207 return voice;
113208 }
114209
115210 async generateTts(text, voiceId, characterName = null) {
116211 const response = await this.fetchTtsGeneration(text, voiceId, characterName);
117212 return response;
118213 }
119214
@@ -125,17 +220,27 @@ class OpenAITtsProvider {
125220 return;
126221 }
127222
128223 async fetchTtsGeneration(inputText, voiceId, characterName = null) {
129224 console.info(`Generating new TTS for voice_id ${voiceId}`);
225+
226+ const requestBody = {
227+ 'text': inputText,
228+ 'voice': voiceId,
229+ 'model': this.settings.model,
230+ 'speed': this.settings.speed,
231+ };
232+
233+ if (this.settings.model === 'gpt-4o-mini-tts' && characterName) {
234+ const instructions = this.settings.characterInstructions?.[characterName];
235+ if (instructions && instructions.trim()) {
236+ requestBody.instructions = substituteParams(instructions);
237+ }
238+ }
239+
130240 const response = await fetch('/api/openai/generate-voice', {
131241 method: 'POST',
132242 headers: getRequestHeaders(),
133243 body: JSON.stringify({requestBody),
134- 'text': inputText,
135- 'voice': voiceId,
136- 'model': this.settings.model,
137- 'speed': this.settings.speed,
138- }),
139244 });
140245
141246 if (!response.ok) {
public/scripts/extensions/tts/settings.html+10 -2
@@ -56,8 +56,16 @@
5656 <small data-i18n="Skip tagged blocks">Skip &lt;tagged&gt; blocks</small>
5757 </label>
5858 <label class="checkbox_label" for="tts_pass_asterisks">
5959 <input type="checkbox" id="tts_pass_asterisks">
6060 <small data-i18n="Pass Asterisks to TTS Engine">Pass Asterisks to TTS Engine</small>
61+ </label>
62+ <label class="checkbox_label" for="tts_multi_voice_enabled"
63+ data-i18n="[title]Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled."
64+ title="Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.">
65+ <input type="checkbox" id="tts_multi_voice_enabled">
66+ <small data-i18n="Different voices for quotes and text inside asterisks">
67+ Different voices for "quotes", *text inside asterisks* and other text
68+ </small>
6169 </label>
6270 </div>
6371 <div id="playback_rate_block" class="range-block">
public/scripts/extensions/tts/style.css+3 -0
@@ -1,3 +1,6 @@
1+@import './css/minimax-tts.css';
2+@import './css/openai-tts.css';
3+
14.voice_preview {
25 margin: 0.25rem 0.5rem;
36 display: flex;
public/scripts/group-chats.js+14 -7
@@ -126,7 +126,7 @@ export const group_generation_mode = {
126126 APPEND_DISABLED: 2,
127127};
128128
129129export const DEFAULT_AUTO_MODE_DELAY = 5;
130130
131131export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, debounce_timeout.quick));
132132let autoModeWorker = null;
@@ -870,14 +870,14 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
870870 if (params && typeof params.force_chid == 'number') {
871871 activatedMembers = [params.force_chid];
872872 } else if (type === 'quiet') {
873873 activatedMembers = activateSwipe(group.members, { allowSystem: true }).slice(0, 1);
874874
875875 if (activatedMembers.length === 0) {
876876 activatedMembers = activateListOrder(group.members.slice(0, 1));
877877 }
878878 }
879879 else if (type === 'swipe' || type === 'continue') {
880880 activatedMembers = activateSwipe(group.members, { allowSystem: false });
881881
882882 if (activatedMembers.length === 0) {
883883 toastr.warning(t`Deleted group member swiped. To get a reply, add them back to the group.`);
@@ -984,15 +984,21 @@ function activateImpersonate(members) {
984984/**
985985 * Activates a group member based on the last message.
986986 * @param {string[]} members Array of group member avatar ids
987+ * @param {Object} [options] Options object
988+ * @param {boolean} [options.allowSystem] Whether to allow system messages
987989 * @returns {number[]} Array of character ids
988990 */
989991function activateSwipe(members, { allowSystem = false } = {}) {
990992 let activatedNames = [];
991993 const lastMessage = chat[chat.length - 1];
992994
993- if (lastMessage.is_user || lastMessage.is_system || lastMessage.extra?.type === system_message_types.NARRATOR) {
995+ if (!lastMessage) {
996+ return [];
997+ }
998+
999+ if (lastMessage.is_user || (!allowSystem && lastMessage.is_system) || lastMessage.extra?.type === system_message_types.NARRATOR) {
9941000 for (const message of chat.slice().reverse()) {
9951001 if (message.is_user || (!allowSystem && message.is_system) || message.extra?.type === system_message_types.NARRATOR) {
9961002 continue;
9971003 }
9981004
@@ -1194,7 +1200,8 @@ export async function editGroup(id, immediately, reload = true) {
11941200 }
11951201
11961202 if (id === selected_group) {
1197- group['chat_metadata'] = structuredClone(chat_metadata);
1203+ // structuredClone may cause issues if metadata has non-cloneable references
1204+ group['chat_metadata'] = JSON.parse(JSON.stringify(chat_metadata));
11981205 }
11991206
12001207 if (immediately) {
public/scripts/instruct-mode.js+67 -29
@@ -1,13 +1,13 @@
11'use strict';
22
33import { extension_prompt_types, name1, name2, online_status, saveSettingsDebounced, substituteParams } from '../script.js';
44import { selected_group } from './group-chats.js';
55import { parseExampleIntoIndividual } from './openai.js';
66import {
77 power_user,
88 context_presets,
99} from './power-user.js';
1010import { onlyUnique, regexFromString, resetScrollHeight } from './utils.js';
1111
1212/**
1313 * @type {any[]} Instruct mode presets.
@@ -24,8 +24,8 @@ const controls = [
2424 { id: 'instruct_enabled', property: 'enabled', isCheckbox: true },
2525 { id: 'instruct_wrap', property: 'wrap', isCheckbox: true },
2626 { id: 'instruct_macro', property: 'macro', isCheckbox: true },
2727 { id: 'instruct_system_sequence_prefixinstruct_story_string_prefix', property: 'system_sequence_prefixstory_string_prefix', isCheckbox: false },
2828 { id: 'instruct_system_sequence_suffixinstruct_story_string_suffix', property: 'system_sequence_suffixstory_string_suffix', isCheckbox: false },
2929 { id: 'instruct_input_sequence', property: 'input_sequence', isCheckbox: false },
3030 { id: 'instruct_input_suffix', property: 'input_suffix', isCheckbox: false },
3131 { id: 'instruct_output_sequence', property: 'output_sequence', isCheckbox: false },
@@ -44,6 +44,7 @@ const controls = [
4444 { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
4545 { id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false },
4646 { id: 'instruct_system_same_as_user', property: 'system_same_as_user', isCheckbox: true, trigger: true },
47+ { id: 'instruct_sequences_as_stop_strings', property: 'sequences_as_stop_strings', isCheckbox: true },
4748];
4849
4950/**
@@ -75,10 +76,12 @@ function migrateInstructModeSettings(settings) {
7576 last_system_sequence: '',
7677 first_input_sequence: '',
7778 last_input_sequence: '',
78- names_force_groups: true,
7979 skip_examples: false,
8080 system_same_as_user: false,
8181 names_behavior: names_behavior_types.FORCE,
82+ sequences_as_stop_strings: true,
83+ story_string_prefix: '',
84+ story_string_suffix: '',
8285 };
8386
8487 for (let key in defaults) {
@@ -86,6 +89,19 @@ function migrateInstructModeSettings(settings) {
8689 settings[key] = defaults[key];
8790 }
8891 }
92+
93+ const obsoleteFields = [
94+ 'names',
95+ 'names_force_groups',
96+ 'system_sequence_prefix',
97+ 'system_sequence_suffix',
98+ ];
99+
100+ for (const field of obsoleteFields) {
101+ if (Object.hasOwn(settings, field)) {
102+ delete settings[field];
103+ }
104+ }
89105}
90106
91107/**
@@ -321,15 +337,20 @@ export function getInstructStoppingSequences({ customInstruct = null, useStopStr
321337
322338 const combined_sequence = [
323339 stop_sequence,
324- input_sequence,
340+ ];
325- output_sequence,
341+
326- first_output_sequence,
342+ if (instruct.sequences_as_stop_strings) {
327- last_output_sequence,
343+ combined_sequence.push(
328- system_sequence,
344+ input_sequence,
329- last_system_sequence,
345+ output_sequence,
330- ].join('\n');
346+ first_output_sequence,
347+ last_output_sequence,
348+ system_sequence,
349+ last_system_sequence,
350+ );
351+ }
331352
332353 combined_sequence.splitjoin('\n').filter(split(line, index, self'\n') => self.indexOffilter(line) === indexonlyUnique).forEach(addInstructSequence);
333354 }
334355
335356 if (useStopStrings ?? power_user.context.use_stop_strings) {
@@ -438,29 +459,46 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
438459/**
439460 * Formats instruct mode system prompt.
440461 * @param {string} systemPrompt System prompt string.
441462 * @param {InstructSettings} customInstruct_customInstruct Custom instruct mode settings.
442463 * @returns {string} Formatted instruct mode system prompt.
464+ * @deprecated Currently doesn't do anything useful.
443465 */
444466export function formatInstructModeSystemPrompt(systemPrompt, customInstruct_customInstruct = null) {
445- if (!systemPrompt) {
467+ return systemPrompt || '';
468+}
469+
470+/**
471+ * Formats instruct mode story string.
472+ * @param {string} storyString Story string and anchors
473+ * @param {object} [params]
474+ * @param {ContextSettings} [params.customContext] Custom context settings.
475+ * @param {InstructSettings} [params.customInstruct] Custom instruct mode settings.
476+ * @returns {string} Formatted instruct mode story string.
477+ */
478+export function formatInstructModeStoryString(storyString, { customContext = null, customInstruct = null } = {}) {
479+ if (!storyString) {
446480 return '';
447481 }
448482
449483 const instructinstructSettings = structuredClone(customInstruct ?? power_user.instruct);
450-
484+ const contextSettings = structuredClone(customContext ?? power_user.context);
451485 const separatorstoryStringPosition = instructcontextSettings.wrapstory_string_position ? '\n' :? ''extension_prompt_types.IN_PROMPT;
452486
453- if (instruct.system_sequence_prefix) {
487+ // Only wrap if not in-chat position (it will be wrapped by message sequences instead)
488+ const applySequences = storyStringPosition !== extension_prompt_types.IN_CHAT;
489+ const separator = instructSettings.wrap ? '\n' : '';
490+ if (applySequences && instructSettings.story_string_prefix) {
454491 // TODO: Replace with a proper 'System' prompt entity name input
455492 const prefix = instructsubstituteParams(instructSettings.system_sequence_prefixstory_string_prefix).replace(/{{name}}/gi, 'System');
456493 systemPromptstoryString = prefix + separator + systemPromptstoryString;
457494 }
458495
459496 if (instructapplySequences && instructSettings.system_sequence_suffixstory_string_suffix) {
460- systemPrompt = systemPrompt + separator + instruct.system_sequence_suffix;
497+ const suffix = substituteParams(instructSettings.story_string_suffix);
498+ storyString = storyString + suffix;
461499 }
462500
463501 return systemPromptstoryString;
464502}
465503
466504/**
@@ -637,13 +675,13 @@ export function getInstructMacros(env) {
637675 const instructMacros = [
638676 // Instruct template macros
639677 {
640678 key: 'instructSystemPromptPrefixinstructStoryStringPrefix',
641679 value: power_user.instruct.system_sequence_prefixstory_string_prefix,
642680 enabled: power_user.instruct.enabled,
643681 },
644682 {
645683 key: 'instructSystemPromptSuffixinstructStoryStringSuffix',
646684 value: power_user.instruct.system_sequence_suffixstory_string_suffix,
647685 enabled: power_user.instruct.enabled,
648686 },
649687 {
public/scripts/kai-settings.js+1 -11
@@ -20,7 +20,7 @@ import {
2020 power_user,
2121} from './power-user.js';
2222import { getEventSourceStream } from './sse-stream.js';
2323import { getSortableDelay, versionCompare } from './utils.js';
2424
2525export let koboldai_settings;
2626export let koboldai_setting_names;
@@ -389,16 +389,6 @@ export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
389389}
390390
391391/**
392- * Compares two version numbers, returning true if srcVersion >= minVersion
393- * @param {string} srcVersion The current version.
394- * @param {string} minVersion The target version number to test against
395- * @returns {boolean} True if srcVersion >= minVersion, false if not
396- */
397-function versionCompare(srcVersion, minVersion) {
398- return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1;
399-}
400-
401-/**
402392 * Sorts the sampler items by the given order.
403393 * @param {any[]} orderArray Sampler order array.
404394 */
public/scripts/openai.js+311 -102
@@ -74,7 +74,7 @@ import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
7474import { t } from './i18n.js';
7575import { ToolManager } from './tool-calling.js';
7676import { accountStorage } from './util/AccountStorage.js';
7777import { COMETAPI_IGNORE_PATTERNS, IGNORE_SYMBOL } from './constants.js';
7878
7979export {
8080 openai_messages_count,
@@ -125,6 +125,7 @@ const max_64k = 65535;
125125const max_128k = 128 * 1000;
126126const max_200k = 200 * 1000;
127127const max_256k = 256 * 1000;
128+const max_400k = 400 * 1000;
128129const max_1mil = 1000 * 1000;
129130const max_2mil = 2000 * 1000;
130131const claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
@@ -178,12 +179,14 @@ export const chat_completion_sources = {
178179 COHERE: 'cohere',
179180 PERPLEXITY: 'perplexity',
180181 GROQ: 'groq',
181- ZEROONEAI: '01ai',
182182 NANOGPT: 'nanogpt',
183183 DEEPSEEK: 'deepseek',
184184 AIMLAPI: 'aimlapi',
185185 XAI: 'xai',
186186 POLLINATIONS: 'pollinations',
187+ MOONSHOT: 'moonshot',
188+ FIREWORKS: 'fireworks',
189+ COMETAPI: 'cometapi',
187190};
188191
189192const character_names_behavior = {
@@ -271,9 +274,11 @@ export const settingsToUpdate = {
271274 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
272275 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
273276 aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
274- zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false, true],
275277 xai_model: ['#model_xai_select', 'xai_model', false, true],
276278 pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true],
279+ moonshot_model: ['#model_moonshot_select', 'moonshot_model', false, true],
280+ fireworks_model: ['#model_fireworks_select', 'fireworks_model', false, true],
281+ cometapi_model: ['#model_cometapi_select', 'cometapi_model', false, true],
277282 custom_model: ['#custom_model_id', 'custom_model', false, true],
278283 custom_url: ['#custom_api_url_text', 'custom_url', false, true],
279284 custom_include_body: ['#custom_include_body', 'custom_include_body', false, true],
@@ -365,11 +370,13 @@ const default_settings = {
365370 perplexity_model: 'sonar-pro',
366371 groq_model: 'llama-3.3-70b-versatile',
367372 nanogpt_model: 'gpt-4o-mini',
368- zerooneai_model: 'yi-large',
369373 deepseek_model: 'deepseek-chat',
370374 aimlapi_model: 'gpt-4o-mini-2024-07-18',
371375 xai_model: 'grok-3-beta',
372376 pollinations_model: 'openai',
377+ cometapi_model: 'gpt-4o',
378+ moonshot_model: 'kimi-latest',
379+ fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
373380 custom_model: '',
374381 custom_url: '',
375382 custom_include_body: '',
@@ -452,11 +459,13 @@ const oai_settings = {
452459 perplexity_model: 'sonar-pro',
453460 groq_model: 'llama-3.1-70b-versatile',
454461 nanogpt_model: 'gpt-4o-mini',
455- zerooneai_model: 'yi-large',
456462 deepseek_model: 'deepseek-chat',
457463 aimlapi_model: 'gpt-4-turbo',
458464 xai_model: 'grok-3-beta',
459465 pollinations_model: 'openai',
466+ cometapi_model: 'gpt-4o',
467+ moonshot_model: 'kimi-latest',
468+ fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
460469 custom_model: '',
461470 custom_url: '',
462471 custom_include_body: '',
@@ -1607,8 +1616,6 @@ export function getChatCompletionModel(source = null) {
16071616 return oai_settings.perplexity_model;
16081617 case chat_completion_sources.GROQ:
16091618 return oai_settings.groq_model;
1610- case chat_completion_sources.ZEROONEAI:
1611- return oai_settings.zerooneai_model;
16121619 case chat_completion_sources.NANOGPT:
16131620 return oai_settings.nanogpt_model;
16141621 case chat_completion_sources.DEEPSEEK:
@@ -1619,6 +1626,12 @@ export function getChatCompletionModel(source = null) {
16191626 return oai_settings.xai_model;
16201627 case chat_completion_sources.POLLINATIONS:
16211628 return oai_settings.pollinations_model;
1629+ case chat_completion_sources.COMETAPI:
1630+ return oai_settings.cometapi_model;
1631+ case chat_completion_sources.MOONSHOT:
1632+ return oai_settings.moonshot_model;
1633+ case chat_completion_sources.FIREWORKS:
1634+ return oai_settings.fireworks_model;
16221635 default:
16231636 console.error(`Unknown chat completion source: ${activeSource}`);
16241637 return '';
@@ -1722,23 +1735,6 @@ function saveModelList(data) {
17221735 }
17231736 }
17241737
1725- if (oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI) {
1726- $('#model_01ai_select').empty();
1727- model_list.forEach((model) => {
1728- $('#model_01ai_select').append(
1729- $('<option>', {
1730- value: model.id,
1731- text: model.id,
1732- }));
1733- });
1734-
1735- if (!oai_settings.zerooneai_model && model_list.length > 0) {
1736- oai_settings.zerooneai_model = model_list[0].id;
1737- }
1738-
1739- $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');
1740- }
1741-
17421738 if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
17431739 $('#model_aimlapi_select').empty();
17441740 const chatModels = model_list.filter(m => m.type === 'chat-completion');
@@ -1888,6 +1884,50 @@ function saveModelList(data) {
18881884
18891885 $('#model_groq_select').val(oai_settings.groq_model).trigger('change');
18901886 }
1887+
1888+ if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS) {
1889+ $('#model_fireworks_select').empty();
1890+ model_list.forEach((model) => {
1891+ if (!model?.supports_chat) {
1892+ return;
1893+ }
1894+ $('#model_fireworks_select').append(
1895+ $('<option>', {
1896+ value: model.id,
1897+ text: model.id,
1898+ }));
1899+ });
1900+
1901+ const selectedModel = model_list.find(model => model.id === oai_settings.fireworks_model);
1902+ if (model_list.length > 0 && (!selectedModel || !oai_settings.fireworks_model)) {
1903+ oai_settings.fireworks_model = model_list[0].id;
1904+ }
1905+
1906+ $('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change');
1907+ }
1908+
1909+ if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
1910+ $('#model_cometapi_select').empty();
1911+
1912+ model_list.forEach((model) => {
1913+ const modelId = model.id.toLowerCase();
1914+ const isIgnoredModel = COMETAPI_IGNORE_PATTERNS.some(pattern => modelId.includes(pattern));
1915+
1916+ if (isIgnoredModel) {
1917+ return;
1918+ }
1919+
1920+ $('#model_cometapi_select').append(new Option(model.id, model.id));
1921+ });
1922+
1923+ const selectedModel = model_list.find(model => model.id === oai_settings.cometapi_model);
1924+ if (model_list.length > 0 && (!selectedModel || !oai_settings.cometapi_model)) {
1925+ oai_settings.cometapi_model = model_list[0].id;
1926+ saveSettingsDebounced();
1927+ }
1928+
1929+ $('#model_cometapi_select').val(oai_settings.cometapi_model).trigger('change');
1930+ }
18911931}
18921932
18931933function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
@@ -2005,6 +2045,7 @@ function getReasoningEffort() {
20052045 chat_completion_sources.OPENROUTER,
20062046 chat_completion_sources.POLLINATIONS,
20072047 chat_completion_sources.PERPLEXITY,
2048+ chat_completion_sources.COMETAPI,
20082049 ];
20092050
20102051 if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
@@ -2015,7 +2056,9 @@ function getReasoningEffort() {
20152056 case reasoning_effort_types.auto:
20162057 return undefined;
20172058 case reasoning_effort_types.min:
2018- return reasoning_effort_types.low;
2059+ return chat_completion_sources.OPENAI === oai_settings.chat_completion_source && /^gpt-5/.test(oai_settings.openai_model)
2060+ ? reasoning_effort_types.min
2061+ : reasoning_effort_types.low;
20192062 case reasoning_effort_types.max:
20202063 return reasoning_effort_types.high;
20212064 default:
@@ -2057,19 +2100,18 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
20572100 const isCohere = oai_settings.chat_completion_source == chat_completion_sources.COHERE;
20582101 const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;
20592102 const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
2060- const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
2061- const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
20622103 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
20632104 const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
20642105 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
20652106 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
2107+ const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
20662108 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
20672109 const isQuiet = type === 'quiet';
20682110 const isImpersonate = type === 'impersonate';
20692111 const isContinue = type === 'continue';
20702112 const stream = oai_settings.stream_openai && !isQuiet && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
20712113 const useLogprobs = !!power_user.request_token_probabilities;
20722114 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi || isMoonshot);
20732115
20742116 const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM];
20752117 if (oai_settings.bias_preset_selected
@@ -2086,6 +2128,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
20862128
20872129 const model = getChatCompletionModel();
20882130 const generate_data = {
2131+ 'type': type,
20892132 'messages': messages,
20902133 'model': model,
20912134 'temperature': Number(oai_settings.temp_openai),
@@ -2216,17 +2259,6 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22162259 delete generate_data.n;
22172260 }
22182261
2219- // https://platform.01.ai/docs#request-body
2220- if (is01AI) {
2221- delete generate_data.logprobs;
2222- delete generate_data.logit_bias;
2223- delete generate_data.top_logprobs;
2224- delete generate_data.n;
2225- delete generate_data.frequency_penalty;
2226- delete generate_data.presence_penalty;
2227- delete generate_data.stop;
2228- }
2229-
22302262 // https://api-docs.deepseek.com/api/create-chat-completion
22312263 if (isDeepSeek) {
22322264 generate_data.top_p = generate_data.top_p || Number.EPSILON;
@@ -2263,7 +2295,21 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22632295 delete generate_data.max_tokens;
22642296 }
22652297
2266- if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI || isPollinations || isAimlapi) && oai_settings.seed >= 0) {
2298+ const seedSupportedSources = [
2299+ chat_completion_sources.OPENAI,
2300+ chat_completion_sources.OPENROUTER,
2301+ chat_completion_sources.MISTRALAI,
2302+ chat_completion_sources.CUSTOM,
2303+ chat_completion_sources.COHERE,
2304+ chat_completion_sources.GROQ,
2305+ chat_completion_sources.NANOGPT,
2306+ chat_completion_sources.XAI,
2307+ chat_completion_sources.POLLINATIONS,
2308+ chat_completion_sources.AIMLAPI,
2309+ chat_completion_sources.VERTEXAI,
2310+ chat_completion_sources.MAKERSUITE,
2311+ ];
2312+ if (seedSupportedSources.includes(oai_settings.chat_completion_source) && oai_settings.seed >= 0) {
22672313 generate_data['seed'] = oai_settings.seed;
22682314 }
22692315
@@ -2290,6 +2336,24 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22902336 }
22912337 }
22922338
2339+ if (isOAI && /^gpt-5/.test(oai_settings.openai_model)) {
2340+ generate_data.max_completion_tokens = generate_data.max_tokens;
2341+ delete generate_data.max_tokens;
2342+ delete generate_data.logprobs;
2343+ delete generate_data.top_logprobs;
2344+ if (/chat-latest/.test(oai_settings.openai_model)) {
2345+ delete generate_data.tools;
2346+ delete generate_data.tool_choice;
2347+ } else {
2348+ delete generate_data.temperature;
2349+ delete generate_data.top_p;
2350+ delete generate_data.frequency_penalty;
2351+ delete generate_data.presence_penalty;
2352+ delete generate_data.logit_bias;
2353+ delete generate_data.stop;
2354+ }
2355+ }
2356+
22932357 if (jsonSchema) {
22942358 generate_data.json_schema = jsonSchema;
22952359 }
@@ -2406,7 +2470,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
24062470 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
24072471 }
24082472 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
24092473 } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI].includes(chat_completion_source)) {
24102474 if (show_thoughts) {
24112475 state.reasoning +=
24122476 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
@@ -2414,6 +2478,12 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
24142478 '';
24152479 }
24162480 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2481+ } else if (chat_completion_source === chat_completion_sources.MISTRALAI) {
2482+ if (show_thoughts) {
2483+ state.reasoning += (data.choices?.filter(x => x?.delta?.content?.[0]?.thinking)?.[0]?.delta?.content?.[0]?.thinking?.[0]?.text || '');
2484+ }
2485+ const content = data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2486+ return Array.isArray(content) ? content.map(x => x.text).filter(x => x).join('') : content;
24172487 } else {
24182488 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
24192489 }
@@ -3365,9 +3435,11 @@ function loadOpenAISettings(data, settings) {
33653435 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
33663436 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
33673437 oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model;
3368- oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
33693438 oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
33703439 oai_settings.pollinations_model = settings.pollinations_model ?? default_settings.pollinations_model;
3440+ oai_settings.cometapi_model = settings.cometapi_model ?? default_settings.cometapi_model;
3441+ oai_settings.moonshot_model = settings.moonshot_model ?? default_settings.moonshot_model;
3442+ oai_settings.fireworks_model = settings.fireworks_model ?? default_settings.fireworks_model;
33713443 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
33723444 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
33733445 oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@@ -3459,13 +3531,14 @@ function loadOpenAISettings(data, settings) {
34593531 $(`#model_nanogpt_select option[value="${oai_settings.nanogpt_model}"`).prop('selected', true);
34603532 $('#model_deepseek_select').val(oai_settings.deepseek_model);
34613533 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
3462- $('#model_01ai_select').val(oai_settings.zerooneai_model);
34633534 $('#model_aimlapi_select').val(oai_settings.aimlapi_model);
34643535 $(`#model_aimlapi_select option[value="${oai_settings.aimlapi_model}"`).prop('selected', true);
34653536 $('#model_xai_select').val(oai_settings.xai_model);
34663537 $(`#model_xai_select option[value="${oai_settings.xai_model}"`).prop('selected', true);
34673538 $('#model_pollinations_select').val(oai_settings.pollinations_model);
34683539 $(`#model_pollinations_select option[value="${oai_settings.pollinations_model}"`).prop('selected', true);
3540+ $('#model_moonshot_select').val(oai_settings.moonshot_model);
3541+ $(`#model_moonshot_select option[value="${oai_settings.moonshot_model}"`).prop('selected', true);
34693542 $('#custom_model_id').val(oai_settings.custom_model);
34703543 $('#custom_api_url_text').val(oai_settings.custom_url);
34713544 $('#openai_max_context').val(oai_settings.openai_max_context);
@@ -3636,6 +3709,7 @@ async function getStatusOpen() {
36363709 if (noValidateSources.includes(oai_settings.chat_completion_source)) {
36373710 let status = t`Key saved; press \"Test Message\" to verify.`;
36383711 setOnlineStatus(status);
3712+ updateFeatureSupportFlags();
36393713 return resultCheckStatus();
36403714 }
36413715
@@ -3707,6 +3781,7 @@ async function getStatusOpen() {
37073781 }
37083782 }
37093783
3784+ updateFeatureSupportFlags();
37103785 return resultCheckStatus();
37113786}
37123787
@@ -3735,10 +3810,12 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
37353810 cohere_model: settings.cohere_model,
37363811 perplexity_model: settings.perplexity_model,
37373812 groq_model: settings.groq_model,
3738- zerooneai_model: settings.zerooneai_model,
37393813 xai_model: settings.xai_model,
37403814 pollinations_model: settings.pollinations_model,
37413815 aimlapi_model: settings.aimlapi_model,
3816+ moonshot_model: settings.moonshot_model,
3817+ fireworks_model: settings.fireworks_model,
3818+ cometapi_model: settings.cometapi_model,
37423819 custom_model: settings.custom_model,
37433820 custom_url: settings.custom_url,
37443821 custom_include_body: settings.custom_include_body,
@@ -4279,6 +4356,9 @@ function getMaxContextOpenAI(value) {
42794356 if (oai_settings.max_context_unlocked) {
42804357 return unlocked_max;
42814358 }
4359+ else if (value.startsWith('gpt-5')) {
4360+ return max_400k;
4361+ }
42824362 else if (value.includes('gpt-4.1')) {
42834363 return max_1mil;
42844364 }
@@ -4334,10 +4414,12 @@ function getMistralMaxContext(model, isUnlocked) {
43344414 }
43354415
43364416 const contextMap = {
4417+ 'codestral-2405': 32768,
43374418 'codestral-2411-rc5': 262144,
43384419 'codestral-2412': 262144,
43394420 'codestral-2501': 262144,
43404421 'codestral-latest2508': 262144256000,
4422+ 'codestral-latest': 256000,
43414423 'codestral-mamba-2407': 262144,
43424424 'codestral-mamba-latest': 262144,
43434425 'open-codestral-mamba': 262144,
@@ -4360,13 +4442,13 @@ function getMistralMaxContext(model, isUnlocked) {
43604442 'pixtral-large-latest': 131072,
43614443 'open-mixtral-8x22b': 65536,
43624444 'open-mixtral-8x22b-2404': 65536,
4363- 'codestral-2405': 32768,
43644445 'mistral-embed': 32768,
43654446 'mistral-large-2402': 32768,
43664447 'mistral-medium': 131072,
43674448 'mistral-medium-2312': 32768,
43684449 'mistral-medium-2505': 131072,
43694450 'mistral-medium-latest2508': 131072262144,
4451+ 'mistral-medium-latest': 262144,
43704452 'mistral-moderation-2411': 32768,
43714453 'mistral-moderation-latest': 32768,
43724454 'mistral-ocr-2503': 32768,
@@ -4449,6 +4531,53 @@ function getGroqMaxContext(model, isUnlocked) {
44494531 return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
44504532}
44514533
4534+function getMoonshotMaxContext(model, isUnlocked) {
4535+ if (isUnlocked) {
4536+ return unlocked_max;
4537+ }
4538+
4539+ const contextMap = {
4540+ 'moonshot-v1-8k': max_8k,
4541+ 'moonshot-v1-32k': max_32k,
4542+ 'moonshot-v1-128k': max_128k,
4543+ 'moonshot-v1-auto': max_128k,
4544+ 'moonshot-v1-8k-vision-preview': max_8k,
4545+ 'moonshot-v1-32k-vision-preview': max_32k,
4546+ 'moonshot-v1-128k-vision-preview': max_128k,
4547+ 'kimi-k2-0711-preview': max_32k,
4548+ 'kimi-latest': max_32k,
4549+ 'kimi-thinking-preview': max_32k,
4550+ };
4551+
4552+ // Return context size if model found, otherwise default to 32k
4553+ return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_32k;
4554+}
4555+
4556+/**
4557+ * Get the maximum context size for the Fireworks model
4558+ * @param {string} model Model identifier
4559+ * @param {boolean} isUnlocked Whether context limits are unlocked
4560+ * @returns {number} Maximum context size in tokens
4561+ */
4562+function getFireworksMaxContext(model, isUnlocked) {
4563+ if (isUnlocked) {
4564+ return unlocked_max;
4565+ }
4566+
4567+ // First check if model info is available from model_list
4568+ if (Array.isArray(model_list) && model_list.length > 0) {
4569+ const modelInfo = model_list.find((record) => record.id === model);
4570+ if (modelInfo?.context_length) {
4571+ return modelInfo.context_length;
4572+ }
4573+ if (modelInfo?.context_window) {
4574+ return modelInfo.context_window;
4575+ }
4576+ }
4577+
4578+ return max_32k;
4579+}
4580+
44524581async function onModelChange() {
44534582 biasCache = undefined;
44544583 let value = String($(this).val() || '');
@@ -4559,11 +4688,6 @@ async function onModelChange() {
45594688 oai_settings.deepseek_model = value;
45604689 }
45614690
4562- if (value && $(this).is('#model_01ai_select')) {
4563- console.log('01.AI model changed to', value);
4564- oai_settings.zerooneai_model = value;
4565- }
4566-
45674691 if (value && $(this).is('#model_custom_select')) {
45684692 console.log('Custom model changed to', value);
45694693 oai_settings.custom_model = value;
@@ -4589,6 +4713,29 @@ async function onModelChange() {
45894713 oai_settings.xai_model = value;
45904714 }
45914715
4716+ if (value && $(this).is('#model_moonshot_select')) {
4717+ console.log('Moonshot model changed to', value);
4718+ oai_settings.moonshot_model = value;
4719+ }
4720+
4721+ if ($(this).is('#model_fireworks_select')) {
4722+ if (!value) {
4723+ console.debug('Null Fireworks model selected. Ignoring.');
4724+ return;
4725+ }
4726+ console.log('Fireworks model changed to', value);
4727+ oai_settings.fireworks_model = value;
4728+ }
4729+
4730+ if ($(this).is('#model_cometapi_select')) {
4731+ if (!value) {
4732+ console.debug('Null CometAPI model selected. Ignoring.');
4733+ return;
4734+ }
4735+ console.log('CometAPI model changed to', value);
4736+ oai_settings.cometapi_model = value;
4737+ }
4738+
45924739 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
45934740 if (oai_settings.max_context_unlocked) {
45944741 $('#openai_max_context').attr('max', max_2mil);
@@ -4688,7 +4835,7 @@ async function onModelChange() {
46884835 else if (['command-light-nightly', 'command-light', 'command'].includes(oai_settings.cohere_model)) {
46894836 $('#openai_max_context').attr('max', max_4k);
46904837 }
46914838 else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-23', 'c4ai-aya-expanse-32b', 'command-nightly', 'command-a-vision-07-2025'].includes(oai_settings.cohere_model)) {
46924839 $('#openai_max_context').attr('max', max_128k);
46934840 }
46944841 else if (['command-a-03-2025'].includes(oai_settings.cohere_model)) {
@@ -4760,30 +4907,6 @@ async function onModelChange() {
47604907 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
47614908 }
47624909
4763- if (oai_settings.chat_completion_source === chat_completion_sources.ZEROONEAI) {
4764- if (oai_settings.max_context_unlocked) {
4765- $('#openai_max_context').attr('max', unlocked_max);
4766- }
4767- else if (['yi-large'].includes(oai_settings.zerooneai_model)) {
4768- $('#openai_max_context').attr('max', max_32k);
4769- }
4770- else if (['yi-vision'].includes(oai_settings.zerooneai_model)) {
4771- $('#openai_max_context').attr('max', max_16k);
4772- }
4773- else if (['yi-large-turbo'].includes(oai_settings.zerooneai_model)) {
4774- $('#openai_max_context').attr('max', max_4k);
4775- }
4776- else {
4777- $('#openai_max_context').attr('max', max_16k);
4778- }
4779-
4780- oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
4781- $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4782-
4783- oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
4784- $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4785- }
4786-
47874910 if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) {
47884911 if (oai_settings.max_context_unlocked) {
47894912 $('#openai_max_context').attr('max', unlocked_max);
@@ -4812,7 +4935,7 @@ async function onModelChange() {
48124935 if (oai_settings.max_context_unlocked) {
48134936 $('#openai_max_context').attr('max', unlocked_max);
48144937 } else if (['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
48154938 $('#openai_max_context').attr('max', max_64kmax_128k);
48164939 } else if (oai_settings.deepseek_model == 'deepseek-coder') {
48174940 $('#openai_max_context').attr('max', max_16k);
48184941 } else {
@@ -4824,6 +4947,13 @@ async function onModelChange() {
48244947 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
48254948 }
48264949
4950+ if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
4951+ $('#openai_max_context').attr('max', oai_settings.max_context_unlocked ? unlocked_max : max_128k);
4952+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4953+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4954+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4955+ }
4956+
48274957 if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
48284958 if (oai_settings.max_context_unlocked) {
48294959 $('#openai_max_context').attr('max', unlocked_max);
@@ -4876,9 +5006,28 @@ async function onModelChange() {
48765006 $('#freq_pen_openai').attr('max', 2).attr('min', -2).val(oai_settings.freq_pen_openai).trigger('input');
48775007 }
48785008
5009+ if (oai_settings.chat_completion_source === chat_completion_sources.MOONSHOT) {
5010+ const maxContext = getMoonshotMaxContext(oai_settings.moonshot_model, oai_settings.max_context_unlocked);
5011+ $('#openai_max_context').attr('max', maxContext);
5012+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5013+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5014+ oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
5015+ $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5016+ }
5017+
5018+ if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS) {
5019+ const maxContext = getFireworksMaxContext(oai_settings.fireworks_model, oai_settings.max_context_unlocked);
5020+ $('#openai_max_context').attr('max', maxContext);
5021+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5022+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5023+ oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5024+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5025+ }
5026+
48795027 $('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));
48805028
48815029 saveSettingsDebounced();
5030+ updateFeatureSupportFlags();
48825031 eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value);
48835032}
48845033
@@ -5081,19 +5230,6 @@ async function onConnectButtonClick(e) {
50815230 }
50825231 }
50835232
5084- if (oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI) {
5085- const api_key_01ai = String($('#api_key_01ai').val()).trim();
5086-
5087- if (api_key_01ai.length) {
5088- await writeSecret(SECRET_KEYS.ZEROONEAI, api_key_01ai);
5089- }
5090-
5091- if (!secret_state[SECRET_KEYS.ZEROONEAI]) {
5092- console.log('No secret key saved for 01.AI');
5093- return;
5094- }
5095- }
5096-
50975233 if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
50985234 const api_key_xai = String($('#api_key_xai').val()).trim();
50995235
@@ -5120,6 +5256,45 @@ async function onConnectButtonClick(e) {
51205256 }
51215257 }
51225258
5259+ if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) {
5260+ const api_key_moonshot = String($('#api_key_moonshot').val()).trim();
5261+
5262+ if (api_key_moonshot.length) {
5263+ await writeSecret(SECRET_KEYS.MOONSHOT, api_key_moonshot);
5264+ }
5265+
5266+ if (!secret_state[SECRET_KEYS.MOONSHOT]) {
5267+ console.log('No secret key saved for Moonshot');
5268+ return;
5269+ }
5270+ }
5271+
5272+ if (oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS) {
5273+ const api_key_fireworks = String($('#api_key_fireworks').val()).trim();
5274+
5275+ if (api_key_fireworks.length) {
5276+ await writeSecret(SECRET_KEYS.FIREWORKS, api_key_fireworks);
5277+ }
5278+
5279+ if (!secret_state[SECRET_KEYS.FIREWORKS]) {
5280+ console.log('No secret key saved for Fireworks');
5281+ return;
5282+ }
5283+ }
5284+
5285+ if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {
5286+ const api_key_cometapi = String($('#api_key_cometapi').val()).trim();
5287+
5288+ if (api_key_cometapi.length) {
5289+ await writeSecret(SECRET_KEYS.COMETAPI, api_key_cometapi);
5290+ }
5291+
5292+ if (!api_key_cometapi && !secret_state[SECRET_KEYS.COMETAPI]) {
5293+ console.log('No secret key saved for CometAPI');
5294+ return;
5295+ }
5296+ }
5297+
51235298 startStatusLoading();
51245299 saveSettingsDebounced();
51255300 await getStatusOpen();
@@ -5166,9 +5341,6 @@ function toggleChatCompletionForms() {
51665341 else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
51675342 $('#model_nanogpt_select').trigger('change');
51685343 }
5169- else if (oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI) {
5170- $('#model_01ai_select').trigger('change');
5171- }
51725344 else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
51735345 $('#model_custom_select').trigger('change');
51745346 }
@@ -5184,6 +5356,16 @@ function toggleChatCompletionForms() {
51845356 else if (oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS) {
51855357 $('#model_pollinations_select').trigger('change');
51865358 }
5359+ else if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) {
5360+ $('#model_moonshot_select').trigger('change');
5361+ }
5362+ else if (oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS) {
5363+ $('#model_fireworks_select').trigger('change');
5364+ }
5365+ else if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {
5366+ $('#model_cometapi_select').trigger('change');
5367+ }
5368+
51875369 $('[data-source]').each(function () {
51885370 const validSources = $(this).data('source').split(',');
51895371 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -5265,17 +5447,17 @@ export function isImageInliningSupported() {
52655447 'gpt-4.1',
52665448 'gpt-4.5-preview',
52675449 'gpt-4o',
5450+ 'gpt-5',
52685451 'o1',
52695452 'o3',
52705453 'o4-mini',
5271- // 01.AI (Yi)
5272- 'yi-vision',
52735454 // Claude
52745455 'claude-3',
52755456 'claude-opus-4',
52765457 'claude-sonnet-4',
52775458 // Cohere
52785459 'c4ai-aya-vision',
5460+ 'command-a-vision',
52795461 // Google AI Studio
52805462 'gemini-1.5',
52815463 'gemini-2.0',
@@ -5288,11 +5470,16 @@ export function isImageInliningSupported() {
52885470 'mistral-small-latest',
52895471 'mistral-medium-latest',
52905472 'mistral-medium-2505',
5473+ 'mistral-medium-2508',
52915474 'pixtral',
52925475 // xAI (Grok)
52935476 'grok-4',
52945477 'grok-2-vision',
52955478 'grok-vision',
5479+ // Moonshot
5480+ 'moonshot-v1-8k-vision-preview',
5481+ 'moonshot-v1-32k-vision-preview',
5482+ 'moonshot-v1-128k-vision-preview',
52965483 ];
52975484
52985485 switch (oai_settings.chat_completion_source) {
@@ -5308,21 +5495,23 @@ export function isImageInliningSupported() {
53085495 case chat_completion_sources.CLAUDE:
53095496 return visionSupportedModels.some(model => oai_settings.claude_model.includes(model));
53105497 case chat_completion_sources.OPENROUTER:
5311- return true;
5498+ return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.modality === 'text+image->text');
53125499 case chat_completion_sources.CUSTOM:
53135500 return true;
5314- case chat_completion_sources.ZEROONEAI:
5315- return visionSupportedModels.some(model => oai_settings.zerooneai_model.includes(model));
53165501 case chat_completion_sources.MISTRALAI:
5317- return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model));
5502+ return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.mistralai_model)?.capabilities?.vision);
53185503 case chat_completion_sources.COHERE:
53195504 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
53205505 case chat_completion_sources.XAI:
53215506 return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
53225507 case chat_completion_sources.AIMLAPI:
5323- return visionSupportedModels.some(model => oai_settings.aimlapi_model.includes(model));
5508+ return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
53245509 case chat_completion_sources.POLLINATIONS:
53255510 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
5511+ case chat_completion_sources.COMETAPI:
5512+ return true;
5513+ case chat_completion_sources.MOONSHOT:
5514+ return visionSupportedModels.some(model => oai_settings.moonshot_model.includes(model));
53265515 default:
53275516 return false;
53285517 }
@@ -5615,7 +5804,20 @@ function updateVertexAIServiceAccountStatus(isValid = false, message = '') {
56155804 }
56165805}
56175806
5807+function updateFeatureSupportFlags() {
5808+ const featureFlags = {
5809+ openai_function_calling_supported: ToolManager.isToolCallingSupported(),
5810+ openai_image_inlining_supported: isImageInliningSupported(),
5811+ openai_video_inlining_supported: isVideoInliningSupported(),
5812+ };
56185813
5814+ for (const [key, value] of Object.entries(featureFlags)) {
5815+ const element = document.getElementById(key);
5816+ if (element) {
5817+ element.dataset.ccToggle = String(value);
5818+ }
5819+ }
5820+}
56195821
56205822export function initOpenAI() {
56215823 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
@@ -5839,6 +6041,7 @@ export function initOpenAI() {
58396041 saveSettingsDebounced();
58406042 reconnectOpenAi();
58416043 forceCharacterEditorTokenize();
6044+ updateFeatureSupportFlags();
58426045 eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source);
58436046 });
58446047
@@ -5903,6 +6106,7 @@ export function initOpenAI() {
59036106
59046107 $('#openai_image_inlining').on('input', function () {
59056108 oai_settings.image_inlining = !!$(this).prop('checked');
6109+ updateFeatureSupportFlags();
59066110 saveSettingsDebounced();
59076111 });
59086112
@@ -5913,6 +6117,7 @@ export function initOpenAI() {
59136117
59146118 $('#openai_video_inlining').on('input', function () {
59156119 oai_settings.video_inlining = !!$(this).prop('checked');
6120+ updateFeatureSupportFlags();
59166121 saveSettingsDebounced();
59176122 });
59186123
@@ -5923,6 +6128,7 @@ export function initOpenAI() {
59236128
59246129 $('#openai_function_calling').on('input', function () {
59256130 oai_settings.function_calling = !!$(this).prop('checked');
6131+ updateFeatureSupportFlags();
59266132 saveSettingsDebounced();
59276133 });
59286134
@@ -5948,6 +6154,7 @@ export function initOpenAI() {
59486154
59496155 $('#custom_prompt_post_processing').on('change', function () {
59506156 oai_settings.custom_prompt_post_processing = String($(this).val());
6157+ updateFeatureSupportFlags();
59516158 saveSettingsDebounced();
59526159 });
59536160
@@ -6107,11 +6314,13 @@ export function initOpenAI() {
61076314 $('#model_groq_select').on('change', onModelChange);
61086315 $('#model_nanogpt_select').on('change', onModelChange);
61096316 $('#model_deepseek_select').on('change', onModelChange);
6110- $('#model_01ai_select').on('change', onModelChange);
61116317 $('#model_aimlapi_select').on('change', onModelChange);
61126318 $('#model_custom_select').on('change', onModelChange);
61136319 $('#model_xai_select').on('change', onModelChange);
61146320 $('#model_pollinations_select').on('change', onModelChange);
6321+ $('#model_cometapi_select').on('change', onModelChange);
6322+ $('#model_moonshot_select').on('change', onModelChange);
6323+ $('#model_fireworks_select').on('change', onModelChange);
61156324 $('#settings_preset_openai').on('change', onSettingsPresetChange);
61166325 $('#new_oai_preset').on('click', onNewPresetClick);
61176326 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/personas.js+25 -11
@@ -317,14 +317,20 @@ async function uploadUserAvatar(url, name) {
317317 formData.append('overwrite_name', name);
318318 }
319319
320320 const response = await fetch('/api/avatars/upload', {
321321 method: 'POST',
322322 headers: getRequestHeaders({ omitContentType: true }),
323323 cache: 'no-cache',
324324 body: formData,
325325 });
326326
327- await getUserAvatars(true, name);
327+ if (!response.ok) {
328+ throw new Error(`Failed to upload avatar: ${response.statusText}`);
329+ }
330+
331+ // Get the actual path from the response
332+ const data = await response.json();
333+ await getUserAvatars(true, data?.path || name);
328334}
329335
330336async function changeUserAvatar(e) {
@@ -367,7 +373,7 @@ async function changeUserAvatar(e) {
367373
368374 const response = await fetch(url, {
369375 method: 'POST',
370376 headers: getRequestHeaders({ omitContentType: true }),
371377 cache: 'no-cache',
372378 body: formData,
373379 });
@@ -375,21 +381,23 @@ async function changeUserAvatar(e) {
375381 if (response.ok) {
376382 const data = await response.json();
377383
384+ const overwriteName = formData.get('overwrite_name');
385+ const dataPath = data?.path;
386+
378387 // If the user uploaded a new avatar, we want to make sure it's not cached
379- const name = formData.get('overwrite_name');
388+ if (overwriteName && dataPath) {
380- if (name) {
389+ await fetch(getUserAvatar(String(dataPath)), { cache: 'reload' });
381390 await fetch(getUserAvatargetThumbnailUrl('persona', String(namedataPath)), { cache: 'no-cachereload' });
382- await fetch(getThumbnailUrl('persona', String(name)), { cache: 'no-cache' });
383391 reloadUserAvatar(true);
384392 }
385393
386394 if (!nameoverwriteName && data.pathdataPath) {
387395 await getUserAvatars();
388396 await delay(5001);
389397 await createPersona(data.pathdataPath);
390398 }
391399
392400 await getUserAvatars(true, namedataPath || data.pathoverwriteName);
393401 }
394402
395403 // Will allow to select the same file twice in a row
@@ -1424,6 +1432,12 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
14241432 // Cache persona list to check if they exist
14251433 const userAvatars = await getUserAvatars(doRender);
14261434
1435+ // Check if the user avatar is set and exists in the list of user avatars
1436+ if (userAvatars.length && !userAvatars.includes(user_avatar)) {
1437+ console.log(`User avatar ${user_avatar} not found in user avatars list, pick the first available one`);
1438+ setUserAvatar(userAvatars[0], { toastPersonaNameChange: false, navigateToCurrent: true });
1439+ }
1440+
14271441 // Define a persona for this chat
14281442 let chatPersona = '';
14291443
public/scripts/power-user.js+75 -7
@@ -27,6 +27,8 @@ import {
2727 doNewChat,
2828 online_status,
2929 messageFormatting,
30+ extension_prompt_types,
31+ extension_prompt_roles,
3032} from '../script.js';
3133import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
3234import {
@@ -227,8 +229,8 @@ export const power_user = {
227229 first_output_sequence: '',
228230 last_input_sequence: '',
229231 last_output_sequence: '',
230232 system_sequence_prefixstory_string_prefix: '',
231233 system_sequence_suffixstory_string_suffix: '',
232234 stop_sequence: '',
233235 wrap: true,
234236 macro: true,
@@ -239,6 +241,7 @@ export const power_user = {
239241 system_same_as_user: false,
240242 /** @deprecated Use output_suffix instead */
241243 separator_sequence: '',
244+ sequences_as_stop_strings: true,
242245 },
243246
244247 context: {
@@ -248,6 +251,9 @@ export const power_user = {
248251 example_separator: defaultExampleSeparator,
249252 use_stop_strings: true,
250253 names_as_stop_strings: true,
254+ story_string_position: extension_prompt_types.IN_PROMPT,
255+ story_string_role: extension_prompt_roles.SYSTEM,
256+ story_string_depth: 1,
251257 },
252258
253259 instruct_derived: false,
@@ -346,6 +352,9 @@ const contextControls = [
346352 { id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
347353 { id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
348354 { id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
355+ { id: 'context_story_string_position', property: 'story_string_position', isCheckbox: false, isGlobalSetting: false, defaultValue: extension_prompt_types.IN_PROMPT, trigger: true },
356+ { id: 'context_story_string_depth', property: 'story_string_depth', isCheckbox: false, isGlobalSetting: false, defaultValue: 1 },
357+ { id: 'context_story_string_role', property: 'story_string_role', isCheckbox: false, isGlobalSetting: false, defaultValue: extension_prompt_roles.SYSTEM },
349358
350359 // Existing power user settings
351360 { id: 'always-force-name2-checkbox', property: 'always_force_name2', isCheckbox: true, isGlobalSetting: true, defaultValue: true },
@@ -1859,6 +1868,47 @@ export function getContextSettings() {
18591868// TODO: Maybe add a refresh button to reset settings to preset
18601869// TODO: Add "global state" if a preset doesn't set the power_user checkboxes
18611870async function loadContextSettings() {
1871+ /**
1872+ * Auto-fix missing fields in the story string
1873+ * @param {ContextSettings} contextSettings Context settings instance
1874+ */
1875+ function autoFixStoryString(contextSettings) {
1876+ // Already migrated, no need to fix
1877+ if (!contextSettings || Object.hasOwn(contextSettings, 'story_string_position')) {
1878+ return;
1879+ }
1880+
1881+ let storyString = contextSettings.story_string || '';
1882+
1883+ /**
1884+ * @param {string} field Missing field name
1885+ * @param {'start'|'end'} position Position of auto-fix
1886+ */
1887+ function autoFixMissingField(field, position) {
1888+ if (storyString.includes(`{{${field}}}`)) {
1889+ return;
1890+ }
1891+
1892+ console.warn(`[Story String Validation] Story String is missing a field: ${field}. Adding it at the ${position}.`);
1893+ const fieldTemplate = `{{#if ${field}}}{{${field}}}\n{{/if}}`;
1894+ const firstCurlyPosition = storyString.includes('{{') ? storyString.indexOf('{{') : 0;
1895+ const lastCurlyPosition = storyString.includes('}}') ? storyString.lastIndexOf('}}') + '}}'.length : storyString.length;
1896+ const lastTrimPosition = storyString.includes('{{trim}}') ? storyString.lastIndexOf('{{trim}}') : storyString.length;
1897+ const endPosition = Math.min(lastTrimPosition, lastCurlyPosition);
1898+ storyString = position === 'start'
1899+ ? storyString.substring(0, firstCurlyPosition) + fieldTemplate + storyString.substring(firstCurlyPosition)
1900+ : storyString.substring(0, endPosition) + fieldTemplate + storyString.substring(endPosition);
1901+ }
1902+
1903+ autoFixMissingField('anchorBefore', 'start');
1904+ autoFixMissingField('anchorAfter', 'end');
1905+
1906+ contextSettings.story_string = storyString;
1907+ }
1908+
1909+ // Migrate story string to add missing fields
1910+ autoFixStoryString(power_user.context);
1911+
18621912 contextControls.forEach(control => {
18631913 const $element = $(`#${control.id}`);
18641914
@@ -1880,7 +1930,10 @@ async function loadContextSettings() {
18801930 // If the setting already exists, no need to duplicate it
18811931 // TODO: Maybe check the power_user object for the setting instead of a flag?
18821932 $element.on('input', async function () {
18831933 constlet value = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
1934+ if (typeof control.defaultValue === 'number') {
1935+ value = Number(value);
1936+ }
18841937 if (control.isGlobalSetting) {
18851938 power_user[control.property] = value;
18861939 } else {
@@ -1892,6 +1945,10 @@ async function loadContextSettings() {
18921945 }
18931946 saveSettingsDebounced();
18941947 });
1948+
1949+ if (control.trigger) {
1950+ $element.trigger('input');
1951+ }
18951952 });
18961953
18971954 context_presets.forEach((preset) => {
@@ -1911,6 +1968,9 @@ async function loadContextSettings() {
19111968 return;
19121969 }
19131970
1971+ // Migrate story string to add missing fields
1972+ autoFixStoryString(preset);
1973+
19141974 power_user.context.preset = name;
19151975
19161976 contextControls.forEach(control => {
@@ -2094,12 +2154,15 @@ export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
20942154 * @param {object} [options] Additional options.
20952155 * @param {string} [options.customStoryString] Custom story string template.
20962156 * @param {InstructSettings} [options.customInstructSettings] Custom instruct settings.
2157+ * @param {ContextSettings} [options.customContextSettings] Custom context settings.
20972158 * @returns {string} The rendered story string.
20982159 */
20992160export function renderStoryString(params, { customStoryString = null, customInstructSettings = null, customContextSettings = null } = {}) {
21002161 try {
2101- const storyString = customStoryString ?? power_user.context.story_string;
21022162 const instructSettings = structuredClone(customInstructSettings ?? power_user.instruct);
2163+ const contextSettings = structuredClone(customContextSettings ?? power_user.context);
2164+ const storyString = customStoryString ?? contextSettings.story_string;
2165+ const storyStringPosition = contextSettings.story_string_position ?? extension_prompt_types.IN_PROMPT;
21032166
21042167 // Validate and log possible warnings/errors
21052168 validateStoryString(storyString, params);
@@ -2117,8 +2180,8 @@ export function renderStoryString(params, { customStoryString = null, customInst
21172180 output = output.replace(/^\n+/, '');
21182181
21192182 // add a newline to the end of the story string if it doesn't have one
21202183 if (output.length > 0 && !output.endsWith('\n') && storyStringPosition !== extension_prompt_types.IN_CHAT) {
21212184 if (!instructSettings.enabled || (instructSettings.wrap && !instructSettings.story_string_suffix)) {
21222185 output += '\n';
21232186 }
21242187 }
@@ -3271,6 +3334,11 @@ jQuery(() => {
32713334 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
32723335 });
32733336
3337+ $('#context_story_string_position').on('input', function () {
3338+ const value = Number($(this).val());
3339+ $('#context_story_string_inject_settings').toggle(value === extension_prompt_types.IN_CHAT);
3340+ });
3341+
32743342 $('#bind_model_templates').on('input', function () {
32753343 if (bindModelTemplates(power_user, online_status)) {
32763344 saveSettingsDebounced();
public/scripts/reasoning.js+11 -0
@@ -118,8 +118,12 @@ export function extractReasoningFromData(data, {
118118 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
119119 case chat_completion_sources.CLAUDE:
120120 return data?.content?.find(part => part.type === 'thinking')?.thinking ?? '';
121+ case chat_completion_sources.MISTRALAI:
122+ return data?.choices?.[0]?.message?.content?.[0]?.thinking?.map(part => part.text)?.filter(x => x)?.join('\n\n') ?? '';
121123 case chat_completion_sources.AIMLAPI:
122124 case chat_completion_sources.POLLINATIONS:
125+ case chat_completion_sources.MOONSHOT:
126+ case chat_completion_sources.COMETAPI:
123127 case chat_completion_sources.CUSTOM: {
124128 return data?.choices?.[0]?.message?.reasoning_content
125129 ?? data?.choices?.[0]?.message?.reasoning
@@ -1071,6 +1075,13 @@ function setReasoningEventHandlers() {
10711075 }
10721076 });
10731077
1078+ $(document).on('click', '.mes_reasoning_close_all', function (e) {
1079+ e.stopPropagation();
1080+ e.preventDefault();
1081+
1082+ $('.mes_reasoning_details[open] .mes_reasoning_header').trigger('click');
1083+ });
1084+
10741085 $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
10751086 e.stopPropagation();
10761087 e.preventDefault();
public/scripts/samplerSelect.js+2 -2
@@ -28,7 +28,7 @@ async function showSamplerSelectPopup() {
2828
2929 const listContainer = $('<div id="apiSamplersList" class="flex-container flexNoGap"></div>');
3030 const APISamplers = await listSamplers(main_api);
3131 listContainer.append(APISamplers.toString());
3232 html.append(listContainer);
3333
3434 const showPromise = new Popup(html, POPUP_TYPE.TEXT, null, { wide: true, large: true, allowVerticalScrolling: true }).show();
@@ -419,7 +419,7 @@ export async function validateDisabledSamplers(redraw = false) {
419419 }
420420 if (redraw) {
421421 let samplersHTML = await listSamplers(main_api);
422422 $('#apiSamplersList').empty().append(samplersHTML.toString());
423423 setSamplerListListeners();
424424 }
425425
public/scripts/secrets.js+13 -3
@@ -48,7 +48,6 @@ export const SECRET_KEYS = {
4848 GROQ: 'api_key_groq',
4949 AZURE_TTS: 'api_key_azure_tts',
5050 FEATHERLESS: 'api_key_featherless',
51- ZEROONEAI: 'api_key_01ai',
5251 HUGGINGFACE: 'api_key_huggingface',
5352 STABILITY: 'api_key_stability',
5453 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
@@ -61,7 +60,12 @@ export const SECRET_KEYS = {
6160 AIMLAPI: 'api_key_aimlapi',
6261 FALAI: 'api_key_falai',
6362 XAI: 'api_key_xai',
63+ FIREWORKS: 'api_key_fireworks',
6464 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
65+ MINIMAX: 'api_key_minimax',
66+ MINIMAX_GROUP_ID: 'minimax_group_id',
67+ MOONSHOT: 'api_key_moonshot',
68+ COMETAPI: 'api_key_cometapi',
6569};
6670
6771const FRIENDLY_NAMES = {
@@ -90,7 +94,6 @@ const FRIENDLY_NAMES = {
9094 [SECRET_KEYS.PERPLEXITY]: 'Perplexity',
9195 [SECRET_KEYS.GROQ]: 'Groq',
9296 [SECRET_KEYS.FEATHERLESS]: 'Featherless',
93- [SECRET_KEYS.ZEROONEAI]: '01.AI',
9497 [SECRET_KEYS.HUGGINGFACE]: 'HuggingFace',
9598 [SECRET_KEYS.NANOGPT]: 'NanoGPT',
9699 [SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)',
@@ -106,12 +109,17 @@ const FRIENDLY_NAMES = {
106109 [SECRET_KEYS.FALAI]: 'FAL.AI',
107110 [SECRET_KEYS.AZURE_TTS]: 'Azure TTS',
108111 [SECRET_KEYS.AIMLAPI]: 'AI/ML API',
112+ [SECRET_KEYS.FIREWORKS]: 'Fireworks AI',
109113 [SECRET_KEYS.DEEPL]: 'DeepL',
110114 [SECRET_KEYS.LIBRE]: 'LibreTranslate',
111115 [SECRET_KEYS.LIBRE_URL]: 'LibreTranslate Endpoint (e.g. http://127.0.0.1:5000/translate)',
112116 [SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)',
113117 [SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)',
114118 [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)',
119+ [SECRET_KEYS.MINIMAX]: 'MiniMax TTS',
120+ [SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID',
121+ [SECRET_KEYS.MOONSHOT]: 'Moonshot AI',
122+ [SECRET_KEYS.COMETAPI]: 'CometAPI',
115123};
116124
117125const INPUT_MAP = {
@@ -139,7 +147,6 @@ const INPUT_MAP = {
139147 [SECRET_KEYS.PERPLEXITY]: '#api_key_perplexity',
140148 [SECRET_KEYS.GROQ]: '#api_key_groq',
141149 [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
142- [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai',
143150 [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
144151 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
145152 [SECRET_KEYS.GENERIC]: '#api_key_generic',
@@ -147,6 +154,9 @@ const INPUT_MAP = {
147154 [SECRET_KEYS.AIMLAPI]: '#api_key_aimlapi',
148155 [SECRET_KEYS.XAI]: '#api_key_xai',
149156 [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json',
157+ [SECRET_KEYS.MOONSHOT]: '#api_key_moonshot',
158+ [SECRET_KEYS.FIREWORKS]: '#api_key_fireworks',
159+ [SECRET_KEYS.COMETAPI]: '#api_key_cometapi',
150160};
151161
152162const getLabel = () => moment().format('L LT');
public/scripts/slash-commands.js+61 -9
@@ -320,7 +320,7 @@ export function initDefaultSlashCommands() {
320320 SlashCommandArgument.fromProps({
321321 description: 'API to connect to',
322322 typeList: [ARGUMENT_TYPE.STRING],
323323 enumList: Object.entries(CONNECT_API_MAP).sort(([a], [b]) => a.localeCompare(b)).map(([api, { selected }]) =>
324324 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
325325 selected[0].toUpperCase() ?? enumIcons.default)),
326326 }),
@@ -1083,6 +1083,44 @@ export function initDefaultSlashCommands() {
10831083 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10841084 name: 'sysgen',
10851085 callback: generateSystemMessage,
1086+ namedArgumentList: [
1087+ SlashCommandNamedArgument.fromProps({
1088+ name: 'trim',
1089+ description: 'Trim the output by the last sentence boundary',
1090+ typeList: [ARGUMENT_TYPE.BOOLEAN],
1091+ defaultValue: 'false',
1092+ isRequired: false,
1093+ enumProvider: commonEnumProviders.boolean('trueFalse'),
1094+ }),
1095+ SlashCommandNamedArgument.fromProps({
1096+ name: 'compact',
1097+ description: 'Use a compact layout for the message',
1098+ typeList: [ARGUMENT_TYPE.BOOLEAN],
1099+ defaultValue: 'false',
1100+ isRequired: false,
1101+ acceptsMultiple: false,
1102+ enumProvider: commonEnumProviders.boolean('trueFalse'),
1103+ }),
1104+ SlashCommandNamedArgument.fromProps({
1105+ name: 'at',
1106+ description: 'Position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
1107+ typeList: [ARGUMENT_TYPE.NUMBER],
1108+ enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1109+ }),
1110+ SlashCommandNamedArgument.fromProps({
1111+ name: 'name',
1112+ description: 'Optional custom display name to use for this system narrator message.',
1113+ typeList: [ARGUMENT_TYPE.STRING],
1114+ }),
1115+ SlashCommandNamedArgument.fromProps({
1116+ name: 'return',
1117+ description: 'The way how you want the return value to be provided',
1118+ typeList: [ARGUMENT_TYPE.STRING],
1119+ defaultValue: 'none',
1120+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1121+ forceEnum: true,
1122+ }),
1123+ ],
10861124 unnamedArgumentList: [
10871125 new SlashCommandArgument(
10881126 'prompt', [ARGUMENT_TYPE.STRING], true,
@@ -1629,6 +1667,14 @@ export function initDefaultSlashCommands() {
16291667 callback: generateCallback,
16301668 returns: 'generated text',
16311669 namedArgumentList: [
1670+ SlashCommandNamedArgument.fromProps({
1671+ name: 'trim',
1672+ description: 'Trim the output by the last sentence boundary',
1673+ typeList: [ARGUMENT_TYPE.BOOLEAN],
1674+ defaultValue: 'false',
1675+ isRequired: false,
1676+ enumProvider: commonEnumProviders.boolean('trueFalse'),
1677+ }),
16321678 new SlashCommandNamedArgument(
16331679 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
16341680 ),
@@ -3679,6 +3725,7 @@ async function generateCallback(args, value) {
36793725 // Prevent generate recursion
36803726 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
36813727 const lock = isTrueBoolean(args?.lock);
3728+ const trim = isTrueBoolean(args?.trim?.toString());
36823729 const as = args?.as || 'system';
36833730 const quietToLoud = as === 'char';
36843731 const length = Number(resolveVariable(args?.length) ?? 0) || 0;
@@ -3690,13 +3737,15 @@ async function generateCallback(args, value) {
36903737
36913738 setEphemeralStopStrings(resolveVariable(args?.stop));
36923739 const name = args?.name;
36933740 const char = name ? findChar({ name: name }) : null;
36943741 /** @type {import('../script.js').GenerateQuietPromptParams} */
36953742 const params = {
36963743 quietPrompt: value,
36973744 quietToLoud: quietToLoud,
36983745 quietName: char?.name ?? name,
36993746 responseLength: length,
3747+ trimToSentence: trim,
3748+ forceChId: char ? characters.indexOf(char) : null,
37003749 };
37013750 const result = await generateQuietPrompt(params);
37023751 return result;
@@ -4358,7 +4407,7 @@ async function continueChatCallback(args, prompt) {
43584407 return '';
43594408}
43604409
43614410export async function generateSystemMessage(_args, prompt) {
43624411 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
43634412
43644413 if (!prompt) {
@@ -4367,13 +4416,14 @@ export async function generateSystemMessage(_, prompt) {
43674416 return '';
43684417 }
43694418
4419+ const trim = isTrueBoolean(args?.trim?.toString());
4420+
43704421 // Generate and regex the output if applicable
43714422 const toast = toastr.info('Please wait', 'Generating...');
43724423 letconst message = await generateQuietPrompt({ quietPrompt: prompt, trimToSentence: trim });
4373- message = getRegexedString(message, regex_placement.SLASH_COMMAND);
4424+ toastr.clear(toast);
43744425
43754426 return await sendNarratorMessage(_args, getRegexedString(message, regex_placement.SLASH_COMMAND));
4376- return '';
43774427}
43784428
43794429function setStoryModeCallback() {
@@ -4801,11 +4851,13 @@ function getModelOptions(quiet) {
48014851 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
48024852 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
48034853 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
4804- { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
48054854 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
48064855 { id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
48074856 { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
48084857 { id: 'model_pollinations_select', api: 'openai', type: chat_completion_sources.POLLINATIONS },
4858+ { id: 'model_moonshot_select', api: 'openai', type: chat_completion_sources.MOONSHOT },
4859+ { id: 'model_fireworks_select', api: 'openai', type: chat_completion_sources.FIREWORKS },
4860+ { id: 'model_cometapi_select', api: 'openai', type: chat_completion_sources.COMETAPI },
48094861 { id: 'model_novel_select', api: 'novel', type: null },
48104862 { id: 'horde_model', api: 'koboldhorde', type: null },
48114863 ];
public/scripts/slash-commands/SlashCommandParser.js+1 -1
@@ -226,7 +226,7 @@ export class SlashCommandParser {
226226
227227 function getQuotedRunRegex() {
228228 try {
229229 return new RegExp('(".+?(?<!\\\\)")|((?:[^\\S+?s\\|"]|"[^"]*")*)(\\||$|\\s)');
230230 } catch {
231231 // fallback for browsers that don't support lookbehind
232232 return /(".+?")|(\S+?)(\||$|\s)/;
public/scripts/st-context.js+2 -0
@@ -59,6 +59,7 @@ import {
5959 ModuleWorkerWrapper,
6060 renderExtensionTemplate,
6161 renderExtensionTemplateAsync,
62+ saveMetadataDebounced,
6263 writeExtensionField,
6364} from './extensions.js';
6465import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js';
@@ -109,6 +110,7 @@ export function getContext() {
109110 onlineStatus: online_status,
110111 maxContext: Number(max_context),
111112 chatMetadata: chat_metadata,
113+ saveMetadataDebounced,
112114 streamingProcessor,
113115 eventSource,
114116 eventTypes: event_types,
public/scripts/stats.js+11 -9
@@ -4,6 +4,7 @@ import { getRequestHeaders, characters, this_chid } from '../script.js';
44import { humanizeGenTime } from './RossAscends-mods.js';
55import { callGenericPopup, POPUP_TYPE } from './popup.js';
66import { registerDebugFunction } from './power-user.js';
7+import { t, translate } from './i18n.js';
78
89let charStats = {};
910
@@ -106,23 +107,24 @@ function createHtml(statsType, stats) {
106107 .duration(stats.date_last_chat - stats.date_first_chat)
107108 .humanize();
108109 }
110+ let statsTypeTranslated = translate(statsType, `stats_header_${statsType}`);
109111
110112 // Create popup HTML with stats
111113 let html = `'<h3>' + t`${statsTypestatsTypeTranslated} Stats` + '</h3>`';
112114 if (statsType === 'User') {
113115 html += createStatBlock('t`Chatting Since'`, `${chatAge} ago`);
114116 } else {
115117 html += createStatBlock('t`First Interaction'`, `${chatAge} ago`);
116118 }
117119 html += createStatBlock('t`Chat Time'`, timeStirng);
118120 html += createStatBlock('t`User Messages'`, stats.user_msg_count);
119121 html += createStatBlock(
120122 't`Character Messages'`,
121123 stats.non_user_msg_count - stats.total_swipe_count,
122124 );
123125 html += createStatBlock('t`User Words'`, stats.user_word_count);
124126 html += createStatBlock('t`Character Words'`, stats.non_user_word_count);
125127 html += createStatBlock('t`Swipes'`, stats.total_swipe_count);
126128
127129 return callGenericPopup(html, POPUP_TYPE.TEXT);
128130}
public/scripts/tags.js+97 -6
@@ -28,6 +28,7 @@ import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
2828import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
2929import { renderTemplateAsync } from './templates.js';
3030import { t, translate } from './i18n.js';
31+import { accountStorage } from './util/AccountStorage.js';
3132
3233export {
3334 TAG_FOLDER_TYPES,
@@ -64,6 +65,12 @@ function getFilterHelper(listSelector) {
6465 return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
6566}
6667
68+const ACTIONABLE_FILTER_STORAGE_KEYS = Object.freeze({
69+ GROUP: 'TagFilterState_GROUP',
70+ FAV: 'TagFilterState_FAV',
71+ FOLDER: 'TagFilterState_FOLDER',
72+});
73+
6774/** @enum {number} */
6875export const tag_filter_type = {
6976 character: 0,
@@ -331,12 +338,14 @@ function getTagBlock(tag, entities, hidden = 0, isUseless = false) {
331338
332339/**
333340 * Applies the favorite filter to the character list.
334341 * @param {FilterHelper} filterHelper_filterHelper Instance of FilterHelper class. Unused since it needs to be applied to both filters.
335342 */
336343function filterByFav(filterHelper_filterHelper) {
337344 const state = toggleTagThreeState($(this));
338345 ACTIONABLE_TAGS.FAV.filter_state = state;
339346 filterHelperaccountStorage.setFilterDatasetItem(FILTER_TYPESACTIONABLE_FILTER_STORAGE_KEYS.FAV, state);
347+ entitiesFilter.setFilterData(FILTER_TYPES.FAV, state);
348+ groupCandidatesFilter.setFilterData(FILTER_TYPES.FAV, state);
340349}
341350
342351/**
@@ -346,6 +355,7 @@ function filterByFav(filterHelper) {
346355function filterByGroups(filterHelper) {
347356 const state = toggleTagThreeState($(this));
348357 ACTIONABLE_TAGS.GROUP.filter_state = state;
358+ accountStorage.setItem(ACTIONABLE_FILTER_STORAGE_KEYS.GROUP, state);
349359 filterHelper.setFilterData(FILTER_TYPES.GROUP, state);
350360}
351361
@@ -363,6 +373,7 @@ function filterByFolder(filterHelper) {
363373
364374 const state = toggleTagThreeState($(this));
365375 ACTIONABLE_TAGS.FOLDER.filter_state = state;
376+ accountStorage.setItem(ACTIONABLE_FILTER_STORAGE_KEYS.FOLDER, state);
366377 filterHelper.setFilterData(FILTER_TYPES.FOLDER, state);
367378}
368379
@@ -393,6 +404,10 @@ function createTagMapFromList(listElement, key) {
393404 * @returns {Tag[]} A list of tags
394405 */
395406function getTagsList(key, sort = true) {
407+ if (key === null || key === undefined) {
408+ return [];
409+ }
410+
396411 if (!Array.isArray(tag_map[key])) {
397412 tag_map[key] = [];
398413 return [];
@@ -1547,6 +1562,44 @@ function onTagsBackupClick() {
15471562 download(blob, filename, 'application/json');
15481563}
15491564
1565+async function onTagsPruneClick() {
1566+ // Get tags which have zero tag map entries
1567+ const allTagsInTagMaps = new Set(Object.values(tag_map).flat());
1568+ const tagsToPrune = tags.filter(tag => !allTagsInTagMaps.has(tag.id));
1569+
1570+ // Get tag maps referring to deleted entities
1571+ const allEntityKeys = new Set([...characters.map(c => String(c.avatar)), ...groups.map(g => String(g.id))]);
1572+ const tagMapsToPrune = Object.keys(tag_map).filter(key => !allEntityKeys.has(key));
1573+
1574+ if (!tagsToPrune.length && !tagMapsToPrune.length) {
1575+ toastr.info(t`No unused tags or references found.`);
1576+ return;
1577+ }
1578+
1579+ const confirm = await Popup.show.confirm(t`Prune ${tagsToPrune.length} tags and ${tagMapsToPrune.length} references`, t`Are you sure you want to remove all unused tags and references to missing or deleted characters and groups?`);
1580+
1581+ if (!confirm) {
1582+ return;
1583+ }
1584+
1585+ for (const tag of tagsToPrune) {
1586+ tags.splice(tags.indexOf(tag), 1);
1587+ }
1588+
1589+ for (const key of tagMapsToPrune) {
1590+ delete tag_map[key];
1591+ }
1592+
1593+ printCharactersDebounced();
1594+ saveSettingsDebounced();
1595+
1596+ // Reprint the tag management popup, without having it to be opened again
1597+ const tagContainer = $('#tag_view_list .tag_view_list_tags');
1598+ printViewTagList(tagContainer);
1599+
1600+ toastr.success(t`Unused tags pruned successfully.`);
1601+}
1602+
15501603function onTagCreateClick() {
15511604 const tagName = getFreeName('New Tag', tags.map(x => x.name));
15521605 const tag = createNewTag(tagName);
@@ -1769,12 +1822,18 @@ const debouncedTagColoring = debounce((tagId, cssProperty, newColor) => {
17691822
17701823function onTagListHintClick() {
17711824 $(this).toggleClass('selected');
1772- $(this).siblings('.tag:not(.actionable)').toggle(100);
1773- $(this).siblings('.innerActionable').toggleClass('hidden');
17741825
1826+ const $tagSiblings = $(this).siblings('.tag:not(.actionable)');
1827+
1828+ if ($(this).hasClass('selected')) {
1829+ $tagSiblings.show();
1830+ } else {
1831+ $tagSiblings.hide();
1832+ }
1833+
1834+ $(this).siblings('.innerActionable').toggleClass('hidden');
17751835 power_user.show_tag_filters = $(this).hasClass('selected');
17761836 saveSettingsDebounced();
1777-
17781837 console.debug('show_tag_filters', power_user.show_tag_filters);
17791838}
17801839
@@ -2178,6 +2237,36 @@ function extractCharacterAvatar(avatarSrc) {
21782237 }
21792238}
21802239
2240+function restoreSavedTagFilters() {
2241+ try {
2242+ const validStates = new Set(Object.keys(FILTER_STATES));
2243+ const readState = (/** @type {string} */ storageKey) => {
2244+ const v = accountStorage.getItem(storageKey);
2245+ return v && validStates.has(v) ? v : null;
2246+ };
2247+
2248+ const favState = readState(ACTIONABLE_FILTER_STORAGE_KEYS.FAV);
2249+ const groupState = readState(ACTIONABLE_FILTER_STORAGE_KEYS.GROUP);
2250+ const folderState = readState(ACTIONABLE_FILTER_STORAGE_KEYS.FOLDER);
2251+
2252+ if (favState) {
2253+ ACTIONABLE_TAGS.FAV.filter_state = favState;
2254+ entitiesFilter.setFilterData(FILTER_TYPES.FAV, favState, true);
2255+ groupCandidatesFilter.setFilterData(FILTER_TYPES.FAV, favState, true);
2256+ }
2257+ if (groupState) {
2258+ ACTIONABLE_TAGS.GROUP.filter_state = groupState;
2259+ entitiesFilter.setFilterData(FILTER_TYPES.GROUP, groupState, true);
2260+ }
2261+ if (folderState) {
2262+ ACTIONABLE_TAGS.FOLDER.filter_state = folderState;
2263+ entitiesFilter.setFilterData(FILTER_TYPES.FOLDER, folderState, true);
2264+ }
2265+ } catch (e) {
2266+ console.warn('Failed to restore actionable filter states from account storage', e);
2267+ }
2268+}
2269+
21812270export function initTags() {
21822271 createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
21832272 createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
@@ -2198,6 +2287,7 @@ export function initTags() {
21982287 $(document).on('click', '.tag_view_create', onTagCreateClick);
21992288 $(document).on('click', '.tag_view_backup', onTagsBackupClick);
22002289 $(document).on('click', '.tag_view_restore', onBackupRestoreClick);
2290+ $(document).on('click', '.tag_view_prune', onTagsPruneClick);
22012291 eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
22022292 eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
22032293
@@ -2238,4 +2328,5 @@ export function initTags() {
22382328 }
22392329
22402330 registerTagsSlashCommands();
2331+ restoreSavedTagFilters();
22412332}
public/scripts/templates/customEndpointAdditionalParameters.html+3 -3
@@ -2,7 +2,7 @@
22 <h3 data-i18n="Additional Parameters">Additional Parameters</h3>
33 <div class="flex1 flex-container flexFlowColumn">
44 <h4 data-i18n="Include Body Parameters">Include Body Parameters</h4>
55 <textarea id="custom_include_body" class="flex1" placeholder="Parameters to be included in the Chat Completion request body (YAML object)&#10;&#10;Example:&#10;- top_k: 20&#10;- repetition_penalty: 1.1" data-i18n="[placeholder]custom_include_body_desc"></textarea>
66 </div>
77 <div class="flex1 flex-container flexFlowColumn">
88 <h4 data-i18n="Exclude Body Parameters">Exclude Body Parameters</h4>
@@ -10,6 +10,6 @@
1010 </div>
1111 <div class="flex1 flex-container flexFlowColumn">
1212 <h4 data-i18n="Include Request Headers">Include Request Headers</h4>
1313 <textarea id="custom_include_headers" class="flex1" placeholder="Additional headers for Chat Completion requests (YAML object)&#10;&#10;Example:&#10;- CustomHeader: custom-value&#10;- AnotherHeader: custom-value" data-i18n="[placeholder]custom_include_headers_desc"></textarea>
1414 </div>
15-</div>
15 \ No newline at end of file
15+</div>
public/scripts/templates/importCharacters.html+0 -1
@@ -10,7 +10,6 @@
1010 <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
1111 <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>
1212 <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>
13- <li><span data-i18n="char_import_9">Soulkyn Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://soulkyn.com/l/en-US/@haruka-509al</tt></li>
1413 <li><span data-i18n="char_import_10">Perchance Character (Direct Link or UUID + .gz)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://perchance.org/ai-character-chat?data=Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt><br><span data-i18n="char_import_example">Example:</span> <tt>Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt></li>
1514 </ul>
1615</div>
public/scripts/templates/macros.html+2 -2
@@ -64,8 +64,8 @@
6464 <li><tt>&lcub;&lcub;chatStart&rcub;&rcub;</tt> – <span data-i18n="help_macros_43">context template chat start line</span></li>
6565 <li><tt>&lcub;&lcub;systemPrompt&rcub;&rcub;</tt> – <span data-i18n="help_macros_44">system prompt content if enabled (either character prompt override if allowed, or defaultSystemPrompt)</span></li>
6666 <li><tt>&lcub;&lcub;defaultSystemPrompt&rcub;&rcub;</tt> – <span data-i18n="help_macros_45">system prompt content</span></li>
6767 <li><tt>&lcub;&lcub;instructSystemPromptPrefixinstructStoryStringPrefix&rcub;&rcub;</tt> – <span data-i18n="help_macros_46">instruct systemstory promptstring prefix sequence</span></li>
6868 <li><tt>&lcub;&lcub;instructSystemPromptSuffixinstructStoryStringSuffix&rcub;&rcub;</tt> – <span data-i18n="help_macros_47">instruct systemstory promptstring suffix sequence</span></li>
6969 <li><tt>&lcub;&lcub;instructUserPrefix&rcub;&rcub;</tt> – <span data-i18n="help_macros_48">instruct user prefix sequence</span></li>
7070 <li><tt>&lcub;&lcub;instructUserSuffix&rcub;&rcub;</tt> – <span data-i18n="help_macros_49">instruct user suffix sequence</span></li>
7171 <li><tt>&lcub;&lcub;instructAssistantPrefix&rcub;&rcub;</tt> – <span data-i18n="help_macros_50">instruct assistant prefix sequence</span></li>
public/scripts/templates/tagManagement.html+4 -0
@@ -1,6 +1,10 @@
11<div class="title_restorable alignItemsBaseline">
22 <h3 data-i18n="Tag Management">Tag Management</h3>
33 <div class="flex-container alignItemsBaseline">
4+ <div class="menu_button menu_button_icon tag_view_prune" data-i18n="[title]Remove unused tags" title="Remove unused tags">
5+ <i class="fa-solid fa-scissors"></i>
6+ <span data-i18n="Prune">Prune</span>
7+ </div>
48 <div class="menu_button menu_button_icon tag_view_backup" data-i18n="[title]Save your tags to a file" title="Save your tags to a file">
59 <i class="fa-solid fa-file-export"></i>
610 <span data-i18n="Backup">Backup</span>
public/scripts/textgen-models.js+7 -0
@@ -24,6 +24,10 @@ export let openRouterModels = [];
2424 * @type {string[]}
2525 */
2626const OPENROUTER_PROVIDERS = [
27+ // An alphabetically separate set of very-dead providers is kept at the top of the list in the docs.
28+ // These do not appear outside the docs: Anyscale, Cent-ML, HuggingFace ... SF Compute, Together 2, 01.AI
29+ // As a visual check, AI21 is the topmost provider in the sidebar of https://openrouter.ai/models, thus we want to copy from this point and below.
30+ // Providers endpoint: https://openrouter.ai/api/v1/providers
2731 'AI21',
2832 'AionLabs',
2933 'Alibaba',
@@ -55,6 +59,7 @@ const OPENROUTER_PROVIDERS = [
5559 'InferenceNet',
5660 'Infermatic',
5761 'Inflection',
62+ 'InoCloud',
5863 'Kluster',
5964 'Lambda',
6065 'Liquid',
@@ -81,7 +86,9 @@ const OPENROUTER_PROVIDERS = [
8186 'Together',
8287 'Ubicloud',
8388 'Venice',
89+ 'WandB',
8490 'xAI',
91+ 'Z.AI',
8592];
8693
8794export async function loadOllamaModels(data) {
public/scripts/textgen-settings.js+4 -1
@@ -743,7 +743,10 @@ async function getStatusTextgen() {
743743 }
744744 }
745745 console.log(`We have chat template ${chat_template.split('\n')[0]}...`);
746- const { context, instruct } = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);
746+ const savedTemplate = power_user.model_templates_mappings[chat_template_hash];
747+ const derivedTemplate = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);
748+ const { context, instruct } = savedTemplate ?? derivedTemplate;
749+
747750 if (wantsContextDerivation && context) {
748751 selectContextPreset(context, { isAuto: true });
749752 }
public/scripts/tokenizers.js+0 -4
@@ -708,10 +708,6 @@ export function getTokenizerModel() {
708708 }
709709 }
710710
711- if (oai_settings.chat_completion_source === chat_completion_sources.ZEROONEAI) {
712- return yiTokenizer;
713- }
714-
715711 // Default to Turbo 3.5
716712 return turboTokenizer;
717713}
public/scripts/tool-calling.js+32 -1
@@ -605,6 +605,34 @@ export class ToolManager {
605605 }
606606 }
607607
608+ if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS && Array.isArray(model_list)) {
609+ const currentModel = model_list.find(model => model.id === oai_settings.fireworks_model);
610+ if (currentModel) {
611+ return currentModel.supports_tools;
612+ }
613+ }
614+
615+ if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER && Array.isArray(model_list)) {
616+ const currentModel = model_list.find(model => model.id === oai_settings.openrouter_model);
617+ if (Array.isArray(currentModel?.supported_parameters)) {
618+ return currentModel.supported_parameters.includes('tools');
619+ }
620+ }
621+
622+ if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI && Array.isArray(model_list)) {
623+ const currentModel = model_list.find(model => model.id === oai_settings.mistralai_model);
624+ if (currentModel && currentModel.capabilities) {
625+ return currentModel.capabilities.function_calling;
626+ }
627+ }
628+
629+ if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI && Array.isArray(model_list)) {
630+ const currentModel = model_list.find(model => model.id === oai_settings.aimlapi_model);
631+ if (Array.isArray(currentModel?.features)) {
632+ return currentModel.features.includes('openai/chat-completion.function');
633+ }
634+ }
635+
608636 const supportedSources = [
609637 chat_completion_sources.OPENAI,
610638 chat_completion_sources.CUSTOM,
@@ -620,6 +648,9 @@ export class ToolManager {
620648 chat_completion_sources.AI21,
621649 chat_completion_sources.XAI,
622650 chat_completion_sources.POLLINATIONS,
651+ chat_completion_sources.MOONSHOT,
652+ chat_completion_sources.FIREWORKS,
653+ chat_completion_sources.COMETAPI,
623654 ];
624655 return supportedSources.includes(oai_settings.chat_completion_source);
625656 }
@@ -723,7 +754,7 @@ export class ToolManager {
723754 }
724755
725756 for (const toolCall of toolCalls) {
726757 if (!toolCall || !toolCall.function || typeof toolCall.function !== 'object') {
727758 continue;
728759 }
729760
public/scripts/utils.js+10 -0
@@ -2550,3 +2550,13 @@ export function textValueMatcher(params, data) {
25502550 // If it doesn't contain the term, don't return anything
25512551 return null;
25522552}
2553+
2554+/**
2555+ * Compares two version numbers, returning true if srcVersion >= minVersion
2556+ * @param {string} srcVersion The current version.
2557+ * @param {string} minVersion The target version number to test against
2558+ * @returns {boolean} True if srcVersion >= minVersion, false if not
2559+ */
2560+export function versionCompare(srcVersion, minVersion) {
2561+ return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1;
2562+}
public/scripts/welcome-screen.js+9 -2
@@ -18,7 +18,10 @@ import {
1818 openCharacterChat,
1919 printCharactersDebounced,
2020 renameGroupOrCharacterChat,
21+ saveSettingsDebounced,
2122 selectCharacterById,
23+ setActiveCharacter,
24+ setActiveGroup,
2225 system_avatar,
2326 system_message_types,
2427 this_chid,
@@ -107,7 +110,7 @@ async function unshallowPermanentAssistant() {
107110 * @returns {string} Greeting message
108111*/
109112function getAssistantGreeting(character) {
110113 const defaultGreeting = t`If you're connected to an API, try asking me something!` + '\n***\n' + t`**Hint:** Set any character as your welcome page assistant from their "More..." menu.`;
111114
112115 if (!character) {
113116 return defaultGreeting;
@@ -126,7 +129,7 @@ function sendAssistantMessage() {
126129 const message = {
127130 name: name,
128131 force_avatar: avatar,
129- mes: greeting + '\n***\n' + t`**Hint:** Set any character as your welcome page assistant from their "More..." menu.`,
132+ mes: greeting,
130133 is_system: false,
131134 is_user: false,
132135 send_date: getMessageTimeStamp(),
@@ -296,6 +299,8 @@ async function openRecentCharacterChat(avatarId, fileName) {
296299
297300 try {
298301 await selectCharacterById(characterId);
302+ setActiveCharacter(avatarId);
303+ saveSettingsDebounced();
299304 const currentChatId = getCurrentChatId();
300305 if (currentChatId === fileName) {
301306 console.debug(`Chat ${fileName} is already open.`);
@@ -322,6 +327,8 @@ async function openRecentGroupChat(groupId, fileName) {
322327
323328 try {
324329 await openGroupById(groupId);
330+ setActiveGroup(groupId);
331+ saveSettingsDebounced();
325332 const currentChatId = getCurrentChatId();
326333 if (currentChatId === fileName) {
327334 console.debug(`Chat ${fileName} is already open.`);
public/scripts/world-info.js+82 -28
@@ -17,7 +17,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
1717import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
1818import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1919import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
2020import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
2121import { StructuredCloneMap } from './util/StructuredCloneMap.js';
2222import { renderTemplateAsync } from './templates.js';
2323import { t } from './i18n.js';
@@ -1916,14 +1916,19 @@ export async function updateWorldInfoList() {
19161916 });
19171917
19181918 if (result.ok) {
19191919 varconst data = await result.json();
1920+ const editorSelected = String($('#world_editor_select').find(':selected').text());
19201921 world_names = data.world_names?.length ? data.world_names : [];
19211922 $('#world_info').find('option[value!=""]').remove();
19221923 $('#world_editor_select').find('option[value!=""]').remove();
19231924
19241925 world_names.forEach((item, i) => {
1925- $('#world_info').append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
1926+ const globalListOption = new Option(item, i.toString());
1926- $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
1927+ globalListOption.selected = selected_world_info.includes(item);
1928+ const editorListOption = new Option(item, i.toString());
1929+ editorListOption.selected = editorSelected === item;
1930+ $('#world_info').append(globalListOption);
1931+ $('#world_editor_select').append(editorListOption);
19271932 });
19281933 }
19291934}
@@ -2481,6 +2486,7 @@ export const originalWIDataKeyMap = {
24812486 'cooldown': 'extensions.cooldown',
24822487 'delay': 'extensions.delay',
24832488 'triggers': 'extensions.triggers',
2489+ 'ignoreBudget': 'extensions.ignore_budget',
24842490};
24852491
24862492/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
@@ -3232,7 +3238,7 @@ export async function getWorldEntry(name, data, entry) {
32323238 return;
32333239 }
32343240 const wrapper = document.createElement('div');
32353241 wrapper.textContent = t`Move/Copy '${sourceName}' to:`;
32363242 const container = document.createElement('div');
32373243 container.appendChild(wrapper);
32383244 container.appendChild(select);
@@ -3240,10 +3246,15 @@ export async function getWorldEntry(name, data, entry) {
32403246 select.addEventListener('change', function () {
32413247 selectedWorldIndex = this.value === '' ? -1 : Number(this.value);
32423248 });
32433249 const popupConfirmpopup = awaitnew callGenericPopupPopup(container, POPUP_TYPE.CONFIRM, '', {
3244- okButton: t`Move`,
32453250 cancelButton: t`Cancel`,
3251+ customButtons: [
3252+ { text: t`Move`, result: POPUP_RESULT.CUSTOM1 },
3253+ { text: t`Copy`, result: POPUP_RESULT.CUSTOM2 },
3254+ ],
32463255 });
3256+ popup.okButton.style.display = 'none'; // Hide the default OK button
3257+ const popupConfirm = await popup.show();
32473258 if (!popupConfirm) return;
32483259 if (selectedWorldIndex === -1) return;
32493260 const selectedValue = world_names[selectedWorldIndex];
@@ -3251,7 +3262,8 @@ export async function getWorldEntry(name, data, entry) {
32513262 toastr.warning(t`Please select a target lorebook.`);
32523263 return;
32533264 }
3254- await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid);
3265+ const deleteOriginal = popupConfirm === POPUP_RESULT.CUSTOM1;
3266+ await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid, { deleteOriginal });
32553267 });
32563268
32573269 let drawerInitialized = false;
@@ -3562,6 +3574,18 @@ export async function getWorldEntry(name, data, entry) {
35623574 .trigger('input', { noSave: true })
35633575 .trigger('change');
35643576
3577+ // Ignore budget
3578+ const ignoreBudgetInput = editTemplate.find('input[name="ignoreBudget"]');
3579+ ignoreBudgetInput.data('uid', entry.uid);
3580+ ignoreBudgetInput.on('input', async function (_, { noSave = false } = {}) {
3581+ const uid = $(this).data('uid');
3582+ const value = $(this).prop('checked');
3583+ data.entries[uid].ignoreBudget = value;
3584+ setWIOriginalDataValue(data, uid, 'extensions.ignore_budget', data.entries[uid].ignoreBudget);
3585+ !noSave && await saveWorldInfo(name, data);
3586+ });
3587+ ignoreBudgetInput.prop('checked', entry.ignoreBudget ?? false).trigger('input', { noSave: true });
3588+
35653589 countTokensDebounced(counter, contentInput.val());
35663590
35673591 editTemplate.find('.inline-drawer-content').css('display', 'none');
@@ -3681,7 +3705,7 @@ export function duplicateWorldInfoEntry(data, uid) {
36813705 }
36823706
36833707 // Exclude uid and gather the rest of the properties
36843708 const originalData = Object.assignstructuredClone({}, data.entries[uid]);
36853709 delete originalData.uid;
36863710
36873711 // Create new entry and copy over data
@@ -3733,6 +3757,7 @@ export const newWorldInfoEntryDefinition = {
37333757 order: { default: 100, type: 'number' },
37343758 position: { default: 0, type: 'number' },
37353759 disable: { default: false, type: 'boolean' },
3760+ ignoreBudget: { default: false, type: 'boolean' },
37363761 excludeRecursion: { default: false, type: 'boolean' },
37373762 preventRecursion: { default: false, type: 'boolean' },
37383763 matchPersonaDescription: { default: false, type: 'boolean' },
@@ -4516,7 +4541,17 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData
45164541 console.debug('[WI] --- PROBABILITY CHECKS ---');
45174542 !newEntries.length && console.debug('[WI] No probability checks to do');
45184543
4544+ let ignoresBudget = newEntries.filter(e => e.ignoreBudget).length;
4545+
45194546 for (const entry of newEntries) {
4547+ ignoresBudget -= (entry.ignoreBudget ? 1 : 0);
4548+ if (token_budget_overflowed && !entry.ignoreBudget) {
4549+ if (ignoresBudget > 0) {
4550+ continue;
4551+ }
4552+ break;
4553+ }
4554+
45204555 function verifyProbability() {
45214556 // If we don't need to roll, it's always true
45224557 if (!entry.useProbability || entry.probability === 100) {
@@ -4550,16 +4585,18 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData
45504585 entry.content = substituteParams(entry.content);
45514586 newContent += `${entry.content}\n`;
45524587
45534588 if (!entry.ignoreBudget && (textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {
4554- console.debug('[WI] --- BUDGET OVERFLOW CHECK ---');
4589+ if (!token_budget_overflowed) {
4555- if (world_info_overflow_alert) {
4590+ console.debug('[WI] --- BUDGET OVERFLOW CHECK ---');
4556- console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
4591+ if (world_info_overflow_alert) {
45574592 toastr console.warningwarn(`World[WI] infobudget of ${budget} reached, stopping after ${allActivatedEntries.size} entries.`, 'World Info');
4558- } else {
4593+ toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
4559- console.debug(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
4594+ } else {
4595+ console.debug(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
4596+ }
4597+ token_budget_overflowed = true;
45604598 }
4561- token_budget_overflowed = true;
4599+ continue;
4562- break;
45634600 }
45644601
45654602 allActivatedEntries.set(`${entry.world}.${entry.uid}`, entry);
@@ -4947,6 +4984,7 @@ function convertAgnaiMemoryBook(inputObj) {
49474984 cooldown: null,
49484985 delay: null,
49494986 triggers: [],
4987+ ignoreBudget: false,
49504988 };
49514989 });
49524990
@@ -4990,6 +5028,7 @@ function convertRisuLorebook(inputObj) {
49905028 cooldown: null,
49915029 delay: null,
49925030 triggers: [],
5031+ ignoreBudget: false,
49935032 };
49945033 });
49955034
@@ -5038,6 +5077,7 @@ function convertNovelLorebook(inputObj) {
50385077 cooldown: null,
50395078 delay: null,
50405079 triggers: [],
5080+ ignoreBudget: false,
50415081 };
50425082 });
50435083
@@ -5095,6 +5135,7 @@ export function convertCharacterBook(characterBook) {
50955135 matchCreatorNotes: entry.extensions?.match_creator_notes ?? false,
50965136 extensions: entry.extensions ?? {},
50975137 triggers: entry.extensions?.triggers || [],
5138+ ignoreBudget: entry.extensions?.ignore_budget ?? false,
50985139 };
50995140 });
51005141
@@ -5422,9 +5463,11 @@ export async function assignLorebookToChat(event) {
54225463 * @param {string} sourceName - The name of the source lorebook file.
54235464 * @param {string} targetName - The name of the target lorebook file.
54245465 * @param {string|number} uid - The UID of the entry to move from the source lorebook.
5466+ * @param {Object} options - Additional options for the move operation.
5467+ * @param {boolean} [options.deleteOriginal=true] - Whether to delete the original entry from the source lorebook after moving it.
54255468 * @returns {Promise<boolean>} True if the move was successful, false otherwise.
54265469 */
54275470export async function moveWorldInfoEntry(sourceName, targetName, uid, { deleteOriginal = true } = {}) {
54285471 if (sourceName === targetName) {
54295472 return false;
54305473 }
@@ -5466,7 +5509,6 @@ export async function moveWorldInfoEntry(sourceName, targetName, uid) {
54665509
54675510 const entryToMove = structuredClone(sourceData.entries[entryUidString]);
54685511
5469-
54705512 const newUid = getFreeWorldEntryUid(targetData);
54715513 if (newUid === null) {
54725514 console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
@@ -5480,20 +5522,20 @@ export async function moveWorldInfoEntry(sourceName, targetName, uid) {
54805522
54815523 targetData.entries[newUid] = entryToMove;
54825524
5483- delete sourceData.entries[entryUidString];
5525+ if (deleteOriginal) {
5484- // Remove from originalData if it exists
5526+ delete sourceData.entries[entryUidString];
5485- deleteWIOriginalDataValue(sourceData, entryUidString);
5527+ // Remove from originalData if it exists
5486- // TODO: setWIOriginalDataValue
5528+ deleteWIOriginalDataValue(sourceData, entryUidString);
5487- console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
5529+ // TODO: setWIOriginalDataValue
5488-
5530+ console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
5531+ }
54895532
54905533 await saveWorldInfo(targetName, targetData, true);
54915534 console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
54925535 await saveWorldInfo(sourceName, sourceData, true);
54935536 console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
54945537
5495-
5538+ console.log(`[WI Move] ${entryToMove.comment} ${deleteOriginal ? 'moved' : 'copied'} successfully to '${targetName}'.`);
5496- console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
54975539
54985540 // Check if the currently viewed book in the editor is the source or target and reload it
54995541 const currentEditorBookIndex = Number($('#world_editor_select').val());
@@ -5504,6 +5546,10 @@ export async function moveWorldInfoEntry(sourceName, targetName, uid) {
55045546 }
55055547 }
55065548
5549+ toastr.success(deleteOriginal
5550+ ? t`Entry moved successfully from '${sourceName}' to '${targetName}'.`
5551+ : t`Entry copied successfully to '${targetName}'.`);
5552+
55075553 return true;
55085554 } catch (error) {
55095555 toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
@@ -5697,6 +5743,14 @@ export function initWorldInfo() {
56975743
56985744 // Not needed on mobile
56995745 if (!isMobile()) {
5746+ $('#world_editor_select').select2({
5747+ placeholder: t`--- Pick to Edit ---`,
5748+ searchInputPlaceholder: t`Search...`,
5749+ allowClear: true,
5750+ closeOnSelect: true,
5751+ multiple: false,
5752+ });
5753+
57005754 $('#world_info').select2({
57015755 width: '100%',
57025756 placeholder: t`No Worlds active. Click here to select.`,
public/style.css+1 -1
@@ -4522,6 +4522,7 @@ input[type="range"]::-webkit-slider-thumb {
45224522.mes_reasoning_actions {
45234523 margin: 0;
45244524 margin-top: 0.5em;
4525+ margin-bottom: 0.5em;
45254526}
45264527
45274528.mes_edit_buttons .menu_button,
@@ -4539,7 +4540,6 @@ input[type="range"]::-webkit-slider-thumb {
45394540}
45404541
45414542.mes_reasoning_actions .edit_button {
4542- margin-bottom: 0.5em;
45434543 opacity: 1;
45444544 filter: brightness(0.7);
45454545}
src/command-line.js+2 -4
@@ -4,7 +4,7 @@ import yargs from 'yargs/yargs';
44import { hideBin } from 'yargs/helpers';
55import ipRegex from 'ip-regex';
66import envPaths from 'env-paths';
77import { canResolve, color, getConfigValue, stringToBool } from './util.js';
88import { initConfig } from './config-init.js';
99
1010/**
@@ -314,10 +314,8 @@ export class CommandLineParser {
314314 },
315315 getBrowserLaunchHostname: async function ({ useIPv6, useIPv4 }) {
316316 if (this.browserLaunchHostname === 'auto') {
317- const localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
318-
319317 if (useIPv6 && useIPv4) {
320318 return (this.browserLaunchAvoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
321319 }
322320
323321 if (useIPv6) {
src/constants.js+3 -1
@@ -173,12 +173,14 @@ export const CHAT_COMPLETION_SOURCES = {
173173 COHERE: 'cohere',
174174 PERPLEXITY: 'perplexity',
175175 GROQ: 'groq',
176- ZEROONEAI: '01ai',
177176 NANOGPT: 'nanogpt',
178177 DEEPSEEK: 'deepseek',
179178 AIMLAPI: 'aimlapi',
180179 XAI: 'xai',
181180 POLLINATIONS: 'pollinations',
181+ MOONSHOT: 'moonshot',
182+ FIREWORKS: 'fireworks',
183+ COMETAPI: 'cometapi',
182184};
183185
184186/**
src/endpoints/avatars.js+1 -1
@@ -53,7 +53,7 @@ router.post('/upload', getFileNameValidationFunction('overwrite_name'), async (r
5353 cacheBuster.bust(request, response);
5454 }
5555
5656 const filename = sanitize(request.body.overwrite_name || `${Date.now()}.png`);
5757 const pathToNewFile = path.join(request.user.directories.avatars, filename);
5858 writeFileAtomicSync(pathToNewFile, image);
5959 fs.unlinkSync(pathToUpload);
src/endpoints/backends/chat-completions.js+149 -22
@@ -59,13 +59,15 @@ const API_PERPLEXITY = 'https://api.perplexity.ai';
5959const API_GROQ = 'https://api.groq.com/openai/v1';
6060const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
6161const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
62-const API_01AI = 'https://api.lingyiwanwu.com/v1';
6362const API_AI21 = 'https://api.ai21.com/studio/v1';
6463const API_NANOGPT = 'https://nano-gpt.com/api/v1';
6564const API_DEEPSEEK = 'https://api.deepseek.com/beta';
6665const API_XAI = 'https://api.x.ai/v1';
6766const API_AIMLAPI = 'https://api.aimlapi.com/v1';
6867const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
68+const API_MOONSHOT = 'https://api.moonshot.ai/v1';
69+const API_FIREWORKS = 'https://api.fireworks.ai/inference/v1';
70+const API_COMETAPI = 'https://api.cometapi.com/v1';
6971
7072/**
7173 * Gets OpenRouter transforms based on the request.
@@ -99,6 +101,23 @@ function getOpenRouterPlugins(request) {
99101}
100102
101103/**
104+ * Hacky way to use JSON schema only if json_object format is supported.
105+ * @param {object} bodyParams Additional body parameters
106+ * @param {object[]} messages Array of messages
107+ * @param {object} jsonSchema JSON schema object
108+ */
109+function setJsonObjectFormat(bodyParams, messages, jsonSchema) {
110+ bodyParams['response_format'] = {
111+ type: 'json_object',
112+ };
113+ const message = {
114+ role: 'user',
115+ content: `JSON schema for the response:\n${JSON.stringify(jsonSchema.value, null, 4)}`,
116+ };
117+ messages.push(message);
118+}
119+
120+/**
102121 * Sends a request to Claude API.
103122 * @param {express.Request} request Express request
104123 * @param {express.Response} response Express response
@@ -132,6 +151,7 @@ async function sendClaudeRequest(request, response) {
132151 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request));
133152 const useThinking = /^claude-(3-7|opus-4|sonnet-4)/.test(request.body.model);
134153 const useWebSearch = /^claude-(3-5|3-7|opus-4|sonnet-4)/.test(request.body.model) && Boolean(request.body.enable_web_search);
154+ const isOpus41 = /^claude-opus-4-1/.test(request.body.model);
135155 const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m';
136156 let fixThinkingPrefill = false;
137157 // Add custom stop sequences
@@ -201,6 +221,14 @@ async function sendClaudeRequest(request, response) {
201221 betaHeaders.push('extended-cache-ttl-2025-04-11');
202222 }
203223
224+ if (isOpus41){
225+ if (requestBody.top_p < 1) {
226+ delete requestBody.temperature;
227+ } else {
228+ delete requestBody.top_p;
229+ }
230+ }
231+
204232 const reasoningEffort = request.body.reasoning_effort;
205233 const budgetTokens = calculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);
206234
@@ -334,6 +362,7 @@ async function sendMakerSuiteRequest(request, response) {
334362 topK: request.body.top_k || undefined,
335363 responseMimeType: responseMimeType,
336364 responseSchema: responseSchema,
365+ seed: request.body.seed,
337366 };
338367
339368 function getGeminiBody() {
@@ -890,7 +919,7 @@ async function sendDeepSeekRequest(request, response) {
890919 const postProcessType = String(request.body.model).endsWith('-reasoner')
891920 ? PROMPT_PROCESSING_TYPE.STRICT_TOOLS
892921 : PROMPT_PROCESSING_TYPE.SEMI_TOOLS;
893922 const processedMessages = addAssistantPrefix(postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request)), bodyParams.tools, 'prefix');
894923
895924 const requestBody = {
896925 'messages': processedMessages,
@@ -1196,10 +1225,6 @@ router.post('/status', async function (request, statusResponse) {
11961225 apiUrl = API_COHERE_V1;
11971226 apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
11981227 headers = {};
1199- } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
1200- apiUrl = API_01AI;
1201- apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
1202- headers = {};
12031228 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {
12041229 apiUrl = API_NANOGPT;
12051230 apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
@@ -1224,6 +1249,18 @@ router.post('/status', async function (request, statusResponse) {
12241249 apiUrl = API_GROQ;
12251250 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
12261251 headers = {};
1252+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {
1253+ apiUrl = API_COMETAPI;
1254+ apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);
1255+ headers = {};
1256+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {
1257+ apiUrl = API_MOONSHOT;
1258+ apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);
1259+ headers = {};
1260+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {
1261+ apiUrl = API_FIREWORKS;
1262+ apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);
1263+ headers = {};
12271264 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MAKERSUITE) {
12281265 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
12291266 apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE);
@@ -1582,6 +1619,22 @@ router.post('/generate', function (request, response) {
15821619 },
15831620 };
15841621 }
1622+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {
1623+ apiUrl = API_FIREWORKS;
1624+ apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);
1625+ headers = {};
1626+ bodyParams = {};
1627+ if (request.body.json_schema) {
1628+ bodyParams['response_format'] = {
1629+ type: 'json_schema',
1630+ json_schema: {
1631+ name: request.body.json_schema.name,
1632+ description: request.body.json_schema.description,
1633+ schema: request.body.json_schema.value,
1634+ strict: request.body.json_schema.strict ?? true,
1635+ },
1636+ };
1637+ }
15851638 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {
15861639 apiUrl = API_NANOGPT;
15871640 apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
@@ -1590,11 +1643,6 @@ router.post('/generate', function (request, response) {
15901643 if (request.body.enable_web_search && !/:online$/.test(request.body.model)) {
15911644 request.body.model = `${request.body.model}:online`;
15921645 }
1593- } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
1594- apiUrl = API_01AI;
1595- apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
1596- headers = {};
1597- bodyParams = {};
15981646 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
15991647 apiUrl = API_POLLINATIONS;
16001648 apiKey = 'NONE';
@@ -1607,17 +1655,24 @@ router.post('/generate', function (request, response) {
16071655 referrer: 'sillytavern',
16081656 seed: request.body.seed ?? Math.floor(Math.random() * 99999999),
16091657 };
1610- // Hack to support JSON schema
16111658 if (request.body.json_schema) {
1612- bodyParams['response_format'] = {
1659+ setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);
1613- type: 'json_object',
1614- };
1615- const message = {
1616- role: 'user',
1617- content: `JSON schema for the response:\n${JSON.stringify(request.body.json_schema.value, null, 4)}`,
1618- };
1619- request.body.messages.push(message);
16201660 }
1661+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {
1662+ apiUrl = API_MOONSHOT;
1663+ apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);
1664+ headers = {};
1665+ bodyParams = {};
1666+ request.body.json_schema
1667+ ? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema)
1668+ : addAssistantPrefix(request.body.messages, [], 'partial');
1669+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {
1670+ apiUrl = API_COMETAPI;
1671+ apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);
1672+ headers = {};
1673+ bodyParams = {
1674+ reasoning_effort: request.body.reasoning_effort,
1675+ };
16211676 } else {
16221677 console.warn('This chat completion source is not supported yet.');
16231678 return response.status(400).send({ error: true });
@@ -1625,8 +1680,26 @@ router.post('/generate', function (request, response) {
16251680
16261681 // A few of OpenAIs reasoning models support reasoning effort
16271682 if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
1628- 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)) {
1683+ const reasoningEffortModels = [
1629- bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1684+ 'o1',
1685+ 'o3-mini',
1686+ 'o3-mini-2025-01-31',
1687+ 'o4-mini',
1688+ 'o4-mini-2025-04-16',
1689+ 'o3',
1690+ 'o3-2025-04-16',
1691+ 'gpt-5',
1692+ 'gpt-5-2025-08-07',
1693+ 'gpt-5-mini',
1694+ 'gpt-5-mini-2025-08-07',
1695+ 'gpt-5-nano',
1696+ 'gpt-5-nano-2025-08-07',
1697+ ];
1698+ const reasoningEffortMap = {
1699+ min: 'minimal',
1700+ };
1701+ if (reasoningEffortModels.includes(request.body.model)) {
1702+ bodyParams['reasoning_effort'] = reasoningEffortMap[request.body.reasoning_effort] ?? request.body.reasoning_effort;
16301703 }
16311704 }
16321705
@@ -1774,3 +1847,57 @@ router.post('/generate', function (request, response) {
17741847 }
17751848 }
17761849});
1850+
1851+const pollinations = express.Router();
1852+
1853+pollinations.post('/models/multimodal', async (_req, res) => {
1854+ try {
1855+ const response = await fetch('https://text.pollinations.ai/models');
1856+
1857+ if (!response.ok) {
1858+ return res.json([]);
1859+ }
1860+
1861+ /** @type {any} */
1862+ const data = await response.json();
1863+
1864+ if (!Array.isArray(data)) {
1865+ return res.json([]);
1866+ }
1867+
1868+ const multimodalModels = data.filter(m => m?.vision).map(m => m.name);
1869+ return res.json(multimodalModels);
1870+ } catch (error) {
1871+ console.error(error);
1872+ return res.sendStatus(500);
1873+ }
1874+});
1875+
1876+router.use('/pollinations', pollinations);
1877+
1878+const aimlapi = express.Router();
1879+
1880+aimlapi.post('/models/multimodal', async (_req, res) => {
1881+ try {
1882+ const response = await fetch('https://api.aimlapi.com/v1/models');
1883+
1884+ if (!response.ok) {
1885+ return res.json([]);
1886+ }
1887+
1888+ /** @type {any} */
1889+ const data = await response.json();
1890+
1891+ if (!Array.isArray(data?.data)) {
1892+ return res.json([]);
1893+ }
1894+
1895+ const multimodalModels = data.data.filter(m => m?.features?.includes('openai/chat-completion.vision')).map(m => m.id);
1896+ return res.json(multimodalModels);
1897+ } catch (error) {
1898+ console.error(error);
1899+ return res.sendStatus(500);
1900+ }
1901+});
1902+
1903+router.use('/aimlapi', aimlapi);
src/endpoints/characters.js+1 -0
@@ -710,6 +710,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
710710 match_scenario: entry.matchScenario ?? false,
711711 match_creator_notes: entry.matchCreatorNotes ?? false,
712712 triggers: entry.triggers ?? [],
713+ ignore_budget: entry.ignoreBudget ?? false,
713714 },
714715 };
715716
src/endpoints/chats.js+1 -1
@@ -33,7 +33,7 @@ export const CHAT_BACKUPS_PREFIX = 'chat_';
3333 */
3434function backupChat(directory, name, chat) {
3535 try {
3636 if (!isBackupEnabled || !fs.existsSync(directory)) {
3737 return;
3838 }
3939
src/endpoints/content-manager.js+41 -206
@@ -369,7 +369,7 @@ async function downloadChubLorebook(id) {
369369
370370async function downloadChubCharacter(id) {
371371 const [creatorName, projectName] = id.split('/');
372372 const result = await fetch(`https://api.chub.ai/api/characters/${creatorName}/${projectName}?full=true`, {
373373 method: 'GET',
374374 headers: { 'Accept': 'application/json', 'User-Agent': USER_AGENT },
375375 });
@@ -382,25 +382,48 @@ async function downloadChubCharacter(id) {
382382
383383 /** @type {any} */
384384 const metadata = await result.json();
385385 const downloadUrl{ definition, topics } = metadata.node?.max_res_url;
386+
387+ /** @type {TavernCardV2} */
388+ const characterCard = {
389+ data: {
390+ name: definition.name,
391+ description: definition.personality,
392+ personality: definition.tavern_personality,
393+ scenario: definition.scenario,
394+ first_mes: definition.first_message,
395+ mes_example: definition.example_dialogs,
396+ creator_notes: definition.description,
397+ system_prompt: definition.system_prompt,
398+ post_history_instructions: definition.post_history_instructions,
399+ alternate_greetings: definition.alternate_greetings,
400+ tags: topics,
401+ creator: creatorName,
402+ character_version: '',
403+ character_book: definition.embedded_lorebook,
404+ extensions: definition.extensions,
405+ },
406+ spec: 'chara_card_v2',
407+ spec_version: '2.0',
408+ };
386409
387- if (!downloadUrl) {
410+ const defaultAvatarPath = path.join(serverDirectory, DEFAULT_AVATAR_PATH);
388- throw new Error('Download URL not found in character metadata');
411+ const defaultAvatarBuffer = fs.readFileSync(defaultAvatarPath);
389- }
390412
391413 constlet downloadResultimageBuffer = await fetch(downloadUrl)defaultAvatarBuffer;
392414
393- if (!downloadResult.ok) {
415+ const imageUrl = metadata.node?.max_res_url;
394- const text = await downloadResult.text();
416+
395- console.error('Chub returned error', downloadResult.statusText, text);
417+ if (imageUrl) {
396- throw new Error('Failed to download character');
418+ const downloadResult = await fetch(imageUrl);
419+ if (downloadResult.ok) {
420+ imageBuffer = Buffer.from(await downloadResult.arrayBuffer());
421+ }
397422 }
398423
399424 const buffer = Buffer.fromwrite(awaitimageBuffer, downloadResultJSON.arrayBufferstringify(characterCard));
400- const fileName =
425+ const fileName = `${sanitize(characterCard.data.name)}.png`;
401- downloadResult.headers.get('content-disposition')?.split('filename=')[1]?.replace(/["']/g, '') ||
426+ const fileType = 'image/png';
402- `${sanitize(projectName)}.png`;
403- const fileType = downloadResult.headers.get('content-type');
404427
405428 return { buffer, fileName, fileType };
406429}
@@ -639,187 +662,6 @@ async function downloadRisuCharacter(uuid) {
639662 return { buffer, fileName, fileType };
640663}
641664
642-/**
643- * Parse Soulkyn URL to extract the character slug.
644- * @param {string} url Soulkyn character URL
645- * @returns {string | null} Slug of the character
646- */
647-function parseSoulkynUrl(url) {
648- // Example: https://soulkyn.com/l/en-US/@kayla-marie
649- const pattern = /^https:\/\/soulkyn\.com\/l\/[a-z]{2}-[A-Z]{2}\/@([\w\d-]+)/i;
650- const match = url.match(pattern);
651- return match ? match[1] : null;
652-}
653-
654-/**
655- * Download Soulkyn character card
656- * @param {string} slug Slug of the character
657- * @returns {Promise<{buffer: Buffer, fileName: string, fileType: string} | null>}
658- */
659-async function downloadSoulkynCharacter(slug) {
660- const soulkynReplacements = [
661- // https://soulkyn.com/l/en-US/help/character-backgrounds-advanced#variables-you-can-use-in-character-background-text
662- { pattern: /__USER_?NAME__/gi, replacement: '{{user}}' },
663- { pattern: /__PERSONA_?NAME__/gi, replacement: '{{char}}' },
664- // ST doesn't support gender-specific pronoun macros
665- { pattern: /__U_PRONOUN_1__/gi, replacement: 'they' },
666- { pattern: /__U_PRONOUN_2__/gi, replacement: 'them' },
667- { pattern: /__U_PRONOUN_3__/gi, replacement: 'their' },
668- { pattern: /__U_PRONOUN_4__/gi, replacement: 'themselves' },
669- { pattern: /__(USER_)?PRONOUN__/gi, replacement: 'they' },
670- { pattern: /__(USER_)?CPRONOUN__/gi, replacement: 'them' },
671- { pattern: /__(USER_)?UPRONOUN__/gi, replacement: 'their' },
672- // HTML tags -> Markdown syntax
673- { pattern: /<(strong|b)>/gi, replacement: '**' },
674- { pattern: /<\/(strong|b)>/gi, replacement: '**' },
675- { pattern: /<(em|i)>/gi, replacement: '*' },
676- { pattern: /<\/(em|i)>/gi, replacement: '*' },
677- ];
678-
679- const normalizeContent = (str) => soulkynReplacements.reduce((acc, { pattern, replacement }) => acc.replace(pattern, replacement), str);
680-
681- try {
682- const url = `https://soulkyn.com/_special/rest/Sk/public/Persona/${slug}`;
683- const result = await fetch(url, {
684- headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
685- });
686- if (result.ok) {
687- /** @type {any} */
688- const soulkynCharData = await result.json();
689-
690- if (soulkynCharData.result !== 'success') {
691- console.error('Soulkyn returned error', soulkynCharData.message);
692- throw new Error(`Failed to download character: ${soulkynCharData.message}`);
693- }
694-
695- // Fetch avatar
696- let avatarBuffer = null;
697- if (soulkynCharData.data?.Avatar?.FWSUUID) {
698- const avatarUrl = `https://rub.soulkyn.com/${soulkynCharData.data.Avatar.FWSUUID}/`;
699- const avatarResult = await fetch(avatarUrl, { headers: { 'User-Agent': USER_AGENT } });
700-
701- if (avatarResult.ok) {
702- const avatarContentType = avatarResult.headers.get('content-type');
703- if (avatarContentType === 'image/png') {
704- avatarBuffer = Buffer.from(await avatarResult.arrayBuffer());
705- } else {
706- console.warn(`Soulkyn character (${slug}) avatar is not PNG: ${avatarContentType}`);
707- }
708- } else {
709- console.warn(`Soulkyn character (${slug}) avatar download failed: ${avatarResult.status}`);
710- }
711- } else {
712- console.warn(`Soulkyn character (${slug}) does not have an avatar`);
713- }
714-
715- // Fallback to default avatar
716- if (!avatarBuffer) {
717- const defaultAvatarPath = path.join(serverDirectory, DEFAULT_AVATAR_PATH);
718- avatarBuffer = fs.readFileSync(defaultAvatarPath);
719- }
720-
721- const d = soulkynCharData.data;
722- soulkynReplacements.push({ pattern: d.Username, replacement: '{{char}}' });
723-
724- // Parse Soulkyn data into character chard
725- const charData = {
726- name: d.Username,
727- first_mes: '',
728- tags: [],
729- description: '',
730- creator: d.User.Username,
731- creator_notes: '',
732- alternate_greetings: [],
733- character_version: '',
734- mes_example: '',
735- post_history_instructions: '',
736- system_prompt: '',
737- scenario: '',
738- personality: '',
739- extensions: {
740- soulkyn_slug: slug,
741- soulkyn_id: d.UUID,
742- },
743- };
744-
745- if (d?.PersonaIntroText) {
746- const match = d.PersonaIntroText.match(/^(?:\[Scenario:\s*([\s\S]*?)\]\s*)?([\s\S]*)$/);
747- if (match) {
748- if (match[1]) {
749- charData.scenario = normalizeContent(match[1].trim());
750- }
751- charData.first_mes = normalizeContent(match[2].trim());
752- }
753- }
754-
755- const descriptionArr = ['Name: {{char}}'];
756- if (d?.Version?.Age) {
757- descriptionArr.push(`Age: ${d.Version.Age}`);
758- }
759- if (d?.Version?.Gender) {
760- descriptionArr.push(`Gender: ${d.Version.Gender}`);
761- }
762- if (d?.Version?.Race?.Name && !d.Version.Race.Name.match(/no preset/i)) {
763- let race = d.Version.Race.Name;
764- if (d.Version.Race?.Description) {
765- race += ` (${d.Version.Race.Description})`;
766- }
767- descriptionArr.push(`Race: ${race}`);
768- }
769- if (d?.PersonalityType) {
770- descriptionArr.push(`Personality type: ${d.PersonalityType}`);
771- }
772- if (Array.isArray(d?.Version?.PropertyPersonality)) {
773- const traits = d.Version.PropertyPersonality.map((t) => t.Value).join(', ');
774- descriptionArr.push(`Personality Traits: ${traits}`);
775- }
776- if (Array.isArray(d?.Version?.PropertyPhysical)) {
777- const traits = d.Version.PropertyPhysical.map((t) => t.Value).join(', ');
778- descriptionArr.push(`Physical Traits: ${traits}`);
779- }
780- if (Array.isArray(d?.Clothes?.Preset)) {
781- descriptionArr.push(`Clothes: ${d.Clothes.Preset.join(', ')}`);
782- }
783- if (d?.Avatar?.Caption) {
784- descriptionArr.push(`Image description featuring {{char}}: ${d.Avatar.Caption.replace(/\n+/g, ' ')}`);
785- }
786- if (d?.Version?.WelcomeMessage) {
787- if (charData.first_mes) {
788- descriptionArr.push(`{{char}}'s self-description: "${d.Version.WelcomeMessage}"`);
789- } else {
790- // Some characters lack `PersonaIntroText`. In that case we use `Version.WelcomeMessage` for `first_mes`
791- charData.first_mes = normalizeContent(d.Version.WelcomeMessage);
792- }
793- }
794- charData.description = normalizeContent(descriptionArr.join('\n'));
795-
796- if (Array.isArray(d?.Version?.ChatExamplesValue)) {
797- charData.mes_example = d.Version.ChatExamplesValue.map((example) => `<START>\n${normalizeContent(example)}`).join('\n');
798- }
799-
800- if (Array.isArray(d?.PersonaTags)) {
801- charData.tags = d.PersonaTags.map((t) => t.Slug);
802- }
803-
804- // Character card
805- const buffer = write(avatarBuffer, JSON.stringify({
806- 'spec': 'chara_card_v2',
807- 'spec_version': '2.0',
808- 'data': charData,
809- }));
810-
811- const fileName = `${sanitize(d.UUID)}.png`;
812- const fileType = 'image/png';
813-
814- return { buffer, fileName, fileType };
815- }
816- } catch (error) {
817- console.error('Error downloading character:', error);
818- throw error;
819- }
820- return null;
821-}
822-
823665/** * Check if the given string is a valid Perchance UUID.
824666 * @param {string} uuid UUID string to check
825667 * @returns {boolean} True if the UUID is valid, false otherwise
@@ -921,13 +763,14 @@ async function downloadPerchanceCharacter(slug) {
921763 return null;
922764}
923765
924-/** * Extracts Perchance character data from a gzipped response.
766+/**
767+ * Extracts Perchance character data from a gzipped response.
925768 * @param {import('node-fetch').Response} result Fetch response containing gzipped character data
926769 * @returns {Promise<Object>} Parsed Perchance character data
927770 * @throws {Error} If the character data is invalid or missing required fields
928771 */
929772async function extractPerchanceCharacterFromGz(result) {
930773 const compressedBuffer = Buffer.from(await result.arrayBuffer());
931774 const decompressedBuffer = zlib.gunzipSync(compressedBuffer);
932775
933776 // inside the gz file, there is a file of the same name without extensions, but it is a json file
@@ -1062,7 +905,6 @@ router.post('/importURL', async (request, response) => {
1062905 const isPygmalionContent = host.includes('pygmalion.chat');
1063906 const isAICharacterCardsContent = host.includes('aicharactercards.com');
1064907 const isRisu = host.includes('realm.risuai.net');
1065- const isSoulkyn = host.includes('soulkyn.com');
1066908 const isPerchance = host.includes('perchance.org');
1067909 const isGeneric = isHostWhitelisted(host);
1068910
@@ -1112,13 +954,6 @@ router.post('/importURL', async (request, response) => {
1112954
1113955 type = 'character';
1114956 result = await downloadRisuCharacter(uuid);
1115- } else if (isSoulkyn) {
1116- const soulkynSlug = parseSoulkynUrl(url);
1117- if (!soulkynSlug) {
1118- return response.sendStatus(404);
1119- }
1120- type = 'character';
1121- result = await downloadSoulkynCharacter(soulkynSlug);
1122957 } else if (isPerchance) {
1123958 const perchanceSlug = parsePerchanceSlug(url);
1124959 if (!perchanceSlug) {
src/endpoints/minimax.js+230 -0
@@ -0,0 +1,230 @@
1+import express from 'express';
2+import fetch from 'node-fetch';
3+import { readSecret, SECRET_KEYS } from './secrets.js';
4+
5+export const router = express.Router();
6+
7+// Audio format MIME type mapping
8+const getAudioMimeType = (format) => {
9+ const mimeTypes = {
10+ 'mp3': 'audio/mpeg',
11+ 'wav': 'audio/wav',
12+ 'pcm': 'audio/pcm',
13+ 'flac': 'audio/flac',
14+ 'aac': 'audio/aac',
15+ };
16+ return mimeTypes[format] || 'audio/mpeg';
17+};
18+
19+router.post('/generate-voice', async (request, response) => {
20+ try {
21+ const {
22+ text,
23+ voiceId,
24+ apiHost = 'https://api.minimax.io',
25+ model = 'speech-02-hd',
26+ speed = 1.0,
27+ volume = 1.0,
28+ pitch = 1.0,
29+ audioSampleRate = 32000,
30+ bitrate = 128000,
31+ format = 'mp3',
32+ language,
33+ } = request.body;
34+
35+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.MINIMAX);
36+ const groupId = readSecret(request.user.directories, SECRET_KEYS.MINIMAX_GROUP_ID);
37+
38+ // Validate required parameters
39+ if (!text || !voiceId || !apiKey || !groupId) {
40+ console.warn('MiniMax TTS: Missing required parameters');
41+ return response.status(400).json({ error: 'Missing required parameters: text, voiceId, apiKey, and groupId are required' });
42+ }
43+
44+ const requestBody = {
45+ model: model,
46+ text: text,
47+ stream: false,
48+ voice_setting: {
49+ voice_id: voiceId,
50+ speed: Number(speed),
51+ vol: Number(volume),
52+ pitch: Number(pitch),
53+ },
54+ audio_setting: {
55+ sample_rate: Number(audioSampleRate),
56+ bitrate: Number(bitrate),
57+ format: format,
58+ channel: 1,
59+ },
60+ };
61+
62+ // Add language parameter if provided
63+ if (language) {
64+ requestBody.lang = language;
65+ }
66+
67+ const apiUrl = `${apiHost}/v1/t2a_v2?GroupId=${groupId}`;
68+
69+ console.debug('MiniMax TTS Request:', {
70+ url: apiUrl,
71+ body: { ...requestBody, voice_setting: { ...requestBody.voice_setting, voice_id: '[REDACTED]' } },
72+ });
73+
74+ const apiResponse = await fetch(apiUrl, {
75+ method: 'POST',
76+ headers: {
77+ 'Authorization': `Bearer ${apiKey}`,
78+ 'Content-Type': 'application/json',
79+ 'MM-API-Source': 'SillyTavern-TTS',
80+ },
81+ body: JSON.stringify(requestBody),
82+ });
83+
84+ if (!apiResponse.ok) {
85+ let errorMessage = `HTTP ${apiResponse.status}`;
86+
87+ try {
88+ // Try to parse JSON error response
89+ /** @type {any} */
90+ const errorData = await apiResponse.json();
91+ console.error('MiniMax TTS API error (JSON):', errorData);
92+
93+ // Check for MiniMax specific error format
94+ const baseResp = errorData?.base_resp;
95+ if (baseResp && baseResp.status_code !== 0) {
96+ if (baseResp.status_code === 1004) {
97+ errorMessage = 'Authentication failed - Please check your API key and API host';
98+ } else {
99+ errorMessage = `API Error: ${baseResp.status_msg}`;
100+ }
101+ } else {
102+ errorMessage = errorData.error?.message || errorData.message || errorData.detail || `HTTP ${apiResponse.status}`;
103+ }
104+ } catch (jsonError) {
105+ // If not JSON, try to read text
106+ try {
107+ const errorText = await apiResponse.text();
108+ console.error('MiniMax TTS API error (Text):', errorText);
109+ if (errorText && errorText.length > 500) {
110+ errorMessage = `HTTP ${apiResponse.status}: Response too large (${errorText.length} characters)`;
111+ } else {
112+ errorMessage = errorText || `HTTP ${apiResponse.status}`;
113+ }
114+ } catch (textError) {
115+ console.error('MiniMax TTS: Failed to read error response:', textError);
116+ errorMessage = `HTTP ${apiResponse.status}: Unable to read error details`;
117+ }
118+ }
119+
120+ console.error('MiniMax TTS API request failed:', errorMessage);
121+ return response.status(500).json({ error: errorMessage });
122+ }
123+
124+ // Parse the response
125+ /** @type {any} */
126+ let responseData;
127+ try {
128+ responseData = await apiResponse.json();
129+ console.debug('MiniMax TTS Response received');
130+ } catch (jsonError) {
131+ console.error('MiniMax TTS: Failed to parse response as JSON:', jsonError);
132+ return response.status(500).json({ error: 'Invalid response format from MiniMax API' });
133+ }
134+
135+ // Check for API error codes in response data
136+ const baseResp = responseData?.base_resp;
137+ if (baseResp && baseResp.status_code !== 0) {
138+ let errorMessage;
139+ if (baseResp.status_code === 1004) {
140+ errorMessage = 'Authentication failed - Please check your API key and API host';
141+ } else {
142+ errorMessage = `API Error: ${baseResp.status_msg}`;
143+ }
144+ console.error('MiniMax TTS API error:', baseResp);
145+ return response.status(500).json({ error: errorMessage });
146+ }
147+
148+ // Process the audio data
149+ if (responseData.data && responseData.data.audio) {
150+ // Process hex-encoded audio data
151+ const hexAudio = responseData.data.audio;
152+
153+ if (!hexAudio || typeof hexAudio !== 'string') {
154+ console.error('MiniMax TTS: Invalid audio data format');
155+ return response.status(500).json({ error: 'Invalid audio data format' });
156+ }
157+
158+ // Remove possible prefix and spaces
159+ const cleanHex = hexAudio.replace(/^0x/, '').replace(/\s/g, '');
160+
161+ // Validate hex string format
162+ if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {
163+ console.error('MiniMax TTS: Invalid hex string format');
164+ return response.status(500).json({ error: 'Invalid audio data format' });
165+ }
166+
167+ // Ensure hex string length is even
168+ const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex;
169+
170+ try {
171+ // Convert hex string to byte array
172+ const hexMatches = paddedHex.match(/.{1,2}/g);
173+ if (!hexMatches) {
174+ console.error('MiniMax TTS: Failed to parse hex string');
175+ return response.status(500).json({ error: 'Invalid hex string format' });
176+ }
177+ const audioBytes = new Uint8Array(hexMatches.map(byte => parseInt(byte, 16)));
178+
179+ if (audioBytes.length === 0) {
180+ console.error('MiniMax TTS: Audio conversion resulted in empty array');
181+ return response.status(500).json({ error: 'Audio data conversion failed' });
182+ }
183+
184+ console.debug(`MiniMax TTS: Converted ${paddedHex.length} hex characters to ${audioBytes.length} bytes`);
185+
186+ // Set appropriate headers and send audio data
187+ const mimeType = getAudioMimeType(format);
188+ response.setHeader('Content-Type', mimeType);
189+ response.setHeader('Content-Length', audioBytes.length);
190+
191+ return response.send(Buffer.from(audioBytes));
192+
193+ } catch (conversionError) {
194+ console.error('MiniMax TTS: Audio conversion error:', conversionError);
195+ return response.status(500).json({ error: `Audio data conversion failed: ${conversionError.message}` });
196+ }
197+ } else if (responseData.data && responseData.data.url) {
198+ // Handle URL-based audio response
199+ console.debug('MiniMax TTS: Received audio URL:', responseData.data.url);
200+
201+ try {
202+ const audioResponse = await fetch(responseData.data.url);
203+ if (!audioResponse.ok) {
204+ console.error('MiniMax TTS: Failed to fetch audio from URL:', audioResponse.status);
205+ return response.status(500).json({ error: `Failed to fetch audio from URL: ${audioResponse.status}` });
206+ }
207+
208+ const audioBuffer = await audioResponse.arrayBuffer();
209+ const mimeType = getAudioMimeType(format);
210+
211+ response.setHeader('Content-Type', mimeType);
212+ response.setHeader('Content-Length', audioBuffer.byteLength);
213+
214+ return response.send(Buffer.from(audioBuffer));
215+ } catch (urlError) {
216+ console.error('MiniMax TTS: Error fetching audio from URL:', urlError);
217+ return response.status(500).json({ error: `Failed to fetch audio: ${urlError.message}` });
218+ }
219+ } else {
220+ // Handle error response
221+ const errorMessage = responseData.base_resp?.status_msg || responseData.error?.message || 'Unknown error';
222+ console.error('MiniMax TTS: No valid audio data in response:', responseData);
223+ return response.status(500).json({ error: `API Error: ${errorMessage}` });
224+ }
225+
226+ } catch (error) {
227+ console.error('MiniMax TTS generation failed:', error);
228+ return response.status(500).json({ error: 'Internal server error' });
229+ }
230+});
src/endpoints/novelai.js+23 -0
@@ -11,6 +11,11 @@ const API_NOVELAI = 'https://api.novelai.net';
1111const TEXT_NOVELAI = 'https://text.novelai.net';
1212const IMAGE_NOVELAI = 'https://image.novelai.net';
1313
14+// Constants for skip_cfg_above_sigma (Variety+) calculation
15+const REFERENCE_PIXEL_COUNT = 1011712; // 832 * 1216 reference image size
16+const SIGMA_MAGIC_NUMBER = 19; // Base sigma multiplier for V3 and V4 models
17+const SIGMA_MAGIC_NUMBER_V4_5 = 58; // Base sigma multiplier for V4.5 models
18+
1419// Ban bracket generation, plus defaults
1520const badWordsList = [
1621 [3], [49356], [1431], [31715], [34387], [20765], [30702], [10691], [49333], [1266],
@@ -112,6 +117,17 @@ function getRepPenaltyWhitelist(model) {
112117 return null;
113118}
114119
120+function calculateSkipCfgAboveSigma(width, height, modelName) {
121+ const magicConstant = modelName?.includes('nai-diffusion-4-5')
122+ ? SIGMA_MAGIC_NUMBER_V4_5
123+ : SIGMA_MAGIC_NUMBER;
124+
125+ const pixelCount = width * height;
126+ const ratio = pixelCount / REFERENCE_PIXEL_COUNT;
127+
128+ return Math.pow(ratio, 0.5) * magicConstant;
129+}
130+
115131export const router = express.Router();
116132
117133router.post('/status', async function (req, res) {
@@ -332,6 +348,13 @@ router.post('/generate-image', async (request, response) => {
332348 sm: request.body.sm ?? false,
333349 sm_dyn: request.body.sm_dyn ?? false,
334350 uncond_scale: 1,
351+ skip_cfg_above_sigma: request.body.variety_boost
352+ ? calculateSkipCfgAboveSigma(
353+ request.body.width ?? 512,
354+ request.body.height ?? 512,
355+ request.body.model ?? 'nai-diffusion',
356+ )
357+ : null,
335358 use_coords: false,
336359 characterPrompts: [],
337360 reference_image_multiple: [],
src/endpoints/openai.js+23 -15
@@ -61,10 +61,6 @@ router.post('/caption-image', async (request, response) => {
6161 key = readSecret(request.user.directories, SECRET_KEYS.VLLM);
6262 }
6363
64- if (request.body.api === 'zerooneai') {
65- key = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
66- }
67-
6864 if (request.body.api === 'aimlapi') {
6965 key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
7066 }
@@ -77,6 +73,10 @@ router.post('/caption-image', async (request, response) => {
7773 key = readSecret(request.user.directories, SECRET_KEYS.COHERE);
7874 }
7975
76+ if (request.body.api === 'moonshot') {
77+ key = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);
78+ }
79+
8080 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations'];
8181 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {
8282 console.warn('No key found for API', request.body.api);
@@ -128,10 +128,6 @@ router.post('/caption-image', async (request, response) => {
128128 apiUrl = `${request.body.server_url}/chat/completions`;
129129 }
130130
131- if (request.body.api === 'zerooneai') {
132- apiUrl = 'https://api.lingyiwanwu.com/v1/chat/completions';
133- }
134-
135131 if (request.body.api === 'aimlapi') {
136132 apiUrl = 'https://api.aimlapi.com/v1/chat/completions';
137133 Object.assign(headers, AIMLAPI_HEADERS);
@@ -161,6 +157,10 @@ router.post('/caption-image', async (request, response) => {
161157 apiUrl = 'https://text.pollinations.ai/openai/chat/completions';
162158 }
163159
160+ if (request.body.api === 'moonshot') {
161+ apiUrl = 'https://api.moonshot.ai/v1/chat/completions';
162+ }
163+
164164 if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) {
165165 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
166166 }
@@ -271,19 +271,27 @@ router.post('/generate-voice', async (request, response) => {
271271 return response.sendStatus(400);
272272 }
273273
274+ const requestBody = {
275+ input: request.body.text,
276+ response_format: 'mp3',
277+ voice: request.body.voice ?? 'alloy',
278+ speed: request.body.speed ?? 1,
279+ model: request.body.model ?? 'tts-1',
280+ };
281+
282+ if (request.body.instructions) {
283+ requestBody.instructions = request.body.instructions;
284+ }
285+
286+ console.debug('OpenAI TTS request', requestBody);
287+
274288 const result = await fetch('https://api.openai.com/v1/audio/speech', {
275289 method: 'POST',
276290 headers: {
277291 'Content-Type': 'application/json',
278292 Authorization: `Bearer ${key}`,
279293 },
280294 body: JSON.stringify({requestBody),
281- input: request.body.text,
282- response_format: 'mp3',
283- voice: request.body.voice ?? 'alloy',
284- speed: request.body.speed ?? 1,
285- model: request.body.model ?? 'tts-1',
286- }),
287295 });
288296
289297 if (!result.ok) {
src/endpoints/secrets.js+5 -1
@@ -41,7 +41,6 @@ export const SECRET_KEYS = {
4141 GROQ: 'api_key_groq',
4242 AZURE_TTS: 'api_key_azure_tts',
4343 FEATHERLESS: 'api_key_featherless',
44- ZEROONEAI: 'api_key_01ai',
4544 HUGGINGFACE: 'api_key_huggingface',
4645 STABILITY: 'api_key_stability',
4746 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
@@ -54,7 +53,12 @@ export const SECRET_KEYS = {
5453 SERPER: 'api_key_serper',
5554 AIMLAPI: 'api_key_aimlapi',
5655 XAI: 'api_key_xai',
56+ FIREWORKS: 'api_key_fireworks',
5757 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
58+ MINIMAX: 'api_key_minimax',
59+ MINIMAX_GROUP_ID: 'minimax_group_id',
60+ MOONSHOT: 'api_key_moonshot',
61+ COMETAPI: 'api_key_cometapi',
5862};
5963
6064/**
src/endpoints/settings.js+5 -0
@@ -131,6 +131,11 @@ async function backupSettings() {
131131 */
132132function backupUserSettings(handle, preventDuplicates) {
133133 const userDirectories = getUserDirectories(handle);
134+
135+ if (!fs.existsSync(userDirectories.root)) {
136+ return;
137+ }
138+
134139 const backupFile = path.join(userDirectories.backups, `${getSettingsBackupFilePrefix(handle)}${generateTimestamp()}.json`);
135140 const sourceFile = path.join(userDirectories.root, SETTINGS_FILE);
136141
src/endpoints/tokenizers.js+35 -10
@@ -1,6 +1,8 @@
11import fs from 'node:fs';
22import path from 'node:path';
33import { Buffer } from 'node:buffer';
4+import zlib from 'node:zlib';
5+import { promisify } from 'node:util';
46
57import express from 'express';
68import fetch from 'node-fetch';
@@ -57,11 +59,12 @@ export const TEXT_COMPLETION_MODELS = [
5759
5860const CHARS_PER_TOKEN = 3.35;
5961const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');
62+const gunzip = promisify(zlib.gunzip);
6063
6164/**
6265 * Gets a path to the tokenizer model. Downloads the model if it's a URL.
6366 * @param {string} model Model URL or path
6467 * @param {string|undefined} fallbackModel Fallback model path\
6568 * @returns {Promise<string>} Path to the tokenizer model
6669 */
6770async function getPathToTokenizer(model, fallbackModel) {
@@ -87,8 +90,24 @@ async function getPathToTokenizer(model, fallbackModel) {
8790 fs.mkdirSync(CACHE_PATH, { recursive: true });
8891 }
8992
93+ // If an uncompressed version exists, return it
94+ const isCompressed = path.extname(fileName) === '.gz';
95+ const uncompressedName = path.basename(fileName, '.gz');
96+ const uncompressedPath = path.join(CACHE_PATH, uncompressedName);
97+ if (isCompressed && fs.existsSync(uncompressedPath)) {
98+ return uncompressedPath;
99+ }
100+
90101 const cachedFile = path.join(CACHE_PATH, fileName);
91102 if (fs.existsSync(cachedFile)) {
103+ // If the file was downloaded manually
104+ if (isCompressed) {
105+ const compressedBuffer = await fs.promises.readFile(cachedFile);
106+ const decompressedBuffer = await gunzip(compressedBuffer);
107+ writeFileAtomicSync(uncompressedPath, decompressedBuffer);
108+ await fs.promises.unlink(cachedFile);
109+ return uncompressedPath;
110+ }
92111 return cachedFile;
93112 }
94113
@@ -103,6 +122,12 @@ async function getPathToTokenizer(model, fallbackModel) {
103122 }
104123
105124 const arrayBuffer = await response.arrayBuffer();
125+ if (isCompressed) {
126+ const decompressedBuffer = await gunzip(arrayBuffer);
127+ writeFileAtomicSync(uncompressedPath, decompressedBuffer);
128+ return uncompressedPath;
129+ }
130+
106131 writeFileAtomicSync(cachedFile, Buffer.from(arrayBuffer));
107132 return cachedFile;
108133 } catch (error) {
@@ -203,8 +228,8 @@ class WebTokenizer {
203228
204229 try {
205230 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);
206231 const arrayBufferfileBuffer = await fs.readFileSyncpromises.readFile(pathToModel).buffer;
207232 this.#instance = await Tokenizer.fromJSON(arrayBufferfileBuffer);
208233 console.info('Instantiated the tokenizer for', path.parse(pathToModel).name);
209234 return this.#instance;
210235 } catch (error) {
@@ -223,11 +248,11 @@ const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model');
223248const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model');
224249const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
225250const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');
226251const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json.gz', 'src/tokenizers/llama3.json');
227252const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json.gz', 'src/tokenizers/llama3.json');
228253const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json.gz', 'src/tokenizers/llama3.json');
229254const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json.gz', 'src/tokenizers/llama3.json');
230255const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json.gz', 'src/tokenizers/llama3.json');
231256
232257export const sentencepieceTokenizers = [
233258 'llama',
@@ -408,7 +433,7 @@ export function getTokenizerModel(requestModel) {
408433 return 'o1';
409434 }
410435
411436 if (requestModel.includes('gpt-5') || requestModel.includes('o3') || requestModel.includes('o4-mini')) {
412437 return 'o1';
413438 }
414439
@@ -1092,7 +1117,7 @@ router.post('/remote/textgenerationwebui/encode', async function (request, respo
10921117
10931118 /** @type {any} */
10941119 const data = await result.json();
10951120 const count = (data?.length ?? data?.count ?? data?.value ?? data?.tokens?.length);
10961121 const ids = (data?.tokens ?? data?.ids ?? []);
10971122
10981123 return response.send({ count, ids });
src/prompt-converters.js+3 -2
@@ -53,15 +53,16 @@ export function getPromptNames(request) {
5353 * Adds an assistant prefix to the last message.
5454 * @param {any[]} prompt Prompt messages array
5555 * @param {any[]} tools Array of tool definitions
56+ * @param {string} property The property to set the prefix on
5657 * @returns {any[]} Transformed messages array
5758 */
5859export function addAssistantPrefix(prompt, tools, property) {
5960 if (!prompt.length) {
6061 return prompt;
6162 }
6263 const hasAnyTools = (Array.isArray(tools) && tools.length > 0) || prompt.some(x => x.role === 'tool');
6364 if (!hasAnyTools && prompt[prompt.length - 1].role === 'assistant') {
6465 prompt[prompt.length - 1].prefix[property] = true;
6566 }
6667 return prompt;
6768}
src/server-startup.js+2 -0
@@ -45,6 +45,7 @@ import { router as koboldRouter } from './endpoints/backends/kobold.js';
4545import { router as textCompletionsRouter } from './endpoints/backends/text-completions.js';
4646import { router as speechRouter } from './endpoints/speech.js';
4747import { router as azureRouter } from './endpoints/azure.js';
48+import { router as minimaxRouter } from './endpoints/minimax.js';
4849import { router as dataMaidRouter } from './endpoints/data-maid.js';
4950
5051/**
@@ -172,6 +173,7 @@ export function setupPrivateEndpoints(app) {
172173 app.use('/api/backends/chat-completions', chatCompletionsRouter);
173174 app.use('/api/speech', speechRouter);
174175 app.use('/api/azure', azureRouter);
176+ app.use('/api/minimax', minimaxRouter);
175177 app.use('/api/data-maid', dataMaidRouter);
176178}
177179