Refactor API keys handling. Remove ST hosting from colab

This commit is contained in:
SillyLossy
2023-05-11 21:08:22 +03:00
parent 9b80c861f0
commit e374703798
15 changed files with 365 additions and 514 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ public/settings.json
/thumbnails /thumbnails
whitelist.txt whitelist.txt
.vscode .vscode
secrets.json

View File

@ -6,68 +6,8 @@
"metadata": {}, "metadata": {},
"source": [ "source": [
"**Links**<br>\n", "**Links**<br>\n",
"SillyTavern GitHub: https://github.com/Cohee1207/SillyTavern<br>\n",
"Extensions API GitHub: https://github.com/Cohee1207/SillyTavern-extras/<br>\n", "Extensions API GitHub: https://github.com/Cohee1207/SillyTavern-extras/<br>\n",
"SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj<br>\n", "SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj"
"Contact the maintainer directly: Cohee#1207"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"id": "_1gpebrnlp5-"
},
"outputs": [],
"source": [
"#@title <b><-- Convert TavernAI characters to SillyTavern format</b>\n",
"\n",
"!mkdir /convert\n",
"%cd /convert\n",
"\n",
"import os\n",
"from google.colab import drive\n",
"\n",
"drive.mount(\"/convert/drive\")\n",
"\n",
"!git clone -b tools https://github.com/EnergoStalin/SillyTavern.git\n",
"%cd SillyTavern\n",
"\n",
"!curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash\n",
"!nvm install 19.1.0\n",
"!nvm use 19.1.0\n",
"\n",
"%cd tools/charaverter\n",
"\n",
"!npm i\n",
"\n",
"path = \"/convert/drive/MyDrive/TavernAI/characters\"\n",
"output = \"/convert/drive/MyDrive/SillyTavern/characters\"\n",
"if not os.path.exists(path):\n",
" path = output\n",
"\n",
"!mkdir -p $output\n",
"!node main.mjs $path $output\n",
"\n",
"drive.flush_and_unmount()\n",
"\n",
"%cd /\n",
"!rm -rf /convert"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "ewkXkyiFP2Hq"
},
"outputs": [],
"source": [
"#@title <-- Tap this if you play on Mobile { display-mode: \"form\" }\n",
"%%html\n",
"<b>Press play on the music player to keep the tab alive, then start KoboldAI below (Uses only 13MB of data)</b><br/>\n",
"<audio src=\"https://raw.githubusercontent.com/KoboldAI/KoboldAI-Client/main/colab/silence.m4a\" controls>"
] ]
}, },
{ {
@ -79,16 +19,6 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"#@title <b><-- Select your model below and then click this to start KoboldAI</b>\n",
"\n",
"Model = \"Руgmаlіоn 6В\" #@param [\"Nerys V2 6B\", \"Erebus 6B\", \"Skein 6B\", \"Janeway 6B\", \"Adventure 6B\", \"Руgmаlіоn 6В\", \"Руgmаlіоn 6В Dev\", \"Lit V2 6B\", \"Lit 6B\", \"Shinen 6B\", \"Nerys 2.7B\", \"AID 2.7B\", \"Erebus 2.7B\", \"Janeway 2.7B\", \"Picard 2.7B\", \"Horni LN 2.7B\", \"Horni 2.7B\", \"Shinen 2.7B\", \"OPT 2.7B\", \"Fairseq Dense 2.7B\", \"Neo 2.7B\", \"Руgwау 6B\", \"Nerybus 6.7B\", \"Руgwау v8p4\", \"PPO-Janeway 6B\", \"PPO Shуgmаlіоn 6B\", \"LLaMA 7B\", \"Janin-GPTJ\", \"Javelin-GPTJ\", \"Javelin-R\", \"Janin-R\", \"Javalion-R\", \"Javalion-GPTJ\", \"Javelion-6B\", \"GPT-J-Руg-PPO-6B\", \"ppo_hh_pythia-6B\", \"ppo_hh_gpt-j\", \"GPT-J-Руg_PPO-6B\", \"GPT-J-Руg_PPO-6B-Dev-V8p4\", \"Dolly_GPT-J-6b\", \"Dolly_Руg-6B\"] {allow-input: true}\n",
"Version = \"Official\" #@param [\"Official\", \"United\"] {allow-input: true}\n",
"Provider = \"Localtunnel\" #@param [\"Localtunnel\"]\n",
"ForceInitSteps = [] #@param {allow-input: true}\n",
"UseGoogleDrive = True #@param {type:\"boolean\"}\n",
"StartKoboldAI = True #@param {type:\"boolean\"}\n",
"ModelsFromDrive = False #@param {type:\"boolean\"}\n",
"UseExtrasExtensions = True #@param {type:\"boolean\"}\n",
"#@markdown Enables hosting of extensions backend for SillyTavern Extras\n", "#@markdown Enables hosting of extensions backend for SillyTavern Extras\n",
"extras_enable_captioning = True #@param {type:\"boolean\"}\n", "extras_enable_captioning = True #@param {type:\"boolean\"}\n",
"#@markdown Loads the image captioning module\n", "#@markdown Loads the image captioning module\n",
@ -102,150 +32,20 @@
"#@markdown * joeddav/distilbert-base-uncased-go-emotions-student = 28 supported emotions\n", "#@markdown * joeddav/distilbert-base-uncased-go-emotions-student = 28 supported emotions\n",
"extras_enable_memory = True #@param {type:\"boolean\"}\n", "extras_enable_memory = True #@param {type:\"boolean\"}\n",
"#@markdown Loads the story summarization module\n", "#@markdown Loads the story summarization module\n",
"Memory_Model = \"Qiliang/bart-large-cnn-samsum-ChatGPT_v3\" #@param [ \"Qiliang/bart-large-cnn-samsum-ChatGPT_v3\", \"Qiliang/bart-large-cnn-samsum-ElectrifAi_v10\", \"distilbart-xsum-12-3\" ]\n", "Memory_Model = \"slauw87/bart_summarisation\" #@param [ \"slauw87/bart_summarisation\", \"Qiliang/bart-large-cnn-samsum-ChatGPT_v3\", \"Qiliang/bart-large-cnn-samsum-ElectrifAi_v10\", \"distilbart-xsum-12-3\" ]\n",
"#@markdown * slauw87/bart_summarisation - general purpose summarization model\n",
"#@markdown * Qiliang/bart-large-cnn-samsum-ChatGPT_v3 - summarization model optimized for chats\n", "#@markdown * Qiliang/bart-large-cnn-samsum-ChatGPT_v3 - summarization model optimized for chats\n",
"#@markdown * Qiliang/bart-large-cnn-samsum-ElectrifAi_v10 - nice results so far, but still being evaluated\n", "#@markdown * Qiliang/bart-large-cnn-samsum-ElectrifAi_v10 - nice results so far, but still being evaluated\n",
"#@markdown * distilbart-xsum-12-3 - faster, but pretty basic alternative\n", "#@markdown * distilbart-xsum-12-3 - faster, but pretty basic alternative\n",
"\n", "\n",
"\n", "import subprocess\n",
"%cd /content\n",
"\n",
"!cat .ii\n",
"!nvidia-smi\n",
"\n",
"import os, subprocess, time, pathlib, json, base64, sys\n",
"\n",
"# ---\n",
"# Utils\n",
"class IncrementialInstall:\n",
" def __init__(self, root = \"/\", tasks = [], force = []):\n",
" self.tasks = tasks\n",
" self.path = os.path.join(root, \".ii\")\n",
" self.completed = list(filter(lambda x: not x in force, self.__completed()))\n",
"\n",
" def __completed(self):\n",
" try:\n",
" with open(self.path) as f:\n",
" return json.load(f)\n",
" except:\n",
" return []\n",
"\n",
" def addTask(self, name, func):\n",
" self.tasks.append({\"name\": name, \"func\": func})\n",
"\n",
" def run(self):\n",
" todo = list(filter(lambda x: not x[\"name\"] in self.completed, self.tasks))\n",
" try:\n",
" for task in todo:\n",
" task[\"func\"]()\n",
" self.completed.append(task[\"name\"])\n",
" finally:\n",
" with open(self.path, \"w\") as f:\n",
" json.dump(self.completed, f)\n",
"\n",
"def create_paths(paths):\n",
" for directory in paths:\n",
" if not os.path.exists(directory):\n",
" os.makedirs(directory)\n",
"\n",
"def link(srcDir, destDir, files):\n",
" '''\n",
" Link source to dest copying dest to source if not present first\n",
" '''\n",
" for file in files:\n",
" source = os.path.join(srcDir, file)\n",
" dest = os.path.join(destDir, file)\n",
" if not os.path.exists(source):\n",
" !cp -r \"$dest\" \"$source\"\n",
" !rm -rf \"$dest\"\n",
" !ln -fs \"$source\" \"$dest\"\n",
"\n",
"from google.colab import drive\n",
"if UseGoogleDrive:\n",
" drive.mount(\"/content/drive/\")\n",
"else:\n",
" create_paths([\n",
" \"/content/drive/MyDrive\"\n",
" ])\n",
"\n",
"ii = IncrementialInstall(force=ForceInitSteps)\n",
"\n",
"# ---\n",
"# SillyTavern py modules\n",
"def cloneTavern():\n",
" %cd /\n",
" !git clone https://github.com/Cohee1207/SillyTavern\n",
" %cd -\n",
" !cp /SillyTavern/colab/*.py ./\n",
"ii.addTask(\"Clone SillyTavern\", cloneTavern)\n",
"ii.run()\n",
"\n",
"from models import GetModels, ModelData\n",
"model = GetModels(Version).get(Model, ModelData(Model, Version))\n",
"\n",
"# ---\n",
"# KoboldAI\n",
"if StartKoboldAI:\n",
" def downloadKobold():\n",
" !wget https://koboldai.org/ckds && chmod +x ckds\n",
" def initKobold():\n",
" !./ckds --init only\n",
"\n",
" ii.addTask(\"Download KoboldAI\", downloadKobold)\n",
" ii.addTask(\"Init KoboldAI\", initKobold)\n",
" \n",
" ii.run()\n",
"\n",
"kargs = [\"/content/ckds\"]\n",
"if not ModelsFromDrive:\n",
" kargs += [\"-x\", \"colab\", \"-l\", \"colab\"]\n",
"if Provider == \"Localtunnel\":\n",
" kargs += [\"--localtunnel\", \"yes\"]\n",
"\n",
"kargs += model.args()\n",
"\n",
"url = \"\"\n",
"print(kargs)\n",
"\n",
"if StartKoboldAI:\n",
" p = subprocess.Popen(kargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
"\n",
" prefix = \"KoboldAI has finished loading and is available at the following link\"\n",
" urlprefix = f\"{prefix}: \"\n",
" ui1prefix = f\"{prefix} for UI 1: \"\n",
" while True:\n",
" line = p.stdout.readline().decode().strip()\n",
" print(line)\n",
" if urlprefix in line:\n",
" url = line.split(urlprefix)[1]\n",
" break\n",
" elif ui1prefix in line:\n",
" url = line.split(ui1prefix)[1]\n",
" break\n",
" elif not line:\n",
" break\n",
" if \"INIT\" in line and \"Transformers\" in line:\n",
" print(\"Model loading... (It will take 2 - 5 minutes)\")\n",
"\n",
"print(url)\n",
"\n",
"\n",
"# ---\n",
"# nodejs\n",
"%cd /\n",
"def installNode():\n",
" !npm install -g n\n",
" !n 19\n",
" !node --version\n",
"ii.addTask(\"Install node\", installNode)\n",
"\n",
"\n", "\n",
"# ---\n", "# ---\n",
"# SillyTavern extras\n", "# SillyTavern extras\n",
"import globals\n", "extras_url = '(disabled)'\n",
"globals.extras_url = '(disabled)'\n", "params = []\n",
"globals.params = []\n", "params.append('--cpu')\n",
"globals.params.append('--cpu')\n", "params.append('--share')\n",
"ExtrasModules = []\n", "ExtrasModules = []\n",
"\n", "\n",
"if (extras_enable_captioning):\n", "if (extras_enable_captioning):\n",
@ -255,73 +55,28 @@
"if (extras_enable_emotions):\n", "if (extras_enable_emotions):\n",
" ExtrasModules.append('classify')\n", " ExtrasModules.append('classify')\n",
"\n", "\n",
"globals.params.append(f'--classification-model={Emotions_Model}')\n", "params.append(f'--classification-model={Emotions_Model}')\n",
"globals.params.append(f'--summarization-model={Memory_Model}')\n", "params.append(f'--summarization-model={Memory_Model}')\n",
"globals.params.append(f'--captioning-model={Captions_Model}')\n", "params.append(f'--captioning-model={Captions_Model}')\n",
"globals.params.append(f'--enable-modules={\",\".join(ExtrasModules)}')\n", "params.append(f'--enable-modules={\",\".join(ExtrasModules)}')\n",
"\n", "\n",
"\n", "\n",
"if UseExtrasExtensions:\n",
" def cloneExtras():\n",
"%cd /\n", "%cd /\n",
"!git clone https://github.com/Cohee1207/SillyTavern-extras\n", "!git clone https://github.com/Cohee1207/SillyTavern-extras\n",
" ii.addTask('clone extras', cloneExtras)\n",
"\n",
" def installRequirements():\n",
"%cd /SillyTavern-extras\n", "%cd /SillyTavern-extras\n",
"!npm install -g localtunnel\n", "!npm install -g localtunnel\n",
"!pip install -r requirements.txt\n", "!pip install -r requirements.txt\n",
"!pip install tensorflow==2.11\n", "!pip install tensorflow==2.11\n",
" ii.addTask('install requirements', installRequirements)\n",
"\n", "\n",
" from extras_server import runServer, extractUrl\n",
" ii.addTask('run server', runServer)\n",
" ii.addTask('extract extras URL', extractUrl)\n",
"\n", "\n",
"%cd /SillyTavern\n", "cmd = f\"python server.py {' '.join(params)}\"\n",
"\n", "print(cmd)\n",
"if UseGoogleDrive:\n", "extras_process = subprocess.Popen(\n",
" %env googledrive=2\n", " cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd='/SillyTavern-extras', shell=True)\n",
"\n", "print('processId:', extras_process.pid)\n",
" def setupTavernPaths():\n", "while True:\n",
" %cd /SillyTavern\n", " line = extras_process.stdout.readline().decode().strip()\n",
" tdrive = \"/content/drive/MyDrive/SillyTavern\"\n", " print(line)\n"
" create_paths([\n",
" tdrive,\n",
" os.path.join(\"public\", \"groups\"),\n",
" os.path.join(\"public\", \"group chats\")\n",
" ])\n",
" link(tdrive, \"public\", [\n",
" \"settings.json\",\n",
" \"backgrounds\",\n",
" \"characters\",\n",
" \"chats\",\n",
" \"User Avatars\",\n",
" \"worlds\",\n",
" \"group chats\",\n",
" \"groups\",\n",
" ])\n",
" ii.addTask(\"Setup Tavern Paths\", setupTavernPaths)\n",
"\n",
"def installTavernDependencies():\n",
" %cd /SillyTavern\n",
" !npm install\n",
" !npm install -g localtunnel\n",
" !npm install -g forever\n",
" !pip install flask-cloudflared==0.0.10\n",
"ii.addTask(\"Install Tavern Dependencies\", installTavernDependencies)\n",
"ii.run()\n",
"\n",
"%env colaburl=$url\n",
"%env SILLY_TAVERN_PORT=5001\n",
"!sed -i 's/listen = true/listen = false/g' config.conf\n",
"!touch stdout.log stderr.log\n",
"!forever start -o stdout.log -e stderr.log server.js\n",
"print(\"KoboldAI LINK:\", url, '###Extensions API LINK###', globals.extras_url, \"###SillyTavern LINK###\", sep=\"\\n\")\n",
"from flask_cloudflared import _run_cloudflared\n",
"cloudflare = _run_cloudflared(5001)\n",
"print(cloudflare)\n",
"!tail -f stdout.log stderr.log"
] ]
} }
], ],

View File

@ -1,40 +0,0 @@
import os
import time
import subprocess
import globals
def runServer():
cmd = f"python server.py {' '.join(globals.params)}"
print(cmd)
extras_process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd='/SillyTavern-extras', shell=True)
print('processId:', extras_process.pid)
while True:
line = extras_process.stdout.readline().decode().strip()
if "Running on " in line:
break
if not line:
print('breaking on line')
break
print(line)
def extractUrl():
subprocess.call(
'nohup lt --port 5100 > ./extras.out 2> ./extras.err &', shell=True)
print('Waiting for lt init...')
time.sleep(5)
while True:
if (os.path.getsize('./extras.out') > 0):
with open('./extras.out', 'r') as f:
lines = f.readlines()
for x in range(len(lines)):
if ('your url is: ' in lines[x]):
print('SillyTavern Extensions URL:')
globals.extras_url = lines[x].split('your url is: ')[1]
print(globals.extras_url)
break
if (os.path.getsize('./extras.err') > 0):
with open('./extras.err', 'r') as f:
print(f.readlines())
break

View File

@ -1,2 +0,0 @@
extras_url = '(disabled)'
params = []

View File

@ -1,77 +0,0 @@
class ModelData:
def __init__(self, name, version = "", revision="", path="", download=""):
self.name = name
self.version = version
self.revision = revision
self.path = path
self.download = download
def __str__(self):
return self.args().__str__()
def args(self):
args = ["-m", self.name]
if (self.version):
args += ["-g", self.version]
if (self.revision):
args += ["-r", self.revision]
return args
class ModelFactory:
def __init__(self, **kwargs):
self.kwargs = kwargs
def NewModelData(self, name, **kwargs):
cpy = self.kwargs.copy()
cpy.update(kwargs)
return ModelData(name = name, **cpy)
def GetModels(Version):
mf = ModelFactory(version=Version)
return {
"Nerys V2 6B": mf.NewModelData("KoboldAI/OPT-6B-nerys-v2"),
"Erebus 6B": mf.NewModelData("KoboldAI/OPT-6.7B-Erebus"),
"Skein 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Skein"),
"Janeway 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Janeway"),
"Adventure 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Adventure"),
"Руgmаlіоn 6В": mf.NewModelData("PygmalionAI/pygmalion-6b"),
"Руgmаlіоn 6В Dev": mf.NewModelData("PygmalionAI/pygmalion-6b", revision="dev"),
"Lit V2 6B": mf.NewModelData("hakurei/litv2-6B-rev3"),
"Lit 6B": mf.NewModelData("hakurei/lit-6B"),
"Shinen 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Shinen"),
"Nerys 2.7B": mf.NewModelData("KoboldAI/fairseq-dense-2.7B-Nerys"),
"Erebus 2.7B": mf.NewModelData("KoboldAI/OPT-2.7B-Erebus"),
"Janeway 2.7B": mf.NewModelData("KoboldAI/GPT-Neo-2.7B-Janeway"),
"Picard 2.7B": mf.NewModelData("KoboldAI/GPT-Neo-2.7B-Picard"),
"AID 2.7B": mf.NewModelData("KoboldAI/GPT-Neo-2.7B-AID"),
"Horni LN 2.7B": mf.NewModelData("KoboldAI/GPT-Neo-2.7B-Horni-LN"),
"Horni 2.7B": mf.NewModelData("KoboldAI/GPT-Neo-2.7B-Horni"),
"Shinen 2.7B": mf.NewModelData("KoboldAI/GPT-Neo-2.7B-Shinen"),
"Fairseq Dense 2.7B": mf.NewModelData("KoboldAI/fairseq-dense-2.7B"),
"OPT 2.7B": mf.NewModelData("facebook/opt-2.7b"),
"Neo 2.7B": mf.NewModelData("EleutherAI/gpt-neo-2.7B"),
"Руgwау 6B": mf.NewModelData("TehVenom/PPO_Pygway-6b"),
"Nerybus 6.7B": mf.NewModelData("KoboldAI/OPT-6.7B-Nerybus-Mix"),
"Руgwау v8p4": mf.NewModelData("TehVenom/PPO_Pygway-V8p4_Dev-6b"),
"PPO-Janeway 6B": mf.NewModelData("TehVenom/PPO_Janeway-6b"),
"PPO Shуgmаlіоn 6B": mf.NewModelData("TehVenom/PPO_Shygmalion-6b"),
"LLaMA 7B": mf.NewModelData("decapoda-research/llama-7b-hf"),
"Janin-GPTJ": mf.NewModelData("digitous/Janin-GPTJ"),
"Javelin-GPTJ": mf.NewModelData("digitous/Javelin-GPTJ"),
"Javelin-R": mf.NewModelData("digitous/Javelin-R"),
"Janin-R": mf.NewModelData("digitous/Janin-R"),
"Javalion-R": mf.NewModelData("digitous/Javalion-R"),
"Javalion-GPTJ": mf.NewModelData("digitous/Javalion-GPTJ"),
"Javelion-6B": mf.NewModelData("Cohee/Javelion-6b"),
"GPT-J-Руg-PPO-6B": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B"),
"ppo_hh_pythia-6B": mf.NewModelData("reciprocate/ppo_hh_pythia-6B"),
"ppo_hh_gpt-j": mf.NewModelData("reciprocate/ppo_hh_gpt-j"),
"Alpaca-7B": mf.NewModelData("chainyo/alpaca-lora-7b"),
"LLaMA 4-bit": mf.NewModelData("decapoda-research/llama-13b-hf-int4"),
"GPT-J-Руg_PPO-6B": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B"),
"GPT-J-Руg_PPO-6B-Dev-V8p4": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B-Dev-V8p4"),
"Dolly_GPT-J-6b": mf.NewModelData("TehVenom/Dolly_GPT-J-6b"),
"Dolly_Руg-6B": mf.NewModelData("TehVenom/AvgMerge_Dolly-Pygmalion-6b")
}

View File

@ -59,6 +59,7 @@
<script type="module" src="scripts/RossAscends-mods.js"></script> <script type="module" src="scripts/RossAscends-mods.js"></script>
<script type="module" src="scripts/slash-commands.js"></script> <script type="module" src="scripts/slash-commands.js"></script>
<script type="module" src="scripts/tags.js"></script> <script type="module" src="scripts/tags.js"></script>
<script type="module" src="scripts/secrets.js"></script>
<script type="text/javascript" src="scripts/toolcool-color-picker.js"></script> <script type="text/javascript" src="scripts/toolcool-color-picker.js"></script>
<title>SillyTavern</title> <title>SillyTavern</title>
@ -1006,7 +1007,8 @@
<h4>API key</h4> <h4>API key</h4>
<h5>Get it here: <a target="_blank" href="https://horde.koboldai.net/register">Register</a> <h5>Get it here: <a target="_blank" href="https://horde.koboldai.net/register">Register</a>
</h5> </h5>
<input id="horde_api_key" name="horde_api_key" class="text_pole" maxlength="500" value="0000000000" autocomplete="off"> <input id="horde_api_key" name="horde_api_key" class="text_pole" maxlength="500" type="text" placeholder="0000000000">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<h4 class="horde_model_title"> <h4 class="horde_model_title">
Model Model
<div id="horde_refresh" title="Refresh models" class="right_menu_button"> <div id="horde_refresh" title="Refresh models" class="right_menu_button">
@ -1036,7 +1038,8 @@
<li>Enter it in the box below:</li> <li>Enter it in the box below:</li>
</ol> </ol>
</span> </span>
<input id="api_key_novel" name="api_key_novel" class="text_pole" maxlength="500" size="35" value="" autocomplete="off"> <input id="api_key_novel" name="api_key_novel" class="text_pole" maxlength="500" size="35" type="text">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<input id="api_button_novel" class="menu_button" type="submit" value="Connect"> <input id="api_button_novel" class="menu_button" type="submit" value="Connect">
<div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div> <div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
<h4>Novel AI Model <h4>Novel AI Model
@ -1089,7 +1092,8 @@
<li>Enter it in the box below:</li> <li>Enter it in the box below:</li>
</ol> </ol>
</span> </span>
<input id="api_key_openai" name="api_key_openai" class="text_pole" maxlength="500" value="" autocomplete="off"> <input id="api_key_openai" name="api_key_openai" class="text_pole" maxlength="500" value="" type="text">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<input id="api_button_openai" class="menu_button" type="submit" value="Connect"> <input id="api_button_openai" class="menu_button" type="submit" value="Connect">
<div id="api_loading_openai" class=" api-load-icon fa-solid fa-hourglass fa-spin"></div> <div id="api_loading_openai" class=" api-load-icon fa-solid fa-hourglass fa-spin"></div>
</form> </form>
@ -1125,8 +1129,8 @@
</ol> </ol>
</span> </span>
<div class="widthFreeExpand"> <div class="widthFreeExpand">
<input id="poe_token" class="text_pole" type="text" placeholder="Example: nTLG2bNvbOi8qxc-DbaSlw%3D%3D" maxlength="100" /> <input id="poe_token" class="text_pole" type="text" maxlength="100" />
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
</div> </div>
<input id="poe_connect" class="menu_button" type="button" value="Connect" /> <input id="poe_connect" class="menu_button" type="button" value="Connect" />

View File

@ -119,6 +119,12 @@ import {
createTagMapFromList, createTagMapFromList,
renameTagKey, renameTagKey,
} from "./scripts/tags.js"; } from "./scripts/tags.js";
import {
SECRET_KEYS,
readSecretState,
secret_state,
writeSecret
} from "./scripts/secrets.js";
//exporting functions and vars for mods //exporting functions and vars for mods
export { export {
@ -543,14 +549,15 @@ $.ajaxPrefilter((options, originalOptions, xhr) => {
}); });
///// initialization protocol //////// ///// initialization protocol ////////
$.get("/csrf-token").then((data) => { $.get("/csrf-token").then(async (data) => {
token = data.token; token = data.token;
getClientVersion(); await readSecretState();
getCharacters(); await getClientVersion();
getSettings("def"); await getSettings("def");
await getCharacters();
await getBackgrounds();
await getUserAvatars();
sendSystemMessage(system_message_types.WELCOME); sendSystemMessage(system_message_types.WELCOME);
getBackgrounds();
getUserAvatars();
}); });
function checkOnlineStatus() { function checkOnlineStatus() {
@ -3490,7 +3497,7 @@ async function displayPastChats() {
//************************************************************ //************************************************************
async function getStatusNovel() { async function getStatusNovel() {
if (is_get_status_novel) { if (is_get_status_novel) {
const data = { key: nai_settings.api_key_novel }; const data = {};
jQuery.ajax({ jQuery.ajax({
type: "POST", // type: "POST", //
@ -5542,17 +5549,25 @@ $(document).ready(function () {
}); });
//Select chat //Select chat
$("#api_button_novel").click(function (e) { $("#api_button_novel").on('click', async function (e) {
e.stopPropagation(); e.stopPropagation();
if ($("#api_key_novel").val() != "") { const api_key_novel = $("#api_key_novel").val().trim();
if (api_key_novel.length) {
await writeSecret(SECRET_KEYS.NOVEL, api_key_novel);
}
if (!secret_state[SECRET_KEYS.NOVEL]) {
console.log('No secret key saved for NovelAI');
return;
}
$("#api_loading_novel").css("display", "inline-block"); $("#api_loading_novel").css("display", "inline-block");
$("#api_button_novel").css("display", "none"); $("#api_button_novel").css("display", "none");
nai_settings.api_key_novel = $.trim($("#api_key_novel").val());
saveSettingsDebounced();
is_get_status_novel = true; is_get_status_novel = true;
is_api_button_press_novel = true; is_api_button_press_novel = true;
}
}); });
$("#anchor_order").change(function () { $("#anchor_order").change(function () {
anchor_order = parseInt($("#anchor_order").find(":selected").val()); anchor_order = parseInt($("#anchor_order").find(":selected").val());
saveSettingsDebounced(); saveSettingsDebounced();

View File

@ -7,21 +7,14 @@ import {
online_status, online_status,
main_api, main_api,
api_server, api_server,
nai_settings,
api_server_textgenerationwebui, api_server_textgenerationwebui,
is_send_press, is_send_press,
getTokenCount, getTokenCount,
menu_type, menu_type,
selectRightMenuWithAnimation,
select_selected_character,
setCharacterId,
} from "../script.js"; } from "../script.js";
import {
select_group_chats,
} from "./group-chats.js";
import { import {
power_user, power_user,
@ -30,8 +23,10 @@ import {
import { LoadLocal, SaveLocal, ClearLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js"; import { LoadLocal, SaveLocal, ClearLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js";
import { selected_group, is_group_generating, getGroupAvatar, groups } from "./group-chats.js"; import { selected_group, is_group_generating, getGroupAvatar, groups } from "./group-chats.js";
import { oai_settings } from "./openai.js"; import {
import { poe_settings } from "./poe.js"; SECRET_KEYS,
secret_state,
} from "./secrets.js";
var NavToggle = document.getElementById("nav-toggle"); var NavToggle = document.getElementById("nav-toggle");
var RPanelPin = document.getElementById("rm_button_panel_pin"); var RPanelPin = document.getElementById("rm_button_panel_pin");
@ -368,13 +363,11 @@ function RA_autoconnect(PrevApi) {
case 'kobold': case 'kobold':
if (api_server && isUrlOrAPIKey(api_server)) { if (api_server && isUrlOrAPIKey(api_server)) {
$("#api_button").click(); $("#api_button").click();
} }
break; break;
case 'novel': case 'novel':
if (nai_settings.api_key_novel) { if (secret_state[SECRET_KEYS.NOVEL]) {
$("#api_button_novel").click(); $("#api_button_novel").click();
} }
break; break;
case 'textgenerationwebui': case 'textgenerationwebui':
@ -383,12 +376,12 @@ function RA_autoconnect(PrevApi) {
} }
break; break;
case 'openai': case 'openai':
if (oai_settings.api_key_openai) { if (secret_state[SECRET_KEYS.OPENAI]) {
$("#api_button_openai").click(); $("#api_button_openai").click();
} }
break; break;
case 'poe': case 'poe':
if (poe_settings.token) { if (secret_state[SECRET_KEYS.POE]) {
$("#poe_connect").click(); $("#poe_connect").click();
} }
break; break;

View File

@ -1,4 +1,4 @@
import { saveSettingsDebounced, changeMainAPI, callPopup, setGenerationProgress, CLIENT_VERSION } from "../script.js"; import { saveSettingsDebounced, changeMainAPI, callPopup, setGenerationProgress, CLIENT_VERSION, getRequestHeaders } from "../script.js";
import { delay } from "./utils.js"; import { delay } from "./utils.js";
export { export {
@ -14,7 +14,6 @@ export {
let models = []; let models = [];
let horde_settings = { let horde_settings = {
api_key: '0000000000',
models: [], models: [],
use_horde: false, use_horde: false,
auto_adjust_response_length: true, auto_adjust_response_length: true,
@ -30,14 +29,6 @@ const getRequestArgs = () => ({
"Client-Agent": CLIENT_VERSION, "Client-Agent": CLIENT_VERSION,
} }
}); });
const postRequestArgs = () => ({
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": horde_settings.api_key,
"Client-Agent": CLIENT_VERSION,
}
});
async function getWorkers() { async function getWorkers() {
const response = await fetch('https://horde.koboldai.net/api/v2/workers?type=text', getRequestArgs()); const response = await fetch('https://horde.koboldai.net/api/v2/workers?type=text', getRequestArgs());
@ -107,8 +98,12 @@ async function generateHorde(prompt, params) {
"models": horde_settings.models, "models": horde_settings.models,
}; };
const response = await fetch("https://horde.koboldai.net/api/v2/generate/text/async", { const response = await fetch("/generate_horde", {
...postRequestArgs(), method: 'POST',
headers: {
...getRequestHeaders(),
"Client-Agent": CLIENT_VERSION,
},
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
@ -176,12 +171,6 @@ async function getHordeModels() {
if (horde_settings.models.length && models.filter(m => horde_settings.models.includes(m.name)).length === 0) { if (horde_settings.models.length && models.filter(m => horde_settings.models.includes(m.name)).length === 0) {
horde_settings.models = []; horde_settings.models = [];
} }
// if no models preselected - select a first one in dropdown
/*if (Array.isArray(horde_settings.models) || horde_settings.models.length == 0) {
$('#horde_model').first()
horde_settings.models = [.find(":selected").val()];
}*/
} }
function loadHordeSettings(settings) { function loadHordeSettings(settings) {
@ -190,7 +179,6 @@ function loadHordeSettings(settings) {
} }
$('#use_horde').prop("checked", horde_settings.use_horde).trigger('input'); $('#use_horde').prop("checked", horde_settings.use_horde).trigger('input');
$('#horde_api_key').val(horde_settings.api_key);
$('#horde_auto_adjust_response_length').prop("checked", horde_settings.auto_adjust_response_length); $('#horde_auto_adjust_response_length').prop("checked", horde_settings.auto_adjust_response_length);
$('#horde_auto_adjust_context_length').prop("checked", horde_settings.auto_adjust_context_length); $('#horde_auto_adjust_context_length').prop("checked", horde_settings.auto_adjust_context_length);
} }
@ -219,11 +207,6 @@ jQuery(function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#horde_api_key").on("input", function () {
horde_settings.api_key = $(this).val();
saveSettingsDebounced();
});
$("#horde_auto_adjust_response_length").on("input", function () { $("#horde_auto_adjust_response_length").on("input", function () {
horde_settings.auto_adjust_response_length = !!$(this).prop("checked"); horde_settings.auto_adjust_response_length = !!$(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();

View File

@ -14,7 +14,6 @@ const nai_settings = {
rep_pen_novel: 1, rep_pen_novel: 1,
rep_pen_size_novel: 100, rep_pen_size_novel: 100,
model_novel: "euterpe-v2", model_novel: "euterpe-v2",
api_key_novel: "",
preset_settings_novel: "Classic-Euterpe", preset_settings_novel: "Classic-Euterpe",
}; };
@ -44,12 +43,6 @@ function loadNovelPreset(preset) {
} }
function loadNovelSettings(settings) { function loadNovelSettings(settings) {
//load Novel API KEY is exists
if (settings.api_key_novel != undefined) {
nai_settings.api_key_novel = settings.api_key_novel;
$("#api_key_novel").val(nai_settings.api_key_novel);
}
//load the rest of the Novel settings without any checks //load the rest of the Novel settings without any checks
nai_settings.model_novel = settings.model_novel; nai_settings.model_novel = settings.model_novel;
$(`#model_novel_select option[value=${nai_settings.model_novel}]`).attr("selected", true); $(`#model_novel_select option[value=${nai_settings.model_novel}]`).attr("selected", true);

View File

@ -23,6 +23,11 @@ import { groups, selected_group } from "./group-chats.js";
import { import {
power_user, power_user,
} from "./power-user.js"; } from "./power-user.js";
import {
SECRET_KEYS,
secret_state,
writeSecret,
} from "./secrets.js";
import { import {
delay, delay,
@ -76,7 +81,6 @@ const tokenCache = {};
const default_settings = { const default_settings = {
preset_settings_openai: 'Default', preset_settings_openai: 'Default',
api_key_openai: '',
temp_openai: 0.9, temp_openai: 0.9,
freq_pen_openai: 0.7, freq_pen_openai: 0.7,
pres_pen_openai: 0.7, pres_pen_openai: 0.7,
@ -101,7 +105,6 @@ const default_settings = {
const oai_settings = { const oai_settings = {
preset_settings_openai: 'Default', preset_settings_openai: 'Default',
api_key_openai: '',
temp_openai: 1.0, temp_openai: 1.0,
freq_pen_openai: 0, freq_pen_openai: 0,
pres_pen_openai: 0, pres_pen_openai: 0,
@ -666,11 +669,6 @@ function countTokens(messages, full = false) {
} }
function loadOpenAISettings(data, settings) { function loadOpenAISettings(data, settings) {
if (settings.api_key_openai != undefined) {
oai_settings.api_key_openai = settings.api_key_openai;
$("#api_key_openai").val(oai_settings.api_key_openai);
}
openai_setting_names = data.openai_setting_names; openai_setting_names = data.openai_setting_names;
openai_settings = data.openai_settings; openai_settings = data.openai_settings;
openai_settings = data.openai_settings; openai_settings = data.openai_settings;
@ -766,7 +764,6 @@ async function getStatusOpen() {
if (is_get_status_openai) { if (is_get_status_openai) {
let data = { let data = {
key: oai_settings.api_key_openai,
reverse_proxy: oai_settings.reverse_proxy, reverse_proxy: oai_settings.reverse_proxy,
}; };
@ -873,13 +870,10 @@ async function saveOpenAIPreset(name, settings) {
} }
async function showApiKeyUsage() { async function showApiKeyUsage() {
const body = JSON.stringify({ key: oai_settings.api_key_openai });
try { try {
const response = await fetch('/openai_usage', { const response = await fetch('/openai_usage', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
body: body,
}); });
if (response.ok) { if (response.ok) {
@ -1168,16 +1162,24 @@ function onReverseProxyInput() {
async function onConnectButtonClick(e) { async function onConnectButtonClick(e) {
e.stopPropagation(); e.stopPropagation();
if ($('#api_key_openai').val() != '') { const api_key_openai = $('#api_key_openai').val().trim();
if (api_key_openai.length) {
await writeSecret(SECRET_KEYS.OPENAI, api_key_openai);
}
if (!secret_state[SECRET_KEYS.OPENAI]) {
console.log('No secret key saved for OpenAI');
return;
}
$("#api_loading_openai").css("display", 'inline-block'); $("#api_loading_openai").css("display", 'inline-block');
$("#api_button_openai").css("display", 'none'); $("#api_button_openai").css("display", 'none');
oai_settings.api_key_openai = $('#api_key_openai').val().trim();
saveSettingsDebounced(); saveSettingsDebounced();
is_get_status_openai = true; is_get_status_openai = true;
is_api_button_press_openai = true; is_api_button_press_openai = true;
await getStatusOpen(); await getStatusOpen();
} }
}
$(document).ready(function () { $(document).ready(function () {
$(document).on('input', '#temp_openai', function () { $(document).on('input', '#temp_openai', function () {

View File

@ -7,6 +7,11 @@ import {
getTokenCount, getTokenCount,
getRequestHeaders, getRequestHeaders,
} from "../script.js"; } from "../script.js";
import {
SECRET_KEYS,
secret_state,
writeSecret,
} from "./secrets.js";
export { export {
is_get_status_poe, is_get_status_poe,
@ -38,7 +43,6 @@ const DEFAULT_CHARACTER_NUDGE_MESSAGE = "[Your the next response shall only be w
const DEFAULT_IMPERSONATION_PROMPT = "[Write 1 reply only in internet RP style from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system.]"; const DEFAULT_IMPERSONATION_PROMPT = "[Write 1 reply only in internet RP style from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system.]";
const poe_settings = { const poe_settings = {
token: '',
bot: 'a2', bot: 'a2',
jailbreak_response: DEFAULT_JAILBREAK_RESPONSE, jailbreak_response: DEFAULT_JAILBREAK_RESPONSE,
jailbreak_message: DEFAULT_JAILBREAK_MESSAGE, jailbreak_message: DEFAULT_JAILBREAK_MESSAGE,
@ -67,7 +71,6 @@ function loadPoeSettings(settings) {
$('#poe_auto_jailbreak').prop('checked', poe_settings.auto_jailbreak); $('#poe_auto_jailbreak').prop('checked', poe_settings.auto_jailbreak);
$('#poe_auto_purge').prop('checked', poe_settings.auto_purge); $('#poe_auto_purge').prop('checked', poe_settings.auto_purge);
$('#poe_streaming').prop('checked', poe_settings.streaming); $('#poe_streaming').prop('checked', poe_settings.streaming);
$('#poe_token').val(poe_settings.token ?? '');
$('#poe_impersonation_prompt').val(poe_settings.impersonation_prompt); $('#poe_impersonation_prompt').val(poe_settings.impersonation_prompt);
selectBot(); selectBot();
} }
@ -78,11 +81,6 @@ function selectBot() {
} }
} }
function onTokenInput() {
poe_settings.token = $('#poe_token').val();
saveSettingsDebounced();
}
function onBotChange() { function onBotChange() {
poe_settings.bot = $('#poe_bots').find(":selected").val(); poe_settings.bot = $('#poe_bots').find(":selected").val();
saveSettingsDebounced(); saveSettingsDebounced();
@ -147,7 +145,6 @@ async function generatePoe(type, finalPrompt, signal) {
async function purgeConversation(count = -1) { async function purgeConversation(count = -1) {
const body = JSON.stringify({ const body = JSON.stringify({
bot: poe_settings.bot, bot: poe_settings.bot,
token: poe_settings.token,
count, count,
}); });
@ -167,7 +164,6 @@ async function sendMessage(prompt, withStreaming, signal) {
const body = JSON.stringify({ const body = JSON.stringify({
bot: poe_settings.bot, bot: poe_settings.bot,
token: poe_settings.token,
streaming: withStreaming && poe_settings.streaming, streaming: withStreaming && poe_settings.streaming,
prompt, prompt,
}); });
@ -213,7 +209,19 @@ async function sendMessage(prompt, withStreaming, signal) {
} }
async function onConnectClick() { async function onConnectClick() {
if (!poe_settings.token || is_poe_button_press) { const api_key_poe = $('#poe_token').val().trim();
if (api_key_poe.length) {
await writeSecret(SECRET_KEYS.POE, api_key_poe);
}
if (!secret_state[SECRET_KEYS.POE]) {
console.error('No secret key saved for Poe');
return;
}
if ( is_poe_button_press) {
console.log('Poe API button is pressed');
return; return;
} }
@ -236,7 +244,7 @@ function setButtonState(value) {
} }
async function checkStatusPoe() { async function checkStatusPoe() {
const body = JSON.stringify({ token: poe_settings.token }); const body = JSON.stringify();
const response = await fetch('/status_poe', { const response = await fetch('/status_poe', {
headers: getRequestHeaders(), headers: getRequestHeaders(),
body: body, body: body,
@ -336,7 +344,6 @@ function onMessageRestoreClick() {
} }
$('document').ready(function () { $('document').ready(function () {
$('#poe_token').on('input', onTokenInput);
$('#poe_bots').on('change', onBotChange); $('#poe_bots').on('change', onBotChange);
$('#poe_connect').on('click', onConnectClick); $('#poe_connect').on('click', onConnectClick);
$('#poe_activation_response').on('input', onResponseInput); $('#poe_activation_response').on('input', onResponseInput);

62
public/scripts/secrets.js Normal file
View File

@ -0,0 +1,62 @@
import { getRequestHeaders } from "../script.js";
export const SECRET_KEYS = {
HORDE: 'api_key_horde',
OPENAI: 'api_key_openai',
POE: 'api_key_poe',
NOVEL: 'api_key_novel',
}
const INPUT_MAP = {
[SECRET_KEYS.HORDE]: '#horde_api_key',
[SECRET_KEYS.OPENAI]: '#api_key_openai',
[SECRET_KEYS.POE]: '#poe_token',
[SECRET_KEYS.NOVEL]: '#api_key_novel',
}
function updateSecretDisplay() {
for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {
const validSecret = !!secret_state[secret_key];
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
$(input_selector).attr('placeholder', placeholder).val('');
}
}
export let secret_state = {};
export async function writeSecret(key, value) {
try {
const response = await fetch('/writesecret', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ key, value }),
});
if (response.ok) {
const text = await response.text();
if (text == 'ok') {
secret_state[key] = true;
updateSecretDisplay();
}
}
} catch {
console.error('Could not write secret value: ', key);
}
}
export async function readSecretState() {
try {
const response = await fetch('/readsecretstate', {
method: 'POST',
headers: getRequestHeaders(),
});
if (response.ok) {
secret_state = await response.json();
updateSecretDisplay();
}
} catch {
console.error('Could not read secrets file');
}
}

View File

@ -15,18 +15,6 @@ On its own Tavern is useless, as it's just a user interface. You have to have ac
### Do I need a powerful PC to run Tavern? ### Do I need a powerful PC to run Tavern?
Since Tavern is only a user interface, it has tiny hardware requirements, it will run on anything. It's the AI system backend that needs to be powerful. Since Tavern is only a user interface, it has tiny hardware requirements, it will run on anything. It's the AI system backend that needs to be powerful.
### I want to try self-hosted easily. Got a Google Colab?
Try on Colab (runs KoboldAI backend and TavernAI Extras server alongside): <a target="_blank" href="https://colab.research.google.com/github/Cohee1207/SillyTavern/blob/main/colab/GPU.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
https://colab.research.google.com/github/Cohee1207/SillyTavern/blob/main/colab/GPU.ipynb
Run on Repl.it:
[![Run on Repl.it](https://replit.com/badge?caption=Run+On+Repl.it)](https://replit.com/new/github/Cohee1207/SillyTavern)
## Mobile support ## Mobile support
> **Note** > **Note**

199
server.js
View File

@ -109,11 +109,9 @@ var response_dw_bg;
var response_getstatus; var response_getstatus;
var response_getstatus_novel; var response_getstatus_novel;
var response_getlastversion; var response_getlastversion;
var api_key_novel;
let response_generate_openai; let response_generate_openai;
let response_getstatus_openai; let response_getstatus_openai;
let api_key_openai;
//RossAscends: Added function to format dates used in files and chat timestamps to a humanized format. //RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.
//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected. //Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.
@ -1384,7 +1382,12 @@ function getImages(path) {
app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus_novel = response) { app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus_novel = response) {
if (!request.body) return response_getstatus_novel.sendStatus(400); if (!request.body) return response_getstatus_novel.sendStatus(400);
api_key_novel = request.body.key; const api_key_novel = readSecret(SECRET_KEYS.NOVEL);
if (!api_key_novel) {
return response_generate_novel.sendStatus(401);
}
var data = {}; var data = {};
var args = { var args = {
data: data, data: data,
@ -1414,6 +1417,12 @@ app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus
app.post("/generate_novelai", jsonParser, function (request, response_generate_novel = response) { app.post("/generate_novelai", jsonParser, function (request, response_generate_novel = response) {
if (!request.body) return response_generate_novel.sendStatus(400); if (!request.body) return response_generate_novel.sendStatus(400);
const api_key_novel = readSecret(SECRET_KEYS.NOVEL);
if (!api_key_novel) {
return response_generate_novel.sendStatus(401);
}
console.log(request.body); console.log(request.body);
var data = { var data = {
"input": request.body.input, "input": request.body.input,
@ -2066,12 +2075,14 @@ async function getPoeClient(token, useCache = false) {
} }
app.post('/status_poe', jsonParser, async (request, response) => { app.post('/status_poe', jsonParser, async (request, response) => {
if (!request.body.token) { const token = readSecret(SECRET_KEYS.POE);
return response.sendStatus(400);
if (!token) {
return response.sendStatus(401);
} }
try { try {
const client = await getPoeClient(request.body.token); const client = await getPoeClient(token);
const botNames = client.get_bot_names(); const botNames = client.get_bot_names();
client.disconnect_ws(); client.disconnect_ws();
@ -2084,11 +2095,12 @@ app.post('/status_poe', jsonParser, async (request, response) => {
}); });
app.post('/purge_poe', jsonParser, async (request, response) => { app.post('/purge_poe', jsonParser, async (request, response) => {
if (!request.body.token) { const token = readSecret(SECRET_KEYS.POE);
return response.sendStatus(400);
if (!token) {
return response.sendStatus(401);
} }
const token = request.body.token;
const bot = request.body.bot ?? POE_DEFAULT_BOT; const bot = request.body.bot ?? POE_DEFAULT_BOT;
const count = request.body.count ?? -1; const count = request.body.count ?? -1;
@ -2106,11 +2118,16 @@ app.post('/purge_poe', jsonParser, async (request, response) => {
}); });
app.post('/generate_poe', jsonParser, async (request, response) => { app.post('/generate_poe', jsonParser, async (request, response) => {
if (!request.body.token || !request.body.prompt) { if (!request.body.prompt) {
return response.sendStatus(400); return response.sendStatus(400);
} }
const token = request.body.token; const token = readSecret(SECRET_KEYS.POE);
if (!token) {
return response.sendStatus(401);
}
const prompt = request.body.prompt; const prompt = request.body.prompt;
const bot = request.body.bot ?? POE_DEFAULT_BOT; const bot = request.body.bot ?? POE_DEFAULT_BOT;
const streaming = request.body.streaming ?? false; const streaming = request.body.streaming ?? false;
@ -2352,7 +2369,13 @@ app.get('/thumbnail', jsonParser, async function (request, response) {
/* OpenAI */ /* OpenAI */
app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_openai = response) { app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_openai = response) {
if (!request.body) return response_getstatus_openai.sendStatus(400); if (!request.body) return response_getstatus_openai.sendStatus(400);
api_key_openai = request.body.key;
const api_key_openai = readSecret(SECRET_KEYS.OPENAI);
if (!api_key_openai) {
return response_getstatus_openai.sendStatus(401);
}
const api_url = new URL(request.body.reverse_proxy || api_openai).toString(); const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
const args = { const args = {
headers: { "Authorization": "Bearer " + api_key_openai } headers: { "Authorization": "Bearer " + api_key_openai }
@ -2455,6 +2478,12 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
if (!request.body) return response_generate_openai.sendStatus(400); if (!request.body) return response_generate_openai.sendStatus(400);
const api_url = new URL(request.body.reverse_proxy || api_openai).toString(); const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
const api_key_openai = readSecret(SECRET_KEYS.OPENAI);
if (!api_key_openai) {
return response_generate_openai.sendStatus(401);
}
const controller = new AbortController(); const controller = new AbortController();
request.socket.removeAllListeners('close'); request.socket.removeAllListeners('close');
request.socket.on('close', function () { request.socket.on('close', function () {
@ -2658,6 +2687,7 @@ const autorunUrl = new URL(
); );
const setupTasks = async function () { const setupTasks = async function () {
migrateSecrets();
ensurePublicDirectoriesExist(); ensurePublicDirectoriesExist();
await ensureThumbnailCache(); await ensureThumbnailCache();
@ -2670,10 +2700,11 @@ const setupTasks = async function () {
if (autorun) open(autorunUrl.toString()); if (autorun) open(autorunUrl.toString());
console.log('SillyTavern is listening on: ' + tavernUrl); console.log('SillyTavern is listening on: ' + tavernUrl);
if (listen && }
!config.whitelistMode &&
!config.basicAuthMode) if (listen && !config.whitelistMode && !config.basicAuthMode) {
console.log('Your SillyTavern is currently open to the public. To increase security, consider enabling whitelisting or basic authentication.') console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.');
process.exit(1);
} }
if (true === cliArguments.ssl) if (true === cliArguments.ssl)
@ -2742,3 +2773,139 @@ function ensurePublicDirectoriesExist() {
} }
} }
} }
const SECRETS_FILE = './secrets.json';
const SETTINGS_FILE = './public/settings.json';
const SECRET_KEYS = {
HORDE: 'api_key_horde',
OPENAI: 'api_key_openai',
POE: 'api_key_poe',
NOVEL: 'api_key_novel',
}
function migrateSecrets() {
if (!fs.existsSync(SETTINGS_FILE)) {
console.log('Settings file does not exist');
return;
}
try {
let modified = false;
const fileContents = fs.readFileSync(SETTINGS_FILE);
const settings = JSON.parse(fileContents);
const oaiKey = settings?.api_key_openai;
const hordeKey = settings?.horde_settings?.api_key;
const poeKey = settings?.poe_settings?.token;
const novelKey = settings?.api_key_novel;
if (typeof oaiKey === 'string') {
console.log('Migrating OpenAI key...');
writeSecret(SECRET_KEYS.OPENAI, oaiKey);
delete settings.api_key_openai;
modified = true;
}
if (typeof hordeKey === 'string') {
console.log('Migrating Horde key...');
writeSecret(SECRET_KEYS.HORDE, hordeKey);
delete settings.hordeKey;
modified = true;
}
if (typeof poeKey === 'string') {
console.log('Migrating Poe key...');
writeSecret(SECRET_KEYS.POE, poeKey);
delete settings.poe_settings.token;
modified = true;
}
if (typeof novelKey === 'string') {
console.log('Migrating Novel key...');
writeSecret(SECRET_KEYS.NOVEL, novelKey);
delete settings.api_key_novel;
modified = true;
}
if (modified) {
console.log('Writing updated settings.json...');
const settingsContent = JSON.stringify(settings);
fs.writeFileSync(SETTINGS_FILE, settingsContent, "utf-8");
}
}
catch (error) {
console.error('Could not migrate secrets file. Proceed with caution.');
}
}
app.post('/writesecret', jsonParser, (request, response) => {
const key = request.body.key;
const value = request.body.value;
writeSecret(key,value);
return response.send('ok');
});
app.post('/readsecretstate', jsonParser, (_, response) => {
if (!fs.existsSync(SECRETS_FILE)) {
return response.send({});
}
try {
const fileContents = fs.readFileSync(SECRETS_FILE);
const secrets = JSON.parse(fileContents);
const state = {};
for (const key of Object.values(SECRET_KEYS)) {
state[key] = !!secrets[key]; // convert to boolean
}
return response.send(state);
} catch (error) {
console.error(error);
return response.send({});
}
});
app.post('/generate_horde', jsonParser, async (request, response) => {
const ANONYMOUS_KEY = "0000000000";
const api_key_horde = readSecret(SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
const url = 'https://horde.koboldai.net/api/v2/generate/text/async';
const args = {
data: request.body,
headers: {
"Content-Type": "application/json",
"Client-Agent": request.header('Client-Agent'),
"apikey": api_key_horde,
}
};
try {
const data = await postAsync(url, args);
return response.send(data);
} catch {
return response.sendStatus(500);
}
});
function writeSecret(key, value) {
if (!fs.existsSync(SECRETS_FILE)) {
const emptyFile = JSON.stringify({});
fs.writeFileSync(SECRETS_FILE, emptyFile, "utf-8");
}
const fileContents = fs.readFileSync(SECRETS_FILE);
const secrets = JSON.parse(fileContents);
secrets[key] = value;
fs.writeFileSync(SECRETS_FILE, JSON.stringify(secrets), "utf-8");
}
function readSecret(key) {
if (!fs.existsSync(SECRETS_FILE)) {
return undefined;
}
const fileContents = fs.readFileSync(SECRETS_FILE);
const secrets = JSON.parse(fileContents);
return secrets[key];
}