diff --git a/.gitignore b/.gitignore index a35d5ddc5..46eeaf2a6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ public/settings.json /thumbnails whitelist.txt .vscode +secrets.json diff --git a/colab/GPU.ipynb b/colab/GPU.ipynb index cf61955e4..74bbee704 100644 --- a/colab/GPU.ipynb +++ b/colab/GPU.ipynb @@ -6,68 +6,8 @@ "metadata": {}, "source": [ "**Links**\n", - "SillyTavern GitHub: https://github.com/Cohee1207/SillyTavern\n", "Extensions API GitHub: https://github.com/Cohee1207/SillyTavern-extras/\n", - "SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj\n", - "Contact the maintainer directly: Cohee#1207" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "id": "_1gpebrnlp5-" - }, - "outputs": [], - "source": [ - "#@title <-- Convert TavernAI characters to SillyTavern format\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", - "Press play on the music player to keep the tab alive, then start KoboldAI below (Uses only 13MB of data)\n", - "" + "SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj" ] }, { @@ -79,16 +19,6 @@ }, "outputs": [], "source": [ - "#@title <-- Select your model below and then click this to start KoboldAI\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", "extras_enable_captioning = True #@param {type:\"boolean\"}\n", "#@markdown Loads the image captioning module\n", @@ -102,150 +32,20 @@ "#@markdown * joeddav/distilbert-base-uncased-go-emotions-student = 28 supported emotions\n", "extras_enable_memory = True #@param {type:\"boolean\"}\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-ElectrifAi_v10 - nice results so far, but still being evaluated\n", "#@markdown * distilbart-xsum-12-3 - faster, but pretty basic alternative\n", "\n", - "\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", + "import subprocess\n", "\n", "# ---\n", "# SillyTavern extras\n", - "import globals\n", - "globals.extras_url = '(disabled)'\n", - "globals.params = []\n", - "globals.params.append('--cpu')\n", + "extras_url = '(disabled)'\n", + "params = []\n", + "params.append('--cpu')\n", + "params.append('--share')\n", "ExtrasModules = []\n", "\n", "if (extras_enable_captioning):\n", @@ -255,73 +55,28 @@ "if (extras_enable_emotions):\n", " ExtrasModules.append('classify')\n", "\n", - "globals.params.append(f'--classification-model={Emotions_Model}')\n", - "globals.params.append(f'--summarization-model={Memory_Model}')\n", - "globals.params.append(f'--captioning-model={Captions_Model}')\n", - "globals.params.append(f'--enable-modules={\",\".join(ExtrasModules)}')\n", + "params.append(f'--classification-model={Emotions_Model}')\n", + "params.append(f'--summarization-model={Memory_Model}')\n", + "params.append(f'--captioning-model={Captions_Model}')\n", + "params.append(f'--enable-modules={\",\".join(ExtrasModules)}')\n", "\n", "\n", - "if UseExtrasExtensions:\n", - " def cloneExtras():\n", - " %cd /\n", - " !git clone https://github.com/Cohee1207/SillyTavern-extras\n", - " ii.addTask('clone extras', cloneExtras)\n", + "%cd /\n", + "!git clone https://github.com/Cohee1207/SillyTavern-extras\n", + "%cd /SillyTavern-extras\n", + "!npm install -g localtunnel\n", + "!pip install -r requirements.txt\n", + "!pip install tensorflow==2.11\n", "\n", - " def installRequirements():\n", - " %cd /SillyTavern-extras\n", - " !npm install -g localtunnel\n", - " !pip install -r requirements.txt\n", - " !pip install tensorflow==2.11\n", - " ii.addTask('install requirements', installRequirements)\n", "\n", - " from extras_server import runServer, extractUrl\n", - " ii.addTask('run server', runServer)\n", - " ii.addTask('extract extras URL', extractUrl)\n", - "\n", - "%cd /SillyTavern\n", - "\n", - "if UseGoogleDrive:\n", - " %env googledrive=2\n", - "\n", - " def setupTavernPaths():\n", - " %cd /SillyTavern\n", - " tdrive = \"/content/drive/MyDrive/SillyTavern\"\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" + "cmd = f\"python server.py {' '.join(params)}\"\n", + "print(cmd)\n", + "extras_process = subprocess.Popen(\n", + " cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd='/SillyTavern-extras', shell=True)\n", + "print('processId:', extras_process.pid)\n", + "while True:\n", + " line = extras_process.stdout.readline().decode().strip()\n", + " print(line)\n" ] } ], diff --git a/colab/extras_server.py b/colab/extras_server.py deleted file mode 100644 index 705612acc..000000000 --- a/colab/extras_server.py +++ /dev/null @@ -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 diff --git a/colab/globals.py b/colab/globals.py deleted file mode 100644 index 50fb89724..000000000 --- a/colab/globals.py +++ /dev/null @@ -1,2 +0,0 @@ -extras_url = '(disabled)' -params = [] \ No newline at end of file diff --git a/colab/models.py b/colab/models.py deleted file mode 100644 index 455e1c6df..000000000 --- a/colab/models.py +++ /dev/null @@ -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") - } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 2fdc5b937..b8b486660 100644 --- a/public/index.html +++ b/public/index.html @@ -59,6 +59,7 @@ + SillyTavern @@ -1006,7 +1007,8 @@ API key Get it here: Register - + + Your API key will removed from here after you click "Connect" for privacy reasons. Model @@ -1036,7 +1038,8 @@ Enter it in the box below: - + + Your API key will removed from here after you click "Connect" for privacy reasons. Novel AI Model @@ -1089,7 +1092,8 @@ Enter it in the box below: - + + Your API key will removed from here after you click "Connect" for privacy reasons. @@ -1125,8 +1129,8 @@ - - + + Your API key will removed from here after you click "Connect" for privacy reasons. diff --git a/public/script.js b/public/script.js index c4f6ceadb..059a13351 100644 --- a/public/script.js +++ b/public/script.js @@ -119,6 +119,12 @@ import { createTagMapFromList, renameTagKey, } from "./scripts/tags.js"; +import { + SECRET_KEYS, + readSecretState, + secret_state, + writeSecret +} from "./scripts/secrets.js"; //exporting functions and vars for mods export { @@ -543,14 +549,15 @@ $.ajaxPrefilter((options, originalOptions, xhr) => { }); ///// initialization protocol //////// -$.get("/csrf-token").then((data) => { +$.get("/csrf-token").then(async (data) => { token = data.token; - getClientVersion(); - getCharacters(); - getSettings("def"); + await readSecretState(); + await getClientVersion(); + await getSettings("def"); + await getCharacters(); + await getBackgrounds(); + await getUserAvatars(); sendSystemMessage(system_message_types.WELCOME); - getBackgrounds(); - getUserAvatars(); }); function checkOnlineStatus() { @@ -3490,7 +3497,7 @@ async function displayPastChats() { //************************************************************ async function getStatusNovel() { if (is_get_status_novel) { - const data = { key: nai_settings.api_key_novel }; + const data = {}; jQuery.ajax({ type: "POST", // @@ -5542,17 +5549,25 @@ $(document).ready(function () { }); //Select chat - $("#api_button_novel").click(function (e) { + $("#api_button_novel").on('click', async function (e) { e.stopPropagation(); - if ($("#api_key_novel").val() != "") { - $("#api_loading_novel").css("display", "inline-block"); - $("#api_button_novel").css("display", "none"); - nai_settings.api_key_novel = $.trim($("#api_key_novel").val()); - saveSettingsDebounced(); - is_get_status_novel = true; - is_api_button_press_novel = true; + 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_button_novel").css("display", "none"); + is_get_status_novel = true; + is_api_button_press_novel = true; }); + $("#anchor_order").change(function () { anchor_order = parseInt($("#anchor_order").find(":selected").val()); saveSettingsDebounced(); diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 9c8e5ba2b..9170f407f 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -7,21 +7,14 @@ import { online_status, main_api, api_server, - nai_settings, api_server_textgenerationwebui, is_send_press, getTokenCount, menu_type, - selectRightMenuWithAnimation, - select_selected_character, - setCharacterId, } from "../script.js"; -import { - select_group_chats, -} from "./group-chats.js"; import { power_user, @@ -30,8 +23,10 @@ import { import { LoadLocal, SaveLocal, ClearLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js"; import { selected_group, is_group_generating, getGroupAvatar, groups } from "./group-chats.js"; -import { oai_settings } from "./openai.js"; -import { poe_settings } from "./poe.js"; +import { + SECRET_KEYS, + secret_state, +} from "./secrets.js"; var NavToggle = document.getElementById("nav-toggle"); var RPanelPin = document.getElementById("rm_button_panel_pin"); @@ -368,13 +363,11 @@ function RA_autoconnect(PrevApi) { case 'kobold': if (api_server && isUrlOrAPIKey(api_server)) { $("#api_button").click(); - } break; case 'novel': - if (nai_settings.api_key_novel) { + if (secret_state[SECRET_KEYS.NOVEL]) { $("#api_button_novel").click(); - } break; case 'textgenerationwebui': @@ -383,12 +376,12 @@ function RA_autoconnect(PrevApi) { } break; case 'openai': - if (oai_settings.api_key_openai) { + if (secret_state[SECRET_KEYS.OPENAI]) { $("#api_button_openai").click(); } break; case 'poe': - if (poe_settings.token) { + if (secret_state[SECRET_KEYS.POE]) { $("#poe_connect").click(); } break; diff --git a/public/scripts/horde.js b/public/scripts/horde.js index f881b8cac..9e6e57a6e 100644 --- a/public/scripts/horde.js +++ b/public/scripts/horde.js @@ -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"; export { @@ -14,7 +14,6 @@ export { let models = []; let horde_settings = { - api_key: '0000000000', models: [], use_horde: false, auto_adjust_response_length: true, @@ -30,14 +29,6 @@ const getRequestArgs = () => ({ "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() { 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, }; - const response = await fetch("https://horde.koboldai.net/api/v2/generate/text/async", { - ...postRequestArgs(), + const response = await fetch("/generate_horde", { + method: 'POST', + headers: { + ...getRequestHeaders(), + "Client-Agent": CLIENT_VERSION, + }, 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) { 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) { @@ -190,7 +179,6 @@ function loadHordeSettings(settings) { } $('#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_context_length').prop("checked", horde_settings.auto_adjust_context_length); } @@ -219,11 +207,6 @@ jQuery(function () { saveSettingsDebounced(); }); - $("#horde_api_key").on("input", function () { - horde_settings.api_key = $(this).val(); - saveSettingsDebounced(); - }); - $("#horde_auto_adjust_response_length").on("input", function () { horde_settings.auto_adjust_response_length = !!$(this).prop("checked"); saveSettingsDebounced(); diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js index 4d2fba33e..629e669ae 100644 --- a/public/scripts/nai-settings.js +++ b/public/scripts/nai-settings.js @@ -14,7 +14,6 @@ const nai_settings = { rep_pen_novel: 1, rep_pen_size_novel: 100, model_novel: "euterpe-v2", - api_key_novel: "", preset_settings_novel: "Classic-Euterpe", }; @@ -44,12 +43,6 @@ function loadNovelPreset(preset) { } 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 nai_settings.model_novel = settings.model_novel; $(`#model_novel_select option[value=${nai_settings.model_novel}]`).attr("selected", true); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index bf572bf63..e8053b44a 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -23,6 +23,11 @@ import { groups, selected_group } from "./group-chats.js"; import { power_user, } from "./power-user.js"; +import { + SECRET_KEYS, + secret_state, + writeSecret, +} from "./secrets.js"; import { delay, @@ -76,7 +81,6 @@ const tokenCache = {}; const default_settings = { preset_settings_openai: 'Default', - api_key_openai: '', temp_openai: 0.9, freq_pen_openai: 0.7, pres_pen_openai: 0.7, @@ -101,7 +105,6 @@ const default_settings = { const oai_settings = { preset_settings_openai: 'Default', - api_key_openai: '', temp_openai: 1.0, freq_pen_openai: 0, pres_pen_openai: 0, @@ -666,11 +669,6 @@ function countTokens(messages, full = false) { } 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_settings = data.openai_settings; openai_settings = data.openai_settings; @@ -766,7 +764,6 @@ async function getStatusOpen() { if (is_get_status_openai) { let data = { - key: oai_settings.api_key_openai, reverse_proxy: oai_settings.reverse_proxy, }; @@ -873,13 +870,10 @@ async function saveOpenAIPreset(name, settings) { } async function showApiKeyUsage() { - const body = JSON.stringify({ key: oai_settings.api_key_openai }); - try { const response = await fetch('/openai_usage', { method: 'POST', headers: getRequestHeaders(), - body: body, }); if (response.ok) { @@ -1168,15 +1162,23 @@ function onReverseProxyInput() { async function onConnectButtonClick(e) { e.stopPropagation(); - if ($('#api_key_openai').val() != '') { - $("#api_loading_openai").css("display", 'inline-block'); - $("#api_button_openai").css("display", 'none'); - oai_settings.api_key_openai = $('#api_key_openai').val().trim(); - saveSettingsDebounced(); - is_get_status_openai = true; - is_api_button_press_openai = true; - await getStatusOpen(); + 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_button_openai").css("display", 'none'); + saveSettingsDebounced(); + is_get_status_openai = true; + is_api_button_press_openai = true; + await getStatusOpen(); } $(document).ready(function () { diff --git a/public/scripts/poe.js b/public/scripts/poe.js index b4d735e69..be3b021e3 100644 --- a/public/scripts/poe.js +++ b/public/scripts/poe.js @@ -7,6 +7,11 @@ import { getTokenCount, getRequestHeaders, } from "../script.js"; +import { + SECRET_KEYS, + secret_state, + writeSecret, +} from "./secrets.js"; export { 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 poe_settings = { - token: '', bot: 'a2', jailbreak_response: DEFAULT_JAILBREAK_RESPONSE, jailbreak_message: DEFAULT_JAILBREAK_MESSAGE, @@ -67,7 +71,6 @@ function loadPoeSettings(settings) { $('#poe_auto_jailbreak').prop('checked', poe_settings.auto_jailbreak); $('#poe_auto_purge').prop('checked', poe_settings.auto_purge); $('#poe_streaming').prop('checked', poe_settings.streaming); - $('#poe_token').val(poe_settings.token ?? ''); $('#poe_impersonation_prompt').val(poe_settings.impersonation_prompt); selectBot(); } @@ -78,11 +81,6 @@ function selectBot() { } } -function onTokenInput() { - poe_settings.token = $('#poe_token').val(); - saveSettingsDebounced(); -} - function onBotChange() { poe_settings.bot = $('#poe_bots').find(":selected").val(); saveSettingsDebounced(); @@ -147,7 +145,6 @@ async function generatePoe(type, finalPrompt, signal) { async function purgeConversation(count = -1) { const body = JSON.stringify({ bot: poe_settings.bot, - token: poe_settings.token, count, }); @@ -167,7 +164,6 @@ async function sendMessage(prompt, withStreaming, signal) { const body = JSON.stringify({ bot: poe_settings.bot, - token: poe_settings.token, streaming: withStreaming && poe_settings.streaming, prompt, }); @@ -213,7 +209,19 @@ async function sendMessage(prompt, withStreaming, signal) { } 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; } @@ -236,7 +244,7 @@ function setButtonState(value) { } async function checkStatusPoe() { - const body = JSON.stringify({ token: poe_settings.token }); + const body = JSON.stringify(); const response = await fetch('/status_poe', { headers: getRequestHeaders(), body: body, @@ -336,7 +344,6 @@ function onMessageRestoreClick() { } $('document').ready(function () { - $('#poe_token').on('input', onTokenInput); $('#poe_bots').on('change', onBotChange); $('#poe_connect').on('click', onConnectClick); $('#poe_activation_response').on('input', onResponseInput); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js new file mode 100644 index 000000000..65096e2ca --- /dev/null +++ b/public/scripts/secrets.js @@ -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'); + } +} \ No newline at end of file diff --git a/readme.md b/readme.md index d1f4e1dda..1fbfae648 100644 --- a/readme.md +++ b/readme.md @@ -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? 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): - - - -https://colab.research.google.com/github/Cohee1207/SillyTavern/blob/main/colab/GPU.ipynb - -Run on Repl.it: -[](https://replit.com/new/github/Cohee1207/SillyTavern) - - ## Mobile support > **Note** diff --git a/server.js b/server.js index 117cfefed..b0e503ea7 100644 --- a/server.js +++ b/server.js @@ -109,11 +109,9 @@ var response_dw_bg; var response_getstatus; var response_getstatus_novel; var response_getlastversion; -var api_key_novel; let response_generate_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. //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) { 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 args = { 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) { 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); var data = { "input": request.body.input, @@ -2066,12 +2075,14 @@ async function getPoeClient(token, useCache = false) { } app.post('/status_poe', jsonParser, async (request, response) => { - if (!request.body.token) { - return response.sendStatus(400); + const token = readSecret(SECRET_KEYS.POE); + + if (!token) { + return response.sendStatus(401); } try { - const client = await getPoeClient(request.body.token); + const client = await getPoeClient(token); const botNames = client.get_bot_names(); client.disconnect_ws(); @@ -2084,11 +2095,12 @@ app.post('/status_poe', jsonParser, async (request, response) => { }); app.post('/purge_poe', jsonParser, async (request, response) => { - if (!request.body.token) { - return response.sendStatus(400); + const token = readSecret(SECRET_KEYS.POE); + + if (!token) { + return response.sendStatus(401); } - const token = request.body.token; const bot = request.body.bot ?? POE_DEFAULT_BOT; 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) => { - if (!request.body.token || !request.body.prompt) { + if (!request.body.prompt) { 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 bot = request.body.bot ?? POE_DEFAULT_BOT; const streaming = request.body.streaming ?? false; @@ -2352,7 +2369,13 @@ app.get('/thumbnail', jsonParser, async function (request, response) { /* OpenAI */ app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_openai = response) { 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 args = { 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); 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(); request.socket.removeAllListeners('close'); request.socket.on('close', function () { @@ -2658,6 +2687,7 @@ const autorunUrl = new URL( ); const setupTasks = async function () { + migrateSecrets(); ensurePublicDirectoriesExist(); await ensureThumbnailCache(); @@ -2670,10 +2700,11 @@ const setupTasks = async function () { if (autorun) open(autorunUrl.toString()); console.log('SillyTavern is listening on: ' + tavernUrl); - 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.') +} + +if (listen && !config.whitelistMode && !config.basicAuthMode) { + console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.'); + process.exit(1); } 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]; +} \ No newline at end of file