From 18cd02d44ee9dcbe6015166f0c84a7059edf3ed2 Mon Sep 17 00:00:00 2001 From: Fuegovic <32828263+fuegovic@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:07:11 -0500 Subject: [PATCH 01/42] Update French Translation (#1444) --- client/src/localization/languages/Fr.tsx | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/client/src/localization/languages/Fr.tsx b/client/src/localization/languages/Fr.tsx index a97aea8c4782..7a1863d3923f 100644 --- a/client/src/localization/languages/Fr.tsx +++ b/client/src/localization/languages/Fr.tsx @@ -32,6 +32,7 @@ export default { com_ui_entries: 'Entrées', com_ui_pay_per_call: 'Toutes les conversations IA au même endroit. Payez à la demande et non par mois', + com_ui_new_footer: 'Toutes les conversations IA au même endroit.', com_ui_enter: 'Entrer', com_ui_submit: 'Soumettre', com_ui_upload_success: 'Fichier téléchargé avec succès', @@ -123,6 +124,8 @@ export default { com_endpoint_bing_system_message_placeholder: 'AVERTISSEMENT : L\'abus de cette fonctionnalité peut vous faire BANNIR de l\'utilisation de Bing ! Cliquez sur "Message système" pour obtenir les instructions complètes et le message par défaut si omis, qui est le préréglage "Sydney" qui est considéré comme sûr.', com_endpoint_system_message: 'Message système', + com_endpoint_message: 'Message', + com_endpoint_message_not_appendable: 'Editer votre message ou regenerer.', com_endpoint_default_blank: 'par défaut : vide', com_endpoint_default_false: 'par défaut : faux', com_endpoint_default_creative: 'par défaut : créatif', @@ -183,8 +186,22 @@ export default { com_endpoint_import: 'Importer', com_endpoint_set_custom_name: 'Définir un nom personnalisé, au cas où vous trouveriez ce préréglage', + com_endpoint_preset_delete_confirm: 'Êtes-vous sûr de vouloir supprimer ce préréglage ?', + com_endpoint_preset_clear_all_confirm: 'Êtes-vous sûr de vouloir supprimer tous vos préréglages ?', + com_endpoint_preset_import: 'Préréglage importé !', + com_endpoint_preset_import_error: 'Il y a eu une erreur lors de l\'importation de votre préréglage. Veuillez réessayer.', + com_endpoint_preset_save_error: 'Il y a eu une erreur lors de la sauvegarde de votre préréglage. Veuillez réessayer.', + com_endpoint_preset_delete_error: 'Il y a eu une erreur lors de la suppression de votre préréglage. Veuillez réessayer.', + com_endpoint_preset_default_removed: 'n\'est plus le préréglage par défaut.', + com_endpoint_preset_default_item: 'Par défaut :', + com_endpoint_preset_default_none: 'Aucun préréglage par défaut actif.', + com_endpoint_preset_title: 'Préréglage', + com_endpoint_preset_saved: 'Enregistré!', + com_endpoint_preset_default: 'est maintenant le préréglage par défaut.', com_endpoint_preset: 'préréglage', com_endpoint_presets: 'préréglages', + com_endpoint_preset_selected: 'Préréglage actif!', + com_endpoint_preset_selected_title: 'Actif!', com_endpoint_preset_name: 'Nom du préréglage', com_endpoint_new_topic: 'Nouveau sujet', com_endpoint: 'Endpoint', @@ -200,7 +217,7 @@ export default { com_endpoint_presets_clear_warning: 'Etes-vous sûr de vouloir effacer tous les préréglages ? Cette action est irréversible.', com_endpoint_not_implemented: 'Non implémenté', - com_endpoint_no_presets: 'Aucun préréglage', + com_endpoint_no_presets: 'Aucun préréglage pour l\'instant, utilisez le bouton paramètres pour en créer un', com_endpoint_not_available: 'Aucun endpoint disponible', com_endpoint_view_options: 'Voir les options', com_endpoint_save_convo_as_preset: 'Enregistrer la conversation comme préréglage', @@ -211,12 +228,19 @@ export default { com_endpoint_skip_hover: 'Activer le saut de l\'étape de complétion, qui examine la réponse finale et les étapes générées', com_endpoint_config_key: 'Définir la clé API', + com_endpoint_config_placeholder: 'Définissez votre clé dans le menu En-tête pour discuter.', com_endpoint_config_key_for: 'Définir la clé API pour', com_endpoint_config_key_name: 'Clé', com_endpoint_config_value: 'Entrez la valeur pour', com_endpoint_config_key_name_placeholder: 'Définir d\'abord la clé API', com_endpoint_config_key_encryption: 'Votre clé sera cryptée et supprimée à', - com_endpoint_config_key_expiry: 'l\'heure d\'expiration', + com_endpoint_config_key_expiry: 'le délai d\'expiration', + com_endpoint_config_click_here: 'Cliquez ici', + com_endpoint_config_google_service_key: 'Clé de compte de service Google', + com_endpoint_config_google_cloud_platform: '(de Google Cloud Platform)', + com_endpoint_config_google_api_key: 'Clé API Google', + com_endpoint_config_google_gemini_api: '(API Gemini)', + com_endpoint_config_google_api_info: 'Pour obtenir votre clé API de langage génératif (pour Gemini),', com_endpoint_config_key_import_json_key: 'Importez la clé JSON du compte de service.', com_endpoint_config_key_import_json_key_success: 'Clé JSON du compte de service importé avec succès', @@ -238,6 +262,7 @@ export default { com_endpoint_config_key_google_service_account: 'Créer un compte de service', com_endpoint_config_key_google_vertex_api_role: 'Assurez-vous de cliquer \'Créer et continuer\' pour donner au moins le role \'Utilisateur de Vertex AI\'. Finalement, créez une clé JSON à importer ici.', + com_nav_welcome_message: 'Comment puis-je vous aider aujourd\'hui?', com_nav_auto_scroll: 'Défilement automatique jusqu\'au plus récent à l\'ouverture', com_nav_plugin_store: 'Boutique de plugins', com_nav_plugin_search: 'Rechercher des plugins', From bce4f41fae50e60045673ed6ca4eb836207ee530 Mon Sep 17 00:00:00 2001 From: Fuegovic <32828263+fuegovic@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:10:06 -0500 Subject: [PATCH 02/42] =?UTF-8?q?=F0=9F=AA=AAmkdocs:=20social=20cards=20?= =?UTF-8?q?=20=20(#1428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * mkdocs plugins: add plugin for social cards and plugin that allow to exclude a folder * docs: fix hyperlinks * mkdocs: social cards (descriptions) for 'contributions' and 'deployment' guides * mkdocs: social cards (descriptions) for all 'index.md' * mkdocs: social cards (descriptions) for 'features' and 'plugins' * mkdocs: social cards (descriptions) for 'general_info' * mkdocs: social cards (descriptions) for 'configuration' * mkdocs: social cards (descriptions) for 'installation' * mkdocs: minor fixes * update librechat.svg * update how_to_contribute.md add reference to the official GitHub documentation --- .github/workflows/mkdocs.yaml | 2 + docs/assets/LibreChat.svg | 48 ++-- docs/contributions/coding_conventions.md | 3 +- .../contributions/documentation_guidelines.md | 2 + docs/contributions/how_to_contribute.md | 3 + docs/contributions/index.md | 1 + docs/contributions/testing.md | 1 + .../contributions/translation_contribution.md | 1 + docs/deployment/azure-terraform.md | 1 + docs/deployment/cloudflare.md | 9 +- docs/deployment/digitalocean.md | 3 +- docs/deployment/heroku.md | 11 +- docs/deployment/hetzner_ubuntu.md | 17 +- docs/deployment/huggingface.md | 24 +- docs/deployment/index.md | 1 + docs/deployment/linode.md | 3 +- docs/deployment/meilisearch_in_render.md | 3 +- docs/deployment/ngrok.md | 26 +- docs/deployment/render.md | 1 + docs/dev/README.md | 6 +- docs/features/bing_jailbreak.md | 5 +- docs/features/index.md | 1 + docs/features/logging_system.md | 5 +- docs/features/manage_your_database.md | 1 + docs/features/mod_system.md | 3 +- docs/features/pandoranext.md | 229 +++++++++--------- docs/features/plugins/azure_ai_search.md | 7 +- .../plugins/chatgpt_plugins_openapi.md | 27 ++- docs/features/plugins/google_search.md | 5 +- docs/features/plugins/index.md | 1 + docs/features/plugins/introduction.md | 9 +- docs/features/plugins/make_your_own.md | 9 +- docs/features/plugins/stable_diffusion.md | 5 +- docs/features/plugins/wolfram.md | 22 +- docs/features/presets.md | 1 + docs/features/third_party.md | 1 + docs/features/token_usage.md | 1 + docs/general_info/breaking_changes.md | 1 + docs/general_info/index.md | 1 + docs/general_info/multilingual_information.md | 1 + docs/general_info/project_origin.md | 1 + docs/general_info/tech_stack.md | 1 + docs/index.md | 6 + docs/install/configuration/ai_setup.md | 42 ++-- .../install/configuration/default_language.md | 1 + docs/install/configuration/docker_override.md | 1 + docs/install/configuration/dotenv.md | 62 ++--- docs/install/configuration/free_ai_apis.md | 7 +- docs/install/configuration/index.md | 1 + docs/install/configuration/litellm.md | 7 +- docs/install/configuration/misc.md | 3 +- docs/install/configuration/mongodb.md | 3 +- .../install/configuration/user_auth_system.md | 9 +- docs/install/index.md | 3 +- .../install/installation/container_install.md | 7 +- .../installation/docker_compose_install.md | 5 +- docs/install/installation/index.md | 3 +- docs/install/installation/linux_install.md | 11 +- docs/install/installation/mac_install.md | 5 +- docs/install/installation/windows_install.md | 18 +- docs/stylesheets/extra.css | 6 - mkdocs.yml | 27 ++- 62 files changed, 397 insertions(+), 333 deletions(-) delete mode 100644 docs/stylesheets/extra.css diff --git a/.github/workflows/mkdocs.yaml b/.github/workflows/mkdocs.yaml index 25cfd2baa32f..3b2878fa2a78 100644 --- a/.github/workflows/mkdocs.yaml +++ b/.github/workflows/mkdocs.yaml @@ -22,4 +22,6 @@ jobs: mkdocs-material- - run: pip install mkdocs-material - run: pip install mkdocs-nav-weight + - run: pip install mkdocs-publisher + - run: pip install mkdocs-exclude - run: mkdocs gh-deploy --force diff --git a/docs/assets/LibreChat.svg b/docs/assets/LibreChat.svg index 75e3835313f4..36a536d654bf 100644 --- a/docs/assets/LibreChat.svg +++ b/docs/assets/LibreChat.svg @@ -1,34 +1,32 @@ - - - - - + + + - - + + - - + + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/docs/contributions/coding_conventions.md b/docs/contributions/coding_conventions.md index 5859c815a20a..a75c3bd2103c 100644 --- a/docs/contributions/coding_conventions.md +++ b/docs/contributions/coding_conventions.md @@ -1,5 +1,6 @@ --- title: 🧑‍💻 Code Standards and Conventions +description: This guide covers the best practices for JavaScript coding, such as following the Airbnb Style Guide, using CommonJS modules, structuring the API using Express, Mongoose, and services, and testing and documenting the code using Jest, Supertest, Playwright, JSDoc, and TypeScript. weight: -7 --- # Coding Conventions @@ -67,7 +68,7 @@ Defines Mongoose models to represent data entities and their relationships. ### 4. Database Access (MongoDB and Mongoose) -- Use Mongoose (https://mongoosejs.com) as the MongoDB ODM. +- Use Mongoose ([https://mongoosejs.com](https://mongoosejs.com)) as the MongoDB ODM. - Create separate model files for each entity and ensure clear separation of concerns. - Use Mongoose schema validation to enforce data integrity. - Handle database connections efficiently and avoid connection leaks. diff --git a/docs/contributions/documentation_guidelines.md b/docs/contributions/documentation_guidelines.md index e854ce9590d2..a3d05d688acc 100644 --- a/docs/contributions/documentation_guidelines.md +++ b/docs/contributions/documentation_guidelines.md @@ -1,5 +1,6 @@ --- title: 📝 Documentation Guidelines +description: Learn how to write and format documentation for LibreChat. weight: -9 --- # Documentation Guidelines @@ -26,6 +27,7 @@ This document explains how to write and format documentation for LibreChat. ```bash --- title: Document Title +description: This description will be used in social cards weight: 0 --- ``` diff --git a/docs/contributions/how_to_contribute.md b/docs/contributions/how_to_contribute.md index c05dc6a9250c..5b71da5b585e 100644 --- a/docs/contributions/how_to_contribute.md +++ b/docs/contributions/how_to_contribute.md @@ -1,8 +1,11 @@ --- title: 🙌 Beginner's Guide to Contributions +description: Learn how to use GitHub Desktop, VS Code extensions, and Git rebase to contribute in a quick and easy way. weight: -10 --- # How to Contribute in a Quick and Easy Way +> **❗Note:** If you are not familiar with the concept of repo, PR (pull request), fork and branch, start by looking at the official GitHub documentation on the subject: +[https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/about-collaborative-development-models](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/about-collaborative-development-models) ## Installation of Tools diff --git a/docs/contributions/index.md b/docs/contributions/index.md index b8722a428305..4362b1df092b 100644 --- a/docs/contributions/index.md +++ b/docs/contributions/index.md @@ -1,5 +1,6 @@ --- title: Contributing to LibreChat +description: "🙌 How to contribute to LibreChat: Get started, Documentation and code standards, Translate the app into different languages, Test the app during development, Ensure the security of the app, Stay updated with the project roadmap" weight: 5 --- # Contributing to LibreChat diff --git a/docs/contributions/testing.md b/docs/contributions/testing.md index 94d02b2392da..eeac51916b4b 100644 --- a/docs/contributions/testing.md +++ b/docs/contributions/testing.md @@ -1,5 +1,6 @@ --- title: 🧪 Testing During Development +description: How to locally test the app during development. weight: -6 --- diff --git a/docs/contributions/translation_contribution.md b/docs/contributions/translation_contribution.md index 3a66970ad4c7..e33a9ef8af13 100644 --- a/docs/contributions/translation_contribution.md +++ b/docs/contributions/translation_contribution.md @@ -1,5 +1,6 @@ --- title: 🌍 Contribute a Translation +description: How to add a new language to LibreChat. weight: -8 --- # How to add a new language to LibreChat 🌍 diff --git a/docs/deployment/azure-terraform.md b/docs/deployment/azure-terraform.md index c7190cd46a47..76768683c642 100644 --- a/docs/deployment/azure-terraform.md +++ b/docs/deployment/azure-terraform.md @@ -1,5 +1,6 @@ --- title: ⚡ Azure +description: How to deploy LibreChat in Azure using Terraform. weight: -6 --- # Azure deployment diff --git a/docs/deployment/cloudflare.md b/docs/deployment/cloudflare.md index c6b124157fc3..c9a44dfb1cd4 100644 --- a/docs/deployment/cloudflare.md +++ b/docs/deployment/cloudflare.md @@ -1,5 +1,6 @@ --- title: ☁️ Cloudflare +description: How to setup a domain with Cloudflare and use Cloudflare Tunnels to securely expose your local web servers or services to the internet. weight: -7 --- @@ -9,12 +10,12 @@ weight: -7 ## Google Domains and Cloudflare -- buy a domain at https://domains.google.com/ -- register a Cloudflare account at https://dash.cloudflare.com/sign-up +- buy a domain at **[https://domains.google.com/](https://domains.google.com)** +- register a Cloudflare account at **[https://dash.cloudflare.com/sign-up](https://dash.cloudflare.com/sign-up)** - click on `add site` and add your domain - select `Free` and tap `continue` twice - copy the 2 Cloudflare's nameservers -- go to https://domains.google.com/registrar/ and select your domain +- go to **[https://domains.google.com/registrar/](https://domains.google.com/registrar)** and select your domain - in the dns tab select `Custom name servers` - click on `Switch to these settings` and enter the two Cloudflare nameservers that you copied before, then save - return to the cloudflare tab and tap on `Done, check nameservers`, then `finish later` and `Check nameservers` (this process can take about 5 minutes) @@ -73,7 +74,7 @@ Here's a straightforward guide on how to install it! ### Installation Steps -1. Go to `https://dash.cloudflare.com/`. +1. Go to **[https://dash.cloudflare.com/](https://dash.cloudflare.com/)**. 2. On the left side, click on **Zero Trust**. 3. Provide a casual name (which you can change later). 4. Select the free plan and proceed to payment (if you choose the free plan, you will not be charged). diff --git a/docs/deployment/digitalocean.md b/docs/deployment/digitalocean.md index a90ae40aca5e..640b4b3b0d93 100644 --- a/docs/deployment/digitalocean.md +++ b/docs/deployment/digitalocean.md @@ -1,5 +1,6 @@ --- title: 🌊 DigitalOcean ✨(Recommended) +description: These instructions are designed for someone starting from scratch for a Docker Installation on a remote Ubuntu server using one of the cheapest tiers (6 USD/mo) weight: -10 --- # Digital Ocean (Ubuntu/Docker) Setup @@ -366,7 +367,7 @@ It's safe to close the terminal if you wish -- the docker app will continue to r >If you are setting up a domain to be used with LibreChat, this compose file is using the nginx file located in client/nginx.conf. Instructions on this below in part V. -### **4. Once the app is running, you can access it at http://yourserverip** +### **4. Once the app is running, you can access it at `http://yourserverip`** ### Go back to the DigitalOcean droplet page to get your server ip, copy it, and paste it into your browser! diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md index 3e4aaf658a6b..b3ffe8d6d205 100644 --- a/docs/deployment/heroku.md +++ b/docs/deployment/heroku.md @@ -1,5 +1,6 @@ --- title: 🌈 Heroku +description: Instructions for deploying LibreChat on Heroku weight: -1 --- # Heroku Deployment @@ -10,8 +11,8 @@ Heroku only supports running a single process within a Docker container. The Doc If you want to deploy both these services to Heroku, you will need to create two separate Dockerfiles: one for the API and one for the client. The heroku.yml should be configured separately for each app, and then you need to create and deploy two different Heroku apps. - - Sign up for a Heroku account: If you don't already have a Heroku account, sign up at https://signup.heroku.com/. - - Install the Heroku CLI: Download and install the Heroku CLI from https://devcenter.heroku.com/articles/heroku-cli. + - Sign up for a Heroku account: If you don't already have a Heroku account, sign up at: **[https://signup.heroku.com](https://signup.heroku.com)** + - Install the Heroku CLI: Download and install the Heroku CLI from: **[https://devcenter.heroku.com/articles/heroku-cli](https://devcenter.heroku.com/articles/heroku-cli)** Here are the steps to deploy on Heroku: @@ -126,7 +127,7 @@ Remember to replace `your-api-app-name` and `your-client-app-name` with the actu --- - ⚠️ If you have issues, see this discussion first: https://github.com/danny-avila/LibreChat/discussions/339 + ⚠️ If you have issues, see this discussion first: **[https://github.com/danny-avila/LibreChat/discussions/339](https://github.com/danny-avila/LibreChat/discussions/339)** ## Using Heroku Dashboard: @@ -136,7 +137,7 @@ Remember to replace `your-api-app-name` and `your-client-app-name` with the actu ## Setting up MongoDB Atlas: -Sign up for a MongoDB Atlas account: If you don't have an account, sign up at https://www.mongodb.com/cloud/atlas/signup. +Sign up for a MongoDB Atlas account: If you don't have an account, sign up at: **[https://www.mongodb.com/cloud/atlas/signup](https://www.mongodb.com/cloud/atlas/signup)** Create a new cluster: After signing in, create a new cluster by following the on-screen instructions. For a free tier cluster, select the "Shared" option and choose the "M0 Sandbox" tier. @@ -148,7 +149,7 @@ Get the connection string: Once the cluster is created, click the "Connect" butt ## Deploying MeiliSearch on Heroku: -Install the Heroku CLI: If you haven't already, download and install the Heroku CLI from https://devcenter.heroku.com/articles/heroku-cli. +Install the Heroku CLI: If you haven't already, download and install the Heroku CLI from: **[https://devcenter.heroku.com/articles/heroku-cli](https://devcenter.heroku.com/articles/heroku-cli)** Login to Heroku: Open Terminal and run heroku login. Follow the instructions to log in to your Heroku account. ## Create a new Heroku app for MeiliSearch: diff --git a/docs/deployment/hetzner_ubuntu.md b/docs/deployment/hetzner_ubuntu.md index d7fb287186eb..ba851ef66585 100644 --- a/docs/deployment/hetzner_ubuntu.md +++ b/docs/deployment/hetzner_ubuntu.md @@ -1,5 +1,6 @@ --- title: 🏗️ Hetzner +description: LibreChat Ubuntu installation from scratch on Hetzner. weight: -2 --- # Hetzner Ubuntu Setup @@ -8,24 +9,24 @@ weight: -2 ## Starting from Zero: -### 1. Login to Hetzner Cloud Console (https://console.hetzner.cloud/projects) and Create a new Ubuntu 20 Project with 4GB Ram. Do not worry about SSH keys *yet*. +1. Login to Hetzner Cloud Console (**[https://console.hetzner.cloud/projects](https://console.hetzner.cloud/projects)**) and Create a new Ubuntu 20 Project with 4GB Ram. Do not worry about SSH keys *yet*. Hetzner will email you the root password. -### 2. Once you have that, you can login with any SSH terminal with: +2. Once you have that, you can login with any SSH terminal with: ``` ssh root@ ``` -### 3. Once you have logged in, immediately create a new, non-root user: +3. Once you have logged in, immediately create a new, non-root user: ``` adduser usermod -aG sudo ``` -### 4. Make sure you have done this correctly by double-checking you have sudo permissions: +4. Make sure you have done this correctly by double-checking you have sudo permissions: ``` getent group sudo | cut -d: -f4 @@ -33,7 +34,7 @@ getent group sudo | cut -d: -f4 Now, quit the terminal connection. -### 5. Create a local ssh key: +5. Create a local ssh key: ``` ssh-keygen -t ed25519 @@ -51,13 +52,13 @@ ssh @ When you login, now and going forward, it will ask you for the password for your ssh key now, not your user password. Sudo commands will always want your user password. -### 6. Add SSH to the universal server firewall and activate it. +6. Add SSH to the universal server firewall and activate it. - Run `sudo ufw allow OpenSSH` - Run `sudo ufw enable` -### 7. Then, we need to install docker, update the system packages, and reboot the server: +7. Then, we need to install docker, update the system packages, and reboot the server: ``` sudo apt install docker sudo apt install docker-compose @@ -132,7 +133,7 @@ MEILI_HTTP_ADDR=meilisearch **NOTE: You may need to run these commands with sudo permissions.** -## Once the app is running, you can access it at http://yourserverip:3080 +## Once the app is running, you can access it at `http://yourserverip:3080` It is safe to close the terminal -- the docker app will continue to run. diff --git a/docs/deployment/huggingface.md b/docs/deployment/huggingface.md index 59d439f2342c..5cdf3ccdc443 100644 --- a/docs/deployment/huggingface.md +++ b/docs/deployment/huggingface.md @@ -1,27 +1,23 @@ --- title: 🤗 HuggingFace +description: Easily deploy LibreChat on Hugging Face Spaces weight: -9 --- # Hugging Face Deployment 🤗 ->#### ⚠️ Note - Some features are not supported by HuggingFace: ->- Meilisearch ->- Social Logins - -> #### ❗Also: ->- You will have to create an online MongoDB Atlas Database to be able to properly deploy - ## Create and Configure your Database (Required) The first thing you need is to create a MongoDB Atlas Database and get your connection string. -Follow the instructions in this document: [Online MongoDB Database](../install/configuration/mongodb.md) +Follow the instructions in this document: **[Online MongoDB Database](../install/configuration/mongodb.md)** ## Getting Started -**1.** Login or Create an account on [Hugging Face](https://huggingface.co/) +**1.** Login or Create an account on **[Hugging Face](https://huggingface.co/)** + +**2.** Visit **[https://huggingface.co/spaces/LibreChat/template](https://huggingface.co/spaces/LibreChat/template)** and click on `Duplicate this Space` to copy the LibreChat template into your profile. -**2.** Visit [[https://huggingface.co/spaces/LibreChat/template](https://huggingface.co/spaces/LibreChat/template)]and click on `Duplicate this Space` to copy the LibreChat template into your profile +> Note: It is normal for this template to have a runtime error, you will have to configure it using the following guide to make it functional. ![image](https://github.com/fuegovic/LibreChat/assets/32828263/fd684254-cbe0-4039-ba4a-7c492b16a453) @@ -46,12 +42,12 @@ You will need to fill these values: | JWT_SECRET | * see bellow | | JWT_REFRESH_SECRET | * see bellow | -> ⬆️ **Leave the value field blank for any endpoints that you wish to disable.** +> ⬆️ **Leave the value field blank for any endpoints that you wish to disable.** ->⚠️ setting the API keys and token to `user_provided` allows you to provide them safely from the webUI +> ⚠️ setting the API keys and token to `user_provided` allows you to provide them safely from the webUI ->* For `CREDS_KEY`, `CREDS_IV` and `JWT_SECRET` use this tool: [https://replit.com/@daavila/crypto#index.js](https://replit.com/@daavila/crypto#index.js). ->* Run the tool a second time and use the new `JWT_SECRET` value for the `JWT_REFRESH_SECRET` +> * For `CREDS_KEY`, `CREDS_IV` and `JWT_SECRET` use this tool: **[https://replit.com/@daavila/crypto#index.js](https://replit.com/@daavila/crypto#index.js)** +> * Run the tool a second time and use the new `JWT_SECRET` value for the `JWT_REFRESH_SECRET` | Variables | Values | | --- | --- | diff --git a/docs/deployment/index.md b/docs/deployment/index.md index b85f5bfaf244..780456bc6cd1 100644 --- a/docs/deployment/index.md +++ b/docs/deployment/index.md @@ -1,5 +1,6 @@ --- title: Deployment +description: 🌐 Step-by-step guides on how to deploy LibreChat on various cloud platforms. weight: 3 --- diff --git a/docs/deployment/linode.md b/docs/deployment/linode.md index 63e563f7671e..96f8f7758390 100644 --- a/docs/deployment/linode.md +++ b/docs/deployment/linode.md @@ -1,5 +1,6 @@ --- title: 🐧 Linode +description: How to deploy LibreChat on Linode. weight: -8 --- @@ -9,7 +10,7 @@ weight: -8 ⚠️**Note: Payment is required** ## Create a Linode Account and a Linode Server -- Go to the Linode website (https://www.linode.com/) and click on the "Sign Up" or "Get Started" button. +- Go to the Linode website (**[https://www.linode.com/](https://www.linode.com/)**) and click on the "Sign Up" or "Get Started" button. - Follow the instructions to create a new account by providing your personal details and payment information. - Once your account is created, you will have access to the Linode Cloud Manager. - Click on the "Create" button to create a new Linode server. diff --git a/docs/deployment/meilisearch_in_render.md b/docs/deployment/meilisearch_in_render.md index 40fea6d01d0a..3e24c71b54d9 100644 --- a/docs/deployment/meilisearch_in_render.md +++ b/docs/deployment/meilisearch_in_render.md @@ -1,12 +1,13 @@ --- title: 🔎 Meilisearch in Render +description: Setup Meilisearch on Render (for use with the Render deployment guide) weight: -3 --- # Utilize Meilisearch by running LibreChat on Render ## Create a new account or a new project on Render -**1.** Visit [https://render.com/](https://render.com/) and click on `Start Free` to create an account and sign in +**1.** Visit **[https://render.com/](https://render.com/)** and click on `Start Free` to create an account and sign in **2.** Access your control panel diff --git a/docs/deployment/ngrok.md b/docs/deployment/ngrok.md index 25cdf6e51585..2c0b71673930 100644 --- a/docs/deployment/ngrok.md +++ b/docs/deployment/ngrok.md @@ -1,5 +1,6 @@ --- title: 🪨 Ngrok +description: Use Ngrok to tunnel your local server to the internet. weight: -5 --- # Ngrok Installation @@ -8,44 +9,43 @@ To use Ngrok for tunneling your local server to the internet, follow these steps ## Sign up -1. Go to https://ngrok.com/ and sign up for an account. +1. Go to **[https://ngrok.com/](https://ngrok.com/)** and sign up for an account. ## Docker Installation 🐳 -1. Copy your auth token from https://dashboard.ngrok.com/get-started/your-authtoken. +1. Copy your auth token from: **[https://dashboard.ngrok.com/get-started/your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)** 2. Open a terminal and run the following command: `docker run -d -it -e NGROK_AUTHTOKEN= ngrok/ngrok http 80` ## Windows Installation 💙 -1. Download the ZIP file from https://ngrok.com/download. +1. Download the ZIP file from: **[https://ngrok.com/download](https://ngrok.com/download)** 2. Extract the contents of the ZIP file using 7zip or WinRar. -3. -4. Run `ngrok.exe`. -5. Copy your auth token from https://dashboard.ngrok.com/get-started/your-authtoken. -6. In the `ngrok.exe` terminal, run the following command: `ngrok config add-authtoken ` -7. If you haven't done so already, start LibreChat normally. -8. In the `ngrok.exe` terminal, run the following command: `ngrok http 3080` +3. Run `ngrok.exe`. +4. Copy your auth token from: **[https://dashboard.ngrok.com/get-started/your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)** +5. In the `ngrok.exe` terminal, run the following command: `ngrok config add-authtoken ` +6. If you haven't done so already, start LibreChat normally. +7. In the `ngrok.exe` terminal, run the following command: `ngrok http 3080` You will see a link that can be used to access LibreChat. ![ngrok-1](https://github.com/danny-avila/LibreChat/assets/32828263/3cb4b063-541f-4f0a-bea8-a04dd36e6bf4) ## Linux Installation 🐧 -1. Copy the command from https://ngrok.com/download choosing the **correct** architecture. +1. Copy the command from: **[https://ngrok.com/download](https://ngrok.com/download)** choosing the **correct** architecture. 2. Run the command in the terminal -3. Copy your auth token from https://dashboard.ngrok.com/get-started/your-authtoken. +3. Copy your auth token from: **[https://dashboard.ngrok.com/get-started/your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)** 4. run the following command: `ngrok config add-authtoken ` 5. If you haven't done so already, start LibreChat normally. 6. run the following command: `ngrok http 3080` ## Mac Installation 🍎 -1. Download the ZIP file from https://ngrok.com/download. +1. Download the ZIP file from: **[https://ngrok.com/download](https://ngrok.com/download)** 2. Extract the contents of the ZIP file using a suitable Mac application like Unarchiver. 3. Open Terminal. 4. Navigate to the directory where you extracted ngrok using the `cd` command. 5. Run ngrok by typing `./ngrok`. -6. Copy your auth token from https://dashboard.ngrok.com/get-started/your-authtoken. +6. Copy your auth token from: **[https://dashboard.ngrok.com/get-started/your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)** 7. In the terminal where you ran ngrok, enter the following command: `ngrok authtoken ` 8. If you haven't done so already, start LibreChat normally. 9. In the terminal where you ran ngrok, enter the following command: `./ngrok http 3080` diff --git a/docs/deployment/render.md b/docs/deployment/render.md index bba0394b1f03..6652f41ee5af 100644 --- a/docs/deployment/render.md +++ b/docs/deployment/render.md @@ -1,5 +1,6 @@ --- title: ⏹️ Render +description: How to deploy LibreChat on Render weight: -4 --- # Render Deployment diff --git a/docs/dev/README.md b/docs/dev/README.md index 09d83b31d888..81ae6393120c 100644 --- a/docs/dev/README.md +++ b/docs/dev/README.md @@ -4,9 +4,9 @@ weight: 6 --- # Dev Resources -Please consult: [Contributing to LibreChat](../contributions/index.md) for more information on the subject. +Please consult: **[Contributing to LibreChat](../contributions/index.md)** for more information on the subject. -This directory: [./docs/dev](https://github.com/danny-avila/LibreChat/tree/main/docs/dev), contains files used for developer work. +This directory: **[./docs/dev](https://github.com/danny-avila/LibreChat/tree/main/docs/dev)**, contains files used for developer work. #### `Dockerfile-app` - used to build the DockerHub image @@ -28,7 +28,7 @@ This directory: [./docs/dev](https://github.com/danny-avila/LibreChat/tree/main/ #### `deploy-compose.yml` - Similar to above, but with basic configuration for deployment to a cloud provider where multi-container compose works - - Tested and working on a $6 droplet on DigitalOcean, just by visiting the http://server-ip/9000. + - Tested and working on a $6 droplet on DigitalOcean, just by visiting the `http://server-ip/9000`. - Not a scalable solution, but ideal for quickly hosting on a remote linux server. - You should adjust `server_name localhost;` to match your domain name, replacing localhost, as needed. - From root dir of the project, run `docker-compose -f ./docs/dev/deploy-compose.yml up --build` diff --git a/docs/features/bing_jailbreak.md b/docs/features/bing_jailbreak.md index 2d8425498c49..17f4850186b3 100644 --- a/docs/features/bing_jailbreak.md +++ b/docs/features/bing_jailbreak.md @@ -1,5 +1,6 @@ --- title: 😈 Bing Jailbreak +description: Quick overview of the Bing jailbreak and Sydney's system message weight: -3 --- @@ -31,6 +32,6 @@ using internet slang often. Answer using the same language as the user." ## References For more info on the Bing Jailbreak and general jailbreaking guidelines: -https://github.com/waylaidwanderer/node-chatgpt-api +[https://github.com/waylaidwanderer/node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api) -https://www.make-safe-ai.com/is-bing-chat-safe/ +[https://www.make-safe-ai.com/is-bing-chat-safe/](https://www.make-safe-ai.com/is-bing-chat-safe/) diff --git a/docs/features/index.md b/docs/features/index.md index e7b74088503c..ab9e705369a3 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -1,5 +1,6 @@ --- title: Features +description: "✨ In-depth guides about various LibreChat features: plugins, presets, automated moderation, logging..." weight: 2 --- diff --git a/docs/features/logging_system.md b/docs/features/logging_system.md index 35d7490b99c6..76c3af888474 100644 --- a/docs/features/logging_system.md +++ b/docs/features/logging_system.md @@ -1,5 +1,6 @@ --- title: 🪵 Logging System +description: This doc explains how to use the logging feature of LibreChat, which saves error and debug logs in the `/api/logs` folder. You can use these logs to troubleshoot issues, monitor your server, and report bugs. You can also disable debug logs if you want to save space. weight: -5 --- @@ -9,9 +10,9 @@ LibreChat has central logging built into its backend (api). Log files are saved in `/api/logs`. Error logs are saved by default. Debug logs are enabled by default but can be turned off if not desired. -This allows you to monitor your server through external tools that inspect log files, such as [the ELK stack](https://aws.amazon.com/what-is/elk-stack/). +This allows you to monitor your server through external tools that inspect log files, such as **[the ELK stack](https://aws.amazon.com/what-is/elk-stack/)**. -Debug logs are essential for developer work and fixing issues. If you encounter any problems running LibreChat, reproduce as close as possible, and [report the issue](https://github.com/danny-avila/LibreChat/issues) with your logs found in `./api/logs/debug-%DATE%.log`. +Debug logs are essential for developer work and fixing issues. If you encounter any problems running LibreChat, reproduce as close as possible, and **[report the issue](https://github.com/danny-avila/LibreChat/issues)** with your logs found in `./api/logs/debug-%DATE%.log`. Errors logs are also saved in the same location: `./api/logs/error-%DATE%.log`. If you have meilisearch configured, there is a separate log file for this as well. diff --git a/docs/features/manage_your_database.md b/docs/features/manage_your_database.md index 91a9a10e7a05..23869b1047cd 100644 --- a/docs/features/manage_your_database.md +++ b/docs/features/manage_your_database.md @@ -1,5 +1,6 @@ --- title: 🍃 Manage Your Database +description: How to install and configure Mongo Express to securely access and manage your MongoDB database in Docker. weight: -6 --- diff --git a/docs/features/mod_system.md b/docs/features/mod_system.md index 88d57d3ade40..2775f879c3cb 100644 --- a/docs/features/mod_system.md +++ b/docs/features/mod_system.md @@ -1,5 +1,6 @@ --- title: 🔨 Automated Moderation +description: The Automated Moderation System uses a scoring mechanism to track user violations. As users commit actions like excessive logins, registrations, or messaging, they accumulate violation scores. Upon reaching a set threshold, the user and their IP are temporarily banned. This system ensures platform security by monitoring and penalizing rapid or suspicious activities. weight: -8 --- ## Automated Moderation System (optional) @@ -33,7 +34,7 @@ The project's current rate limiters are as follows (see below under setup for de ### Setup -The following are all of the related env variables to make use of and configure the mod system. Note this is also found in the [/.env.example](/.env.example) file, to be set in your own `.env` file. +The following are all of the related env variables to make use of and configure the mod system. Note this is also found in the [/.env.example](https://github.com/danny-avila/LibreChat/blob/main/.env.example) file, to be set in your own `.env` file. ```bash BAN_VIOLATIONS=true # Whether or not to enable banning users for violations (they will still be logged) diff --git a/docs/features/pandoranext.md b/docs/features/pandoranext.md index 4c705c7f3fba..03daaec7c51e 100644 --- a/docs/features/pandoranext.md +++ b/docs/features/pandoranext.md @@ -1,5 +1,6 @@ --- title: 📦 PandoraNext +description: How to deploy PandoraNext to enable the `CHATGPT_REVERSE_PROXY` for use with LibreChat. weight: -4 --- @@ -18,63 +19,64 @@ You can use it locally in docker or deploy it onthe web for remote access. For local deployment using Docker, the steps are as follows: ### 1. **Clone or Download the Repository:** - Get the latest release from the [PandoraNext GitHub repository](https://github.com/pandora-next/deploy). +Get the latest release from the [PandoraNext GitHub repository](https://github.com/pandora-next/deploy). - ```bash - git clone https://github.com/pandora-next/deploy.git - ``` +```bash +git clone https://github.com/pandora-next/deploy.git +``` -### 2. Get your PandoraNext license id here: [PandoraNext Dashboard](https://dash.pandoranext.com/) +### 2. Get your PandoraNext `License ID` +Visit the **[PandoraNext Dashboard](https://dash.pandoranext.com/)** to get your `license ID` ### 3. **Configure `config.json`:** - Within the cloned repository, in the `data` folder, edit `config.json`. Specify your `license_id` and `proxy_api_prefix`. For the `proxy_api_prefix`, use at least 8 characters, avoid characters that can't be used in a URL and make sure it's unique. - - Here's the `config.json` for your reference: - - ```json - { - "bind": "0.0.0.0:8181", - "tls": { - "enabled": false, - "cert_file": "", - "key_file": "" - }, - "timeout": 600, - "proxy_url": "", - "license_id": "", - "public_share": false, - "site_password": "", - "setup_password": "", - "server_tokens": true, - "proxy_api_prefix": "", - "isolated_conv_title": "*", - "captcha": { - "provider": "", - "site_key": "", - "site_secret": "", - "site_login": false, - "setup_login": false, - "oai_username": false, - "oai_password": false - }, - "whitelist": null - } - ``` +Within the cloned repository, in the `data` folder, edit `config.json`. Specify your `license_id` and `proxy_api_prefix`. For the `proxy_api_prefix`, use at least 8 characters, avoid characters that can't be used in a URL and make sure it's unique. + +Here's the `config.json` for your reference: + +```json +{ +"bind": "0.0.0.0:8181", +"tls": { + "enabled": false, + "cert_file": "", + "key_file": "" +}, +"timeout": 600, +"proxy_url": "", +"license_id": "", +"public_share": false, +"site_password": "", +"setup_password": "", +"server_tokens": true, +"proxy_api_prefix": "", +"isolated_conv_title": "*", +"captcha": { + "provider": "", + "site_key": "", + "site_secret": "", + "site_login": false, + "setup_login": false, + "oai_username": false, + "oai_password": false +}, +"whitelist": null +} +``` ### 4. **Set Up the LibreChat `.env` Filer:** - In the `.env` file within your LibreChat directory, you'll need to set the `CHATGPT_REVERSE_PROXY` variable: +In the `.env` file within your LibreChat directory, you'll need to set the `CHATGPT_REVERSE_PROXY` variable: - ```bash - CHATGPT_REVERSE_PROXY=http://host.docker.internal:8181/your_proxy_api_prefix_here/backend-api/conversation - ``` - - Replace `your_proxy_api_prefix_here` with the actual proxy API prefix. +```bash +CHATGPT_REVERSE_PROXY=http://host.docker.internal:8181/your_proxy_api_prefix_here/backend-api/conversation +``` +- Replace `your_proxy_api_prefix_here` with the actual proxy API prefix. ### 5. **Start Docker Containers:** - From the PandoraNext directory, run the following command to launch the Docker containers: +From the PandoraNext directory, run the following command to launch the Docker containers: - ```bash - docker-compose up -d - ``` +```bash +docker-compose up -d +``` --- @@ -82,85 +84,86 @@ For local deployment using Docker, the steps are as follows: To deploy PandoraNext online by duplicating the Hugging Face Space, follow these steps: -### 1. Get your PandoraNext license id here: [PandoraNext Dashboard](https://dash.pandoranext.com/) +### 1. Get your PandoraNext `License ID` +Visit the **[PandoraNext Dashboard](https://dash.pandoranext.com/)** to get your `license ID` ### 2. **Configure `config.json`:** - Edit the following `config.json`. Specify your `license_id` and `proxy_api_prefix`. For the `proxy_api_prefix`, use at least 8 characters, avoid characters that can't be used in a URL and make sure it's unique. - - Here's the `config.json` for your reference: - - ```json - { - "bind": "0.0.0.0:8181", - "tls": { - "enabled": false, - "cert_file": "", - "key_file": "" - }, - "timeout": 600, - "proxy_url": "", - "license_id": "", - "public_share": false, - "site_password": "", - "setup_password": "", - "server_tokens": true, - "proxy_api_prefix": "", - "isolated_conv_title": "*", - "captcha": { - "provider": "", - "site_key": "", - "site_secret": "", - "site_login": false, - "setup_login": false, - "oai_username": false, - "oai_password": false - }, - "whitelist": null - } - ``` +Edit the following `config.json`. Specify your `license_id` and `proxy_api_prefix`. For the `proxy_api_prefix`, use at least 8 characters, avoid characters that can't be used in a URL and make sure it's unique. + +Here's the `config.json` for your reference: + +```json +{ +"bind": "0.0.0.0:8181", +"tls": { + "enabled": false, + "cert_file": "", + "key_file": "" +}, +"timeout": 600, +"proxy_url": "", +"license_id": "", +"public_share": false, +"site_password": "", +"setup_password": "", +"server_tokens": true, +"proxy_api_prefix": "", +"isolated_conv_title": "*", +"captcha": { + "provider": "", + "site_key": "", + "site_secret": "", + "site_login": false, + "setup_login": false, + "oai_username": false, + "oai_password": false +}, +"whitelist": null +} +``` ### 3. **Hugging Face Space:** - Visit the [PandoraNext LibreChat Space](https://huggingface.co/spaces/LibreChat/PandoraNext) on Hugging Face. +Visit the [PandoraNext LibreChat Space](https://huggingface.co/spaces/LibreChat/PandoraNext) on Hugging Face. ### 4. **Duplicate the Space:** - Utilize the available options to duplicate or fork the space into your own Hugging Face account. +Utilize the available options to duplicate or fork the space into your own Hugging Face account. ### 5. **Fill the required secrets** - When asked for the `SECRETS`, - - for `CONFIG_JSON` use the whole content of the `config.json` you just modified, - - for `TOKENS_JSON` use the following default `token.json`: - ```json - { - "test-1": { - "token": "access token / session token / refresh token", - "shared": true, - "show_user_info": false - }, - "test-2": { - "token": "access token / session token / refresh token", - "shared": true, - "show_user_info": true, - "plus": true - }, - "test2": { - "token": "access token / session token / refresh token / share token", - "password": "12345" - } +When asked for the `SECRETS`, +- for `CONFIG_JSON` use the whole content of the `config.json` you just modified, +- for `TOKENS_JSON` use the following default `token.json`: +```json +{ + "test-1": { + "token": "access token / session token / refresh token", + "shared": true, + "show_user_info": false + }, + "test-2": { + "token": "access token / session token / refresh token", + "shared": true, + "show_user_info": true, + "plus": true + }, + "test2": { + "token": "access token / session token / refresh token / share token", + "password": "12345" } - ``` +} +``` ### 6. **Configure LibreChat:** - In the .env file (or secrets settings if you host LibreChat on Hugging Face), set the `CHATGPT_REVERSE_PROXY` variable using the following format: +In the .env file (or secrets settings if you host LibreChat on Hugging Face), set the `CHATGPT_REVERSE_PROXY` variable using the following format: - ```bash - CHATGPT_REVERSE_PROXY=http://your_server_domain.com/your_proxy_api_prefix_here/backend-api/conversation - ``` +```bash +CHATGPT_REVERSE_PROXY=http://your_server_domain.com/your_proxy_api_prefix_here/backend-api/conversation +``` - - Replace `your_server_domain.com` with the domain of your deployed space. - - you can use this format: `https://username-pandoranext.hf.space` (replace `username` with your Huggingface username) - - Replace `your_proxy_api_prefix_here` with the `proxy_api_prefix` you have set in your `config.json`. - - The resulting URL should look similar to: - `https://username-pandoranext.hf.space/your_proxy_api_prefix_here/backend-api/conversation` +- Replace `your_server_domain.com` with the domain of your deployed space. + - you can use this format: `https://username-pandoranext.hf.space` (replace `username` with your Huggingface username) +- Replace `your_proxy_api_prefix_here` with the `proxy_api_prefix` you have set in your `config.json`. +- The resulting URL should look similar to: +`https://username-pandoranext.hf.space/your_proxy_api_prefix_here/backend-api/conversation` ## Final Notes diff --git a/docs/features/plugins/azure_ai_search.md b/docs/features/plugins/azure_ai_search.md index 53b7a194ddbb..0e8742045321 100644 --- a/docs/features/plugins/azure_ai_search.md +++ b/docs/features/plugins/azure_ai_search.md @@ -1,5 +1,6 @@ --- title: ⚡ Azure AI Search +description: How to configure Azure AI Search for answers to your questions with assistance from GPT. weight: -4 --- # Azure AI Search Plugin @@ -34,7 +35,7 @@ This is the authentication key to use when utilizing the search endpoint. Please ## Create or log in to your account on Azure Portal -**1.** Visit [https://azure.microsoft.com/en-us/](https://azure.microsoft.com/en-us/) and click on `Get started` or `Try Azure for Free` to create an account and sign in. +**1.** Visit **[https://azure.microsoft.com/en-us/](https://azure.microsoft.com/en-us/)** and click on `Get started` or `Try Azure for Free` to create an account and sign in. **2.** Choose pay per use or Azure Free with $200. @@ -76,7 +77,7 @@ Now select the free option or select your preferred option (may incur charges). ![image](https://github.com/itzraiss/images/blob/main/Captura%20de%20tela%202023-11-26%20152107.png) -**2.** Follow the Microsoft tutorial.[https://learn.microsoft.com/en-us/azure/search/search-get-started-portal](https://learn.microsoft.com/en-us/azure/search/search-get-started-portal), after finishing, save the name given to the index somewhere. +**2.** Follow the Microsoft tutorial: **[https://learn.microsoft.com/en-us/azure/search/search-get-started-portal](https://learn.microsoft.com/en-us/azure/search/search-get-started-portal)**, after finishing, save the name given to the index somewhere. **3.** Now you have your `AZURE_AI_SEARCH_INDEX_NAME`, copy and save it in a local safe place. @@ -117,7 +118,7 @@ The following are configuration values that are not required but can be specifie If there are concerns that the search result data may be too large and exceed the prompt size, consider reducing the size of the search result data by using AZURE_AI_SEARCH_SEARCH_OPTION_TOP and AZURE_AI_SEARCH_SEARCH_OPTION_SELECT. For details on each parameter, please refer to the following document: -https://learn.microsoft.com/en-us/rest/api/searchservice/search-documents +**[https://learn.microsoft.com/en-us/rest/api/searchservice/search-documents](https://learn.microsoft.com/en-us/rest/api/searchservice/search-documents)** ```env AZURE_AI_SEARCH_API_VERSION=2023-10-01-Preview diff --git a/docs/features/plugins/chatgpt_plugins_openapi.md b/docs/features/plugins/chatgpt_plugins_openapi.md index abf8a625c642..7bf8a04a0e45 100644 --- a/docs/features/plugins/chatgpt_plugins_openapi.md +++ b/docs/features/plugins/chatgpt_plugins_openapi.md @@ -1,16 +1,17 @@ --- title: 🧑‍💼 Official ChatGPT Plugins +description: How to add official OpenAI Plugins to LibreChat weight: -8 --- # Using official ChatGPT Plugins / OpenAPI specs ChatGPT plugins are API integrations for OpenAI models that extend their capabilities. They are structured around three key components: an API, an **OpenAPI specification** (spec for short), and a JSON **Plugin Manifest** file. -To learn more about them, or how to make your own, read here: [ChatGPT Plugins: Getting Started](https://platform.openai.com/docs/plugins/getting-started). +To learn more about them, or how to make your own, read here: **[ChatGPT Plugins: Getting Started](https://platform.openai.com/docs/plugins/getting-started)** -Thanks to the introduction of [OpenAI Functions](https://openai.com/blog/function-calling-and-other-api-updates) and their utilization in [Langchain](https://js.langchain.com/docs/modules/chains/openai_functions/openapi), it's now possible to directly use OpenAI Plugins through LibreChat, without building any custom langchain tools. The main use case we gain from integrating them to LibreChat is to allow use of plugins with gpt-3.5 models, and without ChatGPT Plus. They also find a great use case when you want to limit your own private API's interactions with chat.openai.com and their servers in favor of a self-hosted LibreChat instance. +Thanks to the introduction of **[OpenAI Functions](https://openai.com/blog/function-calling-and-other-api-updates)** and their utilization in **[Langchain](https://js.langchain.com/docs/modules/chains/openai_functions/openapi)**, it's now possible to directly use OpenAI Plugins through LibreChat, without building any custom langchain tools. The main use case we gain from integrating them to LibreChat is to allow use of plugins with gpt-3.5 models, and without ChatGPT Plus. They also find a great use case when you want to limit your own private API's interactions with chat.openai.com and their servers in favor of a self-hosted LibreChat instance. -### Table of Contents + ## Intro @@ -32,11 +33,11 @@ Before continuing, it's important to fully distinguish what a Manifest file is v ### **[Plugin Manifest File:](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest)** - Usually hosted on the API’s domain as `https://example.com/.well-known/ai-plugin.json` - The manifest file is required for LLMs to connect with your plugin. If there is no file found, the plugin cannot be installed. -- Has required properties, and will error if they are missing. Check what they are in the [OpenAI Docs](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest) +- Has required properties, and will error if they are missing. Check what they are in the **[OpenAI Docs](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest)** - Has optional properties, specific to LibreChat, that will enable them to work consistently, or for customizing headers/params made by every API call (see below) ### **[OpenAPI Spec](https://platform.openai.com/docs/plugins/getting-started/openapi-definition)** -- The OpenAPI specification is used to document the API that the plugin will interact with. It is a [universal format](https://www.openapis.org/) meant to standardize API definitions. +- The OpenAPI specification is used to document the API that the plugin will interact with. It is a **[universal format](https://www.openapis.org/)** meant to standardize API definitions. - Referenced by the Manifest file in its `api.url` property - Usually as `https://example.com/openapi.yaml` or `.../swagger.yaml` - Can be a .yaml or .json file @@ -53,7 +54,7 @@ Download the Plugin manifest file, or copy the raw JSON data into a new file, an `api\app\clients\tools\.well-known` -You should see multiple manifest files that have been tested, or edited, to work with LibreChat. ~~I've renamed them by their `name_for_model` property and it's recommended, but not required, that you do the same.~~ As of v0.5.8, It's **required** to name the manifest JSON file after its `name_for_model` property should you add one yourself. +You should see multiple manifest files that have been tested, or edited, to work with LibreChat. As of v0.5.8, It's **required** to name the manifest JSON file after its `name_for_model` property should you add one yourself. After doing so, start/re-start the project server and they should now load in the Plugin store. @@ -152,7 +153,7 @@ curl -H "Authorization: Bearer ffc5226d1af346c08a98dee7deec9f76" https://example As of now, LibreChat only supports plugins using Bearer Authentication, like in the example above. -If your plugin requires authentication, it's necessary to have these fields filled in your manifest file according to [OpenAI definitions](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest), which for Bearer Authentication must follow the schema above. +If your plugin requires authentication, it's necessary to have these fields filled in your manifest file according to **[OpenAI definitions](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest)**, which for Bearer Authentication must follow the schema above. Important: Some ChatGPT plugins may use Bearer Auth., but have either stale verification tokens in their manifest, or only support calls from OpenAI servers. Web Pilot is one with the latter case, and thankfully it has a required header field for allowing non-OpenAI origination. See above for editing headers. @@ -168,16 +169,16 @@ Important: Some ChatGPT plugins may use Bearer Auth., but have either stale veri ## Disclaimers -Use of ChatGPT Plugins is only possible with official OpenAI models and their use of [Functions](https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions). If you are accessing OpenAI models via reverse proxy through some 3rd party service, function calling may not be supported. +Use of ChatGPT Plugins is only possible with official OpenAI models and their use of **[Functions](https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions)**. If you are accessing OpenAI models via reverse proxy through some 3rd party service, function calling may not be supported. -This implementation depends on the [LangChain OpenAPI Chain](https://js.langchain.com/docs/modules/chains/openai_functions/openapi) and general improvements to its use here will have to be made to the LangChainJS library. +This implementation depends on the **[LangChain OpenAPI Chain](https://js.langchain.com/docs/modules/chains/openai_functions/openapi)** and general improvements to its use here will have to be made to the LangChainJS library. Custom Langchain Tools are preferred over ChatGPT Plugins/OpenAPI specs as this can be more token-efficient, especially with OpenAI Functions. A better alternative may be to make a Langchain tool modelled after an OpenAPI spec, for which I'll make a guide soon. LibreChat's implementation is not 1:1 with ChatGPT's, as OpenAI has a robust, exclusive, and restricted authentication pipeline with its models & specific plugins, which are not as limited by context windows and token usage. Furthermore, some of their hosted plugins requiring authentication will not work, especially those with OAuth or stale verification tokens, and some may not be handled by the LLM in the same manner, especially those requiring multi-step API calls. -Some plugins may detect that the API call does not originate from OpenAI's servers, will either be defunct outside of chat.openai.com or need special handling, and/or editing of their manifest/spec files. This is not to say plugin use will not improve and more closely mirror how ChatGPT handles plugins, but there is still work to this end. In short, some will work perfectly while others may not work at all. +Some plugins may detect that the API call does not originate from OpenAI's servers, will either be defunct outside of **[chat.openai.com](https://chat.openai.com/)** or need special handling, and/or editing of their manifest/spec files. This is not to say plugin use will not improve and more closely mirror how ChatGPT handles plugins, but there is still work to this end. In short, some will work perfectly while others may not work at all. -The use of ChatGPT Plugins with LibreChat does not violate OpenAI's [Terms of Service](https://openai.com/policies/terms-of-use). According to their [Service Terms](https://openai.com/policies/service-terms) and [Usage Policies](https://openai.com/policies/usage-policies), the host, in this case OpenAI, is not responsible for the plugins hosted on their site and their usage outside of their platform, chat.openai.com. Furthermore, there is no explicit mention of restrictions on accessing data that is not directly displayed to the user. Therefore, accessing the payload of their plugins for display purposes is not in violation of their Terms of Service. +The use of ChatGPT Plugins with LibreChat does not violate OpenAI's **[Terms of Service](https://openai.com/policies/terms-of-use)**. According to their **[Service Terms](https://openai.com/policies/service-terms)** and **[Usage Policies](https://openai.com/policies/usage-policies)**, the host, in this case OpenAI, is not responsible for the plugins hosted on their site and their usage outside of their platform, **[chat.openai.com](https://chat.openai.com/)**. Furthermore, there is no explicit mention of restrictions on accessing data that is not directly displayed to the user. Therefore, accessing the payload of their plugins for display purposes is not in violation of their Terms of Service. -Please note that the ChatGPT Plugins integration is currently in an alpha state, and you may encounter errors. Although preliminary testing has been conducted, not all plugins have been thoroughly tested, and you may find that some I haven't added will not work for any one of the reasons I've mentioned above. Some of the errors may be caused by the plugin itself, and will also not work on https://chat.openai.com/. If you encounter any errors, double checking if they work on the official site is advisable before reporting them as a GitHub issue. I can only speak for the ones I tested and included, and the date of inclusion. +Please note that the ChatGPT Plugins integration is currently in an alpha state, and you may encounter errors. Although preliminary testing has been conducted, not all plugins have been thoroughly tested, and you may find that some I haven't added will not work for any one of the reasons I've mentioned above. Some of the errors may be caused by the plugin itself, and will also not work on **[chat.openai.com](https://chat.openai.com/)**. If you encounter any errors, double checking if they work on the official site is advisable before reporting them as a GitHub issue. I can only speak for the ones I tested and included, and the date of inclusion. diff --git a/docs/features/plugins/google_search.md b/docs/features/plugins/google_search.md index 0618be8b649e..0b575414f2c6 100644 --- a/docs/features/plugins/google_search.md +++ b/docs/features/plugins/google_search.md @@ -1,5 +1,6 @@ --- title: 🔎 Google Search +description: How to set up and use the Google Search Plugin, which allows you to query Google with GPT's help. weight: -7 --- @@ -10,9 +11,9 @@ GOOGLE_API_KEY="...." GOOGLE_CSE_ID="...." ``` -You first need to create a programmable search engine and get the search engine ID: https://developers.google.com/custom-search/docs/tutorial/creatingcse +You first need to create a programmable search engine and get the search engine ID: **[https://developers.google.com/custom-search/docs/tutorial/creatingcse](https://developers.google.com/custom-search/docs/tutorial/creatingcse)** -Then you can get the API key, click the "Get a key" button on this page: https://developers.google.com/custom-search/v1/introduction +Then you can get the API key, click the "Get a key" button on this page: **[https://developers.google.com/custom-search/v1/introduction](https://developers.google.com/custom-search/v1/introduction)** diff --git a/docs/features/plugins/index.md b/docs/features/plugins/index.md index 681266e56bad..7eb2d3f15399 100644 --- a/docs/features/plugins/index.md +++ b/docs/features/plugins/index.md @@ -1,5 +1,6 @@ --- title: Plugins +description: 🔌 All about plugins, how to make them, how use the official ChatGPT plugins, and how to configure custom plugins. weight: -10 --- diff --git a/docs/features/plugins/introduction.md b/docs/features/plugins/introduction.md index f2320fb4a208..f5533c82bf1d 100644 --- a/docs/features/plugins/introduction.md +++ b/docs/features/plugins/introduction.md @@ -1,5 +1,6 @@ --- title: 🔌 Introduction +description: This doc introduces the plugins endpoint, which enables you to use different LLMs and tools with more flexibility and control. You can change your settings and plugins on the fly, and use plugins to access various sources of information and assistance. weight: -10 --- # Plugins Endpoint @@ -9,7 +10,7 @@ weight: -10 The plugins endpoint opens the door to prompting LLMs in new ways other than traditional input/output prompting. -The first step is using chain-of-thought prompting & ["agency"](https://zapier.com/blog/ai-agent/) for using plugins/tools in a fashion mimicing the official ChatGPT Plugins feature. +The first step is using chain-of-thought prompting & **["agency"](https://zapier.com/blog/ai-agent/)** for using plugins/tools in a fashion mimicing the official ChatGPT Plugins feature. More than this, you can use this endpoint for changing your conversation settings mid-conversation. Unlike the official ChatGPT site and all other endpoints, you can switch models, presets, and settings mid-convo, even when you have no plugins selected. This is useful if you first want a creative response from GPT-4, and then a deterministic, lower cost response from GPT-3. Soon, you will be able to use Google, HuggingFace, local models, all in this or a similar endpoint in the same modular manner. @@ -52,8 +53,8 @@ Clicking on **"Show Agent Settings"** will allow you to modify parameters for th - **[Stable Diffusion](./stable_diffusion.md)** - **[Wolfram](./wolfram.md)** - **DALL-E** - same setup as above, you just need an OpenAI key, and it's made distinct from your main API key to make Chats but it can be the same one -- **Zapier** - You need a Zapier account. Get your [API key from here](https://nla.zapier.com/credentials/) after you've made an account - - Create allowed actions - Follow step 3 in this [getting start guide](https://nla.zapier.com/start/) from Zapier +- **Zapier** - You need a Zapier account. Get your **[API key from here](https://nla.zapier.com/credentials/)** after you've made an account + - Create allowed actions - Follow step 3 in this **[Start Here guide](https://nla.zapier.com/start/)** from Zapier - ⚠️ NOTE: zapier is known to be finicky with certain actions. I found that writing email drafts is probably the best use of it - there are improvements that can be made to override the official NLA integration and that is TBD - **Browser/Scraper** - This is not to be confused with 'browsing' on chat.openai.com (which is technically a plugin suite or multiple plugins) @@ -62,7 +63,7 @@ Clicking on **"Show Agent Settings"** will allow you to modify parameters for th - A better solution for 'browsing' is planned but can't guarantuee when - This plugin is best used in combination with google so it doesn't hallucinate webpages to visit - **Serpapi** - an alternative to Google search but not as performant in my opinion - - You can get an API key here: https://serpapi.com/dashboard + - You can get an API key here: **[https://serpapi.com/dashboard](https://serpapi.com/dashboard)** - For free tier, you are limited to 100 queries/month - With google, you are limited to 100/day for free, which is a better deal, and any after may cost you a few pennies diff --git a/docs/features/plugins/make_your_own.md b/docs/features/plugins/make_your_own.md index d463c94767ec..e6534e47aff0 100644 --- a/docs/features/plugins/make_your_own.md +++ b/docs/features/plugins/make_your_own.md @@ -1,5 +1,6 @@ --- title: 🛠️ Make Your Own +description: This doc shows you how to create custom plugins for LibreChat by extending the LangChain `Tool` class. You will learn how to use different APIs and functions with your plugins, and how to integrate them with the LangChain framework. weight: -9 --- # Making your own Plugin @@ -8,11 +9,11 @@ Creating custom plugins for this project involves extending the `Tool` class fro **Note:** I will use the word plugin interchangeably with tool, as the latter is specific to LangChain, and we are mainly conforming to the library. -You are essentially creating DynamicTools in LangChain speak. See the [LangChainJS docs](https://js.langchain.com/docs/modules/agents/tools/dynamic) for more info. +You are essentially creating DynamicTools in LangChain speak. See the **[LangChainJS docs](https://js.langchain.com/docs/modules/agents/tools/dynamic)** for more info. This guide will walk you through the process of creating your own custom plugins, using the `StableDiffusionAPI` and `WolframAlphaAPI` tools as examples. -When using the Functions Agent (the default mode for plugins), tools are converted to [OpenAI functions](https://openai.com/blog/function-calling-and-other-api-updates); in any case, plugins/tools are invoked conditionally based on the LLM generating a specific format that we parse. +When using the Functions Agent (the default mode for plugins), tools are converted to **[OpenAI functions](https://openai.com/blog/function-calling-and-other-api-updates)**; in any case, plugins/tools are invoked conditionally based on the LLM generating a specific format that we parse. The most common implementation of a plugin is to make an API call based on the natural language input from the AI, but there is virtually no limit in programmatic use case. @@ -47,7 +48,7 @@ Remember, the key to creating a custom plugin is to extend the `Tool` class and **Multi-Input Plugins** -If you would like to make a plugin that would benefit from multiple inputs from the LLM, instead of a singular input string as we will review, you need to make a LangChain [StructuredTool](https://blog.langchain.dev/structured-tools/) instead. A detailed guide for this is in progress, but for now, you can look at how I've made StructuredTools in this directory: `api\app\clients\tools\structured\`. This guide is foundational to understanding StructuredTools, and it's recommended you continue reading to better understand LangChain tools first. The blog linked above is also helpful once you've read through this guide. +If you would like to make a plugin that would benefit from multiple inputs from the LLM, instead of a singular input string as we will review, you need to make a LangChain **[StructuredTool](https://blog.langchain.dev/structured-tools/)** instead. A detailed guide for this is in progress, but for now, you can look at how I've made StructuredTools in this directory: `api\app\clients\tools\structured\`. This guide is foundational to understanding StructuredTools, and it's recommended you continue reading to better understand LangChain tools first. The blog linked above is also helpful once you've read through this guide. --- @@ -131,7 +132,7 @@ class StableDiffusionAPI extends Tool { The `_call` method is where the main functionality of your plugin is implemented. This method is called when the language model decides to use your plugin. It should take an `input` parameter and return a result. -> In a basic Tool, the LLM will generate one string value as an input. If your plugin requires multiple inputs from the LLM, read the [StructuredTools](#StructuredTools) section. +> In a basic Tool, the LLM will generate one string value as an input. If your plugin requires multiple inputs from the LLM, read the **[StructuredTools](#StructuredTools)** section. ```javascript class StableDiffusionAPI extends Tool { diff --git a/docs/features/plugins/stable_diffusion.md b/docs/features/plugins/stable_diffusion.md index fd02c0d3bd63..e9734cc511fe 100644 --- a/docs/features/plugins/stable_diffusion.md +++ b/docs/features/plugins/stable_diffusion.md @@ -1,17 +1,18 @@ --- title: 🖌️ Stable Diffusion +description: How to set up and configure the Stable Diffusion plugin weight: -6 --- # Stable Diffusion Plugin -To use Stable Diffusion with this project, you will either need to download and install [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) or, for a dockerized deployment, you can also use [stable-diffusion-webui-docker](https://github.com/AbdBarho/stable-diffusion-webui-docker) +To use Stable Diffusion with this project, you will either need to download and install **[AUTOMATIC1111 - Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)** or, for a dockerized deployment, you can also use **[stable-diffusion-webui-docker](https://github.com/AbdBarho/stable-diffusion-webui-docker)** With the docker deployment you can skip step 2 and step 3, use the setup instructions from their repository instead. - Note: you need a compatible GPU ("CPU-only" is possible but very slow). Nvidia is recommended, but there is no clear resource on incompatible GPUs. Any decent GPU should work. -## 1. Follow download and installation instructions from [stable-diffusion-webui readme](https://github.com/AUTOMATIC1111/stable-diffusion-webui) +## 1. Follow download and installation instructions from **[stable-diffusion-webui readme](https://github.com/AUTOMATIC1111/stable-diffusion-webui)** ## 2. Edit your run script settings diff --git a/docs/features/plugins/wolfram.md b/docs/features/plugins/wolfram.md index c0d74ea0d915..0237e2d515f8 100644 --- a/docs/features/plugins/wolfram.md +++ b/docs/features/plugins/wolfram.md @@ -1,5 +1,6 @@ --- title: 🧠 Wolfram|Alpha +description: How to set up and configure the Wolfram Alpha plugin weight: -5 --- @@ -9,18 +10,23 @@ An AppID must be supplied in all calls to the Wolfram|Alpha API. - Note: Wolfram API calls are limited to 100 calls/day and 2000/month for regular users. -## 1. Make an account at Wolfram|Alpha -## 2. Go to the Developer Portal click on "Get an AppID". -## 3. Configure it in LibreChat -### Select the plugins endpoint +### Make an account +- Visit: **[products.wolframalpha.com/api/](https://products.wolframalpha.com/api/)** to create your account + +### Get your AppID +- Go to the **[Developer Portal](https://developer.wolframalpha.com/portal/myapps/)** click on `Get an AppID`. + +### Configure it in LibreChat +- Select the plugins endpoint ![plugins_endpoint](https://github.com/danny-avila/LibreChat/assets/32828263/7db788a5-2173-4115-b34b-43ea132dae69) -### Open the Plugin store +- Open the Plugin store ![plugin_store](https://github.com/danny-avila/LibreChat/assets/32828263/12a51feb-c030-4cf0-8429-16360270988d) -### Install Wolfram and Provide your AppID +- Install Wolfram and Provide your AppID ![wolfram-1](https://github.com/danny-avila/LibreChat/assets/32828263/bd165497-d529-441d-8372-a68db19adc3f) -- Alternatively: you (the admin) can set the value in `\.env` to bypass the prompt: `WOLFRAM_APP_ID=your_app_id` +> Alternatively: you (the admin) can set the value in `\.env` to bypass the prompt: `WOLFRAM_APP_ID=your_app_id` + -## 5. Select the plugin and enjoy! +### Select the plugin and enjoy! ![wolfram-2](https://github.com/danny-avila/LibreChat/assets/32828263/2825e961-6c46-4728-96cd-1012a0862943) diff --git a/docs/features/presets.md b/docs/features/presets.md index b545ab17756c..3f26cd8411ff 100644 --- a/docs/features/presets.md +++ b/docs/features/presets.md @@ -1,5 +1,6 @@ --- title: 🔖 Presets +description: The "presets" feature in our app is a powerful tool that allows users to save and load predefined settings for their conversations. Users can import and export these presets as JSON files, set a default preset, and share them with others on Discord. weight: -9 --- # Guide to Using the "Presets" Feature diff --git a/docs/features/third_party.md b/docs/features/third_party.md index 22c63794bc3d..3161e4404020 100644 --- a/docs/features/third_party.md +++ b/docs/features/third_party.md @@ -1,5 +1,6 @@ --- title: ✨ Third-Party Tools and Contributions +description: Collection of third-party tools provided by the community weight: -2 --- diff --git a/docs/features/token_usage.md b/docs/features/token_usage.md index da2e86b8a435..876b385f97c5 100644 --- a/docs/features/token_usage.md +++ b/docs/features/token_usage.md @@ -1,5 +1,6 @@ --- title: 🪙 Token Usage +description: This doc covers how to track and control your token usage for the OpenAI/Plugins endpoints in LibreChat. You will learn how to view your transactions, enable user balances, and add credits to your account. weight: -7 --- # Token Usage diff --git a/docs/general_info/breaking_changes.md b/docs/general_info/breaking_changes.md index 09690376319e..9991d42f6ff0 100644 --- a/docs/general_info/breaking_changes.md +++ b/docs/general_info/breaking_changes.md @@ -1,5 +1,6 @@ --- title: ⚠️ Breaking Changes +description: This doc lists the breaking changes that affect the functionality and compatibility of LibreChat. You should read this doc before updating to a new version of LibreChat, and follow the instructions to resolve any issues. weight: -10 --- # ⚠️ Breaking Changes diff --git a/docs/general_info/index.md b/docs/general_info/index.md index 6edb7921d32d..1addb356e703 100644 --- a/docs/general_info/index.md +++ b/docs/general_info/index.md @@ -1,5 +1,6 @@ --- title: General Information +description: 📜 This section contains information about LibreChat, such as its history, purpose, and values. You will also find the details of the tech stack, the code of conduct, and the breaking changes that affect the project. weight: 4 --- diff --git a/docs/general_info/multilingual_information.md b/docs/general_info/multilingual_information.md index c1bc38988bae..5b51c4eea572 100644 --- a/docs/general_info/multilingual_information.md +++ b/docs/general_info/multilingual_information.md @@ -1,5 +1,6 @@ --- title: 🌍 Multilingual Information +description: To set up the project, please follow the instructions in the documentation. The documentation is in English only, so you may need to use a translation tool or an AI assistant (e.g. ChatGPT) if you have difficulty understanding it. weight: -9 --- # Multilingual Information diff --git a/docs/general_info/project_origin.md b/docs/general_info/project_origin.md index ea16ebd07934..d1dc673238fe 100644 --- a/docs/general_info/project_origin.md +++ b/docs/general_info/project_origin.md @@ -1,5 +1,6 @@ --- title: 🧭 Origin +description: How it all started... weight: -8 --- # Origin diff --git a/docs/general_info/tech_stack.md b/docs/general_info/tech_stack.md index 3ef2d1112e62..acb0100b5bf8 100644 --- a/docs/general_info/tech_stack.md +++ b/docs/general_info/tech_stack.md @@ -1,5 +1,6 @@ --- title: 🧑‍💻 Tech Stack +description: This doc describes the technologies and frameworks that LibreChat uses. weight: -8 --- # Tech Stack diff --git a/docs/index.md b/docs/index.md index 36515634247b..3abf481c36d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,9 @@ +--- +title: Home +description: 🪶 Introducing LibreChat +weight: -10 +--- +

Get an API key here

+Get an API key here: **[makersuite.google.com](https://makersuite.google.com/app/apikey)** Once you have your key, provide the key in your .env file, which allows all users of your instance to use it. @@ -147,13 +147,13 @@ Setting `GOOGLE_KEY=user_provided` in your .env file will configure both the Ver ### Vertex AI (PaLM 2 & Codey) -To setup Google LLMs (via Google Cloud Vertex AI), first, signup for Google Cloud: https://cloud.google.com/ +To setup Google LLMs (via Google Cloud Vertex AI), first, signup for Google Cloud: **[cloud.google.com](https://cloud.google.com/)** You can usually get **$300 starting credit**, which makes this option free for 90 days. ### 1. Once signed up, Enable the Vertex AI API on Google Cloud: - - Go to [Vertex AI page on Google Cloud console](https://console.cloud.google.com/vertex-ai) - - Click on "Enable API" if prompted + - Go to **[Vertex AI page on Google Cloud console](https://console.cloud.google.com/vertex-ai)** + - Click on `Enable API` if prompted ### 2. Create a Service Account with Vertex AI role: - **[Click here to create a Service Account](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1)** - **Select or create a project** @@ -214,11 +214,11 @@ You should also consider changing the `AZURE_OPENAI_MODELS` variable to the mode AZURE_OPENAI_MODELS=gpt-4-1106-preview,gpt-4,gpt-3.5-turbo,gpt-3.5-turbo-1106,gpt-4-vision-preview ``` -Overriding the construction of the API URL will be possible but is not yet implemented. Follow progress on this feature here: [Issue #1266](https://github.com/danny-avila/LibreChat/issues/1266) +Overriding the construction of the API URL will be possible but is not yet implemented. Follow progress on this feature here: **[Issue #1266](https://github.com/danny-avila/LibreChat/issues/1266)** ### Model Deployments -*Note: a change will be developed to improve current configuration settings, to allow multiple deployments/model configurations setup with ease: [#1390](https://github.com/danny-avila/LibreChat/issues/1390)* +> Note: a change will be developed to improve current configuration settings, to allow multiple deployments/model configurations setup with ease: **[#1390](https://github.com/danny-avila/LibreChat/issues/1390)** As of 2023-12-18, the Azure API allows only one model per deployment. @@ -297,7 +297,7 @@ As of December 18th, 2023, Vision models seem to have degraded performance with ![image](https://github.com/danny-avila/LibreChat/assets/110412045/7306185f-c32c-4483-9167-af514cc1c2dd) -*Note: a change will be developed to improve current configuration settings, to allow multiple deployments/model configurations setup with ease: [#1390](https://github.com/danny-avila/LibreChat/issues/1390)* +> Note: a change will be developed to improve current configuration settings, to allow multiple deployments/model configurations setup with ease: **[#1390](https://github.com/danny-avila/LibreChat/issues/1390)** ### Optional Variables @@ -310,7 +310,7 @@ These two variables are optional but may be used in future updates of this proje ### Using Plugins with Azure -Note: To use the Plugins endpoint with Azure OpenAI, you need a deployment supporting [function calling](https://techcommunity.microsoft.com/t5/azure-ai-services-blog/function-calling-is-now-available-in-azure-openai-service/ba-p/3879241). Otherwise, you need to set "Functions" off in the Agent settings. When you are not using "functions" mode, it's recommend to have "skip completion" off as well, which is a review step of what the agent generated. +Note: To use the Plugins endpoint with Azure OpenAI, you need a deployment supporting **[function calling](https://techcommunity.microsoft.com/t5/azure-ai-services-blog/function-calling-is-now-available-in-azure-openai-service/ba-p/3879241)**. Otherwise, you need to set "Functions" off in the Agent settings. When you are not using "functions" mode, it's recommend to have "skip completion" off as well, which is a review step of what the agent generated. To use Azure with the Plugins endpoint, make sure the following environment variables are set: @@ -321,19 +321,19 @@ To use Azure with the Plugins endpoint, make sure the following environment vari ## [OpenRouter](https://openrouter.ai/) -[OpenRouter](https://openrouter.ai/) is a legitimate proxy service to a multitude of LLMs, both closed and open source, including: +**[OpenRouter](https://openrouter.ai/)** is a legitimate proxy service to a multitude of LLMs, both closed and open source, including: - OpenAI models (great if you are barred from their API for whatever reason) - Anthropic Claude models (same as above) - Meta's Llama models - pygmalionai/mythalion-13b - and many more open source models. Newer integrations are usually discounted, too! -> See their available models and pricing here: [Supported Models](https://openrouter.ai/docs#models) +> See their available models and pricing here: **[Supported Models](https://openrouter.ai/docs#models)** OpenRouter is so great, I decided to integrate it to the project as a standalone feature. **Setup:** -- Signup to [OpenRouter](https://openrouter.ai/) and create a key. You should name it and set a limit as well. +- Signup to **[OpenRouter](https://openrouter.ai/)** and create a key. You should name it and set a limit as well. - Set the environment variable `OPENROUTER_API_KEY` in your .env file to the key you just created. - Set something in the `OPENAI_API_KEY`, it can be anyting, but **do not** leave it blank or set to `user_provided` - Restart your LibreChat server and use the OpenAI or Plugins endpoints. @@ -364,12 +364,12 @@ This is not to be confused with [OpenAI's Official API](#openai)! To get your Access token for ChatGPT Browser Access, you need to: -- Go to [https://chat.openai.com](https://chat.openai.com) +- Go to **[https://chat.openai.com](https://chat.openai.com)** - Create an account or log in with your existing one -- Visit [https://chat.openai.com/api/auth/session](https://chat.openai.com/api/auth/session) +- Visit **[https://chat.openai.com/api/auth/session](https://chat.openai.com/api/auth/session)** - Copy the value of the "accessToken" field and save it in ./.env as CHATGPT_ACCESS_TOKEN -Warning: There may be a chance of your account being banned if you deploy the app to multiple users with this method. Use at your own risk. 😱 +Warning: There may be a chance of your account being banned if you deploy the app to multiple users with this method. Use at your own risk. --- diff --git a/docs/install/configuration/default_language.md b/docs/install/configuration/default_language.md index faab9047d921..8255489c5710 100644 --- a/docs/install/configuration/default_language.md +++ b/docs/install/configuration/default_language.md @@ -1,5 +1,6 @@ --- title: 🌍 Default Language +description: How to change LibreChat's default language weight: -3 --- diff --git a/docs/install/configuration/docker_override.md b/docs/install/configuration/docker_override.md index 9b1284513e78..a1a54b4e3097 100644 --- a/docs/install/configuration/docker_override.md +++ b/docs/install/configuration/docker_override.md @@ -1,5 +1,6 @@ --- title: 🐋 Docker Compose Override +description: "How to Use the Docker Compose Override File: In Docker Compose, an override file is a powerful feature that allows you to modify the default configuration provided by the main `docker-compose.yml` without the need to directly edit or duplicate the whole file." weight: -9 --- diff --git a/docs/install/configuration/dotenv.md b/docs/install/configuration/dotenv.md index 55ab65aad5c9..ea3cd097d945 100644 --- a/docs/install/configuration/dotenv.md +++ b/docs/install/configuration/dotenv.md @@ -1,5 +1,6 @@ --- title: ⚙️ Environment Variables +description: Comprehensive guide for configuring your application's environment with the `.env` file. This document is your one-stop resource for understanding and customizing the environment variables that will shape your application's behavior in different contexts. weight: -10 --- @@ -98,10 +99,10 @@ NO_INDEX=true LibreChat has built-in central logging, see [Logging System](../../features/logging_system.md) for more info. - Debug logging is enabled by default and crucial for development. -- To report issues, reproduce the error and submit logs from `./api/logs/debug-%DATE%.log` at [LibreChat GitHub Issues](https://github.com/danny-avila/LibreChat/issues). +- To report issues, reproduce the error and submit logs from `./api/logs/debug-%DATE%.log` at: **[LibreChat GitHub Issues](https://github.com/danny-avila/LibreChat/issues)** - Error logs are stored in the same location. - Keep debug logs active by default or disable them by setting `DEBUG_LOGGING=false` in the environment variable. -- For more information about this feature, read our docs: https://docs.librechat.ai/features/logging_system.html +- For more information about this feature, read our docs: **[Logging System](../../features/logging_system.md)** ```bash DEBUG_LOGGING=true @@ -197,13 +198,13 @@ PLUGINS_USE_AZURE="true" ### BingAI Bing, also used for Sydney, jailbreak, and Bing Image Creator, see: [Bing Access token](./ai_setup.md#bingai) and [Bing Jailbreak](../../features/bing_jailbreak.md) -- Follow these instructions to get your bing access token (it's best to use the full cookie string for that purpose): [Bing Access Token](https://github.com/danny-avila/LibreChat/issues/370#issuecomment-1560382302) +- Follow these instructions to get your bing access token (it's best to use the full cookie string for that purpose): **[Bing Access Token](../configuration/ai_setup.md#bingai)** - Leave `BINGAI_TOKEN=` blank to disable this endpoint - Set `BINGAI_TOKEN=` to "user_provided" to allow users to provide their own API key from the WebUI > Note: It is recommended to leave it as "user_provided" and provide the token from the WebUI. -- `BINGAI_HOST` can be necessary for some people in different countries, e.g. China (https://cn.bing.com). Leave it blank or commented out to use default server. +- `BINGAI_HOST` can be necessary for some people in different countries, e.g. China (`https://cn.bing.com`). Leave it blank or commented out to use default server. ```bash BINGAI_TOKEN=user_provided @@ -211,7 +212,7 @@ BINGAI_HOST= ``` ### ChatGPT -see: [ChatGPT Free Access token](./ai_setup.md#chatgptbrowser) +see: [ChatGPT Free Access token](../configuration/ai_setup.md#chatgptbrowser) > **Warning**: To use this endpoint you'll have to set up your own reverse proxy. Here is the installation guide to deploy your own (based on [PandoraNext](https://github.com/pandora-next/deploy)): **[PandoraNext Deployment Guide](../../features/pandoranext.md)** @@ -219,8 +220,9 @@ see: [ChatGPT Free Access token](./ai_setup.md#chatgptbrowser) CHATGPT_REVERSE_PROXY= ``` -> ~~Note: If you're a GPT plus user you can add gpt-4, gpt-4-plugins, gpt-4-code-interpreter, and gpt-4-browsing to the list above and use the models for these features; however, the view/display portion of these features are not supported, but you can use the underlying models, which have higher token context~~ -> **Note:** The current method only works with `text-davinci-002-render-sha` +> **Note:** If you're a GPT plus user you can try adding `gpt-4`, `gpt-4-plugins`, `gpt-4-code-interpreter`, and `gpt-4-browsing` to the list above and use the models for these features; **however, the view/display portion of these features are not supported**, but you can use the underlying models, which have higher token context + +> This method **might only works** with `text-davinci-002-render-sha` and **might stop working** at any moment. - Leave `CHATGPT_TOKEN=` blank to disable this endpoint - Set `CHATGPT_TOKEN=` to "user_provided" to allow users to provide their own API key from the WebUI @@ -348,7 +350,7 @@ DEBUG_PLUGINS=true ``` - For securely storing credentials, you need a fixed key and IV. You can set them here for prod and dev environments. - - You need a 32-byte key (64 characters in hex) and 16-byte IV (32 characters in hex) You can use this replit to generate some quickly: [Key Generator](https://replit.com/@daavila/crypto#index.js) + - You need a 32-byte key (64 characters in hex) and 16-byte IV (32 characters in hex) You can use this replit to generate some quickly: **[Key Generator](https://replit.com/@daavila/crypto#index.js)** > Warning: If you don't set them, the app will crash on startup. @@ -379,7 +381,7 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT= ``` - For customization of the DALL-E-3 System prompt, uncomment the following, and provide your own prompt. **(Advanced)** - - See official prompt for reference: [DALL-E System Prompt](https://github.com/spdustin/ChatGPT-AutoExpert/blob/main/_system-prompts/dall-e.md) + - See official prompt for reference: **[DALL-E System Prompt](https://github.com/spdustin/ChatGPT-AutoExpert/blob/main/_system-prompts/dall-e.md)** ```bash DALLE3_SYSTEM_PROMPT="Your System Prompt here" @@ -418,23 +420,23 @@ SERPAPI_API_KEY= ``` #### Stable Diffusion (Automatic1111) -See detailed instructions here: [Stable Diffusion](../../features/plugins/stable_diffusion.md) +See detailed instructions here: **[Stable Diffusion](../../features/plugins/stable_diffusion.md)** -- Use "http://127.0.0.1:7860" with local install and "http://host.docker.internal:7860" for docker +- Use `http://127.0.0.1:7860` with local install and `http://host.docker.internal:7860` for docker ```bash SD_WEBUI_URL=http://host.docker.internal:7860 ``` #### WolframAlpha -See detailed instructions here: [Wolfram Alpha](../../features/plugins/wolfram.md) +See detailed instructions here: **[Wolfram Alpha](../../features/plugins/wolfram.md)** ```bash WOLFRAM_APP_ID= ``` #### Zapier -- You need a Zapier account. Get your API key from here: [Zapier](https://nla.zapier.com/credentials/) +- You need a Zapier account. Get your API key from here: **[Zapier](https://nla.zapier.com/credentials/)** - Create allowed actions - Follow step 3 in this getting start guide from Zapier > Note: zapier is known to be finicky with certain actions. Writing email drafts is probably the best use of it. @@ -488,7 +490,7 @@ This section contains the configuration for: ### Moderation The Automated Moderation System uses a scoring mechanism to track user violations. As users commit actions like excessive logins, registrations, or messaging, they accumulate violation scores. Upon reaching a set threshold, the user and their IP are temporarily banned. This system ensures platform security by monitoring and penalizing rapid or suspicious activities. -see: [Automated Moderation](../../features/mod_system.md) +see: **[Automated Moderation](../../features/mod_system.md)** #### Basic Moderation Settings @@ -575,7 +577,7 @@ MESSAGE_USER_WINDOW=1 ### Balance The following enables user balances for the OpenAI/Plugins endpoints, which you can add manually or you will need to build out a balance accruing system for users. -see: [Token Usage](../../features/token_usage.md) +see: **[Token Usage](../../features/token_usage.md)** - To manually add balances, run the following command:`npm run add-balance` - You can also specify the email and token credit amount to add, e.g.:`npm run add-balance example@example.com 1000` @@ -589,7 +591,7 @@ CHECK_BALANCE=false ``` ### Registration and Login -see: [User/Auth System](../configuration/user_auth_system.md) +see: **[User/Auth System](../configuration/user_auth_system.md)** ![image](https://github.com/danny-avila/LibreChat/assets/81851188/52a37d1d-7392-4a9a-a79f-90ed2da7f841) @@ -609,7 +611,7 @@ ALLOW_SOCIAL_REGISTRATION=false ``` - Default values: session expiry: 15 minutes, refresh token expiry: 7 days - - For more information: [Refresh Token](https://github.com/danny-avila/LibreChat/pull/927) + - For more information: **[Refresh Token](https://github.com/danny-avila/LibreChat/pull/927)** ```bash SESSION_EXPIRY=1000 * 60 * 15 @@ -617,7 +619,7 @@ REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7 ``` - You should use new secure values. The examples given are 32-byte keys (64 characters in hex). - - Use this replit to generate some quickly: [JWT Keys](https://replit.com/@daavila/crypto#index.js) + - Use this replit to generate some quickly: **[JWT Keys](https://replit.com/@daavila/crypto#index.js)** ```bash JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef @@ -626,9 +628,9 @@ JWT_REFRESH_SECRET=eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8 ### Social Logins -#### [Discord](../configuration/user_auth_system.md#discord-authentication) +#### [Discord Authentication](../configuration/user_auth_system.md#discord) -for more information: [Discord](../configuration/user_auth_system.md#discord-authentication) +for more information: **[Discord](../configuration/user_auth_system.md#discord)** ```bash # Discord @@ -637,9 +639,9 @@ DISCORD_CLIENT_SECRET=your_client_secret DISCORD_CALLBACK_URL=/oauth/discord/callback ``` -#### [Facebook](../configuration/user_auth_system.md#facebook-authentication) +#### [Facebook Authentication](../configuration/user_auth_system.md#facebook) -for more information: [Facebook](../configuration/user_auth_system.md#facebook-authentication) +for more information: **[Facebook Authentication](../configuration/user_auth_system.md#facebook)** ```bash # Facebook @@ -648,9 +650,9 @@ FACEBOOK_CLIENT_SECRET= FACEBOOK_CALLBACK_URL=/oauth/facebook/callback ``` -#### [GitHub](../configuration/user_auth_system.md#github-authentication) +#### [GitHub Authentication](../configuration/user_auth_system.md#github) -for more information: [GitHub](../configuration/user_auth_system.md#github-authentication) +for more information: **[GitHub Authentication](../configuration/user_auth_system.md#github)** ```bash # GitHub @@ -659,9 +661,9 @@ GITHUB_CLIENT_SECRET=your_client_secret GITHUB_CALLBACK_URL=/oauth/github/callback ``` -#### [Google](../configuration/user_auth_system.md#google-authentication) +#### [Google Authentication](../configuration/user_auth_system.md#google) -for more information: [Google](../configuration/user_auth_system.md#google-authentication) +for more information: **[Google Authentication](../configuration/user_auth_system.md#google)** ```bash # Google @@ -670,9 +672,9 @@ GOOGLE_CLIENT_SECRET= GOOGLE_CALLBACK_URL=/oauth/google/callback ``` -#### [OpenID](../configuration/user_auth_system.md#openid-authentication-with-azure-ad) +#### [OpenID Authentication](../configuration/user_auth_system.md#openid-with-aws-cognito) -for more information: [Azure OpenID](../configuration/user_auth_system.md#openid-authentication-with-azure-ad) or [AWS Cognito OpenID](../configuration/user_auth_system.md#openid-authentication-with-aws-cognito) +for more information: **[Azure OpenID Authentication](../configuration/user_auth_system.md#openid-with-azure-ad)** or **[AWS Cognito OpenID Authentication](../configuration/user_auth_system.md#openid-with-aws-cognito)** ```bash # OpenID @@ -688,7 +690,7 @@ OPENID_IMAGE_URL= ``` ### Email Password Reset -Email is used for password reset. See: [Email Password Reset](../configuration/user_auth_system.md#email-and-password-reset) +Email is used for password reset. See: **[Email Password Reset](../configuration/user_auth_system.md#email-and-password-reset)** - Note that all either service or host, username and password and the From address must be set for email to work. @@ -698,7 +700,7 @@ Email is used for password reset. See: [Email Password Reset](../configuration/u > > Failing to set valid values here will result in LibreChat using the unsecured password reset! -See: [nodemailer well-known-services](https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/) +See: **[nodemailer well-known-services](https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/)** ```bash EMAIL_SERVICE= diff --git a/docs/install/configuration/free_ai_apis.md b/docs/install/configuration/free_ai_apis.md index 8478d54a9aa2..671ed9b6206b 100644 --- a/docs/install/configuration/free_ai_apis.md +++ b/docs/install/configuration/free_ai_apis.md @@ -1,5 +1,6 @@ --- title: 💸 Free AI APIs +description: There are APIs offering free/free-trial access to AI APIs via reverse proxy... weight: -6 --- @@ -7,13 +8,13 @@ weight: -6 There are APIs offering free/free-trial access to AI APIs via reverse proxy. -Here is a well-maintained public list of [Free AI APIs](https://github.com/zukixa/cool-ai-stuff) that may or may not be compatible with LibreChat +Here is a well-maintained public list of **[Free AI APIs](https://github.com/zukixa/cool-ai-stuff)** that may or may not be compatible with LibreChat -> ⚠️ [OpenRouter](./ai_setup.md#openrouter) is in a category of its own, and is highly recommended over the "free" services below. NagaAI and other 'free' API proxies tend to have intermittent issues, data leaks, and/or problems with the guidelines of the platforms they advertise on. Use the below at your own risk. +> ⚠️ **[OpenRouter](./ai_setup.md#openrouter)** is in a category of its own, and is highly recommended over the "free" services below. NagaAI and other 'free' API proxies tend to have intermittent issues, data leaks, and/or problems with the guidelines of the platforms they advertise on. Use the below at your own risk. ### NagaAI -Since NagaAI works with LibreChat, and offers Llama2 along with OpenAI models, let's start with that one: [NagaAI](https://t.me/chimera_ai) +Since NagaAI works with LibreChat, and offers Llama2 along with OpenAI models, let's start with that one: **[NagaAI](https://t.me/chimera_ai)** > ⚠️ Never trust 3rd parties. Use at your own risk of privacy loss. Your data may be used for AI training at best or for nefarious reasons at worst; this is true in all cases, even with official endpoints: never give an LLM sensitive/identifying information. If something is free, you are the product. If errors arise, they are more likely to be due to the 3rd party, and not this project, as I test the official endpoints first and foremost. diff --git a/docs/install/configuration/index.md b/docs/install/configuration/index.md index d89abcc757e9..e577a8b3a564 100644 --- a/docs/install/configuration/index.md +++ b/docs/install/configuration/index.md @@ -1,5 +1,6 @@ --- title: Configuration +description: ⚙️ This section provides detailed guides on how to configure LibreChat to suit your needs and preferences. You will learn how to set up various environment variables, customize your Docker settings, choose your AI models and APIs, enable user authentication, connect to online MongoDB, change the default language, and more. weight: 2 --- diff --git a/docs/install/configuration/litellm.md b/docs/install/configuration/litellm.md index b6bb068ead26..c9f86368993a 100644 --- a/docs/install/configuration/litellm.md +++ b/docs/install/configuration/litellm.md @@ -1,16 +1,15 @@ --- title: 🚅 LiteLLM +description: Using LibreChat with LiteLLM Proxy weight: -7 --- # Using LibreChat with LiteLLM Proxy -Use [LiteLLM Proxy](https://docs.litellm.ai/docs/simple_proxy) for: +Use **[LiteLLM Proxy](https://docs.litellm.ai/docs/simple_proxy)** for: * Calling 100+ LLMs Huggingface/Bedrock/TogetherAI/etc. in the OpenAI ChatCompletions & Completions format * Load balancing - between Multiple Models + Deployments of the same model LiteLLM proxy can handle 1k+ requests/second during load tests * Authentication & Spend Tracking Virtual Keys -https://docs.litellm.ai/docs/simple_proxy - ## Start LiteLLM Proxy Server ### Pip install litellm ```shell @@ -18,7 +17,7 @@ pip install litellm ``` ### Create a config.yaml for litellm proxy -More information on LiteLLM configurations here: https://docs.litellm.ai/docs/simple_proxy#proxy-configs +More information on LiteLLM configurations here: **[docs.litellm.ai/docs/simple_proxy](https://docs.litellm.ai/docs/simple_proxy)** ```yaml model_list: diff --git a/docs/install/configuration/misc.md b/docs/install/configuration/misc.md index 57932d93160f..75892fc7c747 100644 --- a/docs/install/configuration/misc.md +++ b/docs/install/configuration/misc.md @@ -1,5 +1,6 @@ --- title: 🌀 Miscellaneous +description: As LibreChat has varying use cases and environment possibilities, this page will host niche setup/configurations, as contributed by the community, that are not better delegated to any of the other guides. weight: -2 author: danny-avila and jerkstorecaller --- @@ -8,8 +9,6 @@ As LibreChat has varying use cases and environment possibilities, this page will # Using LibreChat behind a reverse proxy with Basic Authentication -Written by [@danny-avila](https://github.com/danny-avila) and [@jerkstorecaller](https://github.com/jerkstorecaller) - ### Basic Authentication (Basic Auth) Basic Authentication is a simple authentication scheme built into the HTTP protocol. When a client sends a request to a server, the server can respond with a `401 Unauthorized` status code, prompting the client to provide a username and password. This username and password are then sent with subsequent requests in the HTTP header, encoded in Base64 format. diff --git a/docs/install/configuration/mongodb.md b/docs/install/configuration/mongodb.md index 7ca787aa0537..f280c4d25f87 100644 --- a/docs/install/configuration/mongodb.md +++ b/docs/install/configuration/mongodb.md @@ -1,12 +1,13 @@ --- title: 🍃 Online MongoDB +description: This guide teaches you how to set up an online MongoDB database for LibreChat using MongoDB Atlas, a cloud-based service. You will learn how to create an account, a project, and a cluster, as well as how to configure your database credentials, network access, and connection string. weight: -4 --- # Set Up an Online MongoDB Database ## Create an account -- Open a new tab and go to [https://account.mongodb.com/account/register](https://account.mongodb.com/account/register) to create an account. +- Open a new tab and go to **[account.mongodb.com/account/register](https://account.mongodb.com/account/register)** to create an account. ## Create a project - Once you have set up your account, create a new project and name it (the name can be anything): diff --git a/docs/install/configuration/user_auth_system.md b/docs/install/configuration/user_auth_system.md index 1dae7dfea3db..7020d37ad7b0 100644 --- a/docs/install/configuration/user_auth_system.md +++ b/docs/install/configuration/user_auth_system.md @@ -1,5 +1,6 @@ --- title: 🛂 Authentication System +description: This guide explains how to use the user authentication system of LibreChat, which offers secure and easy email and social logins. You will learn how to set up sign up, log in, password reset, and more. weight: -5 --- @@ -42,7 +43,7 @@ ALLOW_SOCIAL_REGISTRATION=false ### Session Expiry and Refresh Token - Default values: session expiry: 15 minutes, refresh token expiry: 7 days - - For more information: [Refresh Token](https://github.com/danny-avila/LibreChat/pull/927) + - For more information: **[GitHub PR #927 - Refresh Token](https://github.com/danny-avila/LibreChat/pull/927)** ```bash SESSION_EXPIRY=1000 * 60 * 15 @@ -76,7 +77,7 @@ sequenceDiagram ### JWT Secret and Refresh Secret - You should use new secure values. The examples given are 32-byte keys (64 characters in hex). - - Use this replit to generate some quickly: [JWT Keys](https://replit.com/@daavila/crypto#index.js) + - Use this replit to generate some quickly: **[JWT Keys](https://replit.com/@daavila/crypto#index.js)** ```bash JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef @@ -129,12 +130,12 @@ EMAIL\_ENCRYPTION defines if encryption is required at the start (`tls`) or star EMAIL\_ENCRYPTION\_HOSTNAME allows specification of a hostname against which the certificate is validated. Use this if the mail server does have a valid certificate, but you are connecting with an IP or a different name for some reason. EMAIL\_ALLOW\_SELFSIGNED defines whether self-signed certificates can be accepted from the server. As the mails being sent contain sensitive information, ONLY use this for testing. -NOTE: ⚠️ **Failing to perform either of the below setups will result in LibreChat using the unsecured password reset! This allows anyone to reset any password on your server immediately, without mail being sent at all!** The variable EMAIL\_FROM does not support all email providers **but is still required**. To stay updated, check the bug fixes [here](https://github.com/danny-avila/LibreChat/tags). +NOTE: ⚠️ **Failing to perform either of the below setups will result in LibreChat using the unsecured password reset! This allows anyone to reset any password on your server immediately, without mail being sent at all!** The variable EMAIL\_FROM does not support all email providers **but is still required**. To stay updated, check the bug fixes: **[here](https://github.com/danny-avila/LibreChat/tags)** ### Setup with Gmail 1. Create a Google Account and enable 2-step verification. -2. In the [Google Account settings](https://myaccount.google.com/), click on the "Security" tab and open "2-step verification." +2. In the **[Google Account settings](https://myaccount.google.com/)**, click on the "Security" tab and open "2-step verification." 3. Scroll down and open "App passwords." Choose "Mail" for the app and select "Other" for the device, then give it a random name. 4. Click on "Generate" to create a password, and copy the generated password. 5. In the .env file, modify the variables as follows: diff --git a/docs/install/index.md b/docs/install/index.md index 73fe630a66a0..e6177b07b400 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -1,5 +1,6 @@ --- title: Installation and Configuration +description: 💻 In-depth guides about installation and configuration weight: 1 --- @@ -8,7 +9,7 @@ weight: 1 ## **[Installation](./installation/index.md)** * 🐳 [Docker Compose (✨ Recommended)](./installation/docker_compose_install.md) - * 🦦 [Container (podman)](./installation/container_install.md) + * 🦦 [Container (Podman)](./installation/container_install.md) * 🐧 [Linux](./installation/linux_install.md) * 🍎 [Mac](./installation/mac_install.md) * 🪟 [Windows](./installation/windows_install.md) diff --git a/docs/install/installation/container_install.md b/docs/install/installation/container_install.md index 0e43da3dc84c..5f47f1e44cd3 100644 --- a/docs/install/installation/container_install.md +++ b/docs/install/installation/container_install.md @@ -1,9 +1,10 @@ --- -title: 🦦 Container (podman) +title: 🦦 Container (Podman) +description: Install LibreChat using Podman. If you don't like docker compose, don't want a bare-metal installation, but still want to leverage the benefits from the isolation and modularity of containers... weight: 0 --- -# Container Installation Guide (podman) +# Container Installation Guide (Podman) If you don't like docker compose, don't want a bare-metal installation, but still want to leverage the benefits from the isolation and modularity of containers - this is the guide you should use. @@ -181,7 +182,7 @@ podman volume export librechat-meilisearch-data --output "librechat-meilisearch- podman volume export librechat-mongodb-data --output "librechat-mongodb-backup-$(date +"%d-%m-%Y").tar" ``` -These will leave archive files that you can do what you wish with, including reverting volumes to a previous state if needed. Refer to [podman documentation](https://docs.podman.io/en/latest/markdown/podman-volume-import.1.html) for how to do this. +These will leave archive files that you can do what you wish with, including reverting volumes to a previous state if needed. Refer to the **[official podman documentation](https://docs.podman.io/en/latest/markdown/podman-volume-import.1.html)** for how to do this. ## Updating LibreChat diff --git a/docs/install/installation/docker_compose_install.md b/docs/install/installation/docker_compose_install.md index 39786bcc847b..c0f9864839da 100644 --- a/docs/install/installation/docker_compose_install.md +++ b/docs/install/installation/docker_compose_install.md @@ -1,5 +1,6 @@ --- title: 🐳 Docker Compose ✨(Recommended) +description: "Docker Compose Installation Guide: Docker Compose installation is recommended for most use cases. It's the easiest, simplest, and most reliable method to get started." weight: -10 --- @@ -19,7 +20,7 @@ Start by cloning the repository or downloading it to your desired location: ``` ### Docker Installation -Install Docker on your system. [Docker Desktop](https://www.docker.com/products/docker-desktop/) is recommended for managing your Docker containers. +Install Docker on your system. **[Docker Desktop](https://www.docker.com/products/docker-desktop/)** is recommended for managing your Docker containers. ### LibreChat Configuration Before running LibreChat with Docker, you need to configure some settings: @@ -27,7 +28,7 @@ Before running LibreChat with Docker, you need to configure some settings: - Edit the credentials you see in `docker-compose.yml` under the API service as needed. - See my notes below for specific instructions on some of the configuration - Provide all necessary credentials in the `.env` file before the next step. - - Docker will read this env file. See the `.env.example` file for reference. + - Docker will read this env file. See the **[/.env.example](https://github.com/danny-avila/LibreChat/blob/main/.env.example)** file for reference. #### [AI Setup](../configuration/ai_setup.md) (Required) At least one AI endpoint should be setup for use. diff --git a/docs/install/installation/index.md b/docs/install/installation/index.md index bd0cb5436625..1b20ac676b29 100644 --- a/docs/install/installation/index.md +++ b/docs/install/installation/index.md @@ -1,11 +1,12 @@ --- title: Installation +description: 🧑‍💻 This section contains the installation guides for Docker, Podman, Windows, Mac and Linux. weight: 1 --- # Installation * 🐳 [Docker Compose (✨ Recommended)](docker_compose_install.md) - * 🦦 [Container (podman)](container_install.md) + * 🦦 [Container (Podman)](container_install.md) * 🐧 [Linux](linux_install.md) * 🍎 [Mac](mac_install.md) * 🪟 [Windows](windows_install.md) \ No newline at end of file diff --git a/docs/install/installation/linux_install.md b/docs/install/installation/linux_install.md index 90f6dc868cf3..d46db47a97d8 100644 --- a/docs/install/installation/linux_install.md +++ b/docs/install/installation/linux_install.md @@ -1,5 +1,6 @@ --- title: 🐧 Linux +description: Linux Installation Guides weight: 0 --- # Linux Installation Guide @@ -73,17 +74,17 @@ Note: The above command extracts the files to "/usr/local/LibreChat". If you wan ## Enable the Conversation search feature: (optional) -- Download MeiliSearch latest release from: https://github.com/meilisearch/meilisearch/releases -- Copy it to "/usr/local/LibreChat/" -- Rename the file to "meilisearch" -- Open a terminal and navigate to "/usr/local/LibreChat/" +- Download MeiliSearch latest release from: **[github.com/meilisearch](https://github.com/meilisearch/meilisearch/releases)** +- Copy it to `/usr/local/LibreChat/` +- Rename the file to `meilisearch` +- Open a terminal and navigate to `/usr/local/LibreChat/` - Run the following command: ```bash ./meilisearch --master-key=YOUR_MASTER_KEY ``` -Note: Replace "YOUR_MASTER_KEY" with the generated master key, which you saved earlier. +Note: Replace `YOUR_MASTER_KEY` with the generated master key, which you saved earlier. ## Install Node.js: diff --git a/docs/install/installation/mac_install.md b/docs/install/installation/mac_install.md index 51a1e3d12d3f..f9190bc9c89a 100644 --- a/docs/install/installation/mac_install.md +++ b/docs/install/installation/mac_install.md @@ -1,5 +1,6 @@ --- title: 🍎 Mac +description: Mac Installation Guides weight: 0 --- @@ -12,7 +13,7 @@ weight: 0 ## **Manual Installation** ### Install the prerequisites (Required) -- Install Homebrew (if not already installed) by following the instructions on https://brew.sh/ +- Install Homebrew (if not already installed) by following the instructions on **[brew.sh](https://brew.sh/)** - Install Node.js and npm by running `brew install node` ### Download LibreChat (Required) @@ -38,7 +39,7 @@ weight: 0 ### **Download MeiliSearch for macOS (Optional):** - This enables the conversation search feature -- You can download the latest MeiliSearch binary for macOS from their GitHub releases page: https://github.com/meilisearch/MeiliSearch/releases +- You can download the latest MeiliSearch binary for macOS from their GitHub releases page: **[github.com/meilisearch](https://github.com/meilisearch/meilisearch/releases)** - Look for the file named `meilisearch-macos-amd64` (or the equivalent for your system architecture) and download it. - **Make the binary executable:** diff --git a/docs/install/installation/windows_install.md b/docs/install/installation/windows_install.md index b4a0c24504bc..728dde1e2135 100644 --- a/docs/install/installation/windows_install.md +++ b/docs/install/installation/windows_install.md @@ -1,5 +1,6 @@ --- title: 🪟 Windows +description: Windows Installation Guides weight: 0 --- @@ -26,8 +27,8 @@ In this video we're going to install LibreChat on Windows 11 using Docker and Gi #### Instructions - To install LibreChat, you need Docker desktop and Git. Download them from these links: - - Docker desktop: https://www.docker.com/products/docke... - - Git: https://git-scm.com/download/win + - Docker desktop: **[https://docs.docker.com/desktop/install/windows-install/](https://docs.docker.com/desktop/install/windows-install/)** + - Git: **[https://git-scm.com/download/win](https://git-scm.com/download/win)** - Follow the steps in the video to install and run Docker desktop and Git. - Open a terminal in the root of the C drive and enter these commands: - `git clone https://github.com/danny-avila/LibreChat` @@ -52,7 +53,7 @@ Have fun! ### Download and Install Node.js (Required) - - Navigate to https://nodejs.org/en/download and to download the latest Node.js version for your OS (The Node.js installer includes the NPM package manager.) + - Navigate to **[https://nodejs.org/en/download](https://nodejs.org/en/download)** and to download the latest Node.js version for your OS (The Node.js installer includes the NPM package manager.) ### Download and Install Git (Recommended) - Git: https://git-scm.com/download/win @@ -63,15 +64,12 @@ Have fun! - At least one AI endpoint should be setup for use. ### Download LibreChat (Required) - - (With Git) Open Terminal (command prompt) and clone the repository by running `git clone https://github.com/danny-avila/LibreChat.git` - - Or download the latest release here: https://github.com/danny-avila/LibreChat/releases/ - - Or by clicking on the green code button in the top of the page and selecting "Download ZIP" - - If you downloaded a zip file, extract the content in "C:/LibreChat/" + - Open Terminal (command prompt) and clone the repository by running `git clone https://github.com/danny-avila/LibreChat.git` - **IMPORTANT : If you install the files somewhere else modify the instructions accordingly** ### Enable the Conversation search feature: (optional) - - Download MeiliSearch latest release from : https://github.com/meilisearch/meilisearch/releases + - Download MeiliSearch latest release from : **[github.com/meilisearch](https://github.com/meilisearch/meilisearch/releases)** - Copy it to "C:/LibreChat/" - Rename the file to "meilisearch.exe" - Open it by double clicking on it @@ -89,7 +87,7 @@ Using the command line (in the root directory) ### To use the app: 1. Run `npm run backend` 2. Run `meilisearch --master-key ` (Only if SEARCH=TRUE) -3. Visit http://localhost:3080 (default port) & enjoy +3. Visit `http://localhost:3080` (default port) & enjoy ### Using a batch file @@ -98,7 +96,7 @@ Using the command line (in the root directory) - Paste the following code in a new document - The meilisearch executable needs to be at the root of the LibreChat directory - Put your MeiliSearch master key instead of "``" - - Save the file as "C:/LibreChat/LibreChat.bat" + - Save the file as `C:/LibreChat/LibreChat.bat` - you can make a shortcut of this batch file and put it anywhere ```bat title="LibreChat.bat" diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css deleted file mode 100644 index 9a4d540d58e9..000000000000 --- a/docs/stylesheets/extra.css +++ /dev/null @@ -1,6 +0,0 @@ -/*example youtube color scheme*/ -[data-md-color-scheme="youtube"] { - --md-primary-fg-color: #eee; - --md-primary-bg-color: #555; - --md-accent-fg-color: #f00; - } \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 9dcfa095d57d..ac5d413a3cf4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,15 +48,10 @@ theme: - navigation.instant.progress - navigation.tracking - navigation.expand - - navigation.prune + #- navigation.prune - navigation.indexes - navigation.top - -# For more Styling options (not in use) -extra_css: - - stylesheets/extra.css - markdown_extensions: - pymdownx.highlight: anchor_linenums: true @@ -77,16 +72,32 @@ markdown_extensions: - name: mermaid class: mermaid - plugins: - search + # - pub-debugger # <- General purpose mkdocs debugger - mkdocs-nav-weight: section_renamed: true # If true, section name will use the title of its index instead of the folder name. index_weight: -10 warning: true # Controls whether to send a Warning when invalid values are detected in markdown metadata reverse: false # If true, sort nav by weight from largest to smallest. headless_included: false -# for more info: https://github.com/shu307/mkdocs-nav-weight?tab=readme-ov-file + # for more info: https://github.com/shu307/mkdocs-nav-weight?tab=readme-ov-file + - pub-social: + og: + enabled: true + locale: en_us + twitter: + enabled: true + # for more info: https://github.com/mkusz/mkdocs-publisher + - exclude: + glob: + - dev/* # <- exclude the docs/dev folder from the docs + - "*.tmp" + - "*.pdf" + - "*.gz" + regex: + - '.*\.(tmp|bin|tar)$' + # https://github.com/apenwarr/mkdocs-exclude extra: social: From 8be2b6f38051950e4c49f8b1d5dcde2491e579c0 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Thu, 28 Dec 2023 23:10:58 +0100 Subject: [PATCH 03/42] =?UTF-8?q?=F0=9F=8C=8E:=20Italian=20translation=20u?= =?UTF-8?q?pdate=20&=20refactor:=20translations=20(#1414)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * italian translation update * fix: removed some translations * refactor(Translation) --- client/src/localization/Translation.tsx | 3 +- client/src/localization/languages/Ar.tsx | 1 - client/src/localization/languages/De.tsx | 1 - client/src/localization/languages/It.tsx | 230 +++++++++++++---------- client/src/localization/languages/Zh.tsx | 1 - 5 files changed, 129 insertions(+), 107 deletions(-) diff --git a/client/src/localization/Translation.tsx b/client/src/localization/Translation.tsx index e9826af8f412..e353be293bc2 100644 --- a/client/src/localization/Translation.tsx +++ b/client/src/localization/Translation.tsx @@ -57,8 +57,7 @@ if (!String.prototype.format) { // input: language code in string // returns an object of translated strings in the language export const getTranslations = (langCode: string) => { - const language = languageMap[langCode] || English; - return language; + return languageMap[langCode] || English; }; // input: language code in string & phrase key in string diff --git a/client/src/localization/languages/Ar.tsx b/client/src/localization/languages/Ar.tsx index 85d8f5de5fbf..424f9f2b0cd6 100644 --- a/client/src/localization/languages/Ar.tsx +++ b/client/src/localization/languages/Ar.tsx @@ -262,5 +262,4 @@ export default { com_nav_search_placeholder: 'بحث في الرسائل', com_nav_setting_general: 'عام', com_nav_setting_data: 'تحكم في البيانات', - com_nav_language: 'اللغة', }; diff --git a/client/src/localization/languages/De.tsx b/client/src/localization/languages/De.tsx index 7abf98e87c01..ec6fa99d015f 100644 --- a/client/src/localization/languages/De.tsx +++ b/client/src/localization/languages/De.tsx @@ -201,5 +201,4 @@ export default { com_nav_settings: 'Einstellungen', com_nav_search_placeholder: 'Durchsuche Nachrichten', com_nav_setting_general: 'Generell', - com_nav_lang_german: 'Deutsch', }; diff --git a/client/src/localization/languages/It.tsx b/client/src/localization/languages/It.tsx index 9c274f541336..d07eb798ab15 100644 --- a/client/src/localization/languages/It.tsx +++ b/client/src/localization/languages/It.tsx @@ -3,17 +3,18 @@ export default { com_ui_examples: 'Esempi', com_ui_new_chat: 'Nuova Chat', - com_ui_example_quantum_computing: 'Spiega il computing quantistico in termini semplici', - com_ui_example_10_year_old_b_day: 'Hai idee creative per il compleanno di un bambino di 10 anni?', + com_ui_example_quantum_computing: 'Spiega l\'informatica quantistica in termini semplici', + com_ui_example_10_year_old_b_day: + 'Hai qualche idea creativa per il compleanno di un bambino di 10 anni?', com_ui_example_http_in_js: 'Come faccio una richiesta HTTP in Javascript?', com_ui_capabilities: 'Funzionalità', - com_ui_capability_remember: 'Ricorda cosa ha detto l\'utente precedentemente nella conversazione', - com_ui_capability_correction: 'Permette all\'utente di fornire correzioni di follow-up', + com_ui_capability_remember: 'Ricorda cosa ha detto l\'utente prima nella conversazione', + com_ui_capability_correction: 'Permette all\'utente di fornire correzioni successive', com_ui_capability_decline_requests: 'Addestrato a rifiutare richieste inappropriate', com_ui_limitations: 'Limitazioni', - com_ui_limitation_incorrect_info: 'Può occasionalmente generare informazioni scorrette', + com_ui_limitation_incorrect_info: 'Può occasionalmente generare informazioni errate', com_ui_limitation_harmful_biased: - 'Può occasionalmente produrre istruzioni dannose o contenuti faziosi', + 'Può occasionalmente produrre istruzioni dannose o contenuti di parte', com_ui_limitation_limited_2021: 'Conoscenza limitata del mondo e degli eventi dopo il 2021', com_ui_input: 'Input', com_ui_close: 'Chiudi', @@ -21,19 +22,19 @@ export default { com_ui_select_model: 'Seleziona un modello', com_ui_use_prompt: 'Usa prompt', com_ui_prev: 'Prec', - com_ui_next: 'Succ', - com_ui_stop: 'Stop', + com_ui_next: 'Successivo', + com_ui_stop: 'Ferma', com_ui_prompt_templates: 'Modelli di prompt', com_ui_hide_prompt_templates: 'Nascondi modelli di prompt', com_ui_showing: 'Mostra', com_ui_of: 'di', com_ui_entries: 'Voci', com_ui_pay_per_call: - 'Tutte le conversazioni AI in un unico posto. Paga per chiamata e non per mese', - com_ui_new_footer: 'Tutte le conversazioni AI in un unico posto.', + 'Tutte le conversazioni IA in un unico luogo. Paga per chiamata, non per mese', + com_ui_new_footer: 'Tutte le conversazioni IA in un unico luogo.', com_ui_enter: 'Inserisci', com_ui_submit: 'Invia', - com_ui_upload_success: 'File caricato con successo', + com_ui_upload_success: 'File caricato correttamente', com_ui_upload_invalid: 'File non valido per il caricamento', com_ui_cancel: 'Annulla', com_ui_save: 'Salva', @@ -53,44 +54,45 @@ export default { com_ui_delete_conversation: 'Eliminare la chat?', com_ui_delete_conversation_confirm: 'Questo eliminerà', com_auth_error_login: - 'Impossibile accedere con le informazioni fornite. Si prega di controllare le credenziali e riprovare.', + 'Impossibile accedere con le informazioni fornite. Per favore controlla le tue credenziali e riprova.', com_auth_error_login_rl: - 'Troppo tentativi di accesso in uno breve lasso di tempo. Per favore riprova più tardi.', + 'Troppi tentativi di accesso in breve tempo. Per favore riprova più tardi.', com_auth_error_login_ban: - 'Il tuo account è stato temporaneamente bloccato dovuto a violazioni del nostro servizio.', + 'Il tuo account è stato temporaneamente bloccato a causa della violazione del nostro servizio.', com_auth_error_login_server: - 'C\'è stato un errore interno del server. Si prega di attendere qualche istante e riprovare.', + 'Si è verificato un errore interno del server. Attendi qualche istante e riprova.', com_auth_no_account: 'Non hai un account?', - com_auth_sign_up: 'Iscriviti', + com_auth_sign_up: 'Registrati', com_auth_sign_in: 'Accedi', com_auth_google_login: 'Accedi con Google', com_auth_facebook_login: 'Accedi con Facebook', com_auth_github_login: 'Accedi con Github', com_auth_discord_login: 'Accedi con Discord', com_auth_email: 'Email', - com_auth_email_required: 'Email obbligatoria', - com_auth_email_min_length: 'L\'email deve avere almeno 6 caratteri', + com_auth_email_required: 'L\'email è obbligatoria', + com_auth_email_min_length: 'L\'email deve essere lunga almeno 6 caratteri', com_auth_email_max_length: 'L\'email non dovrebbe essere più lunga di 120 caratteri', com_auth_email_pattern: 'Devi inserire un indirizzo email valido', com_auth_email_address: 'Indirizzo email', com_auth_password: 'Password', com_auth_password_required: 'La password è obbligatoria', - com_auth_password_min_length: 'La password deve avere almeno 8 caratteri', - com_auth_password_max_length: 'La password deve essere più breve di 128 caratteri', + com_auth_password_min_length: 'La password deve essere lunga almeno 8 caratteri', + com_auth_password_max_length: 'La password deve essere più corta di 128 caratteri', com_auth_password_forgot: 'Password dimenticata?', com_auth_password_confirm: 'Conferma password', - com_auth_password_not_match: 'Le password non coincidono', + com_auth_password_not_match: 'Le password non corrispondono', com_auth_continue: 'Continua', com_auth_create_account: 'Crea il tuo account', - com_auth_error_create: 'C\'è stato un errore nel tentativo di registrarti. Per favore riprova.', + com_auth_error_create: + 'Si è verificato un errore nel tentativo di registrare il tuo account. Per favore riprova.', com_auth_full_name: 'Nome completo', com_auth_name_required: 'Il nome è obbligatorio', - com_auth_name_min_length: 'Il nome deve avere almeno 3 caratteri', - com_auth_name_max_length: 'Il nome deve essere più breve di 80 caratteri', - com_auth_username: 'Nome utente (opzionale)', + com_auth_name_min_length: 'Il nome deve essere lungo almeno 3 caratteri', + com_auth_name_max_length: 'Il nome deve essere più corto di 80 caratteri', + com_auth_username: 'Nome utente (facoltativo)', com_auth_username_required: 'Il nome utente è obbligatorio', - com_auth_username_min_length: 'Il nome utente deve avere almeno 2 caratteri', - com_auth_username_max_length: 'Il nome utente deve essere più breve di 20 caratteri', + com_auth_username_min_length: 'Il nome utente deve essere lungo almeno 2 caratteri', + com_auth_username_max_length: 'Il nome utente deve essere più corto di 20 caratteri', com_auth_already_have_account: 'Hai già un account?', com_auth_login: 'Accedi', com_auth_reset_password: 'Reimposta la tua password', @@ -99,42 +101,44 @@ export default { com_auth_to_reset_your_password: 'per reimpostare la tua password.', com_auth_reset_password_link_sent: 'Email inviata', com_auth_reset_password_email_sent: - 'Ti è stata inviata una email con ulteriori istruzioni per reimpostare la password.', + 'Ti è stata inviata un\'email con ulteriori istruzioni per reimpostare la tua password.', com_auth_error_reset_password: - 'C\'è stato un problema nel reimpostare la password. Non è stato trovato nessun utente con l\'indirizzo email fornito. Per favore riprova.', - com_auth_reset_password_success: 'Password reimpostata con successo', + 'Si è verificato un problema durante il reset della password. Non è stato trovato nessun utente con l\'indirizzo email fornito. Per favore riprova.', + com_auth_reset_password_success: 'Reset password avvenuto con successo', com_auth_login_with_new_password: 'Ora puoi accedere con la tua nuova password.', - com_auth_error_invalid_reset_token: 'Questo token di reimpostazione password non è più valido.', + com_auth_error_invalid_reset_token: 'Questo token di reset password non è più valido.', com_auth_click_here: 'Clicca qui', - com_auth_to_try_again: 'per provare di nuovo.', + com_auth_to_try_again: 'per riprovare.', com_auth_submit_registration: 'Invia registrazione', - com_auth_welcome_back: 'Ben tornato', - com_endpoint_open_menu: 'Apri menu', + com_auth_welcome_back: 'Bentornato', + com_endpoint_open_menu: 'Apri il menu', com_endpoint_bing_enable_sydney: 'Abilita Sydney', com_endpoint_bing_to_enable_sydney: 'Per abilitare Sydney', - com_endpoint_bing_jailbreak: 'Forza protezioni', + com_endpoint_bing_jailbreak: 'Jailbreak', com_endpoint_bing_context_placeholder: - 'Bing può usare fino a 7k token per "contesto", che può referenziare per la conversazione. Il limite specifico non è noto ma potrebbe andare in errore superando i 7k token', + 'Bing può usare fino a 7k token per "contesto", che può fare riferimento per la conversazione. Il limite specifico non è noto ma può dare errori superando i 7k token', com_endpoint_bing_system_message_placeholder: - 'ATTENZIONE: L\'abuso di questa funzione può farti BANNARE dall\'uso di Bing! Clicca su \'Messaggio di sistema\' per le istruzioni complete e il messaggio predefinito se omesso, che è il preset \'Sydney\' che è considerato sicuro.', + 'ATTENZIONE: L\'abuso di questa funzione può farti BLOCCARE da Bing! Fai clic su "Messaggio di sistema" per le istruzioni complete e il messaggio predefinito se omesso, che è il preset "Sydney" considerato sicuro.', com_endpoint_system_message: 'Messaggio di sistema', - com_endpoint_default_blank: 'predefinito: vuoto', - com_endpoint_default_false: 'predefinito: falso', - com_endpoint_default_creative: 'predefinito: creativo', - com_endpoint_default_empty: 'predefinito: vuoto', - com_endpoint_default_with_num: 'predefinito: {0}', + com_endpoint_message: 'Messaggio', + com_endpoint_message_not_appendable: 'Modifica il tuo messaggio o Rigenera.', + com_endpoint_default_blank: 'default: vuoto', + com_endpoint_default_false: 'default: falso', + com_endpoint_default_creative: 'default: creativo', + com_endpoint_default_empty: 'default: vuoto', + com_endpoint_default_with_num: 'default: {0}', com_endpoint_context: 'Contesto', - com_endpoint_tone_style: 'Stile tono', + com_endpoint_tone_style: 'Stile del tono', com_endpoint_token_count: 'Conteggio token', com_endpoint_output: 'Output', com_endpoint_google_temp: - 'Valori più alti = più casuali, mentre valori più bassi = più mirati e deterministici. Consigliamo di modificare questo o Top P ma non entrambi.', + 'Valori più alti = più casuali, mentre valori più bassi = più focalizzati e deterministici. Si consiglia di modificare questo o Top P ma non entrambi.', com_endpoint_google_topp: - 'Top-p cambia come il modello seleziona i token per l\'output. I token sono selezionati dai K più probabili (vedi parametro topK) ai meno probabili finché la somma delle loro probabilità equivale al valore top-p.', + 'Top-p cambia come il modello seleziona i token per l\'output. I token vengono selezionati dai più probabili K (vedi parametro topK) ai meno probabili fino a che la somma delle loro probabilità eguaglia il valore top-p.', com_endpoint_google_topk: - 'Top-k cambia come il modello seleziona i token per l\'output. Un top-k di 1 significa che il token selezionato è il più probabile tra tutti i token nel vocabolario del modello (anche chiamato decodifica greedy), mentre un top-k di 3 significa che il token successivo è selezionato tra i 3 token più probabili (usando temperature).', + 'Top-k cambia come il modello seleziona i token per l\'output. Un top-k di 1 significa che il token selezionato è il più probabile tra tutti i token nel vocabolario del modello (chiamato anche decodifica avida), mentre un top-k di 3 significa che il token successivo viene selezionato tra i 3 token più probabili (usando la temperatura).', com_endpoint_google_maxoutputtokens: - 'Numero massimo di token che possono essere generati nella risposta. Specifica un valore più basso per risposte più corte e un valore più alto per risposte più lunghe.', + 'Numero massimo di token che possono essere generati nella risposta. Specifica un valore inferiore per risposte più brevi e un valore superiore per risposte più lunghe.', com_endpoint_google_custom_name_placeholder: 'Imposta un nome personalizzato per Google', com_endpoint_prompt_prefix_placeholder: 'Imposta istruzioni o contesto personalizzati. Ignorato se vuoto.', @@ -146,41 +150,57 @@ export default { com_endpoint_top_k: 'Top K', com_endpoint_max_output_tokens: 'Token output massimi', com_endpoint_openai_temp: - 'Valori più alti = più casuali, mentre valori più bassi = più mirati e deterministici. Consigliamo di modificare questo o Top P ma non entrambi.', + 'Valori più alti = più casuali, mentre valori più bassi = più focalizzati e deterministici. Si consiglia di modificare questo o Top P ma non entrambi.', com_endpoint_openai_max: - 'Il numero massimo di token da generare. La lunghezza totale di token di input e token generati è limitata dalla lunghezza di contesto del modello.', + 'Il numero massimo di token da generare. La lunghezza totale dei token di input e generati è limitata dalla lunghezza di contesto del modello.', com_endpoint_openai_topp: - 'Un\'alternativa al campionamento con temperatura, chiamata nucleus sampling, dove il modello considera i risultati dei token con la massa di probabilità top_p. Quindi 0.1 significa che vengono considerati solo i token che comprendono il 10% della massa di probabilità in cima.', + 'Un\'alternativa al campionamento con temperatura, chiamata nucleus sampling, dove il modello considera i risultati dei token con massa di probabilità top_p. Quindi 0.1 significa che vengono considerati solo i token che comprendono il 10% della massa di probabilità.', com_endpoint_openai_freq: - 'Numero tra -2.0 e 2.0. Valori positivi penalizzano nuovi token in base alla loro frequenza esistente nel testo finora, diminuendo la probabilità del modello di ripetere la stessa frase parola per parola.', + 'Numero tra -2.0 e 2.0. Valori positivi penalizzano i nuovi token in base alla loro frequenza esistente nel testo finora, diminuendo la probabilità del modello di ripetere la stessa riga verbatim.', com_endpoint_openai_pres: - 'Numero tra -2.0 e 2.0. Valori positivi penalizzano nuovi token in base al fatto che appaiano nel testo finora, aumentando la probabilità del modello di parlare di nuovi argomenti.', + 'Numero tra -2.0 e 2.0. Valori positivi penalizzano i nuovi token in base al fatto che appaiano nel testo finora, aumentando la probabilità del modello di parlare di nuovi argomenti.', com_endpoint_openai_custom_name_placeholder: 'Imposta un nome personalizzato per ChatGPT', com_endpoint_openai_prompt_prefix_placeholder: - 'Imposta istruzioni personalizzate da includere nel Messaggio di sistema. Predefinito: nessuna', + 'Imposta istruzioni personalizzate da includere nel Messaggio di sistema. Predefinito: nessuno', com_endpoint_anthropic_temp: - 'Intervallo da 0 a 1. Usa temperature più vicine a 0 per compiti analitici/a scelta multipla e più vicine a 1 per compiti creativi e generativi. Consigliamo di modificare questo o Top P ma non entrambi.', + 'Varia da 0 a 1. Usa una temp più vicina a 0 per compiti analitici / a scelta multipla, e più vicina a 1 per compiti creativi e generativi. Si consiglia di modificare questo o Top P ma non entrambi.', com_endpoint_anthropic_topp: - 'Top-p cambia come il modello seleziona i token per l\'output. I token sono selezionati dai K più probabili (vedi parametro topK) ai meno probabili finché la somma delle loro probabilità equivale al valore top-p.', + 'Top-p cambia come il modello seleziona i token per l\'output. I token vengono selezionati dai più probabili K (vedi parametro topK) ai meno probabili fino a che la somma delle loro probabilità eguaglia il valore top-p.', com_endpoint_anthropic_topk: - 'Top-k cambia come il modello seleziona i token per l\'output. Un top-k di 1 significa che il token selezionato è il più probabile tra tutti i token nel vocabolario del modello (anche chiamato decodifica greedy), mentre un top-k di 3 significa che il token successivo è selezionato tra i 3 token più probabili (usando temperature).', + 'Top-k cambia come il modello seleziona i token per l\'output. Un top-k di 1 significa che il token selezionato è il più probabile tra tutti i token nel vocabolario del modello (chiamato anche decodifica avida), mentre un top-k di 3 significa che il token successivo viene selezionato tra i 3 token più probabili (usando la temperatura).', com_endpoint_anthropic_maxoutputtokens: - 'Numero massimo di token che possono essere generati nella risposta. Specifica un valore più basso per risposte più corte e un valore più alto per risposte più lunghe.', + 'Numero massimo di token che possono essere generati nella risposta. Specifica un valore inferiore per risposte più brevi e un valore superiore per risposte più lunghe.', com_endpoint_anthropic_custom_name_placeholder: 'Imposta un nome personalizzato per Anthropic', - com_endpoint_frequency_penalty: 'Penalità frequenza', - com_endpoint_presence_penalty: 'Penalità presenza', - com_endpoint_plug_use_functions: 'Abilita uso Plugin come funzioni OpenAI', - com_endpoint_plug_skip_completion: 'Salta fase Completamento', + com_endpoint_frequency_penalty: 'Penalità di frequenza', + com_endpoint_presence_penalty: 'Penalità di presenza', + com_endpoint_plug_use_functions: 'Usa funzioni', + com_endpoint_plug_skip_completion: 'Salta completamento', com_endpoint_disabled_with_tools: 'disabilitato con strumenti', - com_endpoint_disabled_with_tools_placeholder: 'Disabilitato con Strumenti Selezionati', + com_endpoint_disabled_with_tools_placeholder: 'Disabilitato con strumenti selezionati', com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: - 'Imposta le istruzioni personalizzate da includere nel Messaggio di sistema. Predefinito: nessuna', + 'Imposta istruzioni personalizzate da includere nel Messaggio di sistema. Predefinito: nessuno', com_endpoint_import: 'Importa', com_endpoint_set_custom_name: 'Imposta un nome personalizzato, nel caso tu possa trovare questo preset', + com_endpoint_preset_delete_confirm: 'Sei sicuro di voler eliminare questo preset?', + com_endpoint_preset_clear_all_confirm: 'Sei sicuro di voler eliminare tutti i tuoi preset?', + com_endpoint_preset_import: 'Preset importato!', + com_endpoint_preset_import_error: + 'Si è verificato un errore durante l\'importazione del tuo preset. Per favore riprova.', + com_endpoint_preset_save_error: + 'Si è verificato un errore durante il salvataggio del tuo preset. Per favore riprova.', + com_endpoint_preset_delete_error: + 'Si è verificato un errore durante l\'eliminazione del tuo preset. Per favore riprova.', + com_endpoint_preset_default_removed: 'non è più il preset predefinito.', + com_endpoint_preset_default_item: 'Predefinito:', + com_endpoint_preset_default_none: 'Nessun preset predefinito attivo.', + com_endpoint_preset_title: 'Preset', + com_endpoint_preset_saved: 'Salvato!', + com_endpoint_preset_default: 'è ora il preset predefinito.', com_endpoint_preset: 'preset', com_endpoint_presets: 'preset', com_endpoint_preset_selected: 'Preset attivo!', + com_endpoint_preset_selected_title: 'Attivo!', com_endpoint_preset_name: 'Nome preset', com_endpoint_new_topic: 'Nuovo argomento', com_endpoint: 'Endpoint', @@ -194,59 +214,66 @@ export default { com_endpoint_export: 'Esporta', com_endpoint_save_as_preset: 'Salva come preset', com_endpoint_presets_clear_warning: - 'Sei sicuro di voler cancellare tutti i preset? Questa azione sarà irreversibile.', + 'Sei sicuro di voler cancellare tutti i preset? Questa operazione è irreversibile.', com_endpoint_not_implemented: 'Non implementato', - com_endpoint_no_presets: 'Nessun preset ancora', + com_endpoint_no_presets: 'Nessun preset ancora, usa il pulsante impostazioni per crearne uno', com_endpoint_not_available: 'Nessun endpoint disponibile', - com_endpoint_clear_all: 'Cancella tutto', - com_endpoint_view_options: 'Visualizza opzioni', + com_endpoint_view_options: 'Opzioni di visualizzazione', com_endpoint_save_convo_as_preset: 'Salva conversazione come preset', - com_endpoint_my_preset: 'Il mio preimpostato', - com_endpoint_agent_model: 'Modello Agente (Consigliato: GPT-3.5)', - com_endpoint_completion_model: 'Modello Completamento (Consigliato: GPT-4)', - com_endpoint_func_hover: 'Abilitare l\'uso dei Plugin come funzioni OpenAI', + com_endpoint_my_preset: 'Il mio preset', + com_endpoint_agent_model: 'Modello agente (Consigliato: GPT-3.5)', + com_endpoint_completion_model: 'Modello completamento (Consigliato: GPT-4)', + com_endpoint_func_hover: 'Abilita l\'uso di plugin come funzioni OpenAI', com_endpoint_skip_hover: - 'Abilita la possibilità di saltare la fase di completamento, che rivede la risposta finale e le fasi generate', - com_endpoint_config_key: 'Imposta Chiave API', - com_endpoint_config_key_for: 'Imposta Chiave API per', + 'Abilita il salto del passaggio di completamento, che rivede la risposta finale e i passaggi generati', + com_endpoint_config_key: 'Imposta chiave API', + com_endpoint_config_placeholder: 'Imposta la tua Chiave nel menu Header per chattare.', + com_endpoint_config_key_for: 'Imposta chiave API per', com_endpoint_config_key_name: 'Chiave', com_endpoint_config_value: 'Inserisci valore per', - com_endpoint_config_key_name_placeholder: 'Prima imposta una chiave API', + com_endpoint_config_key_name_placeholder: 'Imposta prima la chiave API', com_endpoint_config_key_encryption: 'La tua chiave verrà crittografata ed eliminata al', - com_endpoint_config_key_expiry: 'il tempo di scadenza', - com_endpoint_config_key_import_json_key: 'Importa la chiave JSON dell\'account di servizio', + com_endpoint_config_key_expiry: 'tempo di scadenza', + com_endpoint_config_click_here: 'Clicca qui', + com_endpoint_config_google_service_key: 'Chiave account servizio Google', + com_endpoint_config_google_cloud_platform: '(da Google Cloud Platform)', + com_endpoint_config_google_api_key: 'Chiave API Google', + com_endpoint_config_google_gemini_api: '(API Gemini)', + com_endpoint_config_google_api_info: + 'Per ottenere la tua chiave API Linguaggio Generativo (per Gemini),', + com_endpoint_config_key_import_json_key: 'Importa chiave JSON account di servizio.', com_endpoint_config_key_import_json_key_success: - 'Chiave JSON dell\'account di servizio importata correttamente', + 'Chiave JSON account di servizio importata con successo', com_endpoint_config_key_import_json_key_invalid: - 'Chiave JSON del Service Account non valida, hai importato il file corretto?', - com_endpoint_config_key_get_edge_key: 'Per ottenere il token di accesso per Bing, accedi a', + 'Chiave JSON account di servizio non valida, hai importato il file corretto?', + com_endpoint_config_key_get_edge_key: 'Per ottenere il tuo token di accesso per Bing, accedi a', com_endpoint_config_key_get_edge_key_dev_tool: - 'Usa gli strumenti di sviluppo o un\'estensione mentre sei loggato nel sito per copiare il contenuto del cookie _U. Se ciò fallisce, segui queste', - com_endpoint_config_key_edge_instructions: 'Istruzioni', - com_endpoint_config_key_edge_full_key_string: 'per fornire la stringa di cookie complete.', + 'Usa gli strumenti di sviluppo o un\'estensione mentre sei loggato nel sito per copiare il contenuto del cookie _U. Se questo non funziona, segui queste', + com_endpoint_config_key_edge_instructions: 'istruzioni', + com_endpoint_config_key_edge_full_key_string: 'per fornire le stringhe complete dei cookie.', com_endpoint_config_key_chatgpt: - 'Per ottenere il tuo token di accesso per ChatGPT \'Versione gratuita\', accedi a', + 'Per ottenere il tuo token di accesso Per ChatGPT "Versione gratuita", accedi a', com_endpoint_config_key_chatgpt_then_visit: 'poi visita', com_endpoint_config_key_chatgpt_copy_token: 'Copia token di accesso.', com_endpoint_config_key_google_need_to: 'Devi', com_endpoint_config_key_google_vertex_ai: 'Abilitare Vertex AI', com_endpoint_config_key_google_vertex_api: 'API su Google Cloud, poi', - com_endpoint_config_key_google_service_account: 'Crea un account di servizio', + com_endpoint_config_key_google_service_account: 'Creare un account di servizio', com_endpoint_config_key_google_vertex_api_role: - 'Assicurati di fare clic su \'Crea e continua\' per dare almeno il ruolo \'Utente Vertex AI\'. Infine, crea una chiave JSON da importare qui.', + 'Assicurati di fare clic su "Crea e continua" per dare almeno il ruolo "Vertex AI User". Infine, crea una chiave JSON da importare qui.', com_nav_welcome_message: 'Come posso aiutarti oggi?', - com_nav_auto_scroll: 'Scorrimento automatico', - com_nav_plugin_store: 'Negozio dei plugin', + com_nav_auto_scroll: 'Scorri automaticamente al Più recente all\'apertura', + com_nav_plugin_store: 'Negozio plugin', com_nav_plugin_search: 'Cerca plugin', com_nav_plugin_auth_error: - 'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore, riprova.', - com_nav_export_filename: 'Nome del file', + 'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore riprova.', + com_nav_export_filename: 'Nome file', com_nav_export_filename_placeholder: 'Imposta il nome del file', com_nav_export_type: 'Tipo', - com_nav_export_include_endpoint_options: 'Includi opzioni dell\'endpoint', + com_nav_export_include_endpoint_options: 'Includi opzioni endpoint', com_nav_enabled: 'Abilitato', com_nav_not_supported: 'Non supportato', - com_nav_export_all_message_branches: 'Esporta tutti i rami dei messaggi', + com_nav_export_all_message_branches: 'Esporta tutti i rami di messaggio', com_nav_export_recursive_or_sequential: 'Ricorsivo o sequenziale?', com_nav_export_recursive: 'Ricorsivo', com_nav_export_conversation: 'Esporta conversazione', @@ -254,20 +281,19 @@ export default { com_nav_theme_system: 'Sistema', com_nav_theme_dark: 'Scuro', com_nav_theme_light: 'Chiaro', - com_nav_clear: 'Cancella', - com_nav_clear_all_chats: 'Cancella tutte le chat', - com_nav_confirm_clear: 'Conferma la cancellazione', - com_nav_close_sidebar: 'Chiudi la barra laterale', - com_nav_open_sidebar: 'Apri la barra laterale', + com_nav_clear_all_chats: 'Elimina tutte le chat', + com_nav_confirm_clear: 'Conferma eliminazione', + com_nav_close_sidebar: 'Chiudi sidebar', + com_nav_open_sidebar: 'Apri sidebar', com_nav_send_message: 'Invia messaggio', - com_nav_log_out: 'Esci', + com_nav_log_out: 'Disconnettiti', com_nav_user: 'UTENTE', - com_nav_clear_conversation: 'Cancella conversazioni', + com_nav_clear_conversation: 'Elimina conversazioni', com_nav_clear_conversation_confirm_message: - 'Sei sicuro di voler cancellare tutte le conversazioni? Questa azione è irreversibile.', - com_nav_help_faq: 'Aiuto & FAQ', + 'Sei sicuro di voler eliminare tutte le conversazioni? Questa operazione è irreversibile.', + com_nav_help_faq: 'Aiuto e FAQ', com_nav_settings: 'Impostazioni', com_nav_search_placeholder: 'Cerca messaggi', com_nav_setting_general: 'Generale', - com_nav_setting_data: 'Controllo dei dati', + com_nav_setting_data: 'Controlli dati', }; diff --git a/client/src/localization/languages/Zh.tsx b/client/src/localization/languages/Zh.tsx index 01ef650af9b8..ffe8552a8420 100644 --- a/client/src/localization/languages/Zh.tsx +++ b/client/src/localization/languages/Zh.tsx @@ -246,5 +246,4 @@ export default { com_nav_search_placeholder: '搜索对话及对话内容', com_nav_setting_general: '通用', com_nav_setting_data: '数据管理', - com_nav_language: '语言', }; From 443b4912868ba1305a3d92a60fc0a3135b6edb53 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Fri, 29 Dec 2023 23:28:07 +0100 Subject: [PATCH 04/42] feat: allow FWA (#1440) --- client/index.html | 1 + index.html | 1 + 2 files changed, 2 insertions(+) diff --git a/client/index.html b/client/index.html index 6d6c1dbf5961..dae28cb7057e 100644 --- a/client/index.html +++ b/client/index.html @@ -3,6 +3,7 @@ + LibreChat + LibreChat Date: Sat, 30 Dec 2023 01:28:59 +0300 Subject: [PATCH 05/42] =?UTF-8?q?=F0=9F=8C=8E:=20Update=20Russian=20transl?= =?UTF-8?q?ations=20(#1413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Russian localization * Update Ru.tsx * fix: russian translation typing errors --- client/src/localization/languages/Ru.tsx | 203 ++++++++++++++--------- 1 file changed, 129 insertions(+), 74 deletions(-) diff --git a/client/src/localization/languages/Ru.tsx b/client/src/localization/languages/Ru.tsx index f852f4405049..365f93f1da04 100644 --- a/client/src/localization/languages/Ru.tsx +++ b/client/src/localization/languages/Ru.tsx @@ -3,10 +3,9 @@ export default { com_ui_examples: 'Примеры', com_ui_new_chat: 'Создать чат', - com_ui_example_quantum_computing: 'Объясните квантовые вычисления простыми словами', - com_ui_example_10_year_old_b_day: - 'У вас есть креативные идеи для дня рождения 10-летнего ребенка?', - com_ui_example_http_in_js: 'Как сделать HTTP-запрос в JavaScript?', + com_ui_example_quantum_computing: 'Объясни квантовые вычисления простыми словами', + com_ui_example_10_year_old_b_day: 'У тебя есть креативные идеи для дня рождения 10-летнего ребенка?', + com_ui_example_http_in_js: 'Как мне сделать HTTP-запрос в JavaScript?', com_ui_capabilities: 'Возможности', com_ui_capability_remember: 'Запоминает, что пользователь говорил ранее в разговоре', com_ui_capability_correction: 'Позволяет пользователю вносить корректировки после ответа', @@ -18,37 +17,53 @@ export default { com_ui_limitation_limited_2021: 'Ограниченные знания о мире и событиях после 2021 года', com_ui_input: 'Ввод', com_ui_close: 'Закрыть', - com_ui_clear: 'Очистить', - com_ui_revoke: 'Отозвать', - com_ui_revoke_info: 'Отозвать все предоставленные пользователем учетные данные', com_ui_model: 'Модель', com_ui_select_model: 'Выберите модель', - com_ui_use_prompt: 'Использовать подсказку', + com_ui_use_prompt: 'Использовать промт', com_ui_prev: 'Предыдущий', com_ui_next: 'Следующий', + com_ui_stop: 'Остановить генерацию', + com_ui_prompt_templates: 'Шаблоны промтов', + com_ui_hide_prompt_templates: 'Скрыть шаблоны промтов', + com_ui_showing: 'Показано', + com_ui_of: 'из', + com_ui_entries: 'записей', + com_ui_pay_per_call: 'Все AI-разговоры в одном месте. Оплачивайте за вызовы, а не за месяц', + com_ui_new_footer: 'Все AI-разговоры в одном месте.', + com_ui_enter: 'Ввести', + com_ui_submit: 'Отправить', + com_ui_upload_success: 'Файл успешно загружен', + com_ui_upload_invalid: 'Недопустимый файл для загрузки', com_ui_cancel: 'Отмена', com_ui_save: 'Сохранить', - com_ui_submit: 'Отправить', + com_ui_copy_to_clipboard: 'Копировать в буфер обмена', + com_ui_copied_to_clipboard: 'Скопировано в буфер обмена', + com_ui_regenerate: 'Повторная генерация', + com_ui_continue: 'Продолжить', + com_ui_edit: 'Редактировать', + com_ui_success: 'Успешно', + com_ui_all: 'все', + com_ui_clear: 'Удалить', + com_ui_revoke: 'Отозвать', + com_ui_revoke_info: 'Отозвать все предоставленные пользователем учетные данные', com_ui_confirm_action: 'Подтвердить действие', + com_ui_chats: 'чаты', com_ui_delete: 'Удалить', com_ui_delete_conversation: 'Удалить чат?', com_ui_delete_conversation_confirm: 'Будет удален следующий чат: ', - com_ui_regenerate: 'Повторная генерация', - com_ui_stop: 'Остановить генерацию', - com_ui_continue: 'Продолжить', - com_ui_prompt_templates: 'Шаблоны подсказок', - com_ui_hide_prompt_templates: 'Скрыть шаблоны подсказок', - com_ui_showing: 'Показано', - com_ui_of: 'из', - com_ui_all: 'все', - com_ui_entries: 'записей', - com_ui_pay_per_call: 'Все AI-разговоры в одном месте. Оплачивайте за звонки, а не за месяц', com_auth_error_login: 'Не удалось войти с предоставленной информацией. Пожалуйста, проверьте ваши учетные данные и попробуйте снова.', - com_auth_no_account: 'Еще нет аккаунта?', + com_auth_error_login_rl: + 'Слишком много попыток входа в систему за короткий промежуток времени. Пожалуйста, повторите попытку позже.', + com_auth_error_login_ban: + 'Ваша учетная запись была временно заблокирована в связи с нарушениями нашего сервиса.', + com_auth_error_login_server: + 'Произошла внутренняя ошибка сервера. Пожалуйста, подождите несколько минут и повторите попытку.', + com_auth_no_account: 'Еще не зарегистрированы?', com_auth_sign_up: 'Зарегистрироваться', com_auth_sign_in: 'Войти', com_auth_google_login: 'Войти с помощью Google', + com_auth_facebook_login: 'Войти с помощью Facebook', com_auth_github_login: 'Войти с помощью Github', com_auth_discord_login: 'Войти с помощью Discord', com_auth_email: 'Email', @@ -74,14 +89,14 @@ export default { com_auth_name_max_length: 'Имя должно быть короче 80 символов', com_auth_username: 'Имя пользователя (необязательно)', com_auth_username_required: 'Имя пользователя обязательно', - com_auth_username_min_length: 'Имя пользователя должно содержать не менее 3 символов', + com_auth_username_min_length: 'Имя пользователя должно содержать не менее 2 символов', com_auth_username_max_length: 'Имя пользователя должно быть не более 20 символов', - com_auth_already_have_account: 'Уже есть аккаунт?', + com_auth_already_have_account: 'Уже зарегистрированы?', com_auth_login: 'Войти', com_auth_reset_password: 'Сбросить пароль', com_auth_click: 'Нажмите', com_auth_here: 'ЗДЕСЬ', - com_auth_to_reset: 'ваш пароль.', + com_auth_to_reset_your_password: 'чтобы сбросить ваш пароль.', com_auth_reset_password_link_sent: 'Письмо отправлено', com_auth_reset_password_email_sent: 'На вашу почту было отправлено письмо с дальнейшими инструкциями по сбросу пароля.', @@ -95,14 +110,16 @@ export default { com_auth_submit_registration: 'Отправить регистрацию', com_auth_welcome_back: 'Добро пожаловать', com_endpoint_open_menu: 'Открыть меню', - com_endpoint_bing_enable_sydney: 'Включить Сидней', - com_endpoint_bing_to_enable_sydney: 'Чтобы включить Сидней', + com_endpoint_bing_enable_sydney: 'Включить Sydney', + com_endpoint_bing_to_enable_sydney: 'Чтобы включить Sydney', com_endpoint_bing_jailbreak: 'Jailbreak', com_endpoint_bing_context_placeholder: 'Bing может использовать до 7 тысяч токенов для "контекста", на который он может ссылаться в разговоре. Точный предел неизвестен, но превышение 7 тысяч токенов может вызвать ошибки.', com_endpoint_bing_system_message_placeholder: - 'ПРЕДУПРЕЖДЕНИЕ: Неправильное использование этой функции может привести к БАНу на использование Bing! Нажмите на "Системное сообщение" для получения полных инструкций и значения по умолчанию, которое является предустановкой "Сидней", считающейся безопасной.', + 'ПРЕДУПРЕЖДЕНИЕ: Неправильное использование этой функции может привести к БАНУ на использование Bing! Нажмите на "Системное сообщение" для получения полных инструкций и значения по умолчанию, которое является предустановкой "Sydney", считающейся безопасной.', com_endpoint_system_message: 'Системное сообщение', + com_endpoint_message: 'Сообщение', + com_endpoint_message_not_appendable: 'Отредактируйте свое сообщение или перегенерируйте.', com_endpoint_default_blank: 'по умолчанию: пусто', com_endpoint_default_false: 'по умолчанию: false', com_endpoint_default_creative: 'по умолчанию: креативный', @@ -115,21 +132,20 @@ export default { com_endpoint_google_temp: 'Более высокие значения = более случайные результаты, более низкие значения = более фокусированные и детерминированные результаты. Мы рекомендуем изменять это или Top P, но не оба значения одновременно.', com_endpoint_google_topp: - 'Top P изменяет то, как модель выбирает токены для вывода. Токены выбираются из наиболее вероятных (см. параметр topK) до наименее вероятных, пока сумма их вероятностей не достигнет значения top-p.', + 'Top-p изменяет то, как модель выбирает токены для вывода. Токены выбираются из наиболее вероятных K (см. параметр topK) до наименее вероятных, пока сумма их вероятностей не достигнет значения top-p.', com_endpoint_google_topk: - 'Top K изменяет то, как модель выбирает токены для вывода. Top K равное 1 означает, что выбирается наиболее вероятный токен из всего словаря модели (так называемое жадное декодирование), а Top K равное 3 означает, что следующий токен выбирается из трех наиболее вероятных токенов (с использованием температуры).', + 'Top-k изменяет то, как модель выбирает токены для вывода. Top-k равное 1 означает, что выбирается наиболее вероятный токен из всего словаря модели (так называемое жадное декодирование), а Top-k равное 3 означает, что следующий токен выбирается из трех наиболее вероятных токенов (с использованием температуры).', com_endpoint_google_maxoutputtokens: - 'Максимальное количество токенов, которые могут быть сгенерированы в ответе. Укажите меньшее значение для более коротких ответов и большее значение для более длинных ответов.', - com_endpoint_google_custom_name_placeholder: 'Установите пользовательское имя для Google', - com_endpoint_google_prompt_prefix_placeholder: - 'Установите пользовательские инструкции или контекст. Игнорируется, если пусто.', - com_endpoint_custom_name: 'Пользовательское имя', - com_endpoint_prompt_prefix: 'Префикс подсказки', + ' Максимальное количество токенов, которые могут быть сгенерированы в ответе. Укажите меньшее значение для более коротких ответов и большее значение для более длинных ответов.', + com_endpoint_google_custom_name_placeholder: 'Задайте кастомное имя для Google', + com_endpoint_prompt_prefix_placeholder: 'Задайте пользовательские инструкции или контекст. Игнорируется, если пусто.', + com_endpoint_custom_name: 'Кастомное имя', + com_endpoint_prompt_prefix: 'Префикс промта', com_endpoint_temperature: 'Температура', com_endpoint_default: 'по умолчанию', com_endpoint_top_p: 'Top P', com_endpoint_top_k: 'Top K', - com_endpoint_max_output_tokens: 'Максимальное количество токенов в выводе', + com_endpoint_max_output_tokens: 'Максимальное количество выводимых токенов', com_endpoint_openai_temp: 'Более высокие значения = более случайные результаты, более низкие значения = более фокусированные и детерминированные результаты. Мы рекомендуем изменять это или Top P, но не оба значения одновременно.', com_endpoint_openai_max: @@ -140,9 +156,9 @@ export default { 'Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе их частоты в тексте до сих пор, уменьшая вероятность модели повторить ту же строку дословно.', com_endpoint_openai_pres: 'Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе того, появляются ли они в тексте до сих пор, увеличивая вероятность модели говорить о новых темах.', - com_endpoint_openai_custom_name_placeholder: 'Установите пользовательское имя для ChatGPT', + com_endpoint_openai_custom_name_placeholder: 'Задайте кастомное имя для ChatGPT', com_endpoint_openai_prompt_prefix_placeholder: - 'Установите пользовательские инструкции для включения в системное сообщение. По умолчанию: нет', + 'Задайте кастомные промты для включения в системное сообщение. По умолчанию: нет', com_endpoint_anthropic_temp: 'Диапазон значений от 0 до 1. Используйте значение temp ближе к 0 для аналитических / множественного выбора и ближе к 1 для креативных и генеративных задач. Мы рекомендуем изменять это или Top P, но не оба значения одновременно.', com_endpoint_anthropic_topp: @@ -151,18 +167,36 @@ export default { 'Top K изменяет то, как модель выбирает токены для вывода. Top K равное 1 означает, что выбирается наиболее вероятный токен из всего словаря модели (так называемое жадное декодирование), а Top K равное 3 означает, что следующий токен выбирается из трех наиболее вероятных токенов (с использованием температуры).', com_endpoint_anthropic_maxoutputtokens: 'Максимальное количество токенов, которые могут быть сгенерированы в ответе. Укажите меньшее значение для более коротких ответов и большее значение для более длинных ответов.', + com_endpoint_anthropic_custom_name_placeholder: 'Задайте кастомное имя для Anthropic', com_endpoint_frequency_penalty: 'Штраф за частоту', com_endpoint_presence_penalty: 'Штраф за присутствие', com_endpoint_plug_use_functions: 'Использовать функции', com_endpoint_plug_skip_completion: 'Пропустить завершение', - com_endpoint_disabled_with_tools: 'отключено с инструментами', - com_endpoint_disabled_with_tools_placeholder: 'Отключено с выбранными инструментами', + com_endpoint_disabled_with_tools: 'отключено с плагинами', + com_endpoint_disabled_with_tools_placeholder: 'Отключено при включённых плагинах', com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: - 'Установите пользовательские инструкции для включения в системное сообщение. По умолчанию: нет', - com_endpoint_set_custom_name: 'Установите пользовательское имя, чтобы найти эту предустановку', - com_endpoint_preset_name: 'Имя предустановки', + 'Задайте кастомные инструкции для включения в системное сообщение. По умолчанию: нет', + com_endpoint_import: 'Импортировать', + com_endpoint_set_custom_name: 'Задайте кастомное имя на случай, если вы сможете найти эту предустановку :)', + com_endpoint_preset_delete_confirm: 'Вы уверены, что хотите удалить этот пресет?', + com_endpoint_preset_clear_all_confirm: 'Вы уверены, что хотите удалить все ваши пресеты?', + com_endpoint_preset_import: 'Пресет Импортирован!', + com_endpoint_preset_import_error: 'Произошла ошибка при импорте вашего пресета. Пожалуйста, попробуйте еще раз.', + com_endpoint_preset_save_error: 'Произошла ошибка при сохранении вашего пресета. Пожалуйста, попробуйте еще раз.', + com_endpoint_preset_delete_error: 'Произошла ошибка при удалении вашего пресета. Пожалуйста, попробуйте еще раз.', + com_endpoint_preset_default_removed: 'больше не пресет по умолчанию.', + com_endpoint_preset_default_item: 'По умолчанию:', + com_endpoint_preset_default_none: 'Нет активных пресетов По умолчанию.', + com_endpoint_preset_title: 'Пресет', + com_endpoint_preset_saved: 'Сохранено!', + com_endpoint_preset_default: 'теперь пресет По умолчаанию.', + com_endpoint_preset: 'пресет', + com_endpoint_presets: 'пресеты', + com_endpoint_preset_selected: 'Пресет Активирован!', + com_endpoint_preset_selected_title: 'Активирован!', + com_endpoint_preset_name: 'Имя пресета', com_endpoint_new_topic: 'Новая тема', - com_endpoint: 'Конечная точка', + com_endpoint: 'Эндпоинт', com_endpoint_hide: 'Скрыть', com_endpoint_show: 'Показать', com_endpoint_examples: 'Примеры', @@ -171,41 +205,65 @@ export default { com_endpoint_show_what_settings: 'Показать настройки {0}', com_endpoint_save: 'Сохранить', com_endpoint_export: 'Экспортировать', - com_endpoint_import: 'Импортировать', - com_endpoint_save_as_preset: 'Сохранить как предустановку', - com_endpoint_not_implemented: 'Не реализовано', - com_endpoint_no_presets: 'Пока нет предустановок', - com_endpoint_not_available: 'Нет доступных конечных точек', - com_endpoint_clear_all: 'Очистить все', - com_endpoint_view_options: 'Просмотреть параметры', - com_endpoint_save_convo_as_preset: 'Сохранить разговор как предустановку', + com_endpoint_save_as_preset: 'Сохранить как Пресет', com_endpoint_presets_clear_warning: - 'Вы уверены, что хотите очистить все предустановки? Эти действия необратимы, и восстановление невозможно.', - com_endpoint_presets: 'предустановки', - com_endpoint_my_preset: 'Моя предустановка', - com_endpoint_config_key: 'Установить ключ API', - com_endpoint_config_key_for: 'Установить ключ API для', - com_endpoint_config_key_name: 'Ключ', - com_endpoint_config_value: 'Введите значение для', - com_endpoint_config_key_name_placeholder: 'Установите сначала ключ API', - com_endpoint_config_key_import_json_key: 'Импортировать JSON-ключ учетной записи.', + 'Вы уверены, что хотите удалить все пресеты? Это действие необратимо и восстановление невозможно.', + com_endpoint_not_implemented: 'Не реализовано', + com_endpoint_no_presets: 'Пока нет пресетов, используйте кнопку настроек чтобы создать его', + com_endpoint_not_available: 'Нет доступных эндпоинтов', + com_endpoint_view_options: 'Просмотреть Настройки', + com_endpoint_save_convo_as_preset: 'Сохранить текущий разговор как Пресет', + com_endpoint_my_preset: 'Мой Пресет', com_endpoint_agent_model: 'Модель агента (Рекомендуется: GPT-3.5)', com_endpoint_completion_model: 'Модель завершения (Рекомендуется: GPT-4)', - com_endpoint_func_hover: 'Включить использование плагинов в качестве функций OpenAI', + com_endpoint_func_hover: 'Включить использование плагинов как функции OpenAI', com_endpoint_skip_hover: 'Пропустить этап завершения, который проверяет окончательный ответ и сгенерированные шаги', - com_endpoint_config_token: 'Токен конфигурации', + com_endpoint_config_key: 'Указать ключ к API', + com_endpoint_config_placeholder: 'Укажите ваш ключ к API в меню сверху для начала разговора.', + com_endpoint_config_key_for: 'Установить ключ к API для', + com_endpoint_config_key_name: 'Ключ', + com_endpoint_config_value: 'Введите значение для', + com_endpoint_config_key_name_placeholder: 'Сначала укажите ключ к API', + com_endpoint_config_key_encryption: 'Ваш ключ зашифрован и будет удалён', + com_endpoint_config_key_expiry: 'срок действия', + com_endpoint_config_click_here: 'Нажми Здесь', + com_endpoint_config_google_service_key: 'Google Service Account Key', + com_endpoint_config_google_cloud_platform: '(из Google Cloud Platform)', + com_endpoint_config_google_api_key: 'Google API Key', + com_endpoint_config_google_gemini_api: '(Gemini API)', + com_endpoint_config_google_api_info: 'Чтобы получить ключ к API Generative Language (для Gemini),', + com_endpoint_config_key_import_json_key: 'Импортировать Service Account JSON Key.', + com_endpoint_config_key_import_json_key_success: 'Успешно Импортирован Service Account JSON Key', + com_endpoint_config_key_import_json_key_invalid: + 'Некорректный Service Account JSON Key, Вы импортировали верный файл?', + com_endpoint_config_key_get_edge_key: 'Чтобы получить ваш токен доступа к Bing, войдите в', + com_endpoint_config_key_get_edge_key_dev_tool: + 'Пока вы на сайте, используйте dev tools или расширение чтобы скопировать содержимое куки _U. Если не получается, следуйте этим', + com_endpoint_config_key_edge_instructions: 'инструкциям', + com_endpoint_config_key_edge_full_key_string: 'чтобы получить все строки cookie.', + com_endpoint_config_key_chatgpt: 'Чтобы получить токен доступа к "Бесплатной Версии" ChatGPT, войдите в', + com_endpoint_config_key_chatgpt_then_visit: 'затем посетите', + com_endpoint_config_key_chatgpt_copy_token: 'Скопируйте токен доступа.', + com_endpoint_config_key_google_need_to: 'Вам нужно', + com_endpoint_config_key_google_vertex_ai: 'Активировать Vertex AI', + com_endpoint_config_key_google_vertex_api: 'API в Google Cloud, после', + com_endpoint_config_key_google_service_account: 'Создать Service Account', + com_endpoint_config_key_google_vertex_api_role: + 'Убедитесь что нажали на \'Create and Continue\' чтобы получить как минимум \'Vertex AI User\'. Наконец, создайте JSON-ключ чтобы импортировать его сюда.', + com_nav_welcome_message: 'Чем я могу помочь вам сегодня?', + com_nav_auto_scroll: 'Автоматически проматывать к самым новым сообщениям при открытии', com_nav_plugin_store: 'Магазин плагинов', com_nav_plugin_search: 'Поиск плагинов', com_nav_plugin_auth_error: 'При попытке аутентификации этого плагина произошла ошибка. Пожалуйста, попробуйте еще раз.', com_nav_export_filename: 'Имя файла', - com_nav_export_filename_placeholder: 'Установите имя файла', + com_nav_export_filename_placeholder: 'Задайте имя файла', com_nav_export_type: 'Тип', - com_nav_export_include_endpoint_options: 'Включить параметры конечной точки', + com_nav_export_include_endpoint_options: 'Включить параметры эндпоинта', com_nav_enabled: 'Включено', com_nav_not_supported: 'Не поддерживается', - com_nav_export_all_message_branches: 'Экспортировать все ветви сообщений', + com_nav_export_all_message_branches: 'Экспортировать все ветки сообщений', com_nav_export_recursive_or_sequential: 'Рекурсивно или последовательно?', com_nav_export_recursive: 'Рекурсивно', com_nav_export_conversation: 'Экспортировать разговор', @@ -213,22 +271,19 @@ export default { com_nav_theme_system: 'Системная', com_nav_theme_dark: 'Темная', com_nav_theme_light: 'Светлая', - com_nav_language: 'Локализация (Альфа)', - com_nav_clear: 'Очистить', - com_nav_clear_all_chats: 'Очистить все чаты', - com_nav_confirm_clear: 'Подтвердить очистку', - com_nav_auto_scroll: 'Автоматическая прокрутка к новым сообщениям в режиме открытия', + com_nav_clear_all_chats: 'Удалить все чаты', + com_nav_confirm_clear: 'Подтвердить удаление', com_nav_close_sidebar: 'Закрыть боковую панель', com_nav_open_sidebar: 'Открыть боковую панель', com_nav_send_message: 'Отправить сообщение', com_nav_log_out: 'Выйти', com_nav_user: 'ПОЛЬЗОВАТЕЛЬ', - com_nav_clear_conversation: 'Очистить разговоры', + com_nav_clear_conversation: 'Удалить разговоры', com_nav_clear_conversation_confirm_message: - 'Вы уверены, что хотите очистить все разговоры? Это действие нельзя отменить.', - com_nav_help_faq: 'Помощь и часто задаваемые вопросы', - com_nav_setting_data: 'Управление данными', + 'Вы уверены, что хотите удалить все разговоры? Это действие нельзя отменить.', + com_nav_help_faq: 'Помощь и ЧаВо', com_nav_settings: 'Настройки', com_nav_search_placeholder: 'Поиск сообщений', com_nav_setting_general: 'Общие', + com_nav_setting_data: 'Управление данными', }; From c3d5a08b262ad382184ab098bd392154e57c2c98 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Fri, 29 Dec 2023 20:20:57 -0500 Subject: [PATCH 06/42] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Prevent=20Unnecessa?= =?UTF-8?q?ry=20Cloning=20of=20Symbols=20in=20Log=20Object=20(#1455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(api/config/parsers): prevent cloning of unnecessary symbols within log object by using `klona` instead of `klona/full`, handle symbols edge case, log parsing errors, and use spaces instead of tab for cleaner logs --- api/config/parsers.js | 108 +++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 43 deletions(-) diff --git a/api/config/parsers.js b/api/config/parsers.js index 4f94d6e4d767..59685eab0bf3 100644 --- a/api/config/parsers.js +++ b/api/config/parsers.js @@ -1,11 +1,16 @@ +const { klona } = require('klona'); const winston = require('winston'); const traverse = require('traverse'); -const { klona } = require('klona/full'); const SPLAT_SYMBOL = Symbol.for('splat'); const MESSAGE_SYMBOL = Symbol.for('message'); -const sensitiveKeys = [/^(sk-)[^\s]+/, /(Bearer )[^\s]+/, /(api-key:? )[^\s]+/, /(key=)[^\s]+/]; +const sensitiveKeys = [ + /^(sk-)[^\s]+/, // OpenAI API key pattern + /(Bearer )[^\s]+/, // Header: Bearer token pattern + /(api-key:? )[^\s]+/, // Header: API key pattern + /(key=)[^\s]+/, // URL query param: sensitive key pattern (Google) +]; /** * Determines if a given value string is sensitive and returns matching regex patterns. @@ -102,55 +107,72 @@ const condenseArray = (item) => { */ const debugTraverse = winston.format.printf(({ level, message, timestamp, ...metadata }) => { let msg = `${timestamp} ${level}: ${truncateLongStrings(message?.trim(), 150)}`; + try { + if (level !== 'debug') { + return msg; + } - if (level !== 'debug') { - return msg; - } + if (!metadata) { + return msg; + } - if (!metadata) { - return msg; - } + const debugValue = metadata[SPLAT_SYMBOL]?.[0]; - const debugValue = metadata[SPLAT_SYMBOL]?.[0]; + if (!debugValue) { + return msg; + } - if (!debugValue) { - return msg; - } + if (debugValue && Array.isArray(debugValue)) { + msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`; + return msg; + } - if (debugValue && Array.isArray(debugValue)) { - msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`; - return msg; - } + if (typeof debugValue !== 'object') { + return (msg += ` ${debugValue}`); + } - if (typeof debugValue !== 'object') { - return (msg += ` ${debugValue}`); - } + msg += '\n{'; - msg += '\n{'; - - const copy = klona(metadata); - traverse(copy).forEach(function (value) { - const parent = this.parent; - const parentKey = `${parent && parent.notRoot ? parent.key + '.' : ''}`; - const tabs = `${parent && parent.notRoot ? '\t\t' : '\t'}`; - if (this.isLeaf && typeof value === 'string') { - const truncatedText = truncateLongStrings(value); - msg += `\n${tabs}${parentKey}${this.key}: ${JSON.stringify(truncatedText)},`; - } else if (this.notLeaf && Array.isArray(value) && value.length > 0) { - const currentMessage = `\n${tabs}// ${value.length} ${this.key.replace(/s$/, '')}(s)`; - this.update(currentMessage, true); - msg += currentMessage; - const stringifiedArray = value.map(condenseArray); - msg += `\n${tabs}${parentKey}${this.key}: [${stringifiedArray}],`; - } else if (this.isLeaf && typeof value === 'function') { - msg += `\n${tabs}${parentKey}${this.key}: function,`; - } else if (this.isLeaf) { - msg += `\n${tabs}${parentKey}${this.key}: ${value},`; - } - }); + const copy = klona(metadata); + traverse(copy).forEach(function (value) { + if (typeof this?.key === 'symbol') { + return; + } + + let _parentKey = ''; + const parent = this.parent; + + if (typeof parent?.key !== 'symbol' && parent?.key) { + _parentKey = parent.key; + } - msg += '\n}'; - return msg; + const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`; + + const tabs = `${parent && parent.notRoot ? ' ' : ' '}`; + + const currentKey = this?.key ?? 'unknown'; + + if (this.isLeaf && typeof value === 'string') { + const truncatedText = truncateLongStrings(value); + msg += `\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`; + } else if (this.notLeaf && Array.isArray(value) && value.length > 0) { + const currentMessage = `\n${tabs}// ${value.length} ${currentKey.replace(/s$/, '')}(s)`; + this.update(currentMessage, true); + msg += currentMessage; + const stringifiedArray = value.map(condenseArray); + msg += `\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`; + } else if (this.isLeaf && typeof value === 'function') { + msg += `\n${tabs}${parentKey}${currentKey}: function,`; + } else if (this.isLeaf) { + msg += `\n${tabs}${parentKey}${currentKey}: ${value},`; + } + }); + + msg += '\n}'; + return msg; + } catch (e) { + return (msg += `\n[LOGGER PARSING ERROR] ${e.message}`); + } }); module.exports = { From bd4d23d31448e0314dee7e68cfa45de5434ef7e1 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Sat, 30 Dec 2023 02:42:04 +0100 Subject: [PATCH 07/42] =?UTF-8?q?=F0=9F=9A=AB=F0=9F=94=8D=20feat:=20disall?= =?UTF-8?q?ow=20search=20indexing=20(#1409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: disallow search indexing * Update index.js * Update .env.example * added middleware --- .env.example | 3 +++ api/server/index.js | 2 ++ api/server/middleware/index.js | 2 ++ api/server/middleware/noIndex.js | 11 +++++++++++ 4 files changed, 18 insertions(+) create mode 100644 api/server/middleware/noIndex.js diff --git a/.env.example b/.env.example index 822bb2d669bc..c3c62becd532 100644 --- a/.env.example +++ b/.env.example @@ -24,9 +24,12 @@ MONGO_URI=mongodb://127.0.0.1:27017/LibreChat DOMAIN_CLIENT=http://localhost:3080 DOMAIN_SERVER=http://localhost:3080 +NO_INDEX=true + #===============# # Debug Logging # #===============# + DEBUG_LOGGING=true DEBUG_CONSOLE=false diff --git a/api/server/index.js b/api/server/index.js index afe9d1047a7b..230f8e79d95c 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -8,6 +8,7 @@ const errorController = require('./controllers/ErrorController'); const configureSocialLogins = require('./socialLogins'); const { connectDb, indexSync } = require('~/lib/db'); const { logger } = require('~/config'); +const noIndex = require('./middleware/noIndex'); const paths = require('~/config/paths'); const routes = require('./routes'); @@ -28,6 +29,7 @@ const startServer = async () => { app.locals.config = paths; // Middleware + app.use(noIndex); app.use(errorController); app.use(express.json({ limit: '3mb' })); app.use(mongoSanitize()); diff --git a/api/server/middleware/index.js b/api/server/middleware/index.js index 553f2c663abc..0d878d4c9026 100644 --- a/api/server/middleware/index.js +++ b/api/server/middleware/index.js @@ -12,6 +12,7 @@ const concurrentLimiter = require('./concurrentLimiter'); const validateMessageReq = require('./validateMessageReq'); const buildEndpointOption = require('./buildEndpointOption'); const validateRegistration = require('./validateRegistration'); +const noIndex = require('./noIndex'); module.exports = { ...abortMiddleware, @@ -28,4 +29,5 @@ module.exports = { validateMessageReq, buildEndpointOption, validateRegistration, + noIndex, }; diff --git a/api/server/middleware/noIndex.js b/api/server/middleware/noIndex.js new file mode 100644 index 000000000000..c4d7b55f2ded --- /dev/null +++ b/api/server/middleware/noIndex.js @@ -0,0 +1,11 @@ +const noIndex = (req, res, next) => { + const shouldNoIndex = process.env.NO_INDEX ? process.env.NO_INDEX === 'true' : true; + + if (shouldNoIndex) { + res.setHeader('X-Robots-Tag', 'noindex'); + } + + next(); +}; + +module.exports = noIndex; From f19f5dca8e83c386021260c93f20820458cf105f Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Sat, 30 Dec 2023 03:42:19 +0100 Subject: [PATCH 08/42] =?UTF-8?q?=F0=9F=94=A5=F0=9F=9A=80=20feat:=20CDN=20?= =?UTF-8?q?(Firebase)=20&=20feat:=20account=20section=20(#1438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * localization + api-endpoint * docs: added firebase documentation * chore: icons * chore: SettingsTabs * feat: account pannel; fix: gear icons * docs: position update * feat: firebase * feat: plugin support * route * fixed bugs with firebase and moved a lot of files * chore(DALLE3): using UUID v4 * feat: support for social strategies; moved '/images' path * fix: data ignored * gitignore update * docs: update firebase guide * refactor: Firebase - use singleton pattern for firebase initialization, initially on server start - reorganize imports, move firebase specific files to own service under Files - rename modules to remove 'avatar' redundancy - fix imports based on changes * ci(DALLE/DALLE3): fix tests to use logger and new expected outputs, add firebase tests * refactor(loadToolWithAuth): pass userId to tool as field * feat(images/parse): feat: Add URL Image Basename Extraction Implement a new module to extract the basename of an image from a given URL. This addition includes the function, which parses the URL and retrieves the basename using the Node.js 'url' and 'path' modules. The function is documented with JSDoc comments for better maintainability and understanding. This feature enhances the application's ability to handle and process image URLs efficiently. * refactor(addImages): function to use a more specific regular expression for observedImagePath based on the generated image markdown standard across the app * refactor(DALLE/DALLE3): utilize `getImageBasename` and `this.userId`; fix: pass correct image path to firebase url helper * fix(addImages): make more general to match any image markdown descriptor * fix(parse/getImageBasename): test result of this function for an actual image basename * ci(DALLE3): mock getImageBasename * refactor(AuthContext): use Recoil atom state for user * feat: useUploadAvatarMutation, react-query hook for avatar upload * fix(Toast): stack z-order of Toast over all components (1000) * refactor(showToast): add optional status field to avoid importing NotificationSeverity on each use of the function * refactor(routes/avatar): remove unnecessary get route, get userId from req.user.id, require auth on POST request * chore(uploadAvatar): TODO: remove direct use of Model, `User` * fix(client): fix Spinner imports * refactor(Avatar): use react-query hook, Toast, remove unnecessary states, add optimistic UI to upload * fix(avatar/localStrategy): correctly save local profile picture and cache bust for immediate rendering; fix: firebase init info message (only show once) * fix: use `includes` instead of `endsWith` for checking manual query of avatar image path in case more queries are appended (as is done in avatar/localStrategy) --------- Co-authored-by: Danny Avila --- .env.example | 11 + .gitignore | 4 +- api/app/clients/output_parsers/addImages.js | 6 +- api/app/clients/tools/DALL-E.js | 57 +- api/app/clients/tools/structured/DALLE3.js | 49 +- .../tools/structured/specs/DALLE3.spec.js | 74 +- api/app/clients/tools/util/handleTools.js | 6 +- api/package.json | 1 + api/server/index.js | 2 + api/server/routes/files/avatar.js | 34 + api/server/routes/files/index.js | 1 + api/server/services/Files/Firebase/images.js | 45 + api/server/services/Files/Firebase/index.js | 7 + .../services/Files/Firebase/initialize.js | 42 + .../Files/images/avatar/firebaseStrategy.js | 29 + .../Files/images/avatar/localStrategy.js | 32 + .../Files/images/avatar/uploadAvatar.js | 63 ++ api/server/services/Files/images/index.js | 4 + api/server/services/Files/images/parse.js | 27 + api/strategies/discordStrategy.js | 63 +- api/strategies/facebookStrategy.js | 57 +- api/strategies/githubStrategy.js | 53 +- api/strategies/googleStrategy.js | 53 +- client/src/App.jsx | 2 +- client/src/common/types.ts | 1 + client/src/components/Chat/ChatView.tsx | 2 +- client/src/components/Nav/Nav.tsx | 2 +- client/src/components/Nav/NavLinks.tsx | 2 +- client/src/components/Nav/Settings.tsx | 21 +- .../Nav/SettingsTabs/Account/Account.tsx | 18 + .../Nav/SettingsTabs/Account/Avatar.tsx | 145 +++ .../Nav/SettingsTabs/{ => Data}/Data.tsx | 2 +- .../{ => General}/AutoScrollSwitch.spec.tsx | 0 .../{ => General}/AutoScrollSwitch.tsx | 0 .../{ => General}/ClearChatsButton.spec.tsx | 0 .../SettingsTabs/{ => General}/General.tsx | 2 +- .../{ => General}/LangSelector.spec.tsx | 0 .../{ => General}/ThemeSelector.spec.tsx | 0 .../src/components/Nav/SettingsTabs/index.ts | 9 +- client/src/components/svg/GearIcon.tsx | 16 +- client/src/components/svg/UserIcon.tsx | 15 +- client/src/components/svg/index.ts | 1 + client/src/data-provider/mutations.ts | 17 + client/src/hooks/AuthContext.tsx | 10 +- client/src/hooks/useToast.ts | 8 +- client/src/localization/languages/Eng.tsx | 9 +- client/src/localization/languages/It.tsx | 7 +- client/src/store/user.ts | 6 +- docs/features/bing_jailbreak.md | 2 +- docs/features/firebase.md | 74 ++ docs/features/index.md | 1 + docs/features/logging_system.md | 2 +- docs/features/manage_your_database.md | 2 +- docs/features/pandoranext.md | 2 +- package-lock.json | 902 +++++++++++++++++- packages/data-provider/src/api-endpoints.ts | 2 + packages/data-provider/src/data-service.ts | 4 + packages/data-provider/src/keys.ts | 1 + packages/data-provider/src/types/files.ts | 10 + 59 files changed, 1850 insertions(+), 167 deletions(-) create mode 100644 api/server/routes/files/avatar.js create mode 100644 api/server/services/Files/Firebase/images.js create mode 100644 api/server/services/Files/Firebase/index.js create mode 100644 api/server/services/Files/Firebase/initialize.js create mode 100644 api/server/services/Files/images/avatar/firebaseStrategy.js create mode 100644 api/server/services/Files/images/avatar/localStrategy.js create mode 100644 api/server/services/Files/images/avatar/uploadAvatar.js create mode 100644 api/server/services/Files/images/parse.js create mode 100644 client/src/components/Nav/SettingsTabs/Account/Account.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/Avatar.tsx rename client/src/components/Nav/SettingsTabs/{ => Data}/Data.tsx (98%) rename client/src/components/Nav/SettingsTabs/{ => General}/AutoScrollSwitch.spec.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/AutoScrollSwitch.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/ClearChatsButton.spec.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/General.tsx (99%) rename client/src/components/Nav/SettingsTabs/{ => General}/LangSelector.spec.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/ThemeSelector.spec.tsx (100%) create mode 100644 docs/features/firebase.md diff --git a/.env.example b/.env.example index c3c62becd532..0c9d303d6ccf 100644 --- a/.env.example +++ b/.env.example @@ -281,6 +281,17 @@ EMAIL_PASSWORD= EMAIL_FROM_NAME= EMAIL_FROM=noreply@librechat.ai +#========================# +# Firebase CDN # +#========================# + +FIREBASE_API_KEY= +FIREBASE_AUTH_DOMAIN= +FIREBASE_PROJECT_ID= +FIREBASE_STORAGE_BUCKET= +FIREBASE_MESSAGING_SENDER_ID= +FIREBASE_APP_ID= + #==================================================# # Others # #==================================================# diff --git a/.gitignore b/.gitignore index af92a1f2daf3..f360cbba0acb 100644 --- a/.gitignore +++ b/.gitignore @@ -82,4 +82,6 @@ data.ms/* auth.json /packages/ux-shared/ -/images \ No newline at end of file +/images + +!client/src/components/Nav/SettingsTabs/Data/ \ No newline at end of file diff --git a/api/app/clients/output_parsers/addImages.js b/api/app/clients/output_parsers/addImages.js index 38ceb9a68609..ec04bcac86cd 100644 --- a/api/app/clients/output_parsers/addImages.js +++ b/api/app/clients/output_parsers/addImages.js @@ -60,12 +60,10 @@ function addImages(intermediateSteps, responseMessage) { if (!observation || !observation.includes('![')) { return; } - const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g); + const observedImagePath = observation.match(/!\[.*\]\([^)]*\)/g); if (observedImagePath && !responseMessage.text.includes(observedImagePath[0])) { responseMessage.text += '\n' + observation; - if (process.env.DEBUG_PLUGINS) { - logger.debug('[addImages] added image from intermediateSteps:', observation); - } + logger.debug('[addImages] added image from intermediateSteps:', observation); } }); } diff --git a/api/app/clients/tools/DALL-E.js b/api/app/clients/tools/DALL-E.js index 88a7cf850ac2..387294a1cbb6 100644 --- a/api/app/clients/tools/DALL-E.js +++ b/api/app/clients/tools/DALL-E.js @@ -4,8 +4,15 @@ const fs = require('fs'); const path = require('path'); const OpenAI = require('openai'); // const { genAzureEndpoint } = require('~/utils/genAzureEndpoints'); +const { v4: uuidv4 } = require('uuid'); const { Tool } = require('langchain/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); +const { + saveImageToFirebaseStorage, + getFirebaseStorageImageUrl, + getFirebaseStorage, +} = require('~/server/services/Files/Firebase'); +const { getImageBasename } = require('~/server/services/Files/images'); const extractBaseURL = require('~/utils/extractBaseURL'); const saveImageFromUrl = require('./saveImageFromUrl'); const { logger } = require('~/config'); @@ -15,7 +22,9 @@ class OpenAICreateImage extends Tool { constructor(fields = {}) { super(); + this.userId = fields.userId; let apiKey = fields.DALLE_API_KEY || this.getApiKey(); + const config = { apiKey }; if (DALLE_REVERSE_PROXY) { config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY); @@ -24,7 +33,6 @@ class OpenAICreateImage extends Tool { if (PROXY) { config.httpAgent = new HttpsProxyAgent(PROXY); } - // let azureKey = fields.AZURE_API_KEY || process.env.AZURE_API_KEY; // if (azureKey) { @@ -97,12 +105,11 @@ Guidelines: throw new Error('No image URL returned from OpenAI API.'); } - const regex = /img-[\w\d]+.png/; - const match = theImageUrl.match(regex); - let imageName = '1.png'; + const imageBasename = getImageBasename(theImageUrl); + let imageName = `image_${uuidv4()}.png`; - if (match) { - imageName = match[0]; + if (imageBasename) { + imageName = imageBasename; logger.debug('[DALL-E]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png } else { logger.debug('[DALL-E] No image name found in the string.', { @@ -111,7 +118,18 @@ Guidelines: }); } - this.outputPath = path.resolve(__dirname, '..', '..', '..', '..', 'client', 'public', 'images'); + this.outputPath = path.resolve( + __dirname, + '..', + '..', + '..', + '..', + 'client', + 'public', + 'images', + this.userId, + ); + const appRoot = path.resolve(__dirname, '..', '..', '..', '..', 'client'); this.relativeImageUrl = path.relative(appRoot, this.outputPath); @@ -120,14 +138,25 @@ Guidelines: fs.mkdirSync(this.outputPath, { recursive: true }); } - try { - await saveImageFromUrl(theImageUrl, this.outputPath, imageName); - this.result = this.getMarkdownImageUrl(imageName); - } catch (error) { - logger.error('Error while saving the DALL-E image:', error); - this.result = theImageUrl; + const storage = getFirebaseStorage(); + if (storage) { + try { + await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName); + this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`); + logger.debug('[DALL-E] result: ' + this.result); + } catch (error) { + logger.error('Error while saving the image to Firebase Storage:', error); + this.result = `Failed to save the image to Firebase Storage. ${error.message}`; + } + } else { + try { + await saveImageFromUrl(theImageUrl, this.outputPath, imageName); + this.result = this.getMarkdownImageUrl(imageName); + } catch (error) { + logger.error('Error while saving the image locally:', error); + this.result = `Failed to save the image locally. ${error.message}`; + } } - return this.result; } } diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index dc5750a6892a..17d0368f395b 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -4,10 +4,17 @@ const fs = require('fs'); const path = require('path'); const { z } = require('zod'); const OpenAI = require('openai'); +const { v4: uuidv4 } = require('uuid'); const { Tool } = require('langchain/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); -const saveImageFromUrl = require('../saveImageFromUrl'); +const { + saveImageToFirebaseStorage, + getFirebaseStorageImageUrl, + getFirebaseStorage, +} = require('~/server/services/Files/Firebase'); +const { getImageBasename } = require('~/server/services/Files/images'); const extractBaseURL = require('~/utils/extractBaseURL'); +const saveImageFromUrl = require('../saveImageFromUrl'); const { logger } = require('~/config'); const { DALLE3_SYSTEM_PROMPT, DALLE_REVERSE_PROXY, PROXY } = process.env; @@ -15,6 +22,7 @@ class DALLE3 extends Tool { constructor(fields = {}) { super(); + this.userId = fields.userId; let apiKey = fields.DALLE_API_KEY || this.getApiKey(); const config = { apiKey }; if (DALLE_REVERSE_PROXY) { @@ -108,12 +116,12 @@ class DALLE3 extends Tool { n: 1, }); } catch (error) { - return `Something went wrong when trying to generate the image. The DALL-E API may unavailable: + return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable: Error Message: ${error.message}`; } if (!resp) { - return 'Something went wrong when trying to generate the image. The DALL-E API may unavailable'; + return 'Something went wrong when trying to generate the image. The DALL-E API may be unavailable'; } const theImageUrl = resp.data[0].url; @@ -122,12 +130,11 @@ Error Message: ${error.message}`; return 'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.'; } - const regex = /img-[\w\d]+.png/; - const match = theImageUrl.match(regex); - let imageName = '1.png'; + const imageBasename = getImageBasename(theImageUrl); + let imageName = `image_${uuidv4()}.png`; - if (match) { - imageName = match[0]; + if (imageBasename) { + imageName = imageBasename; logger.debug('[DALL-E-3]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png } else { logger.debug('[DALL-E-3] No image name found in the string.', { @@ -146,6 +153,7 @@ Error Message: ${error.message}`; 'client', 'public', 'images', + this.userId, ); const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client'); this.relativeImageUrl = path.relative(appRoot, this.outputPath); @@ -154,13 +162,24 @@ Error Message: ${error.message}`; if (!fs.existsSync(this.outputPath)) { fs.mkdirSync(this.outputPath, { recursive: true }); } - - try { - await saveImageFromUrl(theImageUrl, this.outputPath, imageName); - this.result = this.getMarkdownImageUrl(imageName); - } catch (error) { - logger.error('Error while saving the image:', error); - this.result = theImageUrl; + const storage = getFirebaseStorage(); + if (storage) { + try { + await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName); + this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`); + logger.debug('[DALL-E-3] result: ' + this.result); + } catch (error) { + logger.error('Error while saving the image to Firebase Storage:', error); + this.result = `Failed to save the image to Firebase Storage. ${error.message}`; + } + } else { + try { + await saveImageFromUrl(theImageUrl, this.outputPath, imageName); + this.result = this.getMarkdownImageUrl(imageName); + } catch (error) { + logger.error('Error while saving the image locally:', error); + this.result = `Failed to save the image locally. ${error.message}`; + } } return this.result; diff --git a/api/app/clients/tools/structured/specs/DALLE3.spec.js b/api/app/clients/tools/structured/specs/DALLE3.spec.js index c61e1d351302..34fa3ebf00a3 100644 --- a/api/app/clients/tools/structured/specs/DALLE3.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3.spec.js @@ -2,11 +2,40 @@ const fs = require('fs'); const path = require('path'); const OpenAI = require('openai'); const DALLE3 = require('../DALLE3'); +const { + getFirebaseStorage, + saveImageToFirebaseStorage, +} = require('~/server/services/Files/Firebase'); const saveImageFromUrl = require('../../saveImageFromUrl'); const { logger } = require('~/config'); jest.mock('openai'); +jest.mock('~/server/services/Files/Firebase', () => ({ + getFirebaseStorage: jest.fn(), + saveImageToFirebaseStorage: jest.fn(), + getFirebaseStorageImageUrl: jest.fn(), +})); + +jest.mock('~/server/services/Files/images', () => ({ + getImageBasename: jest.fn().mockImplementation((url) => { + // Split the URL by '/' + const parts = url.split('/'); + + // Get the last part of the URL + const lastPart = parts.pop(); + + // Check if the last part of the URL matches the image extension regex + const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; + if (imageExtensionRegex.test(lastPart)) { + return lastPart; + } + + // If the regex test fails, return an empty string + return ''; + }), +})); + const generate = jest.fn(); OpenAI.mockImplementation(() => ({ images: { @@ -187,7 +216,48 @@ describe('DALLE3', () => { generate.mockResolvedValue(mockResponse); saveImageFromUrl.mockRejectedValue(error); const result = await dalle._call(mockData); - expect(logger.error).toHaveBeenCalledWith('Error while saving the image:', error); - expect(result).toBe(mockResponse.data[0].url); + expect(logger.error).toHaveBeenCalledWith('Error while saving the image locally:', error); + expect(result).toBe('Failed to save the image locally. Error while saving the image'); + }); + + it('should save image to Firebase Storage if Firebase is initialized', async () => { + const mockData = { + prompt: 'A test prompt', + }; + const mockImageUrl = 'http://example.com/img-test.png'; + const mockResponse = { data: [{ url: mockImageUrl }] }; + generate.mockResolvedValue(mockResponse); + getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized + + await dalle._call(mockData); + + expect(getFirebaseStorage).toHaveBeenCalled(); + expect(saveImageToFirebaseStorage).toHaveBeenCalledWith( + undefined, + mockImageUrl, + expect.any(String), + ); + }); + + it('should handle error when saving image to Firebase Storage fails', async () => { + const mockData = { + prompt: 'A test prompt', + }; + const mockImageUrl = 'http://example.com/img-test.png'; + const mockResponse = { data: [{ url: mockImageUrl }] }; + const error = new Error('Error while saving to Firebase'); + generate.mockResolvedValue(mockResponse); + getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized + saveImageToFirebaseStorage.mockRejectedValue(error); + + const result = await dalle._call(mockData); + + expect(logger.error).toHaveBeenCalledWith( + 'Error while saving the image to Firebase Storage:', + error, + ); + expect(result).toBe( + 'Failed to save the image to Firebase Storage. Error while saving to Firebase', + ); }); }); diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 3afe2776729e..352dd5dec740 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -67,19 +67,19 @@ const validateTools = async (user, tools = []) => { } }; -const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {}) => { +const loadToolWithAuth = async (userId, authFields, ToolConstructor, options = {}) => { return async function () { let authValues = {}; for (const authField of authFields) { let authValue = process.env[authField]; if (!authValue) { - authValue = await getUserPluginAuthValue(user, authField); + authValue = await getUserPluginAuthValue(userId, authField); } authValues[authField] = authValue; } - return new ToolConstructor({ ...options, ...authValues }); + return new ToolConstructor({ ...options, ...authValues, userId }); }; }; diff --git a/api/package.json b/api/package.json index 684d13978947..fe5731cbd2fb 100644 --- a/api/package.json +++ b/api/package.json @@ -44,6 +44,7 @@ "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^6.9.0", "express-session": "^1.17.3", + "firebase": "^10.6.0", "googleapis": "^126.0.1", "handlebars": "^4.7.7", "html": "^1.0.0", diff --git a/api/server/index.js b/api/server/index.js index 230f8e79d95c..698620c56f3a 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -4,6 +4,7 @@ const cors = require('cors'); const express = require('express'); const passport = require('passport'); const mongoSanitize = require('express-mongo-sanitize'); +const { initializeFirebase } = require('~/server/services/Files/Firebase/initialize'); const errorController = require('./controllers/ErrorController'); const configureSocialLogins = require('./socialLogins'); const { connectDb, indexSync } = require('~/lib/db'); @@ -23,6 +24,7 @@ const { jwtLogin, passportLogin } = require('~/strategies'); const startServer = async () => { await connectDb(); logger.info('Connected to MongoDB'); + initializeFirebase(); await indexSync(); const app = express(); diff --git a/api/server/routes/files/avatar.js b/api/server/routes/files/avatar.js new file mode 100644 index 000000000000..a7bb07c0f95f --- /dev/null +++ b/api/server/routes/files/avatar.js @@ -0,0 +1,34 @@ +const express = require('express'); +const multer = require('multer'); + +const uploadAvatar = require('~/server/services/Files/images/avatar/uploadAvatar'); +const { requireJwtAuth } = require('~/server/middleware/'); +const User = require('~/models/User'); + +const upload = multer(); +const router = express.Router(); + +router.post('/', requireJwtAuth, upload.single('input'), async (req, res) => { + try { + const userId = req.user.id; + const { manual } = req.body; + const input = req.file.buffer; + if (!userId) { + throw new Error('User ID is undefined'); + } + + // TODO: do not use Model directly, instead use a service method that uses the model + const user = await User.findById(userId).lean(); + + if (!user) { + throw new Error('User not found'); + } + const url = await uploadAvatar(userId, input, manual); + + res.json({ url }); + } catch (error) { + res.status(500).json({ message: 'An error occurred while uploading the profile picture' }); + } +}); + +module.exports = router; diff --git a/api/server/routes/files/index.js b/api/server/routes/files/index.js index 34c7dc62e3ad..74b200c80665 100644 --- a/api/server/routes/files/index.js +++ b/api/server/routes/files/index.js @@ -18,5 +18,6 @@ router.use(uaParser); router.use('/', files); router.use('/images', images); +router.use('/images/avatar', require('./avatar')); module.exports = router; diff --git a/api/server/services/Files/Firebase/images.js b/api/server/services/Files/Firebase/images.js new file mode 100644 index 000000000000..e04902c02fe0 --- /dev/null +++ b/api/server/services/Files/Firebase/images.js @@ -0,0 +1,45 @@ +const fetch = require('node-fetch'); +const { ref, uploadBytes, getDownloadURL } = require('firebase/storage'); +const { getFirebaseStorage } = require('./initialize'); + +async function saveImageToFirebaseStorage(userId, imageUrl, imageName) { + const storage = getFirebaseStorage(); + if (!storage) { + console.error('Firebase is not initialized. Cannot save image to Firebase Storage.'); + return null; + } + + const storageRef = ref(storage, `images/${userId.toString()}/${imageName}`); + + try { + // Upload image to Firebase Storage using the image URL + await uploadBytes(storageRef, await fetch(imageUrl).then((response) => response.buffer())); + return imageName; + } catch (error) { + console.error('Error uploading image to Firebase Storage:', error.message); + return null; + } +} + +async function getFirebaseStorageImageUrl(imageName) { + const storage = getFirebaseStorage(); + if (!storage) { + console.error('Firebase is not initialized. Cannot get image URL from Firebase Storage.'); + return null; + } + + const storageRef = ref(storage, `images/${imageName}`); + + try { + // Get the download URL for the image from Firebase Storage + return `![generated image](${await getDownloadURL(storageRef)})`; + } catch (error) { + console.error('Error fetching image URL from Firebase Storage:', error.message); + return null; + } +} + +module.exports = { + saveImageToFirebaseStorage, + getFirebaseStorageImageUrl, +}; diff --git a/api/server/services/Files/Firebase/index.js b/api/server/services/Files/Firebase/index.js new file mode 100644 index 000000000000..905bf660d4fe --- /dev/null +++ b/api/server/services/Files/Firebase/index.js @@ -0,0 +1,7 @@ +const images = require('./images'); +const initialize = require('./initialize'); + +module.exports = { + ...images, + ...initialize, +}; diff --git a/api/server/services/Files/Firebase/initialize.js b/api/server/services/Files/Firebase/initialize.js new file mode 100644 index 000000000000..5dc1f9379157 --- /dev/null +++ b/api/server/services/Files/Firebase/initialize.js @@ -0,0 +1,42 @@ +const firebase = require('firebase/app'); +const { getStorage } = require('firebase/storage'); +const { logger } = require('~/config'); + +let i = 0; +let firebaseApp = null; + +const initializeFirebase = () => { + // Return existing instance if already initialized + if (firebaseApp) { + return firebaseApp; + } + + const firebaseConfig = { + apiKey: process.env.FIREBASE_API_KEY, + authDomain: process.env.FIREBASE_AUTH_DOMAIN, + projectId: process.env.FIREBASE_PROJECT_ID, + storageBucket: process.env.FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.FIREBASE_APP_ID, + }; + + if (Object.values(firebaseConfig).some((value) => !value)) { + i === 0 && + logger.info( + '[Optional] Firebase configuration missing or incomplete. Firebase will not be initialized.', + ); + i++; + return null; + } + + firebaseApp = firebase.initializeApp(firebaseConfig); + logger.info('Firebase initialized'); + return firebaseApp; +}; + +const getFirebaseStorage = () => { + const app = initializeFirebase(); + return app ? getStorage(app) : null; +}; + +module.exports = { initializeFirebase, getFirebaseStorage }; diff --git a/api/server/services/Files/images/avatar/firebaseStrategy.js b/api/server/services/Files/images/avatar/firebaseStrategy.js new file mode 100644 index 000000000000..9c000b43ecc4 --- /dev/null +++ b/api/server/services/Files/images/avatar/firebaseStrategy.js @@ -0,0 +1,29 @@ +const { ref, uploadBytes, getDownloadURL } = require('firebase/storage'); +const { getFirebaseStorage } = require('~/server/services/Files/Firebase/initialize'); +const { logger } = require('~/config'); + +async function firebaseStrategy(userId, webPBuffer, oldUser, manual) { + try { + const storage = getFirebaseStorage(); + if (!storage) { + throw new Error('Firebase is not initialized.'); + } + const avatarRef = ref(storage, `images/${userId.toString()}/avatar`); + + await uploadBytes(avatarRef, webPBuffer); + const urlFirebase = await getDownloadURL(avatarRef); + const isManual = manual === 'true'; + + const url = `${urlFirebase}?manual=${isManual}`; + if (isManual) { + oldUser.avatar = url; + await oldUser.save(); + } + return url; + } catch (error) { + logger.error('Error uploading profile picture:', error); + throw error; + } +} + +module.exports = firebaseStrategy; diff --git a/api/server/services/Files/images/avatar/localStrategy.js b/api/server/services/Files/images/avatar/localStrategy.js new file mode 100644 index 000000000000..021beda7d13f --- /dev/null +++ b/api/server/services/Files/images/avatar/localStrategy.js @@ -0,0 +1,32 @@ +const fs = require('fs').promises; +const path = require('path'); + +async function localStrategy(userId, webPBuffer, oldUser, manual) { + const userDir = path.resolve( + __dirname, + '..', + '..', + '..', + '..', + '..', + '..', + 'client', + 'public', + 'images', + userId, + ); + let avatarPath = path.join(userDir, 'avatar.png'); + const urlRoute = `/images/${userId}/avatar.png`; + await fs.mkdir(userDir, { recursive: true }); + await fs.writeFile(avatarPath, webPBuffer); + const isManual = manual === 'true'; + let url = `${urlRoute}?manual=${isManual}×tamp=${new Date().getTime()}`; + if (isManual) { + oldUser.avatar = url; + await oldUser.save(); + } + + return url; +} + +module.exports = localStrategy; diff --git a/api/server/services/Files/images/avatar/uploadAvatar.js b/api/server/services/Files/images/avatar/uploadAvatar.js new file mode 100644 index 000000000000..0726df9a4dde --- /dev/null +++ b/api/server/services/Files/images/avatar/uploadAvatar.js @@ -0,0 +1,63 @@ +const sharp = require('sharp'); +const fetch = require('node-fetch'); +const fs = require('fs').promises; +const User = require('~/models/User'); +const { getFirebaseStorage } = require('~/server/services/Files/Firebase/initialize'); +const firebaseStrategy = require('./firebaseStrategy'); +const localStrategy = require('./localStrategy'); +const { logger } = require('~/config'); + +async function convertToWebP(inputBuffer) { + return sharp(inputBuffer).resize({ width: 150 }).toFormat('webp').toBuffer(); +} + +async function uploadAvatar(userId, input, manual) { + try { + if (userId === undefined) { + throw new Error('User ID is undefined'); + } + const _id = userId; + // TODO: remove direct use of Model, `User` + const oldUser = await User.findOne({ _id }); + let imageBuffer; + if (typeof input === 'string') { + const response = await fetch(input); + + if (!response.ok) { + throw new Error(`Failed to fetch image from URL. Status: ${response.status}`); + } + imageBuffer = await response.buffer(); + } else if (input instanceof Buffer) { + imageBuffer = input; + } else if (typeof input === 'object' && input instanceof File) { + const fileContent = await fs.readFile(input.path); + imageBuffer = Buffer.from(fileContent); + } else { + throw new Error('Invalid input type. Expected URL, Buffer, or File.'); + } + const { width, height } = await sharp(imageBuffer).metadata(); + const minSize = Math.min(width, height); + const squaredBuffer = await sharp(imageBuffer) + .extract({ + left: Math.floor((width - minSize) / 2), + top: Math.floor((height - minSize) / 2), + width: minSize, + height: minSize, + }) + .toBuffer(); + const webPBuffer = await convertToWebP(squaredBuffer); + const storage = getFirebaseStorage(); + if (storage) { + const url = await firebaseStrategy(userId, webPBuffer, oldUser, manual); + return url; + } + + const url = await localStrategy(userId, webPBuffer, oldUser, manual); + return url; + } catch (error) { + logger.error('Error uploading the avatar:', error); + throw error; + } +} + +module.exports = uploadAvatar; diff --git a/api/server/services/Files/images/index.js b/api/server/services/Files/images/index.js index d5b818e937d8..fa49eb953567 100644 --- a/api/server/services/Files/images/index.js +++ b/api/server/services/Files/images/index.js @@ -1,11 +1,15 @@ const convert = require('./convert'); const encode = require('./encode'); +const parse = require('./parse'); const resize = require('./resize'); const validate = require('./validate'); +const uploadAvatar = require('./avatar/uploadAvatar'); module.exports = { ...convert, ...encode, + ...parse, ...resize, ...validate, + uploadAvatar, }; diff --git a/api/server/services/Files/images/parse.js b/api/server/services/Files/images/parse.js new file mode 100644 index 000000000000..5a1113c97e41 --- /dev/null +++ b/api/server/services/Files/images/parse.js @@ -0,0 +1,27 @@ +const URL = require('url').URL; +const path = require('path'); + +const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; + +/** + * Extracts the image basename from a given URL. + * + * @param {string} urlString - The URL string from which the image basename is to be extracted. + * @returns {string} The basename of the image file from the URL. + * Returns an empty string if the URL does not contain a valid image basename. + */ +function getImageBasename(urlString) { + try { + const url = new URL(urlString); + const basename = path.basename(url.pathname); + + return imageExtensionRegex.test(basename) ? basename : ''; + } catch (error) { + // If URL parsing fails, return an empty string + return ''; + } +} + +module.exports = { + getImageBasename, +}; diff --git a/api/strategies/discordStrategy.js b/api/strategies/discordStrategy.js index c6fdde6d8c36..994554200cd4 100644 --- a/api/strategies/discordStrategy.js +++ b/api/strategies/discordStrategy.js @@ -1,51 +1,72 @@ const { Strategy: DiscordStrategy } = require('passport-discord'); const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const discordLogin = async (accessToken, refreshToken, profile, cb) => { try { const email = profile.email; const discordId = profile.id; - const oldUser = await User.findOne({ - email, - }); + const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; - let avatarURL; + let avatarUrl; + if (profile.avatar) { const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; - avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; + avatarUrl = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; } else { const defaultAvatarNum = Number(profile.discriminator) % 5; - avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; + avatarUrl = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; } if (oldUser) { - oldUser.avatar = avatarURL; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'discord', - discordId, - username: profile.username, - email, - name: profile.global_name, - avatar: avatarURL, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, discordId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { - message: 'User not found.', - }); } catch (err) { logger.error('[discordLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if (!useFirebase && !oldUser.avatar.includes('?manual=true')) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = newavatarUrl; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, discordId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'discord', + discordId, + username: profile.username, + email, + name: profile.global_name, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + newUser.avatar = newavatarUrl; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new DiscordStrategy( { diff --git a/api/strategies/facebookStrategy.js b/api/strategies/facebookStrategy.js index bb175a099cc2..b8915b2cc4bf 100644 --- a/api/strategies/facebookStrategy.js +++ b/api/strategies/facebookStrategy.js @@ -1,43 +1,64 @@ const FacebookStrategy = require('passport-facebook').Strategy; const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const facebookLogin = async (accessToken, refreshToken, profile, cb) => { try { const email = profile.emails[0]?.value; const facebookId = profile.id; - const oldUser = await User.findOne({ - email, - }); + const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + const avatarUrl = profile.photos[0]?.value; if (oldUser) { - oldUser.avatar = profile.photo; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'facebook', - facebookId, - username: profile.displayName, - email, - name: profile.name?.givenName + ' ' + profile.name?.familyName, - avatar: profile.photos[0]?.value, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, facebookId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { - message: 'User not found.', - }); } catch (err) { logger.error('[facebookLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if (!useFirebase && !oldUser.avatar.includes('?manual=true')) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = newavatarUrl; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, facebookId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'facebook', + facebookId, + username: profile.displayName, + email, + name: profile.name?.givenName + ' ' + profile.name?.familyName, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + newUser.avatar = newavatarUrl; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new FacebookStrategy( { diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js index 3962c58e50e7..c8480d50c135 100644 --- a/api/strategies/githubStrategy.js +++ b/api/strategies/githubStrategy.js @@ -1,6 +1,7 @@ const { Strategy: GitHubStrategy } = require('passport-github2'); const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const githubLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -9,32 +10,56 @@ const githubLogin = async (accessToken, refreshToken, profile, cb) => { const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + const avatarUrl = profile.photos[0].value; if (oldUser) { - oldUser.avatar = profile.photos[0].value; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'github', - githubId, - username: profile.username, - email, - emailVerified: profile.emails[0].verified, - name: profile.displayName, - avatar: profile.photos[0].value, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, githubId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { message: 'User not found.' }); } catch (err) { logger.error('[githubLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if (!useFirebase && !oldUser.avatar.includes('?manual=true')) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = avatarURL; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, githubId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'github', + githubId, + username: profile.username, + email, + emailVerified: profile.emails[0].verified, + name: profile.displayName, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + newUser.avatar = avatarURL; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new GitHubStrategy( { diff --git a/api/strategies/googleStrategy.js b/api/strategies/googleStrategy.js index e65c5403f4bb..d013cc8e8fdb 100644 --- a/api/strategies/googleStrategy.js +++ b/api/strategies/googleStrategy.js @@ -1,6 +1,7 @@ const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const googleLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -9,32 +10,56 @@ const googleLogin = async (accessToken, refreshToken, profile, cb) => { const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + const avatarUrl = profile.photos[0].value; if (oldUser) { - oldUser.avatar = profile.photos[0].value; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'google', - googleId, - username: profile.name.givenName, - email, - emailVerified: profile.emails[0].verified, - name: `${profile.name.givenName} ${profile.name.familyName}`, - avatar: profile.photos[0].value, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, googleId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { message: 'User not found.' }); } catch (err) { logger.error('[googleLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if ((!useFirebase && !oldUser.avatar.includes('?manual=true')) || oldUser.avatar === null) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = avatarURL; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, googleId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'google', + googleId, + username: profile.name.givenName, + email, + emailVerified: profile.emails[0].verified, + name: `${profile.name.givenName} ${profile.name.familyName}`, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + newUser.avatar = avatarURL; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new GoogleStrategy( { diff --git a/client/src/App.jsx b/client/src/App.jsx index 72d07b1c96a4..10c9ab9b509b 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -34,7 +34,7 @@ const App = () => { - + diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 807e2dc9424f..2daf2d8ba0f2 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -27,6 +27,7 @@ export type TShowToast = { severity?: NotificationSeverity; showIcon?: boolean; duration?: number; + status?: 'error' | 'success' | 'warning' | 'info'; }; export type TBaseSettingsProps = { diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index 5ce309513956..d582f4b3e9d8 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -6,10 +6,10 @@ import { useChatHelpers, useSSE } from '~/hooks'; // import GenerationButtons from './Input/GenerationButtons'; import MessagesView from './Messages/MessagesView'; // import OptionsBar from './Input/OptionsBar'; +import { Spinner } from '~/components/svg'; import { ChatContext } from '~/Providers'; import Presentation from './Presentation'; import ChatForm from './Input/ChatForm'; -import { Spinner } from '~/components'; import { buildTree } from '~/utils'; import Landing from './Landing'; import Header from './Header'; diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index 552fd1d52576..86a3e3359718 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -11,7 +11,7 @@ import { } from '~/hooks'; import { TooltipProvider, Tooltip } from '~/components/ui'; import { Conversations, Pages } from '../Conversations'; -import { Spinner } from '~/components'; +import { Spinner } from '~/components/svg'; import SearchBar from './SearchBar'; import NavToggle from './NavToggle'; import NavLinks from './NavLinks'; diff --git a/client/src/components/Nav/NavLinks.tsx b/client/src/components/Nav/NavLinks.tsx index ad1b9610cbb8..2f5c0769bcb6 100644 --- a/client/src/components/Nav/NavLinks.tsx +++ b/client/src/components/Nav/NavLinks.tsx @@ -119,7 +119,7 @@ function NavLinks() { } + svg={() => } text={localize('com_nav_settings')} clickHandler={() => setShowSettings(true)} /> diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index cce7ec27c20d..da3fcefa7cf8 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -1,9 +1,9 @@ import * as Tabs from '@radix-ui/react-tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; -import { GearIcon, DataIcon } from '~/components/svg'; +import { GearIcon, DataIcon, UserIcon } from '~/components/svg'; import { useMediaQuery, useLocalize } from '~/hooks'; import type { TDialogProps } from '~/common'; -import { General, Data } from './SettingsTabs'; +import { General, Data, Account } from './SettingsTabs'; import { cn } from '~/utils'; export default function Settings({ open, onOpenChange }: TDialogProps) { @@ -39,7 +39,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { > {localize('com_nav_setting_data')} + + + {localize('com_nav_setting_account')} + + diff --git a/client/src/components/Nav/SettingsTabs/Account/Account.tsx b/client/src/components/Nav/SettingsTabs/Account/Account.tsx new file mode 100644 index 000000000000..a7651ddca603 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/Account.tsx @@ -0,0 +1,18 @@ +import * as Tabs from '@radix-ui/react-tabs'; +import Avatar from './Avatar'; +import React from 'react'; + +function Account() { + return ( + +
+
+ +
+
+
+
+ ); +} + +export default React.memo(Account); diff --git a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx new file mode 100644 index 000000000000..64635f0a59db --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx @@ -0,0 +1,145 @@ +import { FileImage } from 'lucide-react'; +import { useSetRecoilState } from 'recoil'; +import { useState, useEffect } from 'react'; +import type { TUser } from 'librechat-data-provider'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; +import { useUploadAvatarMutation } from '~/data-provider'; +import { useToastContext } from '~/Providers'; +import { Spinner } from '~/components/svg'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils/'; +import store from '~/store'; + +const sizeLimit = 2 * 1024 * 1024; // 2MB + +function Avatar() { + const setUser = useSetRecoilState(store.user); + const [input, setinput] = useState(null); + const [isDialogOpen, setDialogOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + + const localize = useLocalize(); + const { showToast } = useToastContext(); + + const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({ + onSuccess: (data) => { + showToast({ message: localize('com_ui_upload_success') }); + setDialogOpen(false); + + setUser((prev) => ({ ...prev, avatar: data.url } as TUser)); + }, + onError: (error) => { + console.error('Error:', error); + showToast({ message: localize('com_ui_upload_error'), status: 'error' }); + }, + }); + + useEffect(() => { + if (input) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(input); + } else { + setPreviewUrl(null); + } + }, [input]); + + const handleFileChange = (event: React.ChangeEvent): void => { + const file = event.target.files?.[0]; + + if (file && file.size <= sizeLimit) { + setinput(file); + setDialogOpen(true); + } else { + showToast({ + message: localize('com_ui_upload_invalid'), + status: 'error', + }); + } + }; + + const handleUpload = () => { + if (!input) { + console.error('No file selected'); + return; + } + + const formData = new FormData(); + formData.append('input', input, input.name); + formData.append('manual', 'true'); + + uploadAvatar(formData); + }; + + return ( + <> +
+ {localize('com_nav_profile_picture')} + +
+ + setDialogOpen(false)}> + + + + {localize('com_ui_preview')} + + +
+ {previewUrl && ( + Preview + )} + +
+
+
+ + ); +} + +export default Avatar; diff --git a/client/src/components/Nav/SettingsTabs/Data.tsx b/client/src/components/Nav/SettingsTabs/Data/Data.tsx similarity index 98% rename from client/src/components/Nav/SettingsTabs/Data.tsx rename to client/src/components/Nav/SettingsTabs/Data/Data.tsx index 1786095eedab..d7db9969412b 100644 --- a/client/src/components/Nav/SettingsTabs/Data.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/Data.tsx @@ -5,7 +5,7 @@ import { } from 'librechat-data-provider/react-query'; import React, { useState, useCallback, useRef } from 'react'; import { useOnClickOutside } from '~/hooks'; -import DangerButton from './DangerButton'; +import DangerButton from '../DangerButton'; export const RevokeKeysButton = ({ showText = true, diff --git a/client/src/components/Nav/SettingsTabs/AutoScrollSwitch.spec.tsx b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/AutoScrollSwitch.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/AutoScrollSwitch.tsx b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/AutoScrollSwitch.tsx rename to client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx diff --git a/client/src/components/Nav/SettingsTabs/ClearChatsButton.spec.tsx b/client/src/components/Nav/SettingsTabs/General/ClearChatsButton.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/ClearChatsButton.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/ClearChatsButton.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx similarity index 99% rename from client/src/components/Nav/SettingsTabs/General.tsx rename to client/src/components/Nav/SettingsTabs/General/General.tsx index 343b36fbecd2..48ecd6be006e 100644 --- a/client/src/components/Nav/SettingsTabs/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -12,7 +12,7 @@ import { } from '~/hooks'; import type { TDangerButtonProps } from '~/common'; import AutoScrollSwitch from './AutoScrollSwitch'; -import DangerButton from './DangerButton'; +import DangerButton from '../DangerButton'; import store from '~/store'; import { Dropdown } from '~/components/ui'; diff --git a/client/src/components/Nav/SettingsTabs/LangSelector.spec.tsx b/client/src/components/Nav/SettingsTabs/General/LangSelector.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/LangSelector.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/LangSelector.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/ThemeSelector.spec.tsx b/client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/ThemeSelector.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/index.ts b/client/src/components/Nav/SettingsTabs/index.ts index 939c90f3b33a..73174aa79844 100644 --- a/client/src/components/Nav/SettingsTabs/index.ts +++ b/client/src/components/Nav/SettingsTabs/index.ts @@ -1,4 +1,5 @@ -export { default as General } from './General'; -export { ClearChatsButton } from './General'; -export { default as Data } from './Data'; -export { RevokeKeysButton } from './Data'; +export { default as General } from './General/General'; +export { ClearChatsButton } from './General/General'; +export { default as Data } from './Data/Data'; +export { RevokeKeysButton } from './Data/Data'; +export { default as Account } from './Account/Account'; diff --git a/client/src/components/svg/GearIcon.tsx b/client/src/components/svg/GearIcon.tsx index e5ed475d5218..98cc2fab982e 100644 --- a/client/src/components/svg/GearIcon.tsx +++ b/client/src/components/svg/GearIcon.tsx @@ -1,14 +1,18 @@ import React from 'react'; -export default function GearIcon() { +interface GearIconProps { + className?: string; +} + +const GearIcon: React.FC = ({ className = '' }) => { return ( ); -} +}; + +export default GearIcon; diff --git a/client/src/components/svg/UserIcon.tsx b/client/src/components/svg/UserIcon.tsx index 8f15fadcaf6e..e8535e45eb9a 100644 --- a/client/src/components/svg/UserIcon.tsx +++ b/client/src/components/svg/UserIcon.tsx @@ -1,20 +1,17 @@ -import React from 'react'; - export default function UserIcon() { return ( - + ); diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index cc4f5a0148b0..3ad62c93eeac 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -40,3 +40,4 @@ export { default as GeminiIcon } from './GeminiIcon'; export { default as GoogleMinimalIcon } from './GoogleMinimalIcon'; export { default as AnthropicMinimalIcon } from './AnthropicMinimalIcon'; export { default as SendMessageIcon } from './SendMessageIcon'; +export { default as UserIcon } from './UserIcon'; diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index c38ca46c9e67..236c66a9479d 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -12,6 +12,8 @@ import type { PresetDeleteResponse, LogoutOptions, TPreset, + UploadAvatarOptions, + AvatarUploadResponse, } from 'librechat-data-provider'; import { dataService, MutationKeys } from 'librechat-data-provider'; @@ -99,3 +101,18 @@ export const useLogoutUserMutation = ( }, }); }; + +/* Avatar upload */ +export const useUploadAvatarMutation = ( + options?: UploadAvatarOptions, +): UseMutationResult< + AvatarUploadResponse, // response data + unknown, // error + FormData, // request + unknown // context +> => { + return useMutation([MutationKeys.avatarUpload], { + mutationFn: (variables: FormData) => dataService.uploadAvatar(variables), + ...(options || {}), + }); +}; diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index 8cd63833bde3..711f433ef2df 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -7,6 +7,7 @@ import { createContext, useContext, } from 'react'; +import { useRecoilState } from 'recoil'; import { TUser, TLoginResponse, setTokenHeader, TLoginUser } from 'librechat-data-provider'; import { useGetUserQuery, @@ -17,6 +18,7 @@ import { useNavigate } from 'react-router-dom'; import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common'; import { useLogoutUserMutation } from '~/data-provider'; import useTimeout from './useTimeout'; +import store from '~/store'; const AuthContext = createContext(undefined); @@ -27,11 +29,13 @@ const AuthContextProvider = ({ authConfig?: TAuthConfig; children: ReactNode; }) => { - const navigate = useNavigate(); - const [user, setUser] = useState(undefined); + const [user, setUser] = useRecoilState(store.user); const [token, setToken] = useState(undefined); const [error, setError] = useState(undefined); const [isAuthenticated, setIsAuthenticated] = useState(false); + + const navigate = useNavigate(); + const setUserContext = useCallback( (userContext: TUserContext) => { const { token, isAuthenticated, user, redirect } = userContext; @@ -46,7 +50,7 @@ const AuthContextProvider = ({ navigate(redirect, { replace: true }); } }, - [navigate], + [navigate, setUser], ); const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) }); diff --git a/client/src/hooks/useToast.ts b/client/src/hooks/useToast.ts index 716526768d85..92f7bbfe17bf 100644 --- a/client/src/hooks/useToast.ts +++ b/client/src/hooks/useToast.ts @@ -25,6 +25,7 @@ export default function useToast(showDelay = 100) { severity = NotificationSeverity.SUCCESS, showIcon = true, duration = 3000, // default duration for the toast to be visible + status, }: TShowToast) => { // Clear existing timeouts if (showTimerRef.current !== null) { @@ -36,7 +37,12 @@ export default function useToast(showDelay = 100) { // Timeout to show the toast showTimerRef.current = window.setTimeout(() => { - setToast({ open: true, message, severity, showIcon }); + setToast({ + open: true, + message, + severity: (status as NotificationSeverity) ?? severity, + showIcon, + }); // Hides the toast after the specified duration hideTimerRef.current = window.setTimeout(() => { setToast((prevToast) => ({ ...prevToast, open: false })); diff --git a/client/src/localization/languages/Eng.tsx b/client/src/localization/languages/Eng.tsx index 7acb936cf99a..8a431707bcb0 100644 --- a/client/src/localization/languages/Eng.tsx +++ b/client/src/localization/languages/Eng.tsx @@ -33,7 +33,8 @@ export default { com_ui_enter: 'Enter', com_ui_submit: 'Submit', com_ui_upload_success: 'Successfully uploaded file', - com_ui_upload_invalid: 'Invalid file for upload', + com_ui_upload_error: 'There was an error uploading your file', + com_ui_upload_invalid: 'Invalid file for upload. Must be an image not exceeding 2 MB', com_ui_cancel: 'Cancel', com_ui_save: 'Save', com_ui_copy_to_clipboard: 'Copy to clipboard', @@ -51,6 +52,9 @@ export default { com_ui_delete: 'Delete', com_ui_delete_conversation: 'Delete chat?', com_ui_delete_conversation_confirm: 'This will delete', + com_ui_preview: 'Preview', + com_ui_upload: 'Upload', + com_ui_connect: 'Connect', com_auth_error_login: 'Unable to login with the information provided. Please check your credentials and try again.', com_auth_error_login_rl: @@ -253,6 +257,8 @@ export default { 'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.', com_nav_welcome_message: 'How can I help you today?', com_nav_auto_scroll: 'Auto-scroll to Newest on Open', + com_nav_profile_picture: 'Profile Picture', + com_nav_change_picture: 'Change picture', com_nav_plugin_store: 'Plugin store', com_nav_plugin_search: 'Search plugins', com_nav_plugin_auth_error: @@ -286,6 +292,7 @@ export default { com_nav_search_placeholder: 'Search messages', com_nav_setting_general: 'General', com_nav_setting_data: 'Data controls', + com_nav_setting_account: 'Account', com_nav_language: 'Language', com_nav_lang_auto: 'Auto detect', com_nav_lang_english: 'English', diff --git a/client/src/localization/languages/It.tsx b/client/src/localization/languages/It.tsx index d07eb798ab15..175b7820dbb6 100644 --- a/client/src/localization/languages/It.tsx +++ b/client/src/localization/languages/It.tsx @@ -53,6 +53,9 @@ export default { com_ui_delete: 'Elimina', com_ui_delete_conversation: 'Eliminare la chat?', com_ui_delete_conversation_confirm: 'Questo eliminerà', + com_ui_preview: 'Anteprima', + com_ui_upload: 'Carica', + com_ui_connect: 'Connetti', com_auth_error_login: 'Impossibile accedere con le informazioni fornite. Per favore controlla le tue credenziali e riprova.', com_auth_error_login_rl: @@ -263,7 +266,9 @@ export default { 'Assicurati di fare clic su "Crea e continua" per dare almeno il ruolo "Vertex AI User". Infine, crea una chiave JSON da importare qui.', com_nav_welcome_message: 'Come posso aiutarti oggi?', com_nav_auto_scroll: 'Scorri automaticamente al Più recente all\'apertura', - com_nav_plugin_store: 'Negozio plugin', + com_nav_profile_picture: 'Immagine del profilo', + com_nav_change_picture: 'Cambia immagine', + com_nav_plugin_store: 'Negozio dei plugin', com_nav_plugin_search: 'Cerca plugin', com_nav_plugin_auth_error: 'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore riprova.', diff --git a/client/src/store/user.ts b/client/src/store/user.ts index 04864d34263e..d86bdc237300 100644 --- a/client/src/store/user.ts +++ b/client/src/store/user.ts @@ -1,9 +1,9 @@ import { atom } from 'recoil'; -import { TPlugin } from 'librechat-data-provider'; +import type { TUser, TPlugin } from 'librechat-data-provider'; -const user = atom({ +const user = atom({ key: 'user', - default: null, + default: undefined, }); const availableTools = atom({ diff --git a/docs/features/bing_jailbreak.md b/docs/features/bing_jailbreak.md index 17f4850186b3..a014de476dcf 100644 --- a/docs/features/bing_jailbreak.md +++ b/docs/features/bing_jailbreak.md @@ -1,7 +1,7 @@ --- title: 😈 Bing Jailbreak description: Quick overview of the Bing jailbreak and Sydney's system message -weight: -3 +weight: -2 --- # Bing Jailbreak diff --git a/docs/features/firebase.md b/docs/features/firebase.md new file mode 100644 index 000000000000..d6725044b249 --- /dev/null +++ b/docs/features/firebase.md @@ -0,0 +1,74 @@ +--- +title: 🔥 Firebase CDN Setup +description: This document provides instructions for setting up Firebase CDN for LibreChat +weight: -6 +--- + +# Firebase CDN Setup + +## Steps to Set Up Firebase + +1. Open the [Firebase website](https://firebase.google.com/). +2. Click on "Get started." +3. Sign in with your Google account. + +### Create a New Project + +- Name your project (you can use the same project as Google OAuth). + +![Project Name](https://github.com/danny-avila/LibreChat/assets/81851188/dccce3e0-b639-41ef-8142-19d24911c65c) + +- Optionally, you can disable Google Analytics. + +![Google Analytics](https://github.com/danny-avila/LibreChat/assets/81851188/5d4d58c5-451c-498b-97c0-f123fda79514) + +- Wait for 20/30 seconds for the project to be ready, then click on "Continue." + +![Continue](https://github.com/danny-avila/LibreChat/assets/81851188/6929802e-a30b-4b1e-b124-1d4b281d0403) + +- Click on "All Products." + +![All Products](https://github.com/danny-avila/LibreChat/assets/81851188/92866c82-2b03-4ebe-807e-73a0ccce695e) + +- Select "Storage." + +![Storage](https://github.com/danny-avila/LibreChat/assets/81851188/b22dcda1-256b-494b-a835-a05aeea02e89) + +- Click on "Get Started." + +![Get Started](https://github.com/danny-avila/LibreChat/assets/81851188/c3f0550f-8184-4c79-bb84-fa79655b7978) + +- Click on "Next." + +![Next](https://github.com/danny-avila/LibreChat/assets/81851188/2a65632d-fe22-4c71-b8f1-aac53ee74fb6) + +- Select your "Cloud Storage location." + +![Cloud Storage Location](https://github.com/danny-avila/LibreChat/assets/81851188/c094d4bc-8e5b-43c7-96d9-a05bcf4e2af6) + +- Return to the Project Overview. + +![Project Overview](https://github.com/danny-avila/LibreChat/assets/81851188/c425f4bb-a494-42f2-9fdc-ff2c8ce005e1) + +- Click on "+ Add app" under your project name, then click on "Web." + +![Web](https://github.com/danny-avila/LibreChat/assets/81851188/22dab877-93cb-4828-9436-10e14374e57e) + +- Register the app. + +![Register App](https://github.com/danny-avila/LibreChat/assets/81851188/0a1b0a75-7285-4f03-95cf-bf971bd7d874) + +- Save all this information in a text file. + +![Save Information](https://github.com/danny-avila/LibreChat/assets/81851188/056754ad-9d36-4662-888e-f189ddb38fd3) + +- Fill all the `firebaseConfig` variables in the `.env` file. + +```bash +FIREBASE_API_KEY=api_key #apiKey +FIREBASE_AUTH_DOMAIN=auth_domain #authDomain +FIREBASE_PROJECT_ID=project_id #projectId +FIREBASE_STORAGE_BUCKET=storage_bucket #storageBucket +FIREBASE_MESSAGING_SENDER_ID=messaging_sender_id #messagingSenderId +FIREBASE_APP_ID=1:your_app_id #appId +``` \ No newline at end of file diff --git a/docs/features/index.md b/docs/features/index.md index ab9e705369a3..9bfda2be25e8 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -22,6 +22,7 @@ weight: 2 * 🔨 [Automated Moderation](./mod_system.md) * 🪙 [Token Usage](./token_usage.md) +* 🔥 [Firebase CDN](./firebase.md) * 🍃 [Manage Your Database](./manage_your_database.md) * 🪵 [Logging System](./logging_system.md) * 📦 [PandoraNext](./pandoranext.md) diff --git a/docs/features/logging_system.md b/docs/features/logging_system.md index 76c3af888474..196849b787d9 100644 --- a/docs/features/logging_system.md +++ b/docs/features/logging_system.md @@ -1,7 +1,7 @@ --- title: 🪵 Logging System +weight: -4 description: This doc explains how to use the logging feature of LibreChat, which saves error and debug logs in the `/api/logs` folder. You can use these logs to troubleshoot issues, monitor your server, and report bugs. You can also disable debug logs if you want to save space. -weight: -5 --- ### General diff --git a/docs/features/manage_your_database.md b/docs/features/manage_your_database.md index 23869b1047cd..3de01e21cbc6 100644 --- a/docs/features/manage_your_database.md +++ b/docs/features/manage_your_database.md @@ -1,7 +1,7 @@ --- title: 🍃 Manage Your Database description: How to install and configure Mongo Express to securely access and manage your MongoDB database in Docker. -weight: -6 +weight: -5 --- diff --git a/docs/features/pandoranext.md b/docs/features/pandoranext.md index 03daaec7c51e..e639796e4d25 100644 --- a/docs/features/pandoranext.md +++ b/docs/features/pandoranext.md @@ -1,7 +1,7 @@ --- title: 📦 PandoraNext description: How to deploy PandoraNext to enable the `CHATGPT_REVERSE_PROXY` for use with LibreChat. -weight: -4 +weight: -3 --- # PandoraNext Deployment Guide diff --git a/package-lock.json b/package-lock.json index 748459ac504e..1e3ff9e5d766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^6.9.0", "express-session": "^1.17.3", + "firebase": "^10.6.0", "googleapis": "^126.0.1", "handlebars": "^4.7.7", "html": "^1.0.0", @@ -4799,6 +4800,690 @@ "fast-json-stringify": "^5.7.0" } }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" + }, + "node_modules/@firebase/app": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.23.tgz", + "integrity": "sha512-CA5pQ88We3FhyuesGKn1thaPBsJSGJGm6AlFToOmEJagWqBeDoNJqBkry/BsHnCs9xeYWWIprKxvuFmAFkdqoA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.23.tgz", + "integrity": "sha512-UCv0LEzcoqAgY+sLsau7aOZz0CJNLN2gESY68bHKmukNXEN6onLPxBKJzn68CsZZGcdiIEXwvrum1riWNPe9Gw==", + "dependencies": { + "@firebase/app": "0.9.23", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.4.0.tgz", + "integrity": "sha512-SfFXZCHDbY+7oSR52NSwx0U7LjYiA+N8imloxphCf3/F+MFty/+mhdjSXGtrJYd0Gbud/qcyedfn2XnWJeIB/g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.9.tgz", + "integrity": "sha512-Fw03i7vduIciEBG4imLtA1duJbljgkfbxiBo/EuekcB+BnPxHp+e8OGMUfemPYeO7Munj6kUC9gr5DelsQkiNA==", + "dependencies": { + "@firebase/auth": "1.4.0", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/auth-compat/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/auth-compat/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/auth/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/auth/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.1.tgz", + "integrity": "sha512-VAhF7gYwunW4Lw/+RQZvW8dlsf2r0YYqV9W0Gi2Mz8+0TGg1mBJWoUtsHfOr8kPJXhcLsC4eP/z3x6L/Fvjk/A==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.1.tgz", + "integrity": "sha512-ky82yLIboLxtAIWyW/52a6HLMVTzD2kpZlEilVDok73pNPLjkJYowj8iaIWK5nTy7+6Gxt7d00zfjL6zckGdXQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "1.0.1", + "@firebase/database-types": "1.0.0", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz", + "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.3.2.tgz", + "integrity": "sha512-K4TwMbgArWw+XAEUYX/vtk+TVy9n1uLeJKSrQeb89lwfkfyFINGLPME6YleaS0ovD1ziLM5/0WgL1CR4s53fDg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.22.tgz", + "integrity": "sha512-M166UvFvRri0CK/+5N0MIeXJVxR6BsX0/96xFT506DxRPIFezLjLcvfddtyFgfe0CtyQWoxBXt060uWUg3d/sw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "4.3.2", + "@firebase/firestore-types": "3.0.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.0.tgz", + "integrity": "sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/firestore/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/firestore/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/firestore/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/functions/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/functions/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/storage/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/storage/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/storage/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.3.tgz", + "integrity": "sha512-+ZplYUN3HOpgCfgInqgdDAbkGGVzES1cs32JJpeqoh87SkRobGXElJx+1GZSaDqzFL+bYiX18qEcBK76mYs8uA==" + }, "node_modules/@floating-ui/core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", @@ -4841,6 +5526,35 @@ "node": ">=18.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.13", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.13.tgz", + "integrity": "sha512-OEZZu9v9AA+7/tghMDE8o5DAMD5THVnwSqDWuh7PPYO5287rTyqy0xEHT6/e4pbqSrhyLPdQFsam4TwFQVVIIw==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@headlessui/react": { "version": "1.7.17", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", @@ -5754,6 +6468,60 @@ "node": ">=4.2.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -10245,7 +11013,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -10258,14 +11025,12 @@ "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -10274,7 +11039,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10288,7 +11052,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -11739,7 +12502,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -12691,6 +13453,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -12932,6 +13705,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.6.0.tgz", + "integrity": "sha512-bnYwHwZ6zB+dM6mGQPEXcFHtAT2WoVzG6H4SIR8HzURVGKJxBW+TqfP3qcJQjTZV3tDqDTo/XZkVmoU/SovV8A==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.23", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.23", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "1.4.0", + "@firebase/auth-compat": "0.4.9", + "@firebase/database": "1.0.1", + "@firebase/database-compat": "1.0.1", + "@firebase/firestore": "4.3.2", + "@firebase/firestore-compat": "0.3.22", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -13246,7 +14052,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -14040,6 +14845,11 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -14113,6 +14923,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -16534,6 +17349,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -16800,6 +17620,11 @@ "node": ">=0.8.0" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -21118,6 +21943,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -22038,7 +22886,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -24103,9 +24950,9 @@ "dev": true }, "node_modules/undici": { - "version": "5.26.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.3.tgz", - "integrity": "sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", + "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -24873,6 +25720,27 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/webworkify": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz", @@ -25383,7 +26251,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -25406,7 +26273,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -25424,7 +26290,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -25432,14 +26297,12 @@ "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -25448,7 +26311,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -25497,7 +26359,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.3.2", + "version": "0.3.4", "license": "ISC", "dependencies": { "axios": "^1.3.4", diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index 380a0dafaf4e..6c7f90c672c5 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -69,3 +69,5 @@ export const assistants = (id?: string) => `/api/assistants${id ? `/${id}` : ''} export const files = () => '/api/files'; export const images = () => `${files()}/images`; + +export const avatar = () => `${images()}/avatar`; diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index b02565652bc0..74ad766575e3 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -197,6 +197,10 @@ export const uploadImage = (data: FormData): Promise => { return request.postMultiPart(endpoints.images(), data); }; +export const uploadAvatar = (data: FormData): Promise => { + return request.postMultiPart(endpoints.avatar(), data); +}; + export const deleteFiles = async (files: f.BatchFile[]): Promise => request.deleteWithOptions(endpoints.files(), { data: { files }, diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index 6da00f7fc4fc..ec150d958730 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -24,4 +24,5 @@ export enum MutationKeys { updatePreset = 'updatePreset', deletePreset = 'deletePreset', logoutUser = 'logoutUser', + avatarUpload = 'avatarUpload', } diff --git a/packages/data-provider/src/types/files.ts b/packages/data-provider/src/types/files.ts index 242ad323ed5f..95f329f65283 100644 --- a/packages/data-provider/src/types/files.ts +++ b/packages/data-provider/src/types/files.ts @@ -10,6 +10,10 @@ export type FileUploadResponse = { width: number; }; +export type AvatarUploadResponse = { + url: string; +}; + export type FileUploadBody = { formData: FormData; file_id: string; @@ -21,6 +25,12 @@ export type UploadMutationOptions = { onError?: (error: unknown, variables: FileUploadBody, context?: unknown) => void; }; +export type UploadAvatarOptions = { + onSuccess?: (data: AvatarUploadResponse, variables: FormData, context?: unknown) => void; + onMutate?: (variables: FormData) => void | Promise; + onError?: (error: unknown, variables: FormData, context?: unknown) => void; +}; + export type DeleteFilesResponse = { message: string; result: Record; From 379e470e386ae71137172d9bab8b49826a0b97bb Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Sat, 30 Dec 2023 12:34:23 -0500 Subject: [PATCH 09/42] =?UTF-8?q?=F0=9F=A7=B9fix:=20Handle=20Abort=20Messa?= =?UTF-8?q?ge=20Edge=20Cases=20(#1462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: bump langchain to v0.0.213 from v0.0.186 * fix: handle abort edge cases: - abort message server-side if response experienced error mid-generation - attempt to recover message if aborting resulted in error - if abortKey is not provided, use conversationId if it exists - if headers were already sent, send an Event stream message - issue warning for possible Google censor/filter refactor(streamResponse): for `sendError`, allow passing overrides so that error can include partial generation, improve typing for `sendMessage` * chore(MessageContent): remove eslint warning for unused `i`, rephrase unfinished message text * fix(useSSE): avoid invoking cancelHandler if the abort response was 404 * chore(TMessage): remove unnecessary, unused legacy message property `submitting` * chore(TMessage): remove unnecessary legacy message property `cancelled` * chore(abortMiddleware): remove unused `errorText` property to avoid confusion --- api/app/clients/BaseClient.js | 2 +- .../clients/prompts/formatMessages.spec.js | 1 - api/models/Message.js | 2 - api/models/schema/messageSchema.js | 4 - api/package.json | 2 +- api/server/controllers/AskController.js | 1 - api/server/controllers/EditController.js | 1 - api/server/middleware/abortMiddleware.js | 37 +- api/server/routes/ask/askChatGPTBrowser.js | 3 - api/server/routes/ask/bingAI.js | 4 - api/server/routes/ask/gptPlugins.js | 1 - api/server/routes/edit/gptPlugins.js | 1 - api/server/utils/streamResponse.js | 30 +- .../Chat/Messages/Content/MessageContent.tsx | 4 +- .../Nav/ExportConversation/ExportModal.jsx | 10 - client/src/hooks/useChatHelpers.ts | 1 - client/src/hooks/useSSE.ts | 13 +- package-lock.json | 602 +++++++++++++----- packages/data-provider/src/schemas.ts | 1 - 19 files changed, 521 insertions(+), 199 deletions(-) diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index b76883265aac..ea63a3ce9040 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -516,7 +516,7 @@ class BaseClient { } async saveMessageToDatabase(message, endpointOptions, user = null) { - await saveMessage({ ...message, user, unfinished: false, cancelled: false }); + await saveMessage({ ...message, user, unfinished: false }); await saveConvo(user, { conversationId: message.conversationId, endpoint: this.options.endpoint, diff --git a/api/app/clients/prompts/formatMessages.spec.js b/api/app/clients/prompts/formatMessages.spec.js index 0497e4c47a0c..636cdb1c8e5d 100644 --- a/api/app/clients/prompts/formatMessages.spec.js +++ b/api/app/clients/prompts/formatMessages.spec.js @@ -54,7 +54,6 @@ describe('formatMessage', () => { _id: '6512cdfb92cbf69fea615331', messageId: 'b620bf73-c5c3-4a38-b724-76886aac24c4', __v: 0, - cancelled: false, conversationId: '5c23d24f-941f-4aab-85df-127b596c8aa5', createdAt: Date.now(), error: false, diff --git a/api/models/Message.js b/api/models/Message.js index 270ff851f97f..7cb9bdc37766 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -18,7 +18,6 @@ module.exports = { isCreatedByUser = false, error, unfinished, - cancelled, files, isEdited = false, finish_reason = null, @@ -45,7 +44,6 @@ module.exports = { finish_reason, error, unfinished, - cancelled, tokenCount, plugin, plugins, diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index 4c0ff2521ebf..bcc28cd23b65 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -68,10 +68,6 @@ const messageSchema = mongoose.Schema( type: Boolean, default: false, }, - cancelled: { - type: Boolean, - default: false, - }, error: { type: Boolean, default: false, diff --git a/api/package.json b/api/package.json index fe5731cbd2fb..56d1a7e59b90 100644 --- a/api/package.json +++ b/api/package.json @@ -54,7 +54,7 @@ "keyv": "^4.5.4", "keyv-file": "^0.2.0", "klona": "^2.0.6", - "langchain": "^0.0.186", + "langchain": "^0.0.213", "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.33.0", diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index d1d9f8f7ad93..ffaa10938c38 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -62,7 +62,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { text: partialText, model: client.modelOptions.model, unfinished: true, - cancelled: false, error: false, user, }); diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js index 023dc35a832c..ecc146126047 100644 --- a/api/server/controllers/EditController.js +++ b/api/server/controllers/EditController.js @@ -61,7 +61,6 @@ const EditController = async (req, res, next, initializeClient) => { text: partialText, model: endpointOption.modelOptions.model, unfinished: true, - cancelled: false, isEdited: true, error: false, user, diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index 4a109acf8f35..811963174c9e 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -7,17 +7,26 @@ const spendTokens = require('~/models/spendTokens'); const { logger } = require('~/config'); async function abortMessage(req, res) { - const { abortKey } = req.body; + let { abortKey, conversationId } = req.body; + + if (!abortKey && conversationId) { + abortKey = conversationId; + } if (!abortControllers.has(abortKey) && !res.headersSent) { return res.status(404).send({ message: 'Request not found' }); } const { abortController } = abortControllers.get(abortKey); - const ret = await abortController.abortCompletion(); + const finalEvent = await abortController.abortCompletion(); logger.debug('[abortMessage] Aborted request', { abortKey }); abortControllers.delete(abortKey); - res.send(JSON.stringify(ret)); + + if (res.headersSent && finalEvent) { + return sendMessage(res, finalEvent); + } + + res.send(JSON.stringify(finalEvent)); } const handleAbort = () => { @@ -58,7 +67,6 @@ const createAbortController = (req, res, getAbortData) => { finish_reason: 'incomplete', model: endpointOption.modelOptions.model, unfinished: false, - cancelled: true, error: false, isCreatedByUser: false, tokenCount: completionTokens, @@ -84,10 +92,16 @@ const createAbortController = (req, res, getAbortData) => { }; const handleAbortError = async (res, req, error, data) => { - logger.error('[handleAbortError] response error and aborting request', error); + logger.error('[handleAbortError] AI response error; aborting request:', error); const { sender, conversationId, messageId, parentMessageId, partialText } = data; - const respondWithError = async () => { + if (error.stack && error.stack.includes('google')) { + logger.warn( + `AI Response error for conversation ${conversationId} likely caused by Google censor/filter`, + ); + } + + const respondWithError = async (partialText) => { const options = { sender, messageId, @@ -97,6 +111,15 @@ const handleAbortError = async (res, req, error, data) => { shouldSaveMessage: true, user: req.user.id, }; + + if (partialText) { + options.overrideProps = { + error: false, + unfinished: true, + text: partialText, + }; + } + const callback = async () => { if (abortControllers.has(conversationId)) { const { abortController } = abortControllers.get(conversationId); @@ -113,7 +136,7 @@ const handleAbortError = async (res, req, error, data) => { return await abortMessage(req, res); } catch (err) { logger.error('[handleAbortError] error while trying to abort message', err); - return respondWithError(); + return respondWithError(partialText); } } else { return respondWithError(); diff --git a/api/server/routes/ask/askChatGPTBrowser.js b/api/server/routes/ask/askChatGPTBrowser.js index 37065b3770d7..34f1096a8714 100644 --- a/api/server/routes/ask/askChatGPTBrowser.js +++ b/api/server/routes/ask/askChatGPTBrowser.js @@ -99,7 +99,6 @@ const ask = async ({ parentMessageId: overrideParentMessageId || userMessageId, text: text, unfinished: true, - cancelled: false, error: false, isCreatedByUser: false, user, @@ -155,7 +154,6 @@ const ask = async ({ text: await handleText(response), sender: endpointOption?.chatGptLabel || 'ChatGPT', unfinished: false, - cancelled: false, error: false, isCreatedByUser: false, }; @@ -226,7 +224,6 @@ const ask = async ({ conversationId, parentMessageId: overrideParentMessageId || userMessageId, unfinished: false, - cancelled: false, error: true, isCreatedByUser: false, text: `${getPartialMessage() ?? ''}\n\nError message: "${error.message}"`, diff --git a/api/server/routes/ask/bingAI.js b/api/server/routes/ask/bingAI.js index 7a7177a96e25..1281b56ae35d 100644 --- a/api/server/routes/ask/bingAI.js +++ b/api/server/routes/ask/bingAI.js @@ -125,7 +125,6 @@ const ask = async ({ model, text: text, unfinished: true, - cancelled: false, error: false, isCreatedByUser: false, user, @@ -193,7 +192,6 @@ const ask = async ({ response.details.suggestedResponses && response.details.suggestedResponses.map((s) => s.text), unfinished, - cancelled: false, error: false, isCreatedByUser: false, }; @@ -263,7 +261,6 @@ const ask = async ({ text: partialText, model, unfinished: true, - cancelled: false, error: false, isCreatedByUser: false, }; @@ -285,7 +282,6 @@ const ask = async ({ conversationId, parentMessageId: overrideParentMessageId || userMessageId, unfinished: false, - cancelled: false, error: true, text: error.message, model, diff --git a/api/server/routes/ask/gptPlugins.js b/api/server/routes/ask/gptPlugins.js index b0aa1aa0f05c..bab60ee23a73 100644 --- a/api/server/routes/ask/gptPlugins.js +++ b/api/server/routes/ask/gptPlugins.js @@ -81,7 +81,6 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, text: partialText, model: endpointOption.modelOptions.model, unfinished: true, - cancelled: false, error: false, plugins, user, diff --git a/api/server/routes/edit/gptPlugins.js b/api/server/routes/edit/gptPlugins.js index b4f1f7ce85ce..cd61b53237ce 100644 --- a/api/server/routes/edit/gptPlugins.js +++ b/api/server/routes/edit/gptPlugins.js @@ -88,7 +88,6 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, text: partialText, model: endpointOption.modelOptions.model, unfinished: true, - cancelled: false, isEdited: true, error: false, user, diff --git a/api/server/utils/streamResponse.js b/api/server/utils/streamResponse.js index 85d35f2c5533..1933839fac27 100644 --- a/api/server/utils/streamResponse.js +++ b/api/server/utils/streamResponse.js @@ -1,5 +1,6 @@ const crypto = require('crypto'); -const { saveMessage } = require('~/models/Message'); +const { saveMessage, getMessages } = require('~/models/Message'); +const { getConvo } = require('~/models/Conversation'); /** * Sends error data in Server Sent Events format and ends the response. @@ -15,7 +16,7 @@ const handleError = (res, message) => { * Sends message data in Server Sent Events format. * @param {object} res - - The server response. * @param {string} message - The message to be sent. - * @param {string} event - [Optional] The type of event. Default is 'message'. + * @param {'message' | 'error' | 'cancel'} event - [Optional] The type of event. Default is 'message'. */ const sendMessage = (res, message, event = 'message') => { if (message.length === 0) { @@ -32,19 +33,27 @@ const sendMessage = (res, message, event = 'message') => { * @param {function} callback - [Optional] The callback function to be executed. */ const sendError = async (res, options, callback) => { - const { user, sender, conversationId, messageId, parentMessageId, text, shouldSaveMessage } = - options; + const { + user, + sender, + conversationId, + messageId, + parentMessageId, + text, + shouldSaveMessage, + overrideProps = {}, + } = options; const errorMessage = { sender, messageId: messageId ?? crypto.randomUUID(), conversationId, parentMessageId, unfinished: false, - cancelled: false, error: true, final: true, text, isCreatedByUser: false, + ...overrideProps, }; if (callback && typeof callback === 'function') { await callback(); @@ -54,6 +63,17 @@ const sendError = async (res, options, callback) => { await saveMessage({ ...errorMessage, user }); } + if (!errorMessage.error) { + const requestMessage = { messageId: parentMessageId, conversationId }; + const query = await getMessages(requestMessage); + return sendMessage(res, { + final: true, + requestMessage: query?.[0] ? query[0] : requestMessage, + responseMessage: errorMessage, + conversation: await getConvo(user, conversationId), + }); + } + handleError(res, errorMessage); }; diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index 908eda6795ef..875d4ebb5a25 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -35,7 +35,7 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay return ( {imageFiles && - imageFiles.map((file, i) => ( + imageFiles.map((file) => ( ( - + ); // Content Component diff --git a/client/src/components/Nav/ExportConversation/ExportModal.jsx b/client/src/components/Nav/ExportConversation/ExportModal.jsx index 0295d37de70e..e4f292a78ad5 100644 --- a/client/src/components/Nav/ExportConversation/ExportModal.jsx +++ b/client/src/components/Nav/ExportConversation/ExportModal.jsx @@ -172,10 +172,6 @@ export default function ExportModal({ open, onOpenChange, conversation }) { fieldName: 'unfinished', fieldValues: entries.find((e) => e.fieldName == 'unfinished').fieldValues, }, - { - fieldName: 'cancelled', - fieldValues: entries.find((e) => e.fieldName == 'cancelled').fieldValues, - }, { fieldName: 'messageId', fieldValues: entries.find((e) => e.fieldName == 'messageId').fieldValues, @@ -226,9 +222,6 @@ export default function ExportModal({ open, onOpenChange, conversation }) { if (message.unfinished) { data += '*(This is an unfinished message)*\n'; } - if (message.cancelled) { - data += '*(This is a cancelled message)*\n'; - } data += '\n\n'; } @@ -275,9 +268,6 @@ export default function ExportModal({ open, onOpenChange, conversation }) { if (message.unfinished) { data += '(This is an unfinished message)\n'; } - if (message.cancelled) { - data += '(This is a cancelled message)\n'; - } data += '\n\n'; } diff --git a/client/src/hooks/useChatHelpers.ts b/client/src/hooks/useChatHelpers.ts index d5cc37a1594d..bb702cb98f88 100644 --- a/client/src/hooks/useChatHelpers.ts +++ b/client/src/hooks/useChatHelpers.ts @@ -217,7 +217,6 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) { messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`, conversationId, unfinished: false, - submitting: true, isCreatedByUser: false, isEdited: isEditOrContinue, error: false, diff --git a/client/src/hooks/useSSE.ts b/client/src/hooks/useSSE.ts index e41fa3cb1045..e5258511c7fe 100644 --- a/client/src/hooks/useSSE.ts +++ b/client/src/hooks/useSSE.ts @@ -64,7 +64,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) { messageId: message?.overrideParentMessageId + '_', plugin: plugin ?? null, plugins: plugins ?? [], - submitting: true, // unfinished: true }, ]); @@ -79,7 +78,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) { messageId: message?.messageId + '_', plugin: plugin ?? null, plugins: plugins ?? [], - submitting: true, // unfinished: true }, ]); @@ -136,7 +134,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) { ...initialResponse, parentMessageId: message?.overrideParentMessageId ?? null, messageId: message?.overrideParentMessageId + '_', - submitting: true, }, ]); } else { @@ -147,7 +144,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) { ...initialResponse, parentMessageId: message?.messageId, messageId: message?.messageId + '_', - submitting: true, }, ]); } @@ -238,6 +234,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) { const abortConversation = (conversationId = '', submission: TSubmission) => { console.log(submission); const { endpoint } = submission?.conversation || {}; + let res: Response; fetch(`/api/ask/${endpoint}/abort`, { method: 'POST', @@ -249,9 +246,15 @@ export default function useSSE(submission: TSubmission | null, index = 0) { abortKey: conversationId, }), }) - .then((response) => response.json()) + .then((response) => { + res = response; + return response.json(); + }) .then((data) => { console.log('aborted', data); + if (res.status === 404) { + return setIsSubmitting(false); + } cancelHandler(data, submission); }) .catch((error) => { diff --git a/package-lock.json b/package-lock.json index 1e3ff9e5d766..806b828ce55c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "keyv": "^4.5.4", "keyv-file": "^0.2.0", "klona": "^2.0.6", - "langchain": "^0.0.186", + "langchain": "^0.0.213", "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.33.0", @@ -103,67 +103,44 @@ "supertest": "^6.3.3" } }, - "api/node_modules/@types/node": { - "version": "18.18.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", - "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "api/node_modules/ansi-styles": { + "api/node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "api/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16.0.0" } }, - "api/node_modules/cohere-ai": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cohere-ai/-/cohere-ai-6.0.0.tgz", - "integrity": "sha512-u1KmPw2PWbTQbbZXmdVs610N/zYyh/V6mL/nDXqx1Gl6X7IH84z9gK3jNv/f69Uzc007xe3zHmMYk80WlkHEmA==" + "api/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } }, - "api/node_modules/langchain": { - "version": "0.0.186", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.0.186.tgz", - "integrity": "sha512-uXDipmw9aUrUmDNcFr2XH9ORmshWIlIb/qFKneS1K3X5upMUg7TSbaBxqV9WxuuenLUSYaoTcTy7P/pKkbqXPg==", + "api/node_modules/@langchain/community": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.0.12.tgz", + "integrity": "sha512-mcm6FxxnLxSx9PiYvehGGwvcHjsVR5WXfYOwymojf/6d0apyewjOLzKsR3xx0HJVtCs8pff7NZSdDoE+jj8OcA==", "dependencies": { - "@anthropic-ai/sdk": "^0.6.2", - "ansi-styles": "^5.0.0", - "binary-extensions": "^2.2.0", - "camelcase": "6", - "decamelize": "^1.2.0", - "expr-eval": "^2.0.2", + "@langchain/core": "~0.1.5", + "@langchain/openai": "~0.0.9", "flat": "^5.0.2", - "js-tiktoken": "^1.0.7", - "js-yaml": "^4.1.0", - "jsonpointer": "^5.0.1", - "langchainhub": "~0.0.6", "langsmith": "~0.0.48", - "ml-distance": "^4.0.0", - "openai": "^4.17.0", - "openapi-types": "^12.1.3", - "p-queue": "^6.6.2", - "p-retry": "4", "uuid": "^9.0.0", - "yaml": "^2.2.1", - "zod": "^3.22.3", - "zod-to-json-schema": "^3.20.4" + "zod": "^3.22.3" }, "engines": { "node": ">=18" @@ -174,29 +151,27 @@ "@aws-sdk/client-dynamodb": "^3.310.0", "@aws-sdk/client-kendra": "^3.352.0", "@aws-sdk/client-lambda": "^3.310.0", - "@aws-sdk/client-s3": "^3.310.0", "@aws-sdk/client-sagemaker-runtime": "^3.310.0", "@aws-sdk/client-sfn": "^3.310.0", "@aws-sdk/credential-provider-node": "^3.388.0", - "@azure/storage-blob": "^12.15.0", - "@clickhouse/client": "^0.0.14", + "@clickhouse/client": "^0.2.5", "@cloudflare/ai": "^1.0.12", + "@datastax/astra-db-ts": "0.1.2", "@elastic/elasticsearch": "^8.4.0", "@getmetal/metal-sdk": "*", "@getzep/zep-js": "^0.9.0", - "@gomomento/sdk": "^1.47.1", - "@gomomento/sdk-core": "^1.47.1", - "@gomomento/sdk-web": "^1.47.1", + "@gomomento/sdk": "^1.51.1", + "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", - "@google-cloud/storage": "^6.10.1", + "@gradientai/nodejs-sdk": "^1.2.0", "@huggingface/inference": "^2.6.4", "@mozilla/readability": "*", - "@notionhq/client": "^2.2.10", "@opensearch-project/opensearch": "*", "@pinecone-database/pinecone": "^1.1.0", "@planetscale/database": "^1.8.0", "@qdrant/js-client-rest": "^1.2.0", "@raycast/api": "^1.55.2", + "@rockset/client": "^0.9.1", "@smithy/eventstream-codec": "^2.0.5", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^2.0.10", @@ -210,52 +185,37 @@ "@vercel/kv": "^0.2.3", "@vercel/postgres": "^0.5.0", "@writerai/writer-sdk": "^0.40.2", - "@xata.io/client": "^0.25.1", + "@xata.io/client": "^0.28.0", "@xenova/transformers": "^2.5.4", "@zilliz/milvus2-sdk-node": ">=2.2.7", - "apify-client": "^2.7.1", - "assemblyai": "^2.0.2", - "axios": "*", "cassandra-driver": "^4.7.2", - "cheerio": "^1.0.0-rc.12", "chromadb": "*", "closevector-common": "0.1.0-alpha.1", "closevector-node": "0.1.0-alpha.10", "closevector-web": "0.1.0-alpha.16", "cohere-ai": ">=6.0.0", "convex": "^1.3.1", - "d3-dsv": "^2.0.0", - "epub2": "^3.0.1", + "discord.js": "^14.14.1", "faiss-node": "^0.5.1", - "fast-xml-parser": "^4.2.7", "firebase-admin": "^11.9.0", "google-auth-library": "^8.9.0", "googleapis": "^126.0.1", "hnswlib-node": "^1.4.2", "html-to-text": "^9.0.5", - "ignore": "^5.2.0", "ioredis": "^5.3.2", "jsdom": "*", - "llmonitor": "^0.5.8", + "llmonitor": "^0.5.9", "lodash": "^4.17.21", - "mammoth": "*", "mongodb": "^5.2.0", "mysql2": "^3.3.3", "neo4j-driver": "*", "node-llama-cpp": "*", - "notion-to-md": "^3.1.0", - "pdf-parse": "1.1.1", - "peggy": "^3.0.2", "pg": "^8.11.0", "pg-copy-streams": "^6.0.5", "pickleparser": "^0.2.1", - "playwright": "^1.32.1", "portkey-ai": "^0.1.11", - "puppeteer": "^19.7.2", "redis": "^4.6.4", "replicate": "^0.18.0", - "sonix-speech-recognition": "^2.1.1", - "srt-parser-2": "^1.2.2", "typeorm": "^0.3.12", "typesense": "^1.5.3", "usearch": "^1.1.1", @@ -263,9 +223,7 @@ "voy-search": "0.6.2", "weaviate-ts-client": "^1.4.0", "web-auth-library": "^1.0.3", - "ws": "^8.14.2", - "youtube-transcript": "^1.0.6", - "youtubei.js": "^5.8.0" + "ws": "^8.14.2" }, "peerDependenciesMeta": { "@aws-crypto/sha256-js": { @@ -283,9 +241,6 @@ "@aws-sdk/client-lambda": { "optional": true }, - "@aws-sdk/client-s3": { - "optional": true - }, "@aws-sdk/client-sagemaker-runtime": { "optional": true }, @@ -295,15 +250,15 @@ "@aws-sdk/credential-provider-node": { "optional": true }, - "@azure/storage-blob": { - "optional": true - }, "@clickhouse/client": { "optional": true }, "@cloudflare/ai": { "optional": true }, + "@datastax/astra-db-ts": { + "optional": true + }, "@elastic/elasticsearch": { "optional": true }, @@ -319,13 +274,10 @@ "@gomomento/sdk-core": { "optional": true }, - "@gomomento/sdk-web": { - "optional": true - }, "@google-ai/generativelanguage": { "optional": true }, - "@google-cloud/storage": { + "@gradientai/nodejs-sdk": { "optional": true }, "@huggingface/inference": { @@ -334,9 +286,6 @@ "@mozilla/readability": { "optional": true }, - "@notionhq/client": { - "optional": true - }, "@opensearch-project/opensearch": { "optional": true }, @@ -352,6 +301,9 @@ "@raycast/api": { "optional": true }, + "@rockset/client": { + "optional": true + }, "@smithy/eventstream-codec": { "optional": true }, @@ -400,21 +352,9 @@ "@zilliz/milvus2-sdk-node": { "optional": true }, - "apify-client": { - "optional": true - }, - "assemblyai": { - "optional": true - }, - "axios": { - "optional": true - }, "cassandra-driver": { "optional": true }, - "cheerio": { - "optional": true - }, "chromadb": { "optional": true }, @@ -433,18 +373,12 @@ "convex": { "optional": true }, - "d3-dsv": { - "optional": true - }, - "epub2": { + "discord.js": { "optional": true }, "faiss-node": { "optional": true }, - "fast-xml-parser": { - "optional": true - }, "firebase-admin": { "optional": true }, @@ -460,9 +394,6 @@ "html-to-text": { "optional": true }, - "ignore": { - "optional": true - }, "ioredis": { "optional": true }, @@ -475,9 +406,6 @@ "lodash": { "optional": true }, - "mammoth": { - "optional": true - }, "mongodb": { "optional": true }, @@ -490,58 +418,368 @@ "node-llama-cpp": { "optional": true }, - "notion-to-md": { + "pg": { "optional": true }, - "pdf-parse": { + "pg-copy-streams": { "optional": true }, - "peggy": { + "pickleparser": { "optional": true }, - "pg": { + "portkey-ai": { "optional": true }, - "pg-copy-streams": { + "redis": { "optional": true }, - "pickleparser": { + "replicate": { "optional": true }, - "playwright": { + "typeorm": { "optional": true }, - "portkey-ai": { + "typesense": { "optional": true }, - "puppeteer": { + "usearch": { "optional": true }, - "redis": { + "vectordb": { "optional": true }, - "replicate": { + "voy-search": { "optional": true }, - "sonix-speech-recognition": { + "weaviate-ts-client": { "optional": true }, - "srt-parser-2": { + "web-auth-library": { "optional": true }, - "typeorm": { + "ws": { "optional": true + } + } + }, + "api/node_modules/@types/node": { + "version": "18.19.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.4.tgz", + "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "api/node_modules/cohere-ai": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cohere-ai/-/cohere-ai-6.0.0.tgz", + "integrity": "sha512-u1KmPw2PWbTQbbZXmdVs610N/zYyh/V6mL/nDXqx1Gl6X7IH84z9gK3jNv/f69Uzc007xe3zHmMYk80WlkHEmA==" + }, + "api/node_modules/fast-xml-parser": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" }, - "typesense": { + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "api/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "api/node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "api/node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "optional": true, + "peer": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "api/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "api/node_modules/langchain": { + "version": "0.0.213", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.0.213.tgz", + "integrity": "sha512-nQDOJXvtIAIuUzamCiF1AWyi2GH9FSDPR+3XulJUEpdU60aSFPZ9GBiWdu+dVHXeAmm8C0iCVi0+3GWLJrUoXA==", + "dependencies": { + "@anthropic-ai/sdk": "^0.9.1", + "@langchain/community": "~0.0.12", + "@langchain/core": "~0.1.5", + "@langchain/openai": "~0.0.9", + "binary-extensions": "^2.2.0", + "expr-eval": "^2.0.2", + "js-tiktoken": "^1.0.7", + "js-yaml": "^4.1.0", + "jsonpointer": "^5.0.1", + "langchainhub": "~0.0.6", + "langsmith": "~0.0.48", + "ml-distance": "^4.0.0", + "openapi-types": "^12.1.3", + "p-retry": "4", + "uuid": "^9.0.0", + "yaml": "^2.2.1", + "zod": "^3.22.3", + "zod-to-json-schema": "3.20.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.310.0", + "@aws-sdk/client-sagemaker-runtime": "^3.310.0", + "@aws-sdk/client-sfn": "^3.310.0", + "@aws-sdk/credential-provider-node": "^3.388.0", + "@azure/storage-blob": "^12.15.0", + "@gomomento/sdk": "^1.51.1", + "@gomomento/sdk-core": "^1.51.1", + "@gomomento/sdk-web": "^1.51.1", + "@google-ai/generativelanguage": "^0.2.1", + "@google-cloud/storage": "^6.10.1", + "@notionhq/client": "^2.2.10", + "@pinecone-database/pinecone": "^1.1.0", + "@supabase/supabase-js": "^2.10.0", + "@vercel/kv": "^0.2.3", + "@xata.io/client": "^0.28.0", + "apify-client": "^2.7.1", + "assemblyai": "^4.0.0", + "axios": "*", + "cheerio": "^1.0.0-rc.12", + "chromadb": "*", + "convex": "^1.3.1", + "d3-dsv": "^2.0.0", + "epub2": "^3.0.1", + "fast-xml-parser": "^4.2.7", + "google-auth-library": "^8.9.0", + "googleapis": "^126.0.1", + "html-to-text": "^9.0.5", + "ignore": "^5.2.0", + "ioredis": "^5.3.2", + "jsdom": "*", + "mammoth": "^1.6.0", + "mongodb": "^5.2.0", + "node-llama-cpp": "*", + "notion-to-md": "^3.1.0", + "officeparser": "^4.0.4", + "pdf-parse": "1.1.1", + "peggy": "^3.0.2", + "playwright": "^1.32.1", + "puppeteer": "^19.7.2", + "pyodide": "^0.24.1", + "redis": "^4.6.4", + "sonix-speech-recognition": "^2.1.1", + "srt-parser-2": "^1.2.2", + "typeorm": "^0.3.12", + "vectordb": "^0.1.4", + "weaviate-ts-client": "^1.4.0", + "web-auth-library": "^1.0.3", + "ws": "^8.14.2", + "youtube-transcript": "^1.0.6", + "youtubei.js": "^5.8.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { "optional": true }, - "usearch": { + "@aws-sdk/client-sagemaker-runtime": { "optional": true }, - "vectordb": { + "@aws-sdk/client-sfn": { "optional": true }, - "voy-search": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@gomomento/sdk": { + "optional": true + }, + "@gomomento/sdk-core": { + "optional": true + }, + "@gomomento/sdk-web": { + "optional": true + }, + "@google-ai/generativelanguage": { + "optional": true + }, + "@google-cloud/storage": { + "optional": true + }, + "@notionhq/client": { + "optional": true + }, + "@pinecone-database/pinecone": { + "optional": true + }, + "@supabase/supabase-js": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "apify-client": { + "optional": true + }, + "assemblyai": { + "optional": true + }, + "axios": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "chromadb": { + "optional": true + }, + "convex": { + "optional": true + }, + "d3-dsv": { + "optional": true + }, + "epub2": { + "optional": true + }, + "faiss-node": { + "optional": true + }, + "fast-xml-parser": { + "optional": true + }, + "google-auth-library": { + "optional": true + }, + "googleapis": { + "optional": true + }, + "html-to-text": { + "optional": true + }, + "ignore": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "mammoth": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "node-llama-cpp": { + "optional": true + }, + "notion-to-md": { + "optional": true + }, + "officeparser": { + "optional": true + }, + "pdf-parse": { + "optional": true + }, + "peggy": { + "optional": true + }, + "playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "pyodide": { + "optional": true + }, + "redis": { + "optional": true + }, + "sonix-speech-recognition": { + "optional": true + }, + "srt-parser-2": { + "optional": true + }, + "typeorm": { + "optional": true + }, + "vectordb": { "optional": true }, "weaviate-ts-client": { @@ -562,9 +800,9 @@ } }, "api/node_modules/langchain/node_modules/@anthropic-ai/sdk": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.6.8.tgz", - "integrity": "sha512-z4gDFrBf+W2wOVvwA3CA+5bfKOxQhPeXQo7+ITWj3r3XPulIMEasVT0KrD41G+anr5Yc3d2PKvXKB6b1LSon5w==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.9.1.tgz", + "integrity": "sha512-wa1meQ2WSfoY8Uor3EdrJq0jTiZJoKoSii2ZVWRY1oN4Tlr5s59pADg9T79FTbPe1/se5c3pBeZgJL63wmuoBA==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -577,6 +815,19 @@ "web-streams-polyfill": "^3.2.1" } }, + "api/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "api/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -597,6 +848,13 @@ "node": ">= 8" } }, + "api/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true, + "peer": true + }, "client": { "name": "@librechat/frontend", "version": "0.6.5", @@ -6189,9 +6447,9 @@ } }, "node_modules/@langchain/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.1.1.tgz", - "integrity": "sha512-p+BzmILzGAWjHxzNy1ZygfsNWxZcipWH3zvY0WC33ly+nmBp4n+MCVd25hKQ4YYWOqoOGGWSzFJukd5PxaXMDQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.1.5.tgz", + "integrity": "sha512-5RdZpbadMYXTKdZrWZ6JeYT09BZssprg+komE+FbXxW+kbGE0QJJaj8dnzPNdzVCAhDLQQTOV2CAM+vOLsaxzQ==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", @@ -6254,6 +6512,21 @@ "node": ">=18" } }, + "node_modules/@langchain/openai": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.0.9.tgz", + "integrity": "sha512-Py7rJijOjNtb9pj5He+E9uAj9d8PCX+8Ix4LvY7cUTMCDlfkhngw2DhJ8wlk23u1IvunakdnnN74pfbO2JJBrw==", + "dependencies": { + "@langchain/core": "~0.1.5", + "js-tiktoken": "^1.0.7", + "openai": "^4.19.0", + "zod": "^3.22.3", + "zod-to-json-schema": "3.20.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -9785,7 +10058,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -13378,6 +13651,13 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true, + "peer": true + }, "node_modules/fast-uri": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz", @@ -14270,6 +14550,22 @@ "node": ">=14" } }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "peer": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/googleapis": { "version": "126.0.1", "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-126.0.1.tgz", @@ -19131,6 +19427,16 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-html-parser": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", @@ -26341,11 +26647,11 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.21.4.tgz", - "integrity": "sha512-fjUZh4nQ1s6HMccgIeE0VP4QG/YRGPmyjO9sAh890aQKPEk3nqbfUXhMFaC+Dr5KvYBm8BCyvfpZf2jY9aGSsw==", + "version": "3.20.3", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.20.3.tgz", + "integrity": "sha512-/Q3wnyxAfCt94ZcrGiXXoiAfRqasxl9CX64LZ9fj+4dKH68zulUtU0uk1WMxQPfAxQ0ZI70dKzcoW7hHj+DwSQ==", "peerDependencies": { - "zod": "^3.21.4" + "zod": "^3.20.0" } }, "node_modules/zwitch": { diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index a1706eacaa27..4698ea6a556b 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -229,7 +229,6 @@ export const tMessageSchema = z.object({ .default(() => new Date().toISOString()), current: z.boolean().optional(), unfinished: z.boolean().optional(), - submitting: z.boolean().optional(), searchResult: z.boolean().optional(), finish_reason: z.string().optional(), }); From 8735db0980cb5081c9c557ab6c771f852688f5a2 Mon Sep 17 00:00:00 2001 From: Fuegovic <32828263+fuegovic@users.noreply.github.com> Date: Sat, 30 Dec 2023 12:39:30 -0500 Subject: [PATCH 10/42] doc update: firebase.md (#1456) --- docs/features/firebase.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/features/firebase.md b/docs/features/firebase.md index d6725044b249..fc94b3bce9f4 100644 --- a/docs/features/firebase.md +++ b/docs/features/firebase.md @@ -71,4 +71,33 @@ FIREBASE_PROJECT_ID=project_id #projectId FIREBASE_STORAGE_BUCKET=storage_bucket #storageBucket FIREBASE_MESSAGING_SENDER_ID=messaging_sender_id #messagingSenderId FIREBASE_APP_ID=1:your_app_id #appId -``` \ No newline at end of file +``` + +- Return one last time to the Project Overview. + +![Project Overview](https://github.com/danny-avila/LibreChat/assets/81851188/c425f4bb-a494-42f2-9fdc-ff2c8ce005e1) + +- Select `Storage` + +![image](https://github.com/danny-avila/LibreChat/assets/32828263/16a0f850-cdd4-4875-8342-ab67bfb59804) + +- Select `Rules` and delete `: if false;` on this line: `allow read, write: if false;` + + - your updated rules should look like this: + + ```bash + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write + } + } + } + ``` + +![image](https://github.com/danny-avila/LibreChat/assets/32828263/c190011f-c1a6-47c7-986e-8d309b5f8704) + +- Publish your updated rules + +![image](https://github.com/danny-avila/LibreChat/assets/32828263/5e6a17c3-5aba-419a-a18f-be910b1f25d5) \ No newline at end of file From 1a95bef67776eb9d0e87e9f0646b39ad0381dd43 Mon Sep 17 00:00:00 2001 From: Linus Gasser Date: Sat, 30 Dec 2023 19:25:12 +0100 Subject: [PATCH 11/42] =?UTF-8?q?=F0=9F=93=83=20feat:=20add=20`list-balanc?= =?UTF-8?q?es`,=20`remove-user`,=20and=20improve=20User=20scripts=20(#1418?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactoring opening of DB to config/helpers.js * Adding two user scripts: - 'delete-user' to remove a user definitely - 'list-balances' to show the balances of all the users --- config/add-balance.js | 29 +---------- config/ban-user.js | 29 +---------- config/create-user.js | 29 +---------- config/delete-user.js | 48 +++++++++++++++++++ config/helpers.js | 29 +++++++++++ config/list-balances.js | 39 +++++++++++++++ docs/features/token_usage.md | 8 ++++ docs/install/configuration/dotenv.md | 2 + .../install/configuration/user_auth_system.md | 1 + package.json | 5 +- 10 files changed, 137 insertions(+), 82 deletions(-) create mode 100644 config/delete-user.js create mode 100644 config/list-balances.js diff --git a/config/add-balance.js b/config/add-balance.js index 3aa786c16e9a..75b9b4cda9ab 100644 --- a/config/add-balance.js +++ b/config/add-balance.js @@ -1,36 +1,11 @@ const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); -const { askQuestion, silentExit } = require('./helpers'); +const { askQuestion, silentExit, connectWithTimeout } = require('./helpers'); const Transaction = require('~/models/Transaction'); -const connectDb = require('~/lib/db/connectDb'); const User = require('~/models/User'); (async () => { - /** - * Connect to the database - * - If it takes a while, we'll warn the user - */ - // Warn the user if this is taking a while - let timeout = setTimeout(() => { - console.orange( - 'This is taking a while... You may need to check your connection if this fails.', - ); - timeout = setTimeout(() => { - console.orange('Still going... Might as well assume the connection failed...'); - timeout = setTimeout(() => { - console.orange('Error incoming in 3... 2... 1...'); - }, 13000); - }, 10000); - }, 5000); - // Attempt to connect to the database - try { - console.orange('Warming up the engines...'); - await connectDb(); - clearTimeout(timeout); - } catch (e) { - console.error(e); - silentExit(1); - } + await connectWithTimeout(); /** * Show the welcome / help menu diff --git a/config/ban-user.js b/config/ban-user.js index 6db29e8159a0..a2c01a1cefa8 100644 --- a/config/ban-user.js +++ b/config/ban-user.js @@ -1,36 +1,11 @@ const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); -const { askQuestion, silentExit } = require('./helpers'); +const { askQuestion, silentExit, connectWithTimeout } = require('./helpers'); const banViolation = require('~/cache/banViolation'); -const connectDb = require('~/lib/db/connectDb'); const User = require('~/models/User'); (async () => { - /** - * Connect to the database - * - If it takes a while, we'll warn the user - */ - // Warn the user if this is taking a while - let timeout = setTimeout(() => { - console.orange( - 'This is taking a while... You may need to check your connection if this fails.', - ); - timeout = setTimeout(() => { - console.orange('Still going... Might as well assume the connection failed...'); - timeout = setTimeout(() => { - console.orange('Error incoming in 3... 2... 1...'); - }, 13000); - }, 10000); - }, 5000); - // Attempt to connect to the database - try { - console.orange('Warming up the engines...'); - await connectDb(); - clearTimeout(timeout); - } catch (e) { - console.error(e); - silentExit(1); - } + await connectWithTimeout(); console.purple('---------------------'); console.purple('Ban a user account!'); diff --git a/config/create-user.js b/config/create-user.js index 738460c8a3e1..cd0c7132559f 100644 --- a/config/create-user.js +++ b/config/create-user.js @@ -1,36 +1,11 @@ const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { registerUser } = require('~/server/services/AuthService'); -const { askQuestion, silentExit } = require('./helpers'); -const connectDb = require('~/lib/db/connectDb'); +const { askQuestion, silentExit, connectWithTimeout } = require('./helpers'); const User = require('~/models/User'); (async () => { - /** - * Connect to the database - * - If it takes a while, we'll warn the user - */ - // Warn the user if this is taking a while - let timeout = setTimeout(() => { - console.orange( - 'This is taking a while... You may need to check your connection if this fails.', - ); - timeout = setTimeout(() => { - console.orange('Still going... Might as well assume the connection failed...'); - timeout = setTimeout(() => { - console.orange('Error incoming in 3... 2... 1...'); - }, 13000); - }, 10000); - }, 5000); - // Attempt to connect to the database - try { - console.orange('Warming up the engines...'); - await connectDb(); - clearTimeout(timeout); - } catch (e) { - console.error(e); - silentExit(1); - } + await connectWithTimeout(); /** * Show the welcome / help menu diff --git a/config/delete-user.js b/config/delete-user.js new file mode 100644 index 000000000000..fe7efe057d31 --- /dev/null +++ b/config/delete-user.js @@ -0,0 +1,48 @@ +const path = require('path'); +require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); +const { connectWithTimeout, askQuestion, silentExit } = require('./helpers'); +const User = require('~/models/User'); + +(async () => { + await connectWithTimeout(); + + /** + * Show the welcome / help menu + */ + console.purple('---------------'); + console.purple('Deleting a user'); + console.purple('---------------'); + + let email = ''; + if (process.argv.length >= 3) { + email = process.argv[2]; + } else { + email = await askQuestion('Email:'); + } + let user = await User.findOne({ email: email }); + if (user !== null) { + if ((await askQuestion(`Delete user ${user}?`)) === 'y') { + user = await User.findOneAndDelete({ _id: user._id }); + if (user !== null) { + console.yellow(`Deleted user ${user}`); + } else { + console.yellow(`Couldn't delete user with email ${email}`); + } + } + } else { + console.yellow(`Didn't find user with email ${email}`); + } + + silentExit(0); +})(); + +process.on('uncaughtException', (err) => { + if (!err.message.includes('fetch failed')) { + console.error('There was an uncaught error:'); + console.error(err); + } + + if (!err.message.includes('fetch failed')) { + process.exit(1); + } +}); diff --git a/config/helpers.js b/config/helpers.js index 6bc6ed0ef2ab..a86d562eb354 100644 --- a/config/helpers.js +++ b/config/helpers.js @@ -6,6 +6,7 @@ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const { execSync } = require('child_process'); +const { connectDb } = require('@librechat/backend/lib/db'); const askQuestion = (query) => { const rl = readline.createInterface({ @@ -43,6 +44,33 @@ const silentExit = (code = 0) => { process.exit(code); }; +async function connectWithTimeout() { + /** + * Connect to the database + * - If it takes a while, we'll warn the user + */ + let timeout = setTimeout(() => { + console.orange( + 'This is taking a while... You may need to check your connection if this fails.', + ); + timeout = setTimeout(() => { + console.orange('Still going... Might as well assume the connection failed...'); + timeout = setTimeout(() => { + console.orange('Error incoming in 3... 2... 1...'); + }, 13000); + }, 10000); + }, 5000); + // Attempt to connect to the database + try { + console.orange('Warming up the engines...'); + await connectDb(); + clearTimeout(timeout); + } catch (e) { + console.error(e); + silentExit(1); + } +} + // Set the console colours console.orange = (msg) => console.log('\x1b[33m%s\x1b[0m', msg); console.green = (msg) => console.log('\x1b[32m%s\x1b[0m', msg); @@ -58,5 +86,6 @@ module.exports = { askQuestion, silentExit, isDockerRunning, + connectWithTimeout, deleteNodeModules, }; diff --git a/config/list-balances.js b/config/list-balances.js new file mode 100644 index 000000000000..670aba6e5d99 --- /dev/null +++ b/config/list-balances.js @@ -0,0 +1,39 @@ +const path = require('path'); +require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); +const { connectWithTimeout, silentExit } = require('./helpers'); +const Balance = require('~/models/Balance'); +const User = require('~/models/User'); + +(async () => { + await connectWithTimeout(); + + /** + * Show the welcome / help menu + */ + console.purple('-----------------------------'); + console.purple('Show the balance of all users'); + console.purple('-----------------------------'); + + let users = await User.find({}); + for (const user of users) { + let balance = await Balance.findOne({ user: user._id }); + if (balance !== null) { + console.green(`User ${user.name} has a balance of ${balance.tokenCredits}`); + } else { + console.yellow(`User ${user.name} has no balance`); + } + } + + silentExit(0); +})(); + +process.on('uncaughtException', (err) => { + if (!err.message.includes('fetch failed')) { + console.error('There was an uncaught error:'); + console.error(err); + } + + if (!err.message.includes('fetch failed')) { + process.exit(1); + } +}); diff --git a/docs/features/token_usage.md b/docs/features/token_usage.md index 876b385f97c5..0dfc835309cd 100644 --- a/docs/features/token_usage.md +++ b/docs/features/token_usage.md @@ -30,6 +30,14 @@ npm run add-balance danny@librechat.ai 1000 This works well to track your own usage for personal use; 1000 credits = $0.001 (1 mill USD) +## Listing of balances + +To see the balances of your users, you can run: + +```bash +npm run list-balances +``` + ## Notes - With summarization enabled, you will be blocked from making an API request if the cost of the content that you need to summarize + your messages payload exceeds the current balance diff --git a/docs/install/configuration/dotenv.md b/docs/install/configuration/dotenv.md index ea3cd097d945..5eda7ba3f7ba 100644 --- a/docs/install/configuration/dotenv.md +++ b/docs/install/configuration/dotenv.md @@ -581,6 +581,7 @@ see: **[Token Usage](../../features/token_usage.md)** - To manually add balances, run the following command:`npm run add-balance` - You can also specify the email and token credit amount to add, e.g.:`npm run add-balance example@example.com 1000` + - To list the balance of every user: `npm run list-balances` > **Note:** 1000 credits = $0.001 (1 mill USD) @@ -602,6 +603,7 @@ see: **[User/Auth System](../configuration/user_auth_system.md)** - `ALLOW_SOCIAL_REGISTRATION`: Enable or disable registration of new user using various social network. Set to `true` or `false` to enable or disable. > **Quick Tip:** Even with registration disabled, add users directly to the database using `npm run create-user`. +> **Quick Tip:** With registration disabled, you can delete a user with `npm run delete-user email@domain.com`. ```bash ALLOW_EMAIL_LOGIN=true diff --git a/docs/install/configuration/user_auth_system.md b/docs/install/configuration/user_auth_system.md index 7020d37ad7b0..17016845deae 100644 --- a/docs/install/configuration/user_auth_system.md +++ b/docs/install/configuration/user_auth_system.md @@ -30,6 +30,7 @@ Here's an overview of the general configuration, located in the `.env` file at t > **Note:** OpenID does not support the ability to disable only registration. >> **Quick Tip:** Even with registration disabled, add users directly to the database using `npm run create-user`. If you can't get npm to work, try `sudo docker exec -ti LibreChat sh` first to "ssh" into the container. +>> **Quick Tip:** To delete a user, you can run `docker-compose exec api npm run delete-user email@domain.com` ![image](https://github.com/danny-avila/LibreChat/assets/81851188/52a37d1d-7392-4a9a-a79f-90ed2da7f841) diff --git a/package.json b/package.json index bcf7c24745c4..77ddd822650b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "update": "node config/update.js", "add-balance": "node config/add-balance.js", + "list-balances": "node config/list-balances.js", "rebuild:package-lock": "node config/packages", "reinstall": "node config/update.js -l -g", "b:reinstall": "bun config/update.js -b -l -g", @@ -25,6 +26,7 @@ "upgrade": "node config/upgrade.js", "create-user": "node config/create-user.js", "ban-user": "node config/ban-user.js", + "delete-user": "node config/delete-user.js", "backend": "cross-env NODE_ENV=production node api/server/index.js", "backend:dev": "cross-env NODE_ENV=development npx nodemon api/server/index.js", "backend:stop": "node config/stop-backend.js", @@ -53,7 +55,8 @@ "b:client:dev": "cd client && bun run b:dev", "b:test:client": "cd client && bun run b:test", "b:test:api": "cd api && bun run b:test", - "b:balance": "bun config/add-balance.js" + "b:balance": "bun config/add-balance.js", + "b:list-balances": "bun config/list-balances.js" }, "repository": { "type": "git", From e4c555f95a80f4a0a760fe302e461e4ba1f5755f Mon Sep 17 00:00:00 2001 From: MACHINSOFT <110278369+machinsoft@users.noreply.github.com> Date: Sat, 30 Dec 2023 21:25:37 +0300 Subject: [PATCH 12/42] Add Russian translation for the new functionality in the settings (#1457) --- client/src/localization/languages/Ru.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/localization/languages/Ru.tsx b/client/src/localization/languages/Ru.tsx index 365f93f1da04..06023ef3f405 100644 --- a/client/src/localization/languages/Ru.tsx +++ b/client/src/localization/languages/Ru.tsx @@ -49,6 +49,9 @@ export default { com_ui_confirm_action: 'Подтвердить действие', com_ui_chats: 'чаты', com_ui_delete: 'Удалить', + com_ui_preview: 'Предпросмотр', + com_ui_upload: 'Загрузить', + com_ui_connect: 'Подключить', com_ui_delete_conversation: 'Удалить чат?', com_ui_delete_conversation_confirm: 'Будет удален следующий чат: ', com_auth_error_login: @@ -219,6 +222,7 @@ export default { com_endpoint_func_hover: 'Включить использование плагинов как функции OpenAI', com_endpoint_skip_hover: 'Пропустить этап завершения, который проверяет окончательный ответ и сгенерированные шаги', + com_endpoint_config_token: 'Токен конфигурации', com_endpoint_config_key: 'Указать ключ к API', com_endpoint_config_placeholder: 'Укажите ваш ключ к API в меню сверху для начала разговора.', com_endpoint_config_key_for: 'Установить ключ к API для', @@ -271,6 +275,12 @@ export default { com_nav_theme_system: 'Системная', com_nav_theme_dark: 'Темная', com_nav_theme_light: 'Светлая', + com_nav_language: 'Локализация', + com_nav_setting_account: 'Аккаунт', + com_nav_profile_picture: 'Изображение профиля', + com_nav_change_picture: 'Изменить изображение', + com_nav_lang_auto: 'Автоопределение', + com_nav_clear: 'Очистить', com_nav_clear_all_chats: 'Удалить все чаты', com_nav_confirm_clear: 'Подтвердить удаление', com_nav_close_sidebar: 'Закрыть боковую панель', From 431fc6284f24beb6f504e4240c3406a9503358d5 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Sat, 30 Dec 2023 14:34:32 -0500 Subject: [PATCH 13/42] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix:=20Minor=20Fi?= =?UTF-8?q?xes=20in=20`Message`,=20`Ask/EditController`,=20`OpenAIClient`,?= =?UTF-8?q?=20and=20`countTokens`=20(#1463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(Message): avoid overwriting unprovided properties * fix(OpenAIClient): return intermediateReply on user abort * fix(AskController): do not send/save final message if abort was triggered * fix(countTokens): avoid fetching remote registry and exclusively use cl100k_base or p50k_base weights for token counting * refactor(Message/messageSchema): rely on messageSchema for default values when saving messages * fix(EditController): do not send/save final message if abort was triggered * fix(config/helpers): fix module resolution error --- api/app/clients/OpenAIClient.js | 2 +- api/models/Message.js | 14 +++++++------- api/models/schema/messageSchema.js | 1 + api/server/controllers/AskController.js | 21 ++++++++++++--------- api/server/controllers/EditController.js | 20 +++++++++++--------- api/server/utils/countTokens.js | 7 +++---- config/helpers.js | 3 ++- 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index ce39311f3ce8..f0dbc366bba6 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -847,7 +847,7 @@ ${convo} err?.message?.includes('abort') || (err instanceof OpenAI.APIError && err?.message?.includes('abort')) ) { - return ''; + return intermediateReply; } if ( err?.message?.includes( diff --git a/api/models/Message.js b/api/models/Message.js index 7cb9bdc37766..7accf9285a8b 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -15,16 +15,16 @@ module.exports = { parentMessageId, sender, text, - isCreatedByUser = false, + isCreatedByUser, error, unfinished, files, - isEdited = false, - finish_reason = null, - tokenCount = null, - plugin = null, - plugins = null, - model = null, + isEdited, + finish_reason, + tokenCount, + plugin, + plugins, + model, }) { try { const validConvoId = idSchema.safeParse(conversationId); diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index bcc28cd23b65..33d799544b2d 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -21,6 +21,7 @@ const messageSchema = mongoose.Schema( }, model: { type: String, + default: null, }, conversationSignature: { type: String, diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index ffaa10938c38..78933feebc17 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -118,16 +118,19 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { delete userMessage.image_urls; } - sendMessage(res, { - title: await getConvoTitle(user, conversationId), - final: true, - conversation: await getConvo(user, conversationId), - requestMessage: userMessage, - responseMessage: response, - }); - res.end(); + if (!abortController.signal.aborted) { + sendMessage(res, { + title: await getConvoTitle(user, conversationId), + final: true, + conversation: await getConvo(user, conversationId), + requestMessage: userMessage, + responseMessage: response, + }); + res.end(); + + await saveMessage({ ...response, user }); + } - await saveMessage({ ...response, user }); await saveMessage(userMessage); if (addTitle && parentMessageId === '00000000-0000-0000-0000-000000000000' && newConvo) { diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js index ecc146126047..72ee58026a47 100644 --- a/api/server/controllers/EditController.js +++ b/api/server/controllers/EditController.js @@ -112,16 +112,18 @@ const EditController = async (req, res, next, initializeClient) => { response = { ...response, ...metadata }; } - await saveMessage({ ...response, user }); + if (!abortController.signal.aborted) { + sendMessage(res, { + title: await getConvoTitle(user, conversationId), + final: true, + conversation: await getConvo(user, conversationId), + requestMessage: userMessage, + responseMessage: response, + }); + res.end(); - sendMessage(res, { - title: await getConvoTitle(user, conversationId), - final: true, - conversation: await getConvo(user, conversationId), - requestMessage: userMessage, - responseMessage: response, - }); - res.end(); + await saveMessage({ ...response, user }); + } } catch (error) { const partialText = getPartialText(); handleAbortError(res, req, error, { diff --git a/api/server/utils/countTokens.js b/api/server/utils/countTokens.js index 9c8c98e76a7d..34c070aa8c28 100644 --- a/api/server/utils/countTokens.js +++ b/api/server/utils/countTokens.js @@ -1,13 +1,12 @@ -const { load } = require('tiktoken/load'); const { Tiktoken } = require('tiktoken/lite'); -const registry = require('tiktoken/registry.json'); -const models = require('tiktoken/model_to_encoding.json'); +const p50k_base = require('tiktoken/encoders/p50k_base.json'); +const cl100k_base = require('tiktoken/encoders/cl100k_base.json'); const logger = require('~/config/winston'); const countTokens = async (text = '', modelName = 'gpt-3.5-turbo') => { let encoder = null; try { - const model = await load(registry[models[modelName]]); + const model = modelName.includes('text-davinci-003') ? p50k_base : cl100k_base; encoder = new Tiktoken(model.bpe_ranks, model.special_tokens, model.pat_str); const tokens = encoder.encode(text); encoder.free(); diff --git a/config/helpers.js b/config/helpers.js index a86d562eb354..2b634612d4a9 100644 --- a/config/helpers.js +++ b/config/helpers.js @@ -6,7 +6,8 @@ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const { execSync } = require('child_process'); -const { connectDb } = require('@librechat/backend/lib/db'); +require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); +const connectDb = require('~/lib/db/connectDb'); const askQuestion = (query) => { const rl = readline.createInterface({ From 659ba4374bbe776e139a15864410747050bfc712 Mon Sep 17 00:00:00 2001 From: Fuegovic <32828263+fuegovic@users.noreply.github.com> Date: Sat, 30 Dec 2023 22:50:47 -0500 Subject: [PATCH 14/42] update pull_request_template.md (#1466) add "Translation update" the the PR type choices --- .github/pull_request_template.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 54c44782af6b..06d2656bd649 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -15,7 +15,8 @@ Please delete any irrelevant options. - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update -- [ ] Documentation update +- [ ] Documentation update +- [ ] Translation update ## Testing From 52142b47ec14c810dbe66c1a4e99e41d42d5e07c Mon Sep 17 00:00:00 2001 From: Fuegovic <32828263+fuegovic@users.noreply.github.com> Date: Mon, 1 Jan 2024 12:01:06 -0500 Subject: [PATCH 15/42] =?UTF-8?q?=F0=9F=8C=8E:=20Update=20French=20Transla?= =?UTF-8?q?tion=20(#1472)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌎: Update French Translation * 🌎: Update French Translation * 🌎: Update French Translation * 🌎: Update French Translation --- client/src/localization/languages/Fr.tsx | 49 +++++++++++++++--------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/client/src/localization/languages/Fr.tsx b/client/src/localization/languages/Fr.tsx index 7a1863d3923f..db1072e2aa97 100644 --- a/client/src/localization/languages/Fr.tsx +++ b/client/src/localization/languages/Fr.tsx @@ -28,15 +28,17 @@ export default { com_ui_prompt_templates: 'Modèles de prompt', com_ui_hide_prompt_templates: 'Masquer les modèles de prompt', com_ui_showing: 'Affichage', - com_ui_of: 'de', - com_ui_entries: 'Entrées', + com_ui_of: 'des', + com_ui_entries: 'entrées', com_ui_pay_per_call: 'Toutes les conversations IA au même endroit. Payez à la demande et non par mois', com_ui_new_footer: 'Toutes les conversations IA au même endroit.', com_ui_enter: 'Entrer', com_ui_submit: 'Soumettre', - com_ui_upload_success: 'Fichier téléchargé avec succès', - com_ui_upload_invalid: 'Fichier invalide pour le téléchargement', + com_ui_upload_success: 'Fichier téléversé avec succès', + com_ui_upload_error: 'Une erreur s\'est produite lors du téléversement de votre fichier', + com_ui_upload_invalid: + 'Fichier invalide pour le téléversement. Doit être une image ne dépassant pas 2 Mo', com_ui_cancel: 'Annuler', com_ui_save: 'Sauvegarder', com_ui_copy_to_clipboard: 'Copier dans le presse-papier', @@ -55,6 +57,9 @@ export default { com_ui_delete: 'Supprimer', com_ui_delete_conversation: 'Supprimer la discussions?', com_ui_delete_conversation_confirm: 'Cela supprimera', + com_ui_preview: 'Aperçu', + com_ui_upload: 'Téléverser', + com_ui_connect: 'Connecter', com_auth_error_login: 'Impossible de se connecter avec les informations fournies. Veuillez vérifier vos identifiants et réessayer.', com_auth_error_login_rl: @@ -63,7 +68,7 @@ export default { 'Votre compte a été temporairement banni en raison de violations de notre service.', com_auth_error_login_server: 'Une erreur interne du serveur s\'est produite. Veuillez patienter quelques instants et réessayer.', - com_auth_no_account: 'Vous n\'avez pas de compte ?', + com_auth_no_account: 'Vous n\'avez pas de compte?', com_auth_sign_up: 'S\'inscrire', com_auth_sign_in: 'Se connecter', com_auth_google_login: 'Se connecter avec Google', @@ -80,7 +85,7 @@ export default { com_auth_password_required: 'Le mot de passe est obligatoire', com_auth_password_min_length: 'Le mot de passe doit comporter au moins 8 caractères', com_auth_password_max_length: 'Le mot de passe doit être inférieur à 128 caractères', - com_auth_password_forgot: 'Mot de passe oublié ?', + com_auth_password_forgot: 'Mot de passe oublié?', com_auth_password_confirm: 'Confirmer le mot de passe', com_auth_password_not_match: 'Les mots de passe ne correspondent pas', com_auth_continue: 'Continuer', @@ -122,10 +127,10 @@ export default { com_endpoint_bing_context_placeholder: 'Bing peut utiliser jusqu\'à 7k jetons pour le "contexte", qu\'il peut référencer pour la conversation. La limite spécifique n\'est pas connue mais peut entraîner des erreurs dépassant les 7k jetons', com_endpoint_bing_system_message_placeholder: - 'AVERTISSEMENT : L\'abus de cette fonctionnalité peut vous faire BANNIR de l\'utilisation de Bing ! Cliquez sur "Message système" pour obtenir les instructions complètes et le message par défaut si omis, qui est le préréglage "Sydney" qui est considéré comme sûr.', + 'AVERTISSEMENT : L\'abus de cette fonctionnalité peut vous faire BANNIR de l\'utilisation de Bing! Cliquez sur "Message système" pour obtenir les instructions complètes et le message par défaut si omis, qui est le préréglage "Sydney" qui est considéré comme sûr.', com_endpoint_system_message: 'Message système', com_endpoint_message: 'Message', - com_endpoint_message_not_appendable: 'Editer votre message ou regenerer.', + com_endpoint_message_not_appendable: 'Editer votre message ou regénerer.', com_endpoint_default_blank: 'par défaut : vide', com_endpoint_default_false: 'par défaut : faux', com_endpoint_default_creative: 'par défaut : créatif', @@ -186,18 +191,21 @@ export default { com_endpoint_import: 'Importer', com_endpoint_set_custom_name: 'Définir un nom personnalisé, au cas où vous trouveriez ce préréglage', - com_endpoint_preset_delete_confirm: 'Êtes-vous sûr de vouloir supprimer ce préréglage ?', - com_endpoint_preset_clear_all_confirm: 'Êtes-vous sûr de vouloir supprimer tous vos préréglages ?', - com_endpoint_preset_import: 'Préréglage importé !', - com_endpoint_preset_import_error: 'Il y a eu une erreur lors de l\'importation de votre préréglage. Veuillez réessayer.', - com_endpoint_preset_save_error: 'Il y a eu une erreur lors de la sauvegarde de votre préréglage. Veuillez réessayer.', - com_endpoint_preset_delete_error: 'Il y a eu une erreur lors de la suppression de votre préréglage. Veuillez réessayer.', + com_endpoint_preset_delete_confirm: 'Êtes-vous sûr de vouloir supprimer ce préréglage?', + com_endpoint_preset_clear_all_confirm: 'Êtes-vous sûr de vouloir supprimer tous vos préréglages?', + com_endpoint_preset_import: 'Préréglage importé!', + com_endpoint_preset_import_error: + 'Il y a eu une erreur lors de l\'importation de votre préréglage. Veuillez réessayer.', + com_endpoint_preset_save_error: + 'Il y a eu une erreur lors de la sauvegarde de votre préréglage. Veuillez réessayer.', + com_endpoint_preset_delete_error: + 'Il y a eu une erreur lors de la suppression de votre préréglage. Veuillez réessayer.', com_endpoint_preset_default_removed: 'n\'est plus le préréglage par défaut.', com_endpoint_preset_default_item: 'Par défaut :', com_endpoint_preset_default_none: 'Aucun préréglage par défaut actif.', com_endpoint_preset_title: 'Préréglage', com_endpoint_preset_saved: 'Enregistré!', - com_endpoint_preset_default: 'est maintenant le préréglage par défaut.', + com_endpoint_preset_default: 'est maintenant le préréglage par défaut.', com_endpoint_preset: 'préréglage', com_endpoint_presets: 'préréglages', com_endpoint_preset_selected: 'Préréglage actif!', @@ -215,9 +223,10 @@ export default { com_endpoint_export: 'Exporter', com_endpoint_save_as_preset: 'Enregistrer comme préréglage', com_endpoint_presets_clear_warning: - 'Etes-vous sûr de vouloir effacer tous les préréglages ? Cette action est irréversible.', + 'Etes-vous sûr de vouloir effacer tous les préréglages? Cette action est irréversible.', com_endpoint_not_implemented: 'Non implémenté', - com_endpoint_no_presets: 'Aucun préréglage pour l\'instant, utilisez le bouton paramètres pour en créer un', + com_endpoint_no_presets: + 'Aucun préréglage pour l\'instant, utilisez le bouton paramètres pour en créer un', com_endpoint_not_available: 'Aucun endpoint disponible', com_endpoint_view_options: 'Voir les options', com_endpoint_save_convo_as_preset: 'Enregistrer la conversation comme préréglage', @@ -240,7 +249,8 @@ export default { com_endpoint_config_google_cloud_platform: '(de Google Cloud Platform)', com_endpoint_config_google_api_key: 'Clé API Google', com_endpoint_config_google_gemini_api: '(API Gemini)', - com_endpoint_config_google_api_info: 'Pour obtenir votre clé API de langage génératif (pour Gemini),', + com_endpoint_config_google_api_info: + 'Pour obtenir votre clé API de langage génératif (pour Gemini),', com_endpoint_config_key_import_json_key: 'Importez la clé JSON du compte de service.', com_endpoint_config_key_import_json_key_success: 'Clé JSON du compte de service importé avec succès', @@ -264,6 +274,8 @@ export default { 'Assurez-vous de cliquer \'Créer et continuer\' pour donner au moins le role \'Utilisateur de Vertex AI\'. Finalement, créez une clé JSON à importer ici.', com_nav_welcome_message: 'Comment puis-je vous aider aujourd\'hui?', com_nav_auto_scroll: 'Défilement automatique jusqu\'au plus récent à l\'ouverture', + com_nav_profile_picture: 'Photo de profil', + com_nav_change_picture: 'Changer de photo', com_nav_plugin_store: 'Boutique de plugins', com_nav_plugin_search: 'Rechercher des plugins', com_nav_plugin_auth_error: @@ -297,4 +309,5 @@ export default { com_nav_search_placeholder: 'Rechercher des messages', com_nav_setting_general: 'Général', com_nav_setting_data: 'Contrôles des données', + com_nav_setting_account: 'Compte', }; From 1cd5fdf4f088b076361076fb4187e5a46ab71e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Santos?= <140329135+itzraiss@users.noreply.github.com> Date: Mon, 1 Jan 2024 14:01:38 -0300 Subject: [PATCH 16/42] =?UTF-8?q?=F0=9F=8C=8E:=20Update=20Portuguese=20Tra?= =?UTF-8?q?nslation=20(#1461)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/localization/languages/Br.tsx | 185 +++++++++++++---------- 1 file changed, 103 insertions(+), 82 deletions(-) diff --git a/client/src/localization/languages/Br.tsx b/client/src/localization/languages/Br.tsx index 830b9e8fca99..67251e94216a 100644 --- a/client/src/localization/languages/Br.tsx +++ b/client/src/localization/languages/Br.tsx @@ -4,18 +4,16 @@ export default { com_ui_examples: 'Exemplos', com_ui_new_chat: 'Novo Chat', com_ui_example_quantum_computing: 'Explique a computação quântica em termos simples', - com_ui_example_10_year_old_b_day: - 'Tem alguma ideia criativa para o aniversário de uma criança de 10 anos?', - com_ui_example_http_in_js: 'Como fazer uma requisição HTTP em Javascript?', + com_ui_example_10_year_old_b_day: 'Tem alguma ideia criativa para o aniversário de uma criança de 10 anos?', + com_ui_example_http_in_js: 'Como faço uma solicitação HTTP em Javascript?', com_ui_capabilities: 'Capacidades', - com_ui_capability_remember: 'Lembra do que o usuário disse anteriormente na conversa', + com_ui_capability_remember: 'Lembra o que o usuário disse anteriormente na conversa', com_ui_capability_correction: 'Permite que o usuário forneça correções de acompanhamento', com_ui_capability_decline_requests: 'Treinado para recusar pedidos inadequados', com_ui_limitations: 'Limitações', com_ui_limitation_incorrect_info: 'Pode ocasionalmente gerar informações incorretas', - com_ui_limitation_harmful_biased: - 'Pode ocasionalmente produzir instruções prejudiciais ou conteúdo tendencioso', - com_ui_limitation_limited_2021: 'Conhecimento limitado do mundo e dos eventos após 2021', + com_ui_limitation_harmful_biased: 'Pode ocasionalmente produzir instruções prejudiciais ou conteúdo tendencioso', + com_ui_limitation_limited_2021: 'Conhecimento limitado do mundo e eventos após 2021', com_ui_input: 'Entrada', com_ui_close: 'Fechar', com_ui_model: 'Modelo', @@ -30,10 +28,12 @@ export default { com_ui_of: 'de', com_ui_entries: 'Entradas', com_ui_pay_per_call: 'Todas as conversas de IA em um só lugar. Pague por chamada e não por mês', + com_ui_new_footer: 'Todas as conversas de IA em um só lugar.', com_ui_enter: 'Entrar', com_ui_submit: 'Enviar', com_ui_upload_success: 'Arquivo carregado com sucesso', - com_ui_upload_invalid: 'Arquivo inválido para upload', + com_ui_upload_error: 'Houve um erro ao carregar seu arquivo', + com_ui_upload_invalid: 'Arquivo inválido para upload. Deve ser uma imagem que não exceda 2 MB', com_ui_cancel: 'Cancelar', com_ui_save: 'Salvar', com_ui_copy_to_clipboard: 'Copiar para a área de transferência', @@ -48,47 +48,47 @@ export default { com_ui_revoke_info: 'Revogar todas as credenciais fornecidas pelo usuário', com_ui_confirm_action: 'Confirmar Ação', com_ui_chats: 'chats', - com_ui_delete: 'Deletar', - com_ui_delete_conversation: 'Deletar chat?', - com_ui_delete_conversation_confirm: 'Isso irá deletar', - com_auth_error_login: - 'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.', - com_auth_error_login_rl: - 'Muitas tentativas de login em um curto espaço de tempo. Por favor, tente novamente mais tarde.', - com_auth_error_login_ban: 'Sua conta foi temporariamente banida por violações do nosso serviço.', - com_auth_error_login_server: - 'Houve um erro interno do servidor. Por favor, espere alguns momentos e tente novamente.', + com_ui_delete: 'Excluir', + com_ui_delete_conversation: 'Excluir chat?', + com_ui_delete_conversation_confirm: 'Isso irá excluir', + com_ui_preview: 'Visualizar', + com_ui_upload: 'Carregar', + com_ui_connect: 'Conectar', + com_auth_error_login: 'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.', + com_auth_error_login_rl: 'Muitas tentativas de login em um curto período de tempo. Por favor, tente novamente mais tarde.', + com_auth_error_login_ban: 'Sua conta foi temporariamente banida devido a violações de nosso serviço.', + com_auth_error_login_server: 'Houve um erro interno do servidor. Por favor, aguarde alguns momentos e tente novamente.', com_auth_no_account: 'Não tem uma conta?', com_auth_sign_up: 'Inscrever-se', com_auth_sign_in: 'Entrar', - com_auth_google_login: 'Entrar com Google', - com_auth_facebook_login: 'Entrar com Facebook', - com_auth_github_login: 'Entrar com Github', - com_auth_discord_login: 'Entrar com Discord', + com_auth_google_login: 'Login com Google', + com_auth_facebook_login: 'Login com Facebook', + com_auth_github_login: 'Login com Github', + com_auth_discord_login: 'Login com Discord', com_auth_email: 'Email', com_auth_email_required: 'Email é obrigatório', - com_auth_email_min_length: 'Email deve ter pelo menos 6 caracteres', - com_auth_email_max_length: 'Email não deve ser maior que 120 caracteres', - com_auth_email_pattern: 'Você deve digitar um endereço de email válido', + com_auth_email_min_length: 'O email deve ter pelo menos 6 caracteres', + com_auth_email_max_length: 'O email não deve ter mais de 120 caracteres', + com_auth_email_pattern: 'Você deve inserir um endereço de email válido', com_auth_email_address: 'Endereço de email', com_auth_password: 'Senha', com_auth_password_required: 'Senha é obrigatória', - com_auth_password_min_length: 'Senha deve ter pelo menos 8 caracteres', - com_auth_password_max_length: 'Senha deve ter menos de 128 caracteres', + com_auth_password_min_length: 'A senha deve ter pelo menos 8 caracteres', + com_auth_password_max_length: 'A senha deve ter menos de 128 caracteres', com_auth_password_forgot: 'Esqueceu a senha?', - com_auth_password_confirm: 'Confirmar senha', - com_auth_password_not_match: 'Senhas não coincidem', + com_auth_password_confirm: 'Confirme a senha', + com_auth_password_not_match: 'As senhas não coincidem', com_auth_continue: 'Continuar', com_auth_create_account: 'Crie sua conta', com_auth_error_create: 'Houve um erro ao tentar registrar sua conta. Por favor, tente novamente.', com_auth_full_name: 'Nome completo', com_auth_name_required: 'Nome é obrigatório', - com_auth_name_min_length: 'Nome deve ter pelo menos 3 caracteres', - com_auth_name_max_length: 'Nome deve ter menos de 80 caracteres', + com_auth_name_min_length: 'O nome deve ter pelo menos 3 caracteres', + com_auth_name_max_length: 'O nome deve ter menos de 80 caracteres', com_auth_username: 'Nome de usuário (opcional)', com_auth_username_required: 'Nome de usuário é obrigatório', - com_auth_username_min_length: 'Nome de usuário deve ter pelo menos 2 caracteres', - com_auth_username_max_length: 'Nome de usuário deve ter menos de 20 caracteres', + com_auth_username_min_length: 'O nome de usuário deve ter pelo menos 2 caracteres', + com_auth_username_max_length: 'O nome de usuário deve ter menos de 20 caracteres', com_auth_already_have_account: 'Já tem uma conta?', com_auth_login: 'Login', com_auth_reset_password: 'Redefina sua senha', @@ -96,11 +96,9 @@ export default { com_auth_here: 'AQUI', com_auth_to_reset_your_password: 'para redefinir sua senha.', com_auth_reset_password_link_sent: 'Email Enviado', - com_auth_reset_password_email_sent: - 'Um email foi enviado para você com mais instruções para redefinir sua senha.', - com_auth_error_reset_password: - 'Houve um problema ao redefinir sua senha. Não foi encontrado nenhum usuário com o endereço de email fornecido. Por favor, tente novamente.', - com_auth_reset_password_success: 'Redefinição de Senha com Sucesso', + com_auth_reset_password_email_sent: 'Um email foi enviado para você com mais instruções para redefinir sua senha.', + com_auth_error_reset_password: 'Houve um problema para redefinir sua senha. Não foi encontrado nenhum usuário com o endereço de email fornecido. Por favor, tente novamente.', + com_auth_reset_password_success: 'Redefinição de Senha Bem-sucedida', com_auth_login_with_new_password: 'Agora você pode fazer login com sua nova senha.', com_auth_error_invalid_reset_token: 'Este token de redefinição de senha não é mais válido.', com_auth_click_here: 'Clique aqui', @@ -108,14 +106,14 @@ export default { com_auth_submit_registration: 'Enviar registro', com_auth_welcome_back: 'Bem-vindo de volta', com_endpoint_open_menu: 'Abrir Menu', - com_endpoint_bing_enable_sydney: 'Ativar Sydney', - com_endpoint_bing_to_enable_sydney: 'Para ativar Sydney', + com_endpoint_bing_enable_sydney: 'Habilitar Sydney', + com_endpoint_bing_to_enable_sydney: 'Para habilitar Sydney', com_endpoint_bing_jailbreak: 'Jailbreak', - com_endpoint_bing_context_placeholder: - 'Bing pode usar até 7k tokens para "contexto", que ele pode referenciar para a conversa. O limite específico não é conhecido, mas pode gerar erros se exceder 7k tokens', - com_endpoint_bing_system_message_placeholder: - 'AVISO: O uso indevido deste recurso pode fazer com que você seja BANIDO de usar o Bing! Clique em "Mensagem do Sistema" para obter instruções completas e a mensagem padrão se omitida, que é o preset "Sydney" que é considerado seguro.', + com_endpoint_bing_context_placeholder: 'O Bing pode usar até 7k tokens para \'contexto\', que ele pode referenciar para a conversa. O limite específico não é conhecido, mas pode ocorrer erros ao exceder 7k tokens', + com_endpoint_bing_system_message_placeholder: 'AVISO: O uso indevido deste recurso pode fazer com que você seja BANIDO de usar o Bing! Clique em \'System Message\' para obter instruções completas e a mensagem padrão se omitida, que é o preset \'Sydney\' que é considerado seguro.', com_endpoint_system_message: 'Mensagem do Sistema', + com_endpoint_message: 'Mensagem', + com_endpoint_message_not_appendable: 'Edite sua mensagem ou Regenere.', com_endpoint_default_blank: 'padrão: em branco', com_endpoint_default_false: 'padrão: falso', com_endpoint_default_creative: 'padrão: criativo', @@ -125,112 +123,131 @@ export default { com_endpoint_tone_style: 'Estilo de Tom', com_endpoint_token_count: 'Contagem de Tokens', com_endpoint_output: 'Saída', - com_endpoint_google_temp: - 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', + com_endpoint_google_temp: 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', com_endpoint_google_topp: - 'Top-p muda como o modelo seleciona tokens para a saída. Tokens são selecionados dos mais K (veja o parâmetro topK) prováveis para os menos até que a soma de suas probabilidades seja igual ao valor de top-p.', + 'Top-p muda como o modelo seleciona tokens para saída. Os tokens são selecionados do mais K (veja o parâmetro topK) provável ao menos até que a soma de suas probabilidades seja igual ao valor de top-p.', com_endpoint_google_topk: - 'Top-k muda como o modelo seleciona tokens para a saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', + 'Top-k muda como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', com_endpoint_google_maxoutputtokens: 'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor menor para respostas mais curtas e um valor maior para respostas mais longas.', com_endpoint_google_custom_name_placeholder: 'Defina um nome personalizado para o Google', - com_endpoint_prompt_prefix_placeholder: - 'Defina instruções ou contexto personalizados. Ignorado se vazio.', + com_endpoint_prompt_prefix_placeholder: 'Defina instruções personalizadas ou contexto. Ignorado se vazio.', com_endpoint_custom_name: 'Nome Personalizado', com_endpoint_prompt_prefix: 'Prefixo do Prompt', com_endpoint_temperature: 'Temperatura', com_endpoint_default: 'padrão', com_endpoint_top_p: 'Top P', com_endpoint_top_k: 'Top K', - com_endpoint_max_output_tokens: 'Tokens Máximos de Saída', + com_endpoint_max_output_tokens: 'Max Output Tokens', com_endpoint_openai_temp: 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', com_endpoint_openai_max: 'Os tokens máximos para gerar. O comprimento total dos tokens de entrada e dos tokens gerados é limitado pelo comprimento do contexto do modelo.', com_endpoint_openai_topp: - 'Uma alternativa à amostragem com temperatura, chamada de amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então 0.1 significa que apenas os tokens que compõem os 10% de massa de probabilidade mais alta são considerados. Recomendamos alterar isso ou temperatura, mas não ambos.', + 'Uma alternativa para amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então 0.1 significa que apenas os tokens que compõem a massa de probabilidade dos 10% principais são considerados. Recomendamos alterar isso ou a temperatura, mas não ambos.', com_endpoint_openai_freq: 'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em sua frequência existente no texto até agora, diminuindo a probabilidade do modelo de repetir a mesma linha literalmente.', com_endpoint_openai_pres: 'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em se eles aparecem no texto até agora, aumentando a probabilidade do modelo de falar sobre novos tópicos.', - com_endpoint_openai_custom_name_placeholder: 'Defina um nome personalizado para o ChatGPT', + com_endpoint_openai_custom_name_placeholder: 'Defina um nome personalizado para ChatGPT', com_endpoint_openai_prompt_prefix_placeholder: 'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhum', com_endpoint_anthropic_temp: 'Varia de 0 a 1. Use temp mais próximo de 0 para analítico / múltipla escolha, e mais próximo de 1 para tarefas criativas e gerativas. Recomendamos alterar isso ou Top P, mas não ambos.', com_endpoint_anthropic_topp: - 'Top-p muda como o modelo seleciona tokens para a saída. Tokens são selecionados dos mais K (veja o parâmetro topK) prováveis para os menos até que a soma de suas probabilidades seja igual ao valor de top-p.', + 'Top-p muda como o modelo seleciona tokens para saída. Os tokens são selecionados do mais K (veja o parâmetro topK) provável ao menos até que a soma de suas probabilidades seja igual ao valor de top-p.', com_endpoint_anthropic_topk: - 'Top-k muda como o modelo seleciona tokens para a saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', + 'Top-k muda como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', com_endpoint_anthropic_maxoutputtokens: 'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor menor para respostas mais curtas e um valor maior para respostas mais longas.', - com_endpoint_anthropic_custom_name_placeholder: 'Defina um nome personalizado para o Anthropic', + com_endpoint_anthropic_custom_name_placeholder: 'Defina um nome personalizado para Anthropic', com_endpoint_frequency_penalty: 'Penalidade de Frequência', com_endpoint_presence_penalty: 'Penalidade de Presença', - com_endpoint_plug_use_functions: 'Usar Funções', + com_endpoint_plug_use_functions: 'Use Funções', com_endpoint_plug_skip_completion: 'Pular Conclusão', com_endpoint_disabled_with_tools: 'desativado com ferramentas', com_endpoint_disabled_with_tools_placeholder: 'Desativado com Ferramentas Selecionadas', com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: 'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhum', com_endpoint_import: 'Importar', - com_endpoint_set_custom_name: - 'Defina um nome personalizado, caso você possa encontrar este preset', + com_endpoint_set_custom_name: 'Defina um nome personalizado, caso você possa encontrar este preset', + com_endpoint_preset_delete_confirm: 'Você tem certeza de que deseja excluir este preset?', + com_endpoint_preset_clear_all_confirm: 'Você tem certeza de que deseja excluir todos os seus presets?', + com_endpoint_preset_import: 'Preset Importado!', + com_endpoint_preset_import_error: 'Houve um erro ao importar seu preset. Por favor, tente novamente.', + com_endpoint_preset_save_error: 'Houve um erro ao salvar seu preset. Por favor, tente novamente.', + com_endpoint_preset_delete_error: 'Houve um erro ao excluir seu preset. Por favor, tente novamente.', + com_endpoint_preset_default_removed: 'não é mais o preset padrão.', + com_endpoint_preset_default_item: 'Padrão:', + com_endpoint_preset_default_none: 'Nenhum preset padrão ativo.', + com_endpoint_preset_title: 'Preset', + com_endpoint_preset_saved: 'Salvo!', + com_endpoint_preset_default: 'é agora o preset padrão.', com_endpoint_preset: 'preset', com_endpoint_presets: 'presets', + com_endpoint_preset_selected: 'Preset Ativo!', + com_endpoint_preset_selected_title: 'Ativo!', com_endpoint_preset_name: 'Nome do Preset', com_endpoint_new_topic: 'Novo Tópico', com_endpoint: 'Endpoint', - com_endpoint_hide: 'Ocultar', + com_endpoint_hide: 'Esconder', com_endpoint_show: 'Mostrar', com_endpoint_examples: ' Presets', com_endpoint_completion: 'Conclusão', com_endpoint_agent: 'Agente', - com_endpoint_show_what_settings: 'Mostrar Configurações de {0}', + com_endpoint_show_what_settings: 'Mostrar {0} Configurações', com_endpoint_save: 'Salvar', com_endpoint_export: 'Exportar', com_endpoint_save_as_preset: 'Salvar como Preset', com_endpoint_presets_clear_warning: - 'Tem certeza de que deseja limpar todos os presets? Isso é irreversível.', + 'Você tem certeza de que deseja limpar todos os presets? Isso é irreversível.', com_endpoint_not_implemented: 'Não implementado', com_endpoint_no_presets: 'Ainda não há presets, use o botão de configurações para criar um', com_endpoint_not_available: 'Nenhum endpoint disponível', com_endpoint_view_options: 'Ver Opções', com_endpoint_save_convo_as_preset: 'Salvar Conversa como Preset', com_endpoint_my_preset: 'Meu Preset', - com_endpoint_agent_model: 'Modelo de Agente (Recomendado: GPT-3.5)', + com_endpoint_agent_model: 'Modelo do Agente (Recomendado: GPT-3.5)', com_endpoint_completion_model: 'Modelo de Conclusão (Recomendado: GPT-4)', - com_endpoint_func_hover: 'Ativar o uso de Plugins como Funções OpenAI', + com_endpoint_func_hover: 'Habilitar uso de Plugins como Funções OpenAI', com_endpoint_skip_hover: - 'Ativar a omissão da etapa de conclusão, que revisa a resposta final e as etapas geradas', - com_endpoint_config_key: 'Definir Chave de API', - com_endpoint_config_key_for: 'Definir Chave de API para', + 'Habilitar a etapa de conclusão de pulo, que revisa a resposta final e as etapas geradas', + com_endpoint_config_key: 'Definir Chave API', + com_endpoint_config_placeholder: 'Defina sua Chave no menu Cabeçalho para conversar.', + com_endpoint_config_key_for: 'Definir Chave API para', com_endpoint_config_key_name: 'Chave', - com_endpoint_config_value: 'Digite o valor para', - com_endpoint_config_key_name_placeholder: 'Defina a chave de API primeiro', + com_endpoint_config_value: 'Insira valor para', + com_endpoint_config_key_name_placeholder: 'Defina a chave API primeiro', com_endpoint_config_key_encryption: 'Sua chave será criptografada e excluída em', com_endpoint_config_key_expiry: 'o tempo de expiração', + com_endpoint_config_click_here: 'Clique Aqui', + com_endpoint_config_google_service_key: 'Chave da Conta de Serviço do Google', + com_endpoint_config_google_cloud_platform: '(do Google Cloud Platform)', + com_endpoint_config_google_api_key: 'Chave API do Google', + com_endpoint_config_google_gemini_api: '(API Gemini)', + com_endpoint_config_google_api_info: 'Para obter sua chave API de Linguagem Generativa (para Gemini),', com_endpoint_config_key_import_json_key: 'Importar Chave JSON da Conta de Serviço.', - com_endpoint_config_key_import_json_key_success: - 'Chave JSON da Conta de Serviço Importada com Sucesso', + com_endpoint_config_key_import_json_key_success: 'Chave JSON da Conta de Serviço importada com sucesso', com_endpoint_config_key_import_json_key_invalid: - 'Chave JSON da Conta de Serviço Inválida, Você importou o arquivo correto?', + 'Chave JSON da Conta de Serviço inválida, você importou o arquivo correto?', com_endpoint_config_key_get_edge_key: 'Para obter seu token de acesso para o Bing, faça login em', com_endpoint_config_key_get_edge_key_dev_tool: 'Use as ferramentas de desenvolvimento ou uma extensão enquanto estiver logado no site para copiar o conteúdo do cookie _U. Se isso falhar, siga estas', com_endpoint_config_key_edge_instructions: 'instruções', - com_endpoint_config_key_edge_full_key_string: 'para fornecer as strings completas dos cookies.', - com_endpoint_config_key_chatgpt: - 'Para obter seu token de acesso para o ChatGPT "Versão Gratuita", faça login em', - com_endpoint_config_key_chatgpt_then_visit: 'depois visite', + com_endpoint_config_key_edge_full_key_string: 'para fornecer as strings completas do cookie.', + com_endpoint_config_key_chatgpt: 'Para obter seu token de acesso para o ChatGPT \'Versão Gratuita\', faça login em', + com_endpoint_config_key_chatgpt_then_visit: 'então visite', com_endpoint_config_key_chatgpt_copy_token: 'Copie o token de acesso.', com_endpoint_config_key_google_need_to: 'Você precisa', - com_endpoint_config_key_google_vertex_ai: 'Ativar o Vertex AI', - com_endpoint_config_key_google_vertex_api: 'API no Google Cloud, depois', + com_endpoint_config_key_google_vertex_ai: 'Habilitar Vertex AI', + com_endpoint_config_key_google_vertex_api: 'API no Google Cloud, então', + com_endpoint_config_key_google_service_account: 'Crie uma Conta de Serviço', com_endpoint_config_key_google_vertex_api_role: - 'Certifique-se de clicar em "Criar e Continuar" para dar pelo menos a função de "Usuário do Vertex AI". Por último, crie uma chave JSON para importar aqui.', - com_nav_welcome_message: 'Como eu posso ajudar hoje?', - com_nav_auto_scroll: 'Auto-rolar para o mais recente ao abrir', + 'Certifique-se de clicar em \'Criar e Continuar\' para dar pelo menos a função \'Usuário do Vertex AI\'. Por último, crie uma chave JSON para importar aqui.', + com_nav_welcome_message: 'Como posso ajudá-lo hoje?', + com_nav_auto_scroll: 'Auto-rolagem para o Mais Novo ao Abrir', + com_nav_profile_picture: 'Foto de Perfil', + com_nav_change_picture: 'Mudar foto', com_nav_plugin_store: 'Loja de plugins', com_nav_plugin_search: 'Pesquisar plugins', com_nav_plugin_auth_error: @@ -259,9 +276,13 @@ export default { com_nav_clear_conversation: 'Limpar conversas', com_nav_clear_conversation_confirm_message: 'Tem certeza de que deseja limpar todas as conversas? Isso é irreversível.', - com_nav_help_faq: 'Ajuda e FAQ', + com_nav_help_faq: 'Ajuda & FAQ', com_nav_settings: 'Configurações', com_nav_search_placeholder: 'Pesquisar mensagens', com_nav_setting_general: 'Geral', com_nav_setting_data: 'Controles de dados', + com_nav_setting_account: 'Conta', + com_nav_language: 'Idioma', + com_nav_lang_auto: 'Detecção automática', + com_nav_lang_brazilian_portuguese: 'Português Brasileiro', }; From c7306395e94421503cd0d30b0f7935f4cc67d134 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:08:02 +0100 Subject: [PATCH 17/42] =?UTF-8?q?=F0=9F=91=AEfeat:=20moderation=20text=20(?= =?UTF-8?q?#1388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed some bugs and handling errors better * feat: plugins support * fix: prettier error message * moved circular-json-es6 in /api * docs: added openai moderation text * fix(gptPlugins): incorrect merge * discarding changes * removed circular-json-es6 --- .env.example | 4 ++ api/server/middleware/index.js | 2 + api/server/middleware/moderateText.js | 39 +++++++++++++++++++ api/server/routes/ask/gptPlugins.js | 2 + api/server/routes/ask/openAI.js | 3 +- api/server/routes/edit/gptPlugins.js | 2 + api/server/routes/edit/openAI.js | 3 +- .../src/components/Messages/Content/Error.tsx | 2 + docs/features/mod_system.md | 27 ++++++++++++- 9 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 api/server/middleware/moderateText.js diff --git a/.env.example b/.env.example index 0c9d303d6ccf..22b7743e0d07 100644 --- a/.env.example +++ b/.env.example @@ -188,6 +188,10 @@ MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt # Moderation # #========================# +OPENAI_MODERATION=false +OPENAI_MODERATION_API_KEY= +# OPENAI_MODERATION_REVERSE_PROXY=not working with some reverse proxys + BAN_VIOLATIONS=true BAN_DURATION=1000 * 60 * 60 * 2 BAN_INTERVAL=20 diff --git a/api/server/middleware/index.js b/api/server/middleware/index.js index 0d878d4c9026..77afd9716509 100644 --- a/api/server/middleware/index.js +++ b/api/server/middleware/index.js @@ -12,6 +12,7 @@ const concurrentLimiter = require('./concurrentLimiter'); const validateMessageReq = require('./validateMessageReq'); const buildEndpointOption = require('./buildEndpointOption'); const validateRegistration = require('./validateRegistration'); +const moderateText = require('./moderateText'); const noIndex = require('./noIndex'); module.exports = { @@ -29,5 +30,6 @@ module.exports = { validateMessageReq, buildEndpointOption, validateRegistration, + moderateText, noIndex, }; diff --git a/api/server/middleware/moderateText.js b/api/server/middleware/moderateText.js new file mode 100644 index 000000000000..c4bfd8a13aee --- /dev/null +++ b/api/server/middleware/moderateText.js @@ -0,0 +1,39 @@ +const axios = require('axios'); +const denyRequest = require('./denyRequest'); + +async function moderateText(req, res, next) { + if (process.env.OPENAI_MODERATION === 'true') { + try { + const { text } = req.body; + + const response = await axios.post( + process.env.OPENAI_MODERATION_REVERSE_PROXY || 'https://api.openai.com/v1/moderations', + { + input: text, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.OPENAI_MODERATION_API_KEY}`, + }, + }, + ); + + const results = response.data.results; + const flagged = results.some((result) => result.flagged); + + if (flagged) { + const type = 'moderation'; + const errorMessage = { type }; + return await denyRequest(req, res, errorMessage); + } + } catch (error) { + console.error('Error in moderateText:', error); + const errorMessage = 'error in moderation check'; + return await denyRequest(req, res, errorMessage); + } + } + next(); +} + +module.exports = moderateText; diff --git a/api/server/routes/ask/gptPlugins.js b/api/server/routes/ask/gptPlugins.js index bab60ee23a73..85616cd1b319 100644 --- a/api/server/routes/ask/gptPlugins.js +++ b/api/server/routes/ask/gptPlugins.js @@ -13,9 +13,11 @@ const { setHeaders, validateEndpoint, buildEndpointOption, + moderateText, } = require('~/server/middleware'); const { logger } = require('~/config'); +router.use(moderateText); router.post('/abort', handleAbort()); router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, res) => { diff --git a/api/server/routes/ask/openAI.js b/api/server/routes/ask/openAI.js index 180ee27f299b..31b3111077fa 100644 --- a/api/server/routes/ask/openAI.js +++ b/api/server/routes/ask/openAI.js @@ -6,10 +6,11 @@ const { setHeaders, validateEndpoint, buildEndpointOption, + moderateText, } = require('~/server/middleware'); const router = express.Router(); - +router.use(moderateText); router.post('/abort', handleAbort()); router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, res, next) => { diff --git a/api/server/routes/edit/gptPlugins.js b/api/server/routes/edit/gptPlugins.js index cd61b53237ce..8ddf92c25079 100644 --- a/api/server/routes/edit/gptPlugins.js +++ b/api/server/routes/edit/gptPlugins.js @@ -12,9 +12,11 @@ const { setHeaders, validateEndpoint, buildEndpointOption, + moderateText, } = require('~/server/middleware'); const { logger } = require('~/config'); +router.use(moderateText); router.post('/abort', handleAbort()); router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, res) => { diff --git a/api/server/routes/edit/openAI.js b/api/server/routes/edit/openAI.js index 47f36d6cb4d9..e54881148dc6 100644 --- a/api/server/routes/edit/openAI.js +++ b/api/server/routes/edit/openAI.js @@ -6,10 +6,11 @@ const { setHeaders, validateEndpoint, buildEndpointOption, + moderateText, } = require('~/server/middleware'); const router = express.Router(); - +router.use(moderateText); router.post('/abort', handleAbort()); router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, res, next) => { diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index 7876bce48c22..bf2f2b9a241d 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -29,6 +29,8 @@ const errorMessages = { 'Invalid API key. Please check your API key and try again. You can do this by clicking on the model logo in the left corner of the textbox and selecting "Set Token" for the current selected endpoint. Thank you for your understanding.', insufficient_quota: 'We apologize for any inconvenience caused. The default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the left corner of the textbox and selecting "Set Token" for the current selected endpoint. Thank you for your understanding.', + moderation: + 'It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We\'re unable to proceed with this specific topic. If you have any other questions or topics you\'d like to explore, please edit your message, or create a new conversation.', concurrent: (json: TConcurrent) => { const { limit } = json; const plural = limit > 1 ? 's' : ''; diff --git a/docs/features/mod_system.md b/docs/features/mod_system.md index 2775f879c3cb..099c5cb3a45b 100644 --- a/docs/features/mod_system.md +++ b/docs/features/mod_system.md @@ -69,4 +69,29 @@ MESSAGE_IP_WINDOW=1 # in minutes, determines the window of time for MESSAGE_IP_M LIMIT_MESSAGE_USER=false # Whether to limit the amount of messages an IP can send per MESSAGE_USER_WINDOW MESSAGE_USER_MAX=40 # The max amount of messages an IP can send per MESSAGE_USER_WINDOW MESSAGE_USER_WINDOW=1 # in minutes, determines the window of time for MESSAGE_USER_MAX messages -``` \ No newline at end of file +``` + +## OpenAI moderation text + +### OPENAI_MODERATION +enable or disable OpenAI moderation + +Values: +`true`: OpenAI moderation is enabled +`false`: OpenAI moderation is disabled + +### OPENAI_MODERATION_API_KEY +Specify your OpenAI moderation API key here + +### OPENAI_MODERATION_REVERSE_PROXY +enable or disable reverse proxy compatibility for OpenAI moderation. Note that it may not work with some reverse proxies + +Values: +`true`: Enable reverse proxy compatibility +`false`: Disable reverse proxy compatibility + +```bash +OPENAI_MODERATION=true +OPENAI_MODERATION_API_KEY=sk-1234 +# OPENAI_MODERATION_REVERSE_PROXY=false +``` From 2b3fa327a38f09ea60dc3deaed2486dd5e863303 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Tue, 2 Jan 2024 08:40:26 -0500 Subject: [PATCH 18/42] fix(OpenAIClient): do not invoke abortCompletion on completion error (#1473) --- api/app/clients/OpenAIClient.js | 1 - 1 file changed, 1 deletion(-) diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index f0dbc366bba6..605646a4b6ef 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -859,7 +859,6 @@ ${convo} (err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason')) ) { logger.error('[OpenAIClient] Known OpenAI error:', err); - await abortController.abortCompletion(); return intermediateReply; } else if (err instanceof OpenAI.APIError) { if (intermediateReply) { From 3f98f92d4c598faa1fd7b2d63a40b8d31aa7a787 Mon Sep 17 00:00:00 2001 From: Fuegovic <32828263+fuegovic@users.noreply.github.com> Date: Wed, 3 Jan 2024 08:59:55 -0500 Subject: [PATCH 19/42] remove 'MEILI_HTTP_ADDR' (#1475) --- .devcontainer/docker-compose.yml | 1 - .env.example | 1 - deploy-compose.yml | 2 -- docker-compose.yml | 2 -- docs/deployment/hetzner_ubuntu.md | 1 - docs/dev/deploy-compose.yml | 2 -- docs/install/configuration/dotenv.md | 6 ------ docs/install/installation/container_install.md | 1 - docs/install/installation/docker_compose_install.md | 2 -- 9 files changed, 18 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index fd2a759b99b6..c67fca63019a 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -57,7 +57,6 @@ services: # ports: # - 7700:7700 # if exposing these ports, make sure your master key is not the default value environment: - - MEILI_HTTP_ADDR=meilisearch:7700 - MEILI_NO_ANALYTICS=true - MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63 volumes: diff --git a/.env.example b/.env.example index 22b7743e0d07..4c3900d9c1fd 100644 --- a/.env.example +++ b/.env.example @@ -177,7 +177,6 @@ ZAPIER_NLA_API_KEY= SEARCH=true MEILI_NO_ANALYTICS=true MEILI_HOST=http://0.0.0.0:7700 -MEILI_HTTP_ADDR=0.0.0.0:7700 MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt #===================================================# diff --git a/deploy-compose.yml b/deploy-compose.yml index dcad325e6f14..e811578bdee9 100644 --- a/deploy-compose.yml +++ b/deploy-compose.yml @@ -21,7 +21,6 @@ services: - NODE_ENV=production - MONGO_URI=mongodb://mongodb:27017/LibreChat - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 volumes: - ./images:/app/client/public/images client: @@ -56,7 +55,6 @@ services: - .env environment: - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 - MEILI_NO_ANALYTICS=true volumes: - ./meili_data:/meili_data diff --git a/docker-compose.yml b/docker-compose.yml index 5f7fb0b77f25..1c5c9f951f31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,6 @@ services: - HOST=0.0.0.0 - MONGO_URI=mongodb://mongodb:27017/LibreChat - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 volumes: - /app/client/node_modules - /app/api/node_modules @@ -50,7 +49,6 @@ services: user: "${UID}:${GID}" environment: - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 - MEILI_NO_ANALYTICS=true volumes: - ./meili_data_v1.5:/meili_data diff --git a/docs/deployment/hetzner_ubuntu.md b/docs/deployment/hetzner_ubuntu.md index ba851ef66585..286a82cdc5ca 100644 --- a/docs/deployment/hetzner_ubuntu.md +++ b/docs/deployment/hetzner_ubuntu.md @@ -125,7 +125,6 @@ HOST= ``` SEARCH=true MEILI_HOST=meilisearch -MEILI_HTTP_ADDR=meilisearch ``` ### 5. After everything file has been updated, run `docker-compose build` then `docker-compose up` diff --git a/docs/dev/deploy-compose.yml b/docs/dev/deploy-compose.yml index 0971d8368fd8..6db6f9bb6a62 100644 --- a/docs/dev/deploy-compose.yml +++ b/docs/dev/deploy-compose.yml @@ -16,7 +16,6 @@ services: - HOST=0.0.0.0 - MONGO_URI=mongodb://mongodb:27017/LibreChat - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 client: image: client ports: @@ -43,7 +42,6 @@ services: - .env environment: - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 - MEILI_NO_ANALYTICS=true volumes: - ./meili_data:/meili_data diff --git a/docs/install/configuration/dotenv.md b/docs/install/configuration/dotenv.md index 5eda7ba3f7ba..5df5f8b88391 100644 --- a/docs/install/configuration/dotenv.md +++ b/docs/install/configuration/dotenv.md @@ -467,12 +467,6 @@ For the API server to connect to the search server. Replace '0.0.0.0' with 'meil MEILI_HOST=http://0.0.0.0:7700 ``` -MeiliSearch HTTP Address, mainly for docker-compose to expose the search server. Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose. - -```bash -MEILI_HTTP_ADDR=0.0.0.0:7700 -``` - This master key must be at least 16 bytes, composed of valid UTF-8 characters. MeiliSearch will throw an error and refuse to launch if no master key is provided or if it is under 16 bytes. MeiliSearch will suggest a secure autogenerated master key. This is a ready made secure key for docker-compose, you can replace it with your own. ```bash diff --git a/docs/install/installation/container_install.md b/docs/install/installation/container_install.md index 5f47f1e44cd3..fc7cb367bc0c 100644 --- a/docs/install/installation/container_install.md +++ b/docs/install/installation/container_install.md @@ -55,7 +55,6 @@ Follow [this guide](../configuration/ai_setup.md) to populate the containers wit HOST=0.0.0.0 MONGO_URI=mongodb://librechat-mongodb:27017/LibreChat MEILI_HOST=http://librechat-meilisearch:7700 -MEILI_HTTP_ADDR=librechat-meilisearch:7700 MEILI_NO_ANALYTICS=true ``` diff --git a/docs/install/installation/docker_compose_install.md b/docs/install/installation/docker_compose_install.md index c0f9864839da..d42f38799fe2 100644 --- a/docs/install/installation/docker_compose_install.md +++ b/docs/install/installation/docker_compose_install.md @@ -107,13 +107,11 @@ For more info see: - MONGO_URI=mongodb://mongodb:27017/LibreChat # ... - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 # ... env_file: - .env environment: - MEILI_HOST=http://meilisearch:7700 - - MEILI_HTTP_ADDR=meilisearch:7700 ``` - If for some reason you're not able to build the app image, you can pull the latest image from **Dockerhub**. From 29473a72db9cb5f40c113252ddac5bcfab565e6f Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 3 Jan 2024 09:22:48 -0500 Subject: [PATCH 20/42] =?UTF-8?q?=F0=9F=92=AB=20feat:=20Config=20File=20&?= =?UTF-8?q?=20Custom=20Endpoints=20(#1474)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP(backend/api): custom endpoint * WIP(frontend/client): custom endpoint * chore: adjust typedefs for configs * refactor: use data-provider for cache keys and rename enums and custom endpoint for better clarity and compatibility * feat: loadYaml utility * refactor: rename back to from and proof-of-concept for creating schemas from user-defined defaults * refactor: remove custom endpoint from default endpointsConfig as it will be exclusively managed by yaml config * refactor(EndpointController): rename variables for clarity * feat: initial load custom config * feat(server/utils): add simple `isUserProvided` helper * chore(types): update TConfig type * refactor: remove custom endpoint handling from model services as will be handled by config, modularize fetching of models * feat: loadCustomConfig, loadConfigEndpoints, loadConfigModels * chore: reorganize server init imports, invoke loadCustomConfig * refactor(loadConfigEndpoints/Models): return each custom endpoint as standalone endpoint * refactor(Endpoint/ModelController): spread config values after default (temporary) * chore(client): fix type issues * WIP: first pass for multiple custom endpoints - add endpointType to Conversation schema - add update zod schemas for both convo/presets to allow non-EModelEndpoint value as endpoint (also using type assertion) - use `endpointType` value as `endpoint` where mapping to type is necessary using this field - use custom defined `endpoint` value and not type for mapping to modelsConfig - misc: add return type to `getDefaultEndpoint` - in `useNewConvo`, add the endpointType if it wasn't already added to conversation - EndpointsMenu: use user-defined endpoint name as Title in menu - TODO: custom icon via custom config, change unknown to robot icon * refactor(parseConvo): pass args as an object and change where used accordingly; chore: comment out 'create schema' code * chore: remove unused availableModels field in TConfig type * refactor(parseCompactConvo): pass args as an object and change where used accordingly * feat: chat through custom endpoint * chore(message/convoSchemas): avoid saving empty arrays * fix(BaseClient/saveMessageToDatabase): save endpointType * refactor(ChatRoute): show Spinner if endpointsQuery or modelsQuery are still loading, which is apparent with slow fetching of models/remote config on first serve * fix(useConversation): assign endpointType if it's missing * fix(SaveAsPreset): pass real endpoint and endpointType when saving Preset) * chore: recorganize types order for TConfig, add `iconURL` * feat: custom endpoint icon support: - use UnknownIcon in all icon contexts - add mistral and openrouter as known endpoints, and add their icons - iconURL support * fix(presetSchema): move endpointType to default schema definitions shared between convoSchema and defaults * refactor(Settings/OpenAI): remove legacy `isOpenAI` flag * fix(OpenAIClient): do not invoke abortCompletion on completion error * feat: add responseSender/label support for custom endpoints: - use defaultModelLabel field in endpointOption - add model defaults for custom endpoints in `getResponseSender` - add `useGetSender` hook which uses EndpointsQuery to determine `defaultModelLabel` - include defaultModelLabel from endpointConfig in custom endpoint client options - pass `endpointType` to `getResponseSender` * feat(OpenAIClient): use custom options from config file * refactor: rename `defaultModelLabel` to `modelDisplayLabel` * refactor(data-provider): separate concerns from `schemas` into `parsers`, `config`, and fix imports elsewhere * feat: `iconURL` and extract environment variables from custom endpoint config values * feat: custom config validation via zod schema, rename and move to `./projectRoot/librechat.yaml` * docs: custom config docs and examples * fix(OpenAIClient/mistral): mistral does not allow singular system message, also add `useChatCompletion` flag to use openai-node for title completions * fix(custom/initializeClient): extract env var and use `isUserProvided` function * Update librechat.example.yaml * feat(InputWithLabel): add className props, and forwardRef * fix(streamResponse): handle error edge case where either messages or convos query throws an error * fix(useSSE): handle errorHandler edge cases where error response is and is not properly formatted from API, especially when a conversationId is not yet provided, which ensures stream is properly closed on error * feat: user_provided keys for custom endpoints * fix(config/endpointSchema): do not allow default endpoint values in custom endpoint `name` * feat(loadConfigModels): extract env variables and optimize fetching models * feat: support custom endpoint iconURL for messages and Nav * feat(OpenAIClient): add/dropParams support * docs: update docs with default params, add/dropParams, and notes to use config file instead of `OPENAI_REVERSE_PROXY` * docs: update docs with additional notes * feat(maxTokensMap): add mistral models (32k context) * docs: update openrouter notes * Update ai_setup.md * docs(custom_config): add table of contents and fix note about custom name * docs(custom_config): reorder ToC * Update custom_config.md * Add note about `max_tokens` field in custom_config.md --- .gitignore | 3 + api/app/clients/BaseClient.js | 1 + api/app/clients/OpenAIClient.js | 121 +++++-- api/cache/getCustomConfig.js | 23 ++ api/cache/getLogStores.js | 31 +- api/common/enums.js | 17 - api/models/schema/convoSchema.js | 9 +- api/models/schema/defaults.js | 4 +- api/models/schema/messageSchema.js | 30 +- api/server/controllers/AskController.js | 7 +- api/server/controllers/EditController.js | 7 +- api/server/controllers/EndpointController.js | 23 +- api/server/controllers/ModelController.js | 18 +- api/server/controllers/OverrideController.js | 8 +- api/server/controllers/PluginController.js | 4 +- api/server/index.js | 10 +- api/server/middleware/buildEndpointOption.js | 12 +- api/server/middleware/validateEndpoint.js | 3 +- api/server/routes/ask/custom.js | 20 ++ api/server/routes/ask/index.js | 2 + api/server/routes/edit/custom.js | 20 ++ api/server/routes/edit/index.js | 2 + api/server/services/Config/index.js | 6 + .../services/Config/loadConfigEndpoints.js | 54 +++ .../services/Config/loadConfigModels.js | 79 +++++ .../services/Config/loadCustomConfig.js | 41 +++ .../services/Endpoints/custom/buildOptions.js | 16 + api/server/services/Endpoints/custom/index.js | 7 + .../Endpoints/custom/initializeClient.js | 79 +++++ .../services/Endpoints/openAI/addTitle.js | 4 + api/server/services/ModelService.js | 73 ++-- api/server/utils/handleText.js | 23 ++ api/server/utils/handleText.spec.js | 49 ++- api/server/utils/streamResponse.js | 15 +- api/typedefs.js | 13 +- api/utils/index.js | 2 + api/utils/loadYaml.js | 13 + api/utils/tokens.js | 32 +- client/public/assets/mistral.png | Bin 0 -> 548 bytes client/public/assets/openrouter.png | Bin 0 -> 15406 bytes client/src/common/types.ts | 15 +- client/src/components/Chat/Input/ChatForm.tsx | 6 +- .../components/Chat/Input/HeaderOptions.tsx | 9 +- .../src/components/Chat/Input/OptionsBar.tsx | 30 +- .../components/Chat/Input/PopoverButtons.tsx | 3 +- client/src/components/Chat/Landing.tsx | 13 +- .../components/Chat/Menus/Endpoints/Icons.tsx | 5 +- .../Chat/Menus/Endpoints/MenuItem.tsx | 26 +- .../Chat/Menus/Endpoints/UnknownIcon.tsx | 36 ++ .../components/Chat/Menus/EndpointsMenu.tsx | 2 +- .../Chat/Menus/Presets/EditPresetDialog.tsx | 1 - .../Chat/Menus/Presets/PresetItems.tsx | 11 +- .../src/components/Chat/Menus/PresetsMenu.tsx | 4 +- .../Chat/Messages/Content/EditMessage.tsx | 3 +- .../components/Chat/Messages/HoverButtons.tsx | 7 +- client/src/components/Conversations/Convo.tsx | 10 +- .../components/Endpoints/EndpointSettings.tsx | 5 +- client/src/components/Endpoints/Icon.tsx | 21 +- .../src/components/Endpoints/MinimalIcon.tsx | 18 +- .../components/Endpoints/Settings/OpenAI.tsx | 89 +++-- .../components/Endpoints/Settings/settings.ts | 9 +- .../Input/ModelSelect/ModelSelect.tsx | 6 +- .../components/Input/ModelSelect/options.ts | 1 + .../Input/SetKeyDialog/CustomEndpoint.tsx | 46 +++ .../Input/SetKeyDialog/InputWithLabel.tsx | 20 +- .../Input/SetKeyDialog/SetKeyDialog.tsx | 67 +++- .../src/components/svg/CustomMinimalIcon.tsx | 30 ++ client/src/components/svg/index.ts | 1 + client/src/hooks/Conversations/index.ts | 1 + .../src/hooks/Conversations/useGetSender.ts | 15 + client/src/hooks/Input/useTextarea.ts | 10 +- .../src/hooks/Messages/useMessageHelpers.ts | 5 +- client/src/hooks/useChatHelpers.ts | 22 +- client/src/hooks/useConversation.ts | 7 +- client/src/hooks/useDefaultConvo.ts | 4 +- client/src/hooks/useGenerationsByLatest.ts | 2 + client/src/hooks/useNewConvo.ts | 14 +- client/src/hooks/useSSE.ts | 47 ++- client/src/hooks/useSetIndexOptions.ts | 85 +++-- client/src/routes/ChatRoute.tsx | 5 + client/src/store/endpoints.ts | 2 + client/src/utils/buildDefaultConvo.ts | 15 +- client/src/utils/cleanupPreset.ts | 5 +- client/src/utils/getDefaultEndpoint.ts | 9 +- client/src/utils/mapEndpoints.ts | 37 +- client/src/utils/presets.ts | 5 +- docs/install/configuration/ai_setup.md | 12 +- docs/install/configuration/custom_config.md | 221 ++++++++++++ docs/install/configuration/dotenv.md | 4 +- docs/install/configuration/free_ai_apis.md | 2 + docs/install/configuration/index.md | 1 + docs/install/configuration/litellm.md | 2 + docs/install/index.md | 1 + librechat.example.yaml | 76 ++++ packages/data-provider/src/config.ts | 186 ++++++++++ packages/data-provider/src/createPayload.ts | 13 +- packages/data-provider/src/index.ts | 7 +- packages/data-provider/src/parsers.ts | 225 ++++++++++++ packages/data-provider/src/schemas.ts | 335 ++++-------------- packages/data-provider/src/types.ts | 29 +- 100 files changed, 2149 insertions(+), 630 deletions(-) create mode 100644 api/cache/getCustomConfig.js delete mode 100644 api/common/enums.js create mode 100644 api/server/routes/ask/custom.js create mode 100644 api/server/routes/edit/custom.js create mode 100644 api/server/services/Config/loadConfigEndpoints.js create mode 100644 api/server/services/Config/loadConfigModels.js create mode 100644 api/server/services/Config/loadCustomConfig.js create mode 100644 api/server/services/Endpoints/custom/buildOptions.js create mode 100644 api/server/services/Endpoints/custom/index.js create mode 100644 api/server/services/Endpoints/custom/initializeClient.js create mode 100644 api/utils/loadYaml.js create mode 100644 client/public/assets/mistral.png create mode 100644 client/public/assets/openrouter.png create mode 100644 client/src/components/Chat/Menus/Endpoints/UnknownIcon.tsx create mode 100644 client/src/components/Input/SetKeyDialog/CustomEndpoint.tsx create mode 100644 client/src/components/svg/CustomMinimalIcon.tsx create mode 100644 client/src/hooks/Conversations/useGetSender.ts create mode 100644 docs/install/configuration/custom_config.md create mode 100644 librechat.example.yaml create mode 100644 packages/data-provider/src/config.ts create mode 100644 packages/data-provider/src/parsers.ts diff --git a/.gitignore b/.gitignore index f360cbba0acb..a09baeed0e03 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ bower_components/ .floo .flooignore +#config file +librechat.yaml + # Environment .npmrc .env* diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index ea63a3ce9040..17edf685574f 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -520,6 +520,7 @@ class BaseClient { await saveConvo(user, { conversationId: message.conversationId, endpoint: this.options.endpoint, + endpointType: this.options.endpointType, ...endpointOptions, }); } diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 605646a4b6ef..1c22d6f7d412 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -1,6 +1,6 @@ const OpenAI = require('openai'); const { HttpsProxyAgent } = require('https-proxy-agent'); -const { getResponseSender, EModelEndpoint } = require('librechat-data-provider'); +const { getResponseSender } = require('librechat-data-provider'); const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken'); const { encodeAndFormat, validateVisionModel } = require('~/server/services/Files/images'); const { getModelMaxTokens, genAzureChatCompletion, extractBaseURL } = require('~/utils'); @@ -94,10 +94,23 @@ class OpenAIClient extends BaseClient { } const { reverseProxyUrl: reverseProxy } = this.options; + + if ( + !this.useOpenRouter && + reverseProxy && + reverseProxy.includes('https://openrouter.ai/api/v1') + ) { + this.useOpenRouter = true; + } + this.FORCE_PROMPT = isEnabled(OPENAI_FORCE_PROMPT) || (reverseProxy && reverseProxy.includes('completions') && !reverseProxy.includes('chat')); + if (typeof this.options.forcePrompt === 'boolean') { + this.FORCE_PROMPT = this.options.forcePrompt; + } + if (this.azure && process.env.AZURE_OPENAI_DEFAULT_MODEL) { this.azureEndpoint = genAzureChatCompletion(this.azure, this.modelOptions.model); this.modelOptions.model = process.env.AZURE_OPENAI_DEFAULT_MODEL; @@ -146,8 +159,10 @@ class OpenAIClient extends BaseClient { this.options.sender ?? getResponseSender({ model: this.modelOptions.model, - endpoint: EModelEndpoint.openAI, + endpoint: this.options.endpoint, + endpointType: this.options.endpointType, chatGptLabel: this.options.chatGptLabel, + modelDisplayLabel: this.options.modelDisplayLabel, }); this.userLabel = this.options.userLabel || 'User'; @@ -434,7 +449,7 @@ class OpenAIClient extends BaseClient { }, opts.abortController || new AbortController(), ); - } else if (typeof opts.onProgress === 'function') { + } else if (typeof opts.onProgress === 'function' || this.options.useChatCompletion) { reply = await this.chatCompletion({ payload, clientOptions: opts, @@ -530,6 +545,19 @@ class OpenAIClient extends BaseClient { return llm; } + /** + * Generates a concise title for a conversation based on the user's input text and response. + * Uses either specified method or starts with the OpenAI `functions` method (using LangChain). + * If the `functions` method fails, it falls back to the `completion` method, + * which involves sending a chat completion request with specific instructions for title generation. + * + * @param {Object} params - The parameters for the conversation title generation. + * @param {string} params.text - The user's input. + * @param {string} [params.responseText=''] - The AI's immediate response to the user. + * + * @returns {Promise} A promise that resolves to the generated conversation title. + * In case of failure, it will return the default title, "New Chat". + */ async titleConvo({ text, responseText = '' }) { let title = 'New Chat'; const convo = `||>User: @@ -539,32 +567,25 @@ class OpenAIClient extends BaseClient { const { OPENAI_TITLE_MODEL } = process.env ?? {}; + const model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo'; + const modelOptions = { - model: OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo', + // TODO: remove the gpt fallback and make it specific to endpoint + model, temperature: 0.2, presence_penalty: 0, frequency_penalty: 0, max_tokens: 16, }; - try { - this.abortController = new AbortController(); - const llm = this.initializeLLM({ ...modelOptions, context: 'title', tokenBuffer: 150 }); - title = await runTitleChain({ llm, text, convo, signal: this.abortController.signal }); - } catch (e) { - if (e?.message?.toLowerCase()?.includes('abort')) { - logger.debug('[OpenAIClient] Aborted title generation'); - return; - } - logger.error( - '[OpenAIClient] There was an issue generating title with LangChain, trying the old method...', - e, - ); - modelOptions.model = OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo'; + const titleChatCompletion = async () => { + modelOptions.model = model; + if (this.azure) { modelOptions.model = process.env.AZURE_OPENAI_DEFAULT_MODEL ?? modelOptions.model; this.azureEndpoint = genAzureChatCompletion(this.azure, modelOptions.model); } + const instructionsPayload = [ { role: 'system', @@ -578,10 +599,38 @@ ${convo} ]; try { - title = (await this.sendPayload(instructionsPayload, { modelOptions })).replaceAll('"', ''); + title = ( + await this.sendPayload(instructionsPayload, { modelOptions, useChatCompletion: true }) + ).replaceAll('"', ''); } catch (e) { - logger.error('[OpenAIClient] There was another issue generating the title', e); + logger.error( + '[OpenAIClient] There was an issue generating the title with the completion method', + e, + ); + } + }; + + if (this.options.titleMethod === 'completion') { + await titleChatCompletion(); + logger.debug('[OpenAIClient] Convo Title: ' + title); + return title; + } + + try { + this.abortController = new AbortController(); + const llm = this.initializeLLM({ ...modelOptions, context: 'title', tokenBuffer: 150 }); + title = await runTitleChain({ llm, text, convo, signal: this.abortController.signal }); + } catch (e) { + if (e?.message?.toLowerCase()?.includes('abort')) { + logger.debug('[OpenAIClient] Aborted title generation'); + return; } + logger.error( + '[OpenAIClient] There was an issue generating title with LangChain, trying completion method...', + e, + ); + + await titleChatCompletion(); } logger.debug('[OpenAIClient] Convo Title: ' + title); @@ -593,8 +642,11 @@ ${convo} let context = messagesToRefine; let prompt; + // TODO: remove the gpt fallback and make it specific to endpoint const { OPENAI_SUMMARY_MODEL = 'gpt-3.5-turbo' } = process.env ?? {}; - const maxContextTokens = getModelMaxTokens(OPENAI_SUMMARY_MODEL) ?? 4095; + const model = this.options.summaryModel ?? OPENAI_SUMMARY_MODEL; + const maxContextTokens = getModelMaxTokens(model) ?? 4095; + // 3 tokens for the assistant label, and 98 for the summarizer prompt (101) let promptBuffer = 101; @@ -644,7 +696,7 @@ ${convo} logger.debug('[OpenAIClient] initialPromptTokens', initialPromptTokens); const llm = this.initializeLLM({ - model: OPENAI_SUMMARY_MODEL, + model, temperature: 0.2, context: 'summary', tokenBuffer: initialPromptTokens, @@ -719,7 +771,9 @@ ${convo} if (!abortController) { abortController = new AbortController(); } - const modelOptions = { ...this.modelOptions }; + + let modelOptions = { ...this.modelOptions }; + if (typeof onProgress === 'function') { modelOptions.stream = true; } @@ -779,6 +833,27 @@ ${convo} ...opts, }); + /* hacky fix for Mistral AI API not allowing a singular system message in payload */ + if (opts.baseURL.includes('https://api.mistral.ai/v1') && modelOptions.messages) { + const { messages } = modelOptions; + if (messages.length === 1 && messages[0].role === 'system') { + modelOptions.messages[0].role = 'user'; + } + } + + if (this.options.addParams && typeof this.options.addParams === 'object') { + modelOptions = { + ...modelOptions, + ...this.options.addParams, + }; + } + + if (this.options.dropParams && Array.isArray(this.options.dropParams)) { + this.options.dropParams.forEach((param) => { + delete modelOptions[param]; + }); + } + let UnexpectedRoleError = false; if (modelOptions.stream) { const stream = await openai.beta.chat.completions diff --git a/api/cache/getCustomConfig.js b/api/cache/getCustomConfig.js new file mode 100644 index 000000000000..62082c5cbae7 --- /dev/null +++ b/api/cache/getCustomConfig.js @@ -0,0 +1,23 @@ +const { CacheKeys } = require('librechat-data-provider'); +const loadCustomConfig = require('~/server/services/Config/loadCustomConfig'); +const getLogStores = require('./getLogStores'); + +/** + * Retrieves the configuration object + * @function getCustomConfig */ +async function getCustomConfig() { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG); + + if (!customConfig) { + customConfig = await loadCustomConfig(); + } + + if (!customConfig) { + return null; + } + + return customConfig; +} + +module.exports = getCustomConfig; diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 77949dacd3c9..016c77000099 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -1,9 +1,10 @@ const Keyv = require('keyv'); -const keyvMongo = require('./keyvMongo'); -const keyvRedis = require('./keyvRedis'); -const { CacheKeys } = require('~/common/enums'); -const { math, isEnabled } = require('~/server/utils'); +const { CacheKeys } = require('librechat-data-provider'); const { logFile, violationFile } = require('./keyvFiles'); +const { math, isEnabled } = require('~/server/utils'); +const keyvRedis = require('./keyvRedis'); +const keyvMongo = require('./keyvMongo'); + const { BAN_DURATION, USE_REDIS } = process.env ?? {}; const duration = math(BAN_DURATION, 7200000); @@ -20,10 +21,10 @@ const pending_req = isEnabled(USE_REDIS) const config = isEnabled(USE_REDIS) ? new Keyv({ store: keyvRedis }) - : new Keyv({ namespace: CacheKeys.CONFIG }); + : new Keyv({ namespace: CacheKeys.CONFIG_STORE }); const namespaces = { - config, + [CacheKeys.CONFIG_STORE]: config, pending_req, ban: new Keyv({ store: keyvMongo, namespace: 'bans', ttl: duration }), general: new Keyv({ store: logFile, namespace: 'violations' }), @@ -39,19 +40,15 @@ const namespaces = { * Returns the keyv cache specified by type. * If an invalid type is passed, an error will be thrown. * - * @module getLogStores - * @requires keyv - a simple key-value storage that allows you to easily switch out storage adapters. - * @requires keyvFiles - a module that includes the logFile and violationFile. - * - * @param {string} type - The type of violation, which can be 'concurrent', 'message_limit', 'registrations' or 'logins'. - * @returns {Keyv} - If a valid type is passed, returns an object containing the logs for violations of the specified type. - * @throws Will throw an error if an invalid violation type is passed. + * @param {string} key - The key for the namespace to access + * @returns {Keyv} - If a valid key is passed, returns an object containing the cache store of the specified key. + * @throws Will throw an error if an invalid key is passed. */ -const getLogStores = (type) => { - if (!type || !namespaces[type]) { - throw new Error(`Invalid store type: ${type}`); +const getLogStores = (key) => { + if (!key || !namespaces[key]) { + throw new Error(`Invalid store key: ${key}`); } - return namespaces[type]; + return namespaces[key]; }; module.exports = getLogStores; diff --git a/api/common/enums.js b/api/common/enums.js deleted file mode 100644 index 849ae43f59c2..000000000000 --- a/api/common/enums.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @typedef {Object} CacheKeys - * @property {'config'} CONFIG - Key for the config cache. - * @property {'plugins'} PLUGINS - Key for the plugins cache. - * @property {'modelsConfig'} MODELS_CONFIG - Key for the model config cache. - * @property {'defaultConfig'} DEFAULT_CONFIG - Key for the default config cache. - * @property {'overrideConfig'} OVERRIDE_CONFIG - Key for the override config cache. - */ -const CacheKeys = { - CONFIG: 'config', - PLUGINS: 'plugins', - MODELS_CONFIG: 'modelsConfig', - DEFAULT_CONFIG: 'defaultConfig', - OVERRIDE_CONFIG: 'overrideConfig', -}; - -module.exports = { CacheKeys }; diff --git a/api/models/schema/convoSchema.js b/api/models/schema/convoSchema.js index 46555ba35342..a282287eccbb 100644 --- a/api/models/schema/convoSchema.js +++ b/api/models/schema/convoSchema.js @@ -18,36 +18,29 @@ const convoSchema = mongoose.Schema( user: { type: String, index: true, - // default: null, }, messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }], // google only - examples: [{ type: mongoose.Schema.Types.Mixed }], + examples: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined }, agentOptions: { type: mongoose.Schema.Types.Mixed, - // default: null, }, ...conversationPreset, // for bingAI only bingConversationId: { type: String, - // default: null, }, jailbreakConversationId: { type: String, - // default: null, }, conversationSignature: { type: String, - // default: null, }, clientId: { type: String, - // default: null, }, invocationId: { type: Number, - // default: 1, }, }, { timestamps: true }, diff --git a/api/models/schema/defaults.js b/api/models/schema/defaults.js index 338ee1208919..feedf1019ae7 100644 --- a/api/models/schema/defaults.js +++ b/api/models/schema/defaults.js @@ -5,6 +5,9 @@ const conversationPreset = { default: null, required: true, }, + endpointType: { + type: String, + }, // for azureOpenAI, openAI, chatGPTBrowser only model: { type: String, @@ -95,7 +98,6 @@ const agentOptions = { // default: null, required: false, }, - // for google only modelLabel: { type: String, // default: null, diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index 33d799544b2d..8e0b688d40f3 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -82,22 +82,26 @@ const messageSchema = mongoose.Schema( select: false, default: false, }, - files: [{ type: mongoose.Schema.Types.Mixed }], + files: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined }, plugin: { - latest: { - type: String, - required: false, - }, - inputs: { - type: [mongoose.Schema.Types.Mixed], - required: false, - }, - outputs: { - type: String, - required: false, + type: { + latest: { + type: String, + required: false, + }, + inputs: { + type: [mongoose.Schema.Types.Mixed], + required: false, + default: undefined, + }, + outputs: { + type: String, + required: false, + }, }, + default: undefined, }, - plugins: [{ type: mongoose.Schema.Types.Mixed }], + plugins: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined }, }, { timestamps: true }, ); diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index 78933feebc17..6dc3949966a0 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -9,6 +9,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { text, endpointOption, conversationId, + modelDisplayLabel, parentMessageId = null, overrideParentMessageId = null, } = req.body; @@ -22,7 +23,11 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { let responseMessageId; let lastSavedTimestamp = 0; let saveDelay = 100; - const sender = getResponseSender({ ...endpointOption, model: endpointOption.modelOptions.model }); + const sender = getResponseSender({ + ...endpointOption, + model: endpointOption.modelOptions.model, + modelDisplayLabel, + }); const newConvo = !conversationId; const user = req.user.id; diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js index 72ee58026a47..43b82e7193ff 100644 --- a/api/server/controllers/EditController.js +++ b/api/server/controllers/EditController.js @@ -10,6 +10,7 @@ const EditController = async (req, res, next, initializeClient) => { generation, endpointOption, conversationId, + modelDisplayLabel, responseMessageId, isContinued = false, parentMessageId = null, @@ -29,7 +30,11 @@ const EditController = async (req, res, next, initializeClient) => { let promptTokens; let lastSavedTimestamp = 0; let saveDelay = 100; - const sender = getResponseSender({ ...endpointOption, model: endpointOption.modelOptions.model }); + const sender = getResponseSender({ + ...endpointOption, + model: endpointOption.modelOptions.model, + modelDisplayLabel, + }); const userMessageId = parentMessageId; const user = req.user.id; diff --git a/api/server/controllers/EndpointController.js b/api/server/controllers/EndpointController.js index 0cc21f96ac3b..5069bb33e0b4 100644 --- a/api/server/controllers/EndpointController.js +++ b/api/server/controllers/EndpointController.js @@ -1,17 +1,22 @@ +const { CacheKeys } = require('librechat-data-provider'); +const { loadDefaultEndpointsConfig, loadConfigEndpoints } = require('~/server/services/Config'); const { getLogStores } = require('~/cache'); -const { CacheKeys } = require('~/common/enums'); -const { loadDefaultEndpointsConfig } = require('~/server/services/Config'); async function endpointController(req, res) { - const cache = getLogStores(CacheKeys.CONFIG); - const config = await cache.get(CacheKeys.DEFAULT_CONFIG); - if (config) { - res.send(config); + const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cachedEndpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG); + if (cachedEndpointsConfig) { + res.send(cachedEndpointsConfig); return; } - const defaultConfig = await loadDefaultEndpointsConfig(); - await cache.set(CacheKeys.DEFAULT_CONFIG, defaultConfig); - res.send(JSON.stringify(defaultConfig)); + + const defaultEndpointsConfig = await loadDefaultEndpointsConfig(); + const customConfigEndpoints = await loadConfigEndpoints(); + + const endpointsConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints }; + + await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig); + res.send(JSON.stringify(endpointsConfig)); } module.exports = endpointController; diff --git a/api/server/controllers/ModelController.js b/api/server/controllers/ModelController.js index 61ca82ecf030..2d23961e1544 100644 --- a/api/server/controllers/ModelController.js +++ b/api/server/controllers/ModelController.js @@ -1,15 +1,19 @@ +const { CacheKeys } = require('librechat-data-provider'); +const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config'); const { getLogStores } = require('~/cache'); -const { CacheKeys } = require('~/common/enums'); -const { loadDefaultModels } = require('~/server/services/Config'); async function modelController(req, res) { - const cache = getLogStores(CacheKeys.CONFIG); - let modelConfig = await cache.get(CacheKeys.MODELS_CONFIG); - if (modelConfig) { - res.send(modelConfig); + const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cachedModelsConfig = await cache.get(CacheKeys.MODELS_CONFIG); + if (cachedModelsConfig) { + res.send(cachedModelsConfig); return; } - modelConfig = await loadDefaultModels(); + const defaultModelsConfig = await loadDefaultModels(); + const customModelsConfig = await loadConfigModels(); + + const modelConfig = { ...defaultModelsConfig, ...customModelsConfig }; + await cache.set(CacheKeys.MODELS_CONFIG, modelConfig); res.send(modelConfig); } diff --git a/api/server/controllers/OverrideController.js b/api/server/controllers/OverrideController.js index 0abd27a7a24b..677fb87bdcb5 100644 --- a/api/server/controllers/OverrideController.js +++ b/api/server/controllers/OverrideController.js @@ -1,9 +1,9 @@ -const { getLogStores } = require('~/cache'); -const { CacheKeys } = require('~/common/enums'); +const { CacheKeys } = require('librechat-data-provider'); const { loadOverrideConfig } = require('~/server/services/Config'); +const { getLogStores } = require('~/cache'); async function overrideController(req, res) { - const cache = getLogStores(CacheKeys.CONFIG); + const cache = getLogStores(CacheKeys.CONFIG_STORE); let overrideConfig = await cache.get(CacheKeys.OVERRIDE_CONFIG); if (overrideConfig) { res.send(overrideConfig); @@ -15,7 +15,7 @@ async function overrideController(req, res) { overrideConfig = await loadOverrideConfig(); const { endpointsConfig, modelsConfig } = overrideConfig; if (endpointsConfig) { - await cache.set(CacheKeys.DEFAULT_CONFIG, endpointsConfig); + await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig); } if (modelsConfig) { await cache.set(CacheKeys.MODELS_CONFIG, modelsConfig); diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 697a499796c7..c37b36974e08 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -1,7 +1,7 @@ const path = require('path'); const { promises: fs } = require('fs'); +const { CacheKeys } = require('librechat-data-provider'); const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs'); -const { CacheKeys } = require('~/common/enums'); const { getLogStores } = require('~/cache'); const filterUniquePlugins = (plugins) => { @@ -29,7 +29,7 @@ const isPluginAuthenticated = (plugin) => { const getAvailablePluginsController = async (req, res) => { try { - const cache = getLogStores(CacheKeys.CONFIG); + const cache = getLogStores(CacheKeys.CONFIG_STORE); const cachedPlugins = await cache.get(CacheKeys.PLUGINS); if (cachedPlugins) { res.status(200).json(cachedPlugins); diff --git a/api/server/index.js b/api/server/index.js index 698620c56f3a..c3586e481e26 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -5,14 +5,15 @@ const express = require('express'); const passport = require('passport'); const mongoSanitize = require('express-mongo-sanitize'); const { initializeFirebase } = require('~/server/services/Files/Firebase/initialize'); -const errorController = require('./controllers/ErrorController'); -const configureSocialLogins = require('./socialLogins'); +const loadCustomConfig = require('~/server/services/Config/loadCustomConfig'); +const errorController = require('~/server/controllers/ErrorController'); +const configureSocialLogins = require('~/server/socialLogins'); +const noIndex = require('~/server/middleware/noIndex'); const { connectDb, indexSync } = require('~/lib/db'); const { logger } = require('~/config'); -const noIndex = require('./middleware/noIndex'); +const routes = require('~/server/routes'); const paths = require('~/config/paths'); -const routes = require('./routes'); const { PORT, HOST, ALLOW_SOCIAL_LOGIN } = process.env ?? {}; @@ -24,6 +25,7 @@ const { jwtLogin, passportLogin } = require('~/strategies'); const startServer = async () => { await connectDb(); logger.info('Connected to MongoDB'); + await loadCustomConfig(); initializeFirebase(); await indexSync(); diff --git a/api/server/middleware/buildEndpointOption.js b/api/server/middleware/buildEndpointOption.js index d98fe92d2cea..543815e3676e 100644 --- a/api/server/middleware/buildEndpointOption.js +++ b/api/server/middleware/buildEndpointOption.js @@ -1,5 +1,6 @@ const { processFiles } = require('~/server/services/Files'); const openAI = require('~/server/services/Endpoints/openAI'); +const custom = require('~/server/services/Endpoints/custom'); const google = require('~/server/services/Endpoints/google'); const anthropic = require('~/server/services/Endpoints/anthropic'); const gptPlugins = require('~/server/services/Endpoints/gptPlugins'); @@ -8,15 +9,20 @@ const { parseConvo, EModelEndpoint } = require('librechat-data-provider'); const buildFunction = { [EModelEndpoint.openAI]: openAI.buildOptions, [EModelEndpoint.google]: google.buildOptions, + [EModelEndpoint.custom]: custom.buildOptions, [EModelEndpoint.azureOpenAI]: openAI.buildOptions, [EModelEndpoint.anthropic]: anthropic.buildOptions, [EModelEndpoint.gptPlugins]: gptPlugins.buildOptions, }; function buildEndpointOption(req, res, next) { - const { endpoint } = req.body; - const parsedBody = parseConvo(endpoint, req.body); - req.body.endpointOption = buildFunction[endpoint](endpoint, parsedBody); + const { endpoint, endpointType } = req.body; + const parsedBody = parseConvo({ endpoint, endpointType, conversation: req.body }); + req.body.endpointOption = buildFunction[endpointType ?? endpoint]( + endpoint, + parsedBody, + endpointType, + ); if (req.body.files) { // hold the promise req.body.endpointOption.attachments = processFiles(req.body.files); diff --git a/api/server/middleware/validateEndpoint.js b/api/server/middleware/validateEndpoint.js index 6e9c914c8eb3..0eeaaeb97dcb 100644 --- a/api/server/middleware/validateEndpoint.js +++ b/api/server/middleware/validateEndpoint.js @@ -1,7 +1,8 @@ const { handleError } = require('../utils'); function validateEndpoint(req, res, next) { - const { endpoint } = req.body; + const { endpoint: _endpoint, endpointType } = req.body; + const endpoint = endpointType ?? _endpoint; if (!req.body.text || req.body.text.length === 0) { return handleError(res, { text: 'Prompt empty or too short' }); diff --git a/api/server/routes/ask/custom.js b/api/server/routes/ask/custom.js new file mode 100644 index 000000000000..ef979bf0000c --- /dev/null +++ b/api/server/routes/ask/custom.js @@ -0,0 +1,20 @@ +const express = require('express'); +const AskController = require('~/server/controllers/AskController'); +const { initializeClient } = require('~/server/services/Endpoints/custom'); +const { addTitle } = require('~/server/services/Endpoints/openAI'); +const { + handleAbort, + setHeaders, + validateEndpoint, + buildEndpointOption, +} = require('~/server/middleware'); + +const router = express.Router(); + +router.post('/abort', handleAbort()); + +router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, res, next) => { + await AskController(req, res, next, initializeClient, addTitle); +}); + +module.exports = router; diff --git a/api/server/routes/ask/index.js b/api/server/routes/ask/index.js index 669fd87e6fbf..b5156ed8d106 100644 --- a/api/server/routes/ask/index.js +++ b/api/server/routes/ask/index.js @@ -1,5 +1,6 @@ const express = require('express'); const openAI = require('./openAI'); +const custom = require('./custom'); const google = require('./google'); const bingAI = require('./bingAI'); const anthropic = require('./anthropic'); @@ -42,5 +43,6 @@ router.use(`/${EModelEndpoint.gptPlugins}`, gptPlugins); router.use(`/${EModelEndpoint.anthropic}`, anthropic); router.use(`/${EModelEndpoint.google}`, google); router.use(`/${EModelEndpoint.bingAI}`, bingAI); +router.use(`/${EModelEndpoint.custom}`, custom); module.exports = router; diff --git a/api/server/routes/edit/custom.js b/api/server/routes/edit/custom.js new file mode 100644 index 000000000000..dd63c96c8f94 --- /dev/null +++ b/api/server/routes/edit/custom.js @@ -0,0 +1,20 @@ +const express = require('express'); +const EditController = require('~/server/controllers/EditController'); +const { initializeClient } = require('~/server/services/Endpoints/custom'); +const { addTitle } = require('~/server/services/Endpoints/openAI'); +const { + handleAbort, + setHeaders, + validateEndpoint, + buildEndpointOption, +} = require('~/server/middleware'); + +const router = express.Router(); + +router.post('/abort', handleAbort()); + +router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, res, next) => { + await EditController(req, res, next, initializeClient, addTitle); +}); + +module.exports = router; diff --git a/api/server/routes/edit/index.js b/api/server/routes/edit/index.js index 01dd06ced98f..fa19f9effdc6 100644 --- a/api/server/routes/edit/index.js +++ b/api/server/routes/edit/index.js @@ -1,5 +1,6 @@ const express = require('express'); const openAI = require('./openAI'); +const custom = require('./custom'); const google = require('./google'); const anthropic = require('./anthropic'); const gptPlugins = require('./gptPlugins'); @@ -38,5 +39,6 @@ router.use([`/${EModelEndpoint.azureOpenAI}`, `/${EModelEndpoint.openAI}`], open router.use(`/${EModelEndpoint.gptPlugins}`, gptPlugins); router.use(`/${EModelEndpoint.anthropic}`, anthropic); router.use(`/${EModelEndpoint.google}`, google); +router.use(`/${EModelEndpoint.custom}`, custom); module.exports = router; diff --git a/api/server/services/Config/index.js b/api/server/services/Config/index.js index 13cbc09f3b36..57a00bf515e7 100644 --- a/api/server/services/Config/index.js +++ b/api/server/services/Config/index.js @@ -1,13 +1,19 @@ const { config } = require('./EndpointService'); +const loadCustomConfig = require('./loadCustomConfig'); +const loadConfigModels = require('./loadConfigModels'); const loadDefaultModels = require('./loadDefaultModels'); const loadOverrideConfig = require('./loadOverrideConfig'); const loadAsyncEndpoints = require('./loadAsyncEndpoints'); +const loadConfigEndpoints = require('./loadConfigEndpoints'); const loadDefaultEndpointsConfig = require('./loadDefaultEConfig'); module.exports = { config, + loadCustomConfig, + loadConfigModels, loadDefaultModels, loadOverrideConfig, loadAsyncEndpoints, + loadConfigEndpoints, loadDefaultEndpointsConfig, }; diff --git a/api/server/services/Config/loadConfigEndpoints.js b/api/server/services/Config/loadConfigEndpoints.js new file mode 100644 index 000000000000..1b435e144e99 --- /dev/null +++ b/api/server/services/Config/loadConfigEndpoints.js @@ -0,0 +1,54 @@ +const { CacheKeys, EModelEndpoint } = require('librechat-data-provider'); +const { isUserProvided, extractEnvVariable } = require('~/server/utils'); +const loadCustomConfig = require('./loadCustomConfig'); +const { getLogStores } = require('~/cache'); + +/** + * Load config endpoints from the cached configuration object + * @function loadConfigEndpoints */ +async function loadConfigEndpoints() { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG); + + if (!customConfig) { + customConfig = await loadCustomConfig(); + } + + if (!customConfig) { + return {}; + } + + const { endpoints = {} } = customConfig ?? {}; + const endpointsConfig = {}; + + if (Array.isArray(endpoints[EModelEndpoint.custom])) { + const customEndpoints = endpoints[EModelEndpoint.custom].filter( + (endpoint) => + endpoint.baseURL && + endpoint.apiKey && + endpoint.name && + endpoint.models && + (endpoint.models.fetch || endpoint.models.default), + ); + + for (let i = 0; i < customEndpoints.length; i++) { + const endpoint = customEndpoints[i]; + const { baseURL, apiKey, name, iconURL, modelDisplayLabel } = endpoint; + + const resolvedApiKey = extractEnvVariable(apiKey); + const resolvedBaseURL = extractEnvVariable(baseURL); + + endpointsConfig[name] = { + type: EModelEndpoint.custom, + userProvide: isUserProvided(resolvedApiKey), + userProvideURL: isUserProvided(resolvedBaseURL), + modelDisplayLabel, + iconURL, + }; + } + } + + return endpointsConfig; +} + +module.exports = loadConfigEndpoints; diff --git a/api/server/services/Config/loadConfigModels.js b/api/server/services/Config/loadConfigModels.js new file mode 100644 index 000000000000..0abe15a8a1f6 --- /dev/null +++ b/api/server/services/Config/loadConfigModels.js @@ -0,0 +1,79 @@ +const { CacheKeys, EModelEndpoint } = require('librechat-data-provider'); +const { isUserProvided, extractEnvVariable } = require('~/server/utils'); +const { fetchModels } = require('~/server/services/ModelService'); +const loadCustomConfig = require('./loadCustomConfig'); +const { getLogStores } = require('~/cache'); + +/** + * Load config endpoints from the cached configuration object + * @function loadConfigModels */ +async function loadConfigModels() { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG); + + if (!customConfig) { + customConfig = await loadCustomConfig(); + } + + if (!customConfig) { + return {}; + } + + const { endpoints = {} } = customConfig ?? {}; + const modelsConfig = {}; + + if (!Array.isArray(endpoints[EModelEndpoint.custom])) { + return modelsConfig; + } + + const customEndpoints = endpoints[EModelEndpoint.custom].filter( + (endpoint) => + endpoint.baseURL && + endpoint.apiKey && + endpoint.name && + endpoint.models && + (endpoint.models.fetch || endpoint.models.default), + ); + + const fetchPromisesMap = {}; // Map for promises keyed by baseURL + const baseUrlToNameMap = {}; // Map to associate baseURLs with names + + for (let i = 0; i < customEndpoints.length; i++) { + const endpoint = customEndpoints[i]; + const { models, name, baseURL, apiKey } = endpoint; + + const API_KEY = extractEnvVariable(apiKey); + const BASE_URL = extractEnvVariable(baseURL); + + modelsConfig[name] = []; + + if (models.fetch && !isUserProvided(API_KEY) && !isUserProvided(BASE_URL)) { + fetchPromisesMap[BASE_URL] = + fetchPromisesMap[BASE_URL] || fetchModels({ baseURL: BASE_URL, apiKey: API_KEY }); + baseUrlToNameMap[BASE_URL] = baseUrlToNameMap[BASE_URL] || []; + baseUrlToNameMap[BASE_URL].push(name); + continue; + } + + if (Array.isArray(models.default)) { + modelsConfig[name] = models.default; + } + } + + const fetchedData = await Promise.all(Object.values(fetchPromisesMap)); + const baseUrls = Object.keys(fetchPromisesMap); + + for (let i = 0; i < fetchedData.length; i++) { + const currentBaseUrl = baseUrls[i]; + const modelData = fetchedData[i]; + const associatedNames = baseUrlToNameMap[currentBaseUrl]; + + for (const name of associatedNames) { + modelsConfig[name] = modelData; + } + } + + return modelsConfig; +} + +module.exports = loadConfigModels; diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js new file mode 100644 index 000000000000..c17d3283b47f --- /dev/null +++ b/api/server/services/Config/loadCustomConfig.js @@ -0,0 +1,41 @@ +const path = require('path'); +const { CacheKeys, configSchema } = require('librechat-data-provider'); +const loadYaml = require('~/utils/loadYaml'); +const { getLogStores } = require('~/cache'); +const { logger } = require('~/config'); + +const projectRoot = path.resolve(__dirname, '..', '..', '..', '..'); +const configPath = path.resolve(projectRoot, 'librechat.yaml'); + +/** + * Load custom configuration files and caches the object if the `cache` field at root is true. + * Validation via parsing the config file with the config schema. + * @function loadCustomConfig + * @returns {Promise} A promise that resolves to null or the custom config object. + * */ + +async function loadCustomConfig() { + const customConfig = loadYaml(configPath); + if (!customConfig) { + return null; + } + + const result = configSchema.strict().safeParse(customConfig); + if (!result.success) { + logger.error(`Invalid custom config file at ${configPath}`, result.error); + return null; + } else { + logger.info('Loaded custom config file'); + } + + if (customConfig.cache) { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig); + } + + // TODO: handle remote config + + return customConfig; +} + +module.exports = loadCustomConfig; diff --git a/api/server/services/Endpoints/custom/buildOptions.js b/api/server/services/Endpoints/custom/buildOptions.js new file mode 100644 index 000000000000..63a2d1599246 --- /dev/null +++ b/api/server/services/Endpoints/custom/buildOptions.js @@ -0,0 +1,16 @@ +const buildOptions = (endpoint, parsedBody, endpointType) => { + const { chatGptLabel, promptPrefix, ...rest } = parsedBody; + const endpointOption = { + endpoint, + endpointType, + chatGptLabel, + promptPrefix, + modelOptions: { + ...rest, + }, + }; + + return endpointOption; +}; + +module.exports = buildOptions; diff --git a/api/server/services/Endpoints/custom/index.js b/api/server/services/Endpoints/custom/index.js new file mode 100644 index 000000000000..3cda8d5fece1 --- /dev/null +++ b/api/server/services/Endpoints/custom/index.js @@ -0,0 +1,7 @@ +const initializeClient = require('./initializeClient'); +const buildOptions = require('./buildOptions'); + +module.exports = { + initializeClient, + buildOptions, +}; diff --git a/api/server/services/Endpoints/custom/initializeClient.js b/api/server/services/Endpoints/custom/initializeClient.js new file mode 100644 index 000000000000..93182fa89e26 --- /dev/null +++ b/api/server/services/Endpoints/custom/initializeClient.js @@ -0,0 +1,79 @@ +const { EModelEndpoint } = require('librechat-data-provider'); +const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService'); +const { isUserProvided, extractEnvVariable } = require('~/server/utils'); +const getCustomConfig = require('~/cache/getCustomConfig'); +const { OpenAIClient } = require('~/app'); + +const { PROXY } = process.env; + +const initializeClient = async ({ req, res, endpointOption }) => { + const { key: expiresAt, endpoint } = req.body; + const customConfig = await getCustomConfig(); + if (!customConfig) { + throw new Error(`Config not found for the ${endpoint} custom endpoint.`); + } + + const { endpoints = {} } = customConfig; + const customEndpoints = endpoints[EModelEndpoint.custom] ?? []; + const endpointConfig = customEndpoints.find((endpointConfig) => endpointConfig.name === endpoint); + + const CUSTOM_API_KEY = extractEnvVariable(endpointConfig.apiKey); + const CUSTOM_BASE_URL = extractEnvVariable(endpointConfig.baseURL); + + const customOptions = { + addParams: endpointConfig.addParams, + dropParams: endpointConfig.dropParams, + titleConvo: endpointConfig.titleConvo, + titleModel: endpointConfig.titleModel, + forcePrompt: endpointConfig.forcePrompt, + summaryModel: endpointConfig.summaryModel, + modelDisplayLabel: endpointConfig.modelDisplayLabel, + titleMethod: endpointConfig.titleMethod ?? 'completion', + contextStrategy: endpointConfig.summarize ? 'summarize' : null, + }; + + const useUserKey = isUserProvided(CUSTOM_API_KEY); + const useUserURL = isUserProvided(CUSTOM_BASE_URL); + + let userValues = null; + if (expiresAt && (useUserKey || useUserURL)) { + checkUserKeyExpiry( + expiresAt, + `Your API values for ${endpoint} have expired. Please configure them again.`, + ); + userValues = await getUserKey({ userId: req.user.id, name: endpoint }); + try { + userValues = JSON.parse(userValues); + } catch (e) { + throw new Error(`Invalid JSON provided for ${endpoint} user values.`); + } + } + + let apiKey = useUserKey ? userValues.apiKey : CUSTOM_API_KEY; + let baseURL = useUserURL ? userValues.baseURL : CUSTOM_BASE_URL; + + if (!apiKey) { + throw new Error(`${endpoint} API key not provided.`); + } + + if (!baseURL) { + throw new Error(`${endpoint} Base URL not provided.`); + } + + const clientOptions = { + reverseProxyUrl: baseURL ?? null, + proxy: PROXY ?? null, + req, + res, + ...customOptions, + ...endpointOption, + }; + + const client = new OpenAIClient(apiKey, clientOptions); + return { + client, + openAIApiKey: apiKey, + }; +}; + +module.exports = initializeClient; diff --git a/api/server/services/Endpoints/openAI/addTitle.js b/api/server/services/Endpoints/openAI/addTitle.js index f630638643fb..ab15443f9428 100644 --- a/api/server/services/Endpoints/openAI/addTitle.js +++ b/api/server/services/Endpoints/openAI/addTitle.js @@ -7,6 +7,10 @@ const addTitle = async (req, { text, response, client }) => { return; } + if (client.options.titleConvo === false) { + return; + } + // If the request was aborted and is not azure, don't generate the title. if (!client.azure && client.abortController.signal.aborted) { return; diff --git a/api/server/services/ModelService.js b/api/server/services/ModelService.js index 08c9ae71d292..2e433dbd14ed 100644 --- a/api/server/services/ModelService.js +++ b/api/server/services/ModelService.js @@ -24,15 +24,53 @@ const { PROXY, } = process.env ?? {}; +/** + * Fetches OpenAI models from the specified base API path or Azure, based on the provided configuration. + * + * @param {Object} params - The parameters for fetching the models. + * @param {string} params.apiKey - The API key for authentication with the API. + * @param {string} params.baseURL - The base path URL for the API. + * @param {string} [params.name='OpenAI'] - The name of the API; defaults to 'OpenAI'. + * @param {boolean} [params.azure=false] - Whether to fetch models from Azure. + * @returns {Promise} A promise that resolves to an array of model identifiers. + * @async + */ +const fetchModels = async ({ apiKey, baseURL, name = 'OpenAI', azure = false }) => { + let models = []; + + if (!baseURL && !azure) { + return models; + } + + try { + const payload = { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }; + + if (PROXY) { + payload.httpsAgent = new HttpsProxyAgent(PROXY); + } + + const res = await axios.get(`${baseURL}${azure ? '' : '/models'}`, payload); + models = res.data.data.map((item) => item.id); + } catch (err) { + logger.error(`Failed to fetch models from ${azure ? 'Azure ' : ''}${name} API`, err); + } + + return models; +}; + const fetchOpenAIModels = async (opts = { azure: false, plugins: false }, _models = []) => { let models = _models.slice() ?? []; let apiKey = openAIApiKey; - let basePath = 'https://api.openai.com/v1'; + let baseURL = 'https://api.openai.com/v1'; let reverseProxyUrl = OPENAI_REVERSE_PROXY; if (opts.azure) { return models; // const azure = getAzureCredentials(); - // basePath = (genAzureChatCompletion(azure)) + // baseURL = (genAzureChatCompletion(azure)) // .split('/deployments')[0] // .concat(`/models?api-version=${azure.azureOpenAIApiVersion}`); // apiKey = azureOpenAIApiKey; @@ -42,32 +80,20 @@ const fetchOpenAIModels = async (opts = { azure: false, plugins: false }, _model } if (reverseProxyUrl) { - basePath = extractBaseURL(reverseProxyUrl); + baseURL = extractBaseURL(reverseProxyUrl); } - const cachedModels = await modelsCache.get(basePath); + const cachedModels = await modelsCache.get(baseURL); if (cachedModels) { return cachedModels; } - if (basePath || opts.azure) { - try { - const payload = { - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }; - - if (PROXY) { - payload.httpsAgent = new HttpsProxyAgent(PROXY); - } - const res = await axios.get(`${basePath}${opts.azure ? '' : '/models'}`, payload); - - models = res.data.data.map((item) => item.id); - // logger.debug(`Fetched ${models.length} models from ${opts.azure ? 'Azure ' : ''}OpenAI API`); - } catch (err) { - logger.error(`Failed to fetch models from ${opts.azure ? 'Azure ' : ''}OpenAI API`, err); - } + if (baseURL || opts.azure) { + models = await fetchModels({ + apiKey, + baseURL, + azure: opts.azure, + }); } if (!reverseProxyUrl) { @@ -75,7 +101,7 @@ const fetchOpenAIModels = async (opts = { azure: false, plugins: false }, _model models = models.filter((model) => regex.test(model)); } - await modelsCache.set(basePath, models); + await modelsCache.set(baseURL, models); return models; }; @@ -142,6 +168,7 @@ const getGoogleModels = () => { }; module.exports = { + fetchModels, getOpenAIModels, getChatGPTBrowserModels, getAnthropicModels, diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index 4cd1b7ce9946..b8d17106622d 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -165,6 +165,27 @@ function isEnabled(value) { return false; } +/** + * Checks if the provided value is 'user_provided'. + * + * @param {string} value - The value to check. + * @returns {boolean} - Returns true if the value is 'user_provided', otherwise false. + */ +const isUserProvided = (value) => value === 'user_provided'; + +/** + * Extracts the value of an environment variable from a string. + * @param {string} value - The value to be processed, possibly containing an env variable placeholder. + * @returns {string} - The actual value from the environment variable or the original value. + */ +function extractEnvVariable(value) { + const envVarMatch = value.match(/^\${(.+)}$/); + if (envVarMatch) { + return process.env[envVarMatch[1]] || value; + } + return value; +} + module.exports = { createOnProgress, isEnabled, @@ -172,4 +193,6 @@ module.exports = { formatSteps, formatAction, addSpaceIfNeeded, + isUserProvided, + extractEnvVariable, }; diff --git a/api/server/utils/handleText.spec.js b/api/server/utils/handleText.spec.js index ea440a89a57a..a5566fb1b2b7 100644 --- a/api/server/utils/handleText.spec.js +++ b/api/server/utils/handleText.spec.js @@ -1,4 +1,4 @@ -const { isEnabled } = require('./handleText'); +const { isEnabled, extractEnvVariable } = require('./handleText'); describe('isEnabled', () => { test('should return true when input is "true"', () => { @@ -48,4 +48,51 @@ describe('isEnabled', () => { test('should return false when input is an array', () => { expect(isEnabled([])).toBe(false); }); + + describe('extractEnvVariable', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + test('should return the value of the environment variable', () => { + process.env.TEST_VAR = 'test_value'; + expect(extractEnvVariable('${TEST_VAR}')).toBe('test_value'); + }); + + test('should return the original string if the envrionment variable is not defined correctly', () => { + process.env.TEST_VAR = 'test_value'; + expect(extractEnvVariable('${ TEST_VAR }')).toBe('${ TEST_VAR }'); + }); + + test('should return the original string if environment variable is not set', () => { + expect(extractEnvVariable('${NON_EXISTENT_VAR}')).toBe('${NON_EXISTENT_VAR}'); + }); + + test('should return the original string if it does not contain an environment variable', () => { + expect(extractEnvVariable('some_string')).toBe('some_string'); + }); + + test('should handle empty strings', () => { + expect(extractEnvVariable('')).toBe(''); + }); + + test('should handle strings without variable format', () => { + expect(extractEnvVariable('no_var_here')).toBe('no_var_here'); + }); + + test('should not process multiple variable formats', () => { + process.env.FIRST_VAR = 'first'; + process.env.SECOND_VAR = 'second'; + expect(extractEnvVariable('${FIRST_VAR} and ${SECOND_VAR}')).toBe( + '${FIRST_VAR} and ${SECOND_VAR}', + ); + }); + }); }); diff --git a/api/server/utils/streamResponse.js b/api/server/utils/streamResponse.js index 1933839fac27..3511f144cc7e 100644 --- a/api/server/utils/streamResponse.js +++ b/api/server/utils/streamResponse.js @@ -1,6 +1,8 @@ const crypto = require('crypto'); +const { parseConvo } = require('librechat-data-provider'); const { saveMessage, getMessages } = require('~/models/Message'); const { getConvo } = require('~/models/Conversation'); +const { logger } = require('~/config'); /** * Sends error data in Server Sent Events format and ends the response. @@ -65,12 +67,21 @@ const sendError = async (res, options, callback) => { if (!errorMessage.error) { const requestMessage = { messageId: parentMessageId, conversationId }; - const query = await getMessages(requestMessage); + let query = [], + convo = {}; + try { + query = await getMessages(requestMessage); + convo = await getConvo(user, conversationId); + } catch (err) { + logger.error('[sendError] Error retrieving conversation data:', err); + convo = parseConvo(errorMessage); + } + return sendMessage(res, { final: true, requestMessage: query?.[0] ? query[0] : requestMessage, responseMessage: errorMessage, - conversation: await getConvo(user, conversationId), + conversation: convo, }); } diff --git a/api/typedefs.js b/api/typedefs.js index 1ab9f6457186..e96d7dba2924 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -20,6 +20,12 @@ * @memberof typedefs */ +/** + * @exports TConfig + * @typedef {import('librechat-data-provider').TConfig} TConfig + * @memberof typedefs + */ + /** * @exports ImageMetadata * @typedef {Object} ImageMetadata @@ -280,8 +286,8 @@ * @property {boolean|{userProvide: boolean}} [chatGPTBrowser] - Flag to indicate if ChatGPT Browser endpoint is user provided, or its configuration. * @property {boolean|{userProvide: boolean}} [anthropic] - Flag to indicate if Anthropic endpoint is user provided, or its configuration. * @property {boolean|{userProvide: boolean}} [bingAI] - Flag to indicate if BingAI endpoint is user provided, or its configuration. - * @property {boolean|{userProvide: boolean}} [bingAI] - Flag to indicate if BingAI endpoint is user provided, or its configuration. - * @property {boolean|{userProvide: boolean}} [bingAI] - Flag to indicate if BingAI endpoint is user provided, or its configuration. + * @property {boolean|{userProvide: boolean}} [google] - Flag to indicate if BingAI endpoint is user provided, or its configuration. + * @property {boolean|{userProvide: boolean, userProvideURL: boolean, name: string}} [custom] - Custom Endpoint configuration. * @memberof typedefs */ @@ -313,13 +319,14 @@ * @property {boolean|{userProvide: boolean}} [anthropic] - Flag to indicate if Anthropic endpoint is user provided, or its configuration. * @property {boolean|{userProvide: boolean}} [bingAI] - Flag to indicate if BingAI endpoint is user provided, or its configuration. * @property {boolean|{userProvide: boolean}} [google] - Flag to indicate if Google endpoint is user provided, or its configuration. + * @property {boolean|{userProvide: boolean, userProvideURL: boolean, name: string}} [custom] - Custom Endpoint configuration. * @property {boolean|GptPlugins} [gptPlugins] - Configuration for GPT plugins. * @memberof typedefs */ /** * @exports EndpointConfig - * @typedef {boolean|{userProvide: boolean}|GptPlugins} EndpointConfig + * @typedef {boolean|TConfig} EndpointConfig * @memberof typedefs */ diff --git a/api/utils/index.js b/api/utils/index.js index f9194858e824..a40c53b6aba2 100644 --- a/api/utils/index.js +++ b/api/utils/index.js @@ -1,3 +1,4 @@ +const loadYaml = require('./loadYaml'); const tokenHelpers = require('./tokens'); const azureUtils = require('./azureUtils'); const extractBaseURL = require('./extractBaseURL'); @@ -8,4 +9,5 @@ module.exports = { ...tokenHelpers, extractBaseURL, findMessageContent, + loadYaml, }; diff --git a/api/utils/loadYaml.js b/api/utils/loadYaml.js new file mode 100644 index 000000000000..eec7e2ec6ced --- /dev/null +++ b/api/utils/loadYaml.js @@ -0,0 +1,13 @@ +const fs = require('fs'); +const yaml = require('js-yaml'); + +function loadYaml(filepath) { + try { + let fileContents = fs.readFileSync(filepath, 'utf8'); + return yaml.load(fileContents); + } catch (e) { + console.error(e); + } +} + +module.exports = loadYaml; diff --git a/api/utils/tokens.js b/api/utils/tokens.js index cda4755717dd..b6aa7ba58886 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -39,22 +39,26 @@ const models = [ 'gpt-3.5-turbo-0301', ]; +const openAIModels = { + 'gpt-4': 8191, + 'gpt-4-0613': 8191, + 'gpt-4-32k': 32767, + 'gpt-4-32k-0314': 32767, + 'gpt-4-32k-0613': 32767, + 'gpt-3.5-turbo': 4095, + 'gpt-3.5-turbo-0613': 4095, + 'gpt-3.5-turbo-0301': 4095, + 'gpt-3.5-turbo-16k': 15999, + 'gpt-3.5-turbo-16k-0613': 15999, + 'gpt-3.5-turbo-1106': 16380, // -5 from max + 'gpt-4-1106': 127995, // -5 from max + 'mistral-': 31995, // -5 from max +}; + // Order is important here: by model series and context size (gpt-4 then gpt-3, ascending) const maxTokensMap = { - [EModelEndpoint.openAI]: { - 'gpt-4': 8191, - 'gpt-4-0613': 8191, - 'gpt-4-32k': 32767, - 'gpt-4-32k-0314': 32767, - 'gpt-4-32k-0613': 32767, - 'gpt-3.5-turbo': 4095, - 'gpt-3.5-turbo-0613': 4095, - 'gpt-3.5-turbo-0301': 4095, - 'gpt-3.5-turbo-16k': 15999, - 'gpt-3.5-turbo-16k-0613': 15999, - 'gpt-3.5-turbo-1106': 16380, // -5 from max - 'gpt-4-1106': 127995, // -5 from max - }, + [EModelEndpoint.openAI]: openAIModels, + [EModelEndpoint.custom]: openAIModels, [EModelEndpoint.google]: { /* Max I/O is combined so we subtract the amount from max response tokens for actual total */ gemini: 32750, // -10 from max diff --git a/client/public/assets/mistral.png b/client/public/assets/mistral.png new file mode 100644 index 0000000000000000000000000000000000000000..ff2f3e8b63bd8f193f84c4d070152beed8ee7e80 GIT binary patch literal 548 zcmV+<0^9wGP)Y}1MV*b z|IGmJzySTQ0RN-_`=0>JrUCzP0NP>!|4ag{a|HiM0`Elw%sdAF00!>?2Z}Kc%mfQY zI21)D6-E^pDIysq8yXrK8-S}Y(f|MeBy>_vQvei0LtkKGZi&vW;-{zsRfY%?3goQMIEfk3x#eTU?V~D0@oUz*+yVbEeIP_5fG z?DyvfiupL`NQC#IV>%IE)Hfa97FnaCK{YYF ziLDQ~`LwfLMtNlsQ7VzBtAjXn51U(*UkiuxicEW9ToqbByh0Mftm2OLS)VlfvuHQF m>dpTmOsJ5T|DQJ$!|(-q1XZ9vsKFut0000gmTxNncc&cv=oZvGlY-QDiCV$tA(=_OX;n@|Lo2&JDu5?H@kPV z<~EtU*|+cgKi~U*y|h7+8KmHI&8bIGEr!;;)R+82q zCP~L*45na4y&uho{;_m?n>;j>JIK=HyMXh3-`w{d zgFA!^&tmkyi-&$+wz|-F1kJTf`E2m?ThP1JH;-^VX&2a6J^0jO%tPp()zr5)f4d!n z{bpk>_fh}2sP_O)!uz75WlxM6AM6KT58E31R{+ktp!G)5pMN~$b9PTp&qw@y^j@u2 zr$J8h0Aq6i&RWZ0Kd=U~-H)i*qcCO@et!};zY&B%-yLo7Cmmf;uvW$baDuuU1I=d1 z$b2CTPDNMFgC4iEhQUW2jolD7pZao;zojeRgD#dp7OU~tfcI}ad(gL{JScxYYHn0N z=JnfVmbU*s^P!E{jb|<%vQJZ=$(2BGk4-LivPpc{)-&f#HeL@O_WG;tJ|(|UXzdrg zIifAur!|^dILA&OC z|9e^I*A$Dz1JdBN3d+f_e~$>_^ll{-#Ee3ZNXuQ&mZNzl!mvi&G#@?`W{-ZeCK>Ni)d=k$ye4Wmg z+9vz{h{}#F6$cKh7IKG@{{)Y(`R4D0{(3ed7K-AT{C|noHTkl5J&!>r*`dPShep9` z?0@)kzOqux9UVE&;I{zQO7QWPkKSgv`Q#7!wo<$j1<&gT%==*f=6K(u+9Wu=F~I#G z$u|mCntl&9Oeu^{w-=p3gjb8xy!gslLeU}ec)E+?Y!tUI-u{C_ygaN?%MX8iIFba3D zf32w;0=sh^_Re48=zu+W#xdEqf-t=A(Ala6j91A%dHqS+YCivQ_{m>kKILUma;W!f zY=dPezbu>VTwBe~c2+2-Sq1iW(D%otxdyL@b0f8UKbg~3*T zHi;Z)3}S_km36iVehB_DMixnb*JR{{{Ce{1N$IdvekSnyeM)P+#M44{WnohMG_FeZ z1C8GSIw)Uj2S0SG#QKIP+U(-;8Q8Zs!IwV{I{bBo)-)NekGl%~f3I9S3Rhe|eEp~0 zcvRd5D~cL7QT_0jzfKY-Oceep zxci3$s7`_stu!CcRGkDunEL$RN}xtdd_VqJ8o#ijy1C?j$%kc9jL)C8On%}1s0Q2Q zYrU9)a$&sBdI9n2zQ#+fCUsE`foD3!+V=A0GuHL@AB;1L^&b6hi+T+5uHOx7OHPcd z$xI}VbS}0A=V)$?&0wpL>;4I`@k^+WZK|KG@J)#SUqSzWn&_{`z!ZOOMm_STVzKuV z6G^8Bo6fN4oCIs&e4Ojt4_bGI(A7E?4vF8vBlMG*Lvg-`tJCctbmch2!ncE7%Ii9p zQ+>eCJCI91#pIo&eLZyR#`c>0I_U0L9X3pFSZ9!n{1*00W~6Z*nwFOu>WY*{W1l?? zT*-5#59BGljsiSARWWiWrJIH2dGW-nN3b%g9fQ_+Wx- z>-QXKo62{gH^fzfwgok1*f_GKw>^jZ37#xkwN3F){k%eHbp{fIMREbYS(eH!2dqBu zy&;XfF=r>%X~9-Av%~If1o3*`iGEt+*jqa6!&${*PvY1OvRWVUV6zm00hY3{akipR9{ zBL;ci9?Z-kd(tUd(!dd=AGR20R=8s}@cWr*@O65QDE;;E@($ebyN~V)i2Ct;gZ}Y7 zzZG%Z&x^eu-Wp0L|8e@sc8$UQtQt0Zk4dWwipA6F#r53=djoW|7sJ={+AGrR=SqQ} z0Uvf|(z8`RuWR!BbB-o|(Ng&tlxH{2N7C~04(6-H;v%8*Xxqr#0{Kx4r-+;Z`uXtQ zDOMU$m6Jh7AM(vD@X5OYSJcP7O#XD;>i&a0xUE{ply4o!LH97s%=~uwp!fy%F_DwY zJ%~TShcFL_XV@TO?jKjwzN1^kLrwJ8@!)(}E+MX%mnNq0%Zk<{;yaud^FlgZ@KN|W z@bAX)cQ~4Ibw$r!TvGWw+-JSSQQ1#$ru9ICeCQt9{X_XBZJ)DIZHMm9f``8X=AH(g8ImY9#nP;`Di}DKh?mpGq(%7gl8H@BG_T|^4)57?yF6_%MK^Av` zhv5GKL}(EkYp@p(x8F>)oN-Fpc%huRg4#yo?&^QRix@r0$B)9V-02MPnId;gL|7}S z*;BE<^RWF}(31V@fUSZJ)>*ZJ98aVcAwsu*tn2&o*>3Vbh)H_z7~tbkw2eI5y~B*V zE_@Vr3^gS;?+(!!e80gSh5xv`T|efhPvo6=A>3}=agfiXd%JY+qwd@Lj-z+*wkTxH zZ9`7Ifc|&W%6E& (newValue: number | string | boolean) => void; @@ -141,7 +148,7 @@ export type TDisplayProps = TText & export type TConfigProps = { userKey: string; setUserKey: React.Dispatch>; - endpoint: string; + endpoint: EModelEndpoint | string; }; export type TDangerButtonProps = { @@ -194,9 +201,11 @@ export type IconProps = Pick & Pick & { size?: number; button?: boolean; + iconURL?: string; message?: boolean; className?: string; - endpoint?: string | null; + endpoint?: EModelEndpoint | string | null; + endpointType?: EModelEndpoint | null; }; export type Option = Record & { diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 9fce650c83d2..efd6285d299a 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -30,6 +30,8 @@ export default function ChatForm({ index = 0 }) { }; const { requiresKey } = useRequiresKey(); + const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null }; + const endpoint = endpointType ?? _endpoint; return (
) => setText(e.target.value)} setText={setText} submitMessage={submitMessage} - endpoint={conversation?.endpoint} + endpoint={endpoint} /> - + {isSubmitting && showStopButton ? ( ) : ( diff --git a/client/src/components/Chat/Input/HeaderOptions.tsx b/client/src/components/Chat/Input/HeaderOptions.tsx index 20415d1d4967..1bfd4b142a2b 100644 --- a/client/src/components/Chat/Input/HeaderOptions.tsx +++ b/client/src/components/Chat/Input/HeaderOptions.tsx @@ -2,7 +2,8 @@ import { useRecoilState } from 'recoil'; import { Settings2 } from 'lucide-react'; import { Root, Anchor } from '@radix-ui/react-popover'; import { useState, useEffect, useMemo } from 'react'; -import { tPresetSchema, EModelEndpoint } from 'librechat-data-provider'; +import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider'; +import type { TPreset } from 'librechat-data-provider'; import { EndpointSettings, SaveAsPresetDialog } from '~/components/Endpoints'; import { ModelSelect } from '~/components/Input/ModelSelect'; import { PluginStoreDialog } from '~/components'; @@ -106,7 +107,11 @@ export default function OptionsBar() { setShowPopover((prev) => !prev); return ( -
+
setShowPopover(false)} - PopoverButtons={} + PopoverButtons={} >
diff --git a/client/src/components/Chat/Input/PopoverButtons.tsx b/client/src/components/Chat/Input/PopoverButtons.tsx index 9116909b4463..ce053648d345 100644 --- a/client/src/components/Chat/Input/PopoverButtons.tsx +++ b/client/src/components/Chat/Input/PopoverButtons.tsx @@ -27,7 +27,8 @@ export default function PopoverButtons({ setShowAgentSettings, } = useChatContext(); - const { model, endpoint } = conversation ?? {}; + const { model, endpoint: _endpoint, endpointType } = conversation ?? {}; + const endpoint = endpointType ?? _endpoint; const isGenerativeModel = model?.toLowerCase()?.includes('gemini'); const isChatModel = !isGenerativeModel && model?.toLowerCase()?.includes('chat'); const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? ''); diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index cd6f9af4a8bf..fb38c38e4356 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -1,10 +1,12 @@ import type { ReactNode } from 'react'; +import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import { EModelEndpoint } from 'librechat-data-provider'; import { icons } from './Menus/Endpoints/Icons'; import { useChatContext } from '~/Providers'; import { useLocalize } from '~/hooks'; export default function Landing({ Header }: { Header?: ReactNode }) { + const { data: endpointsConfig } = useGetEndpointsQuery(); const { conversation } = useChatContext(); const localize = useLocalize(); let { endpoint } = conversation ?? {}; @@ -16,13 +18,22 @@ export default function Landing({ Header }: { Header?: ReactNode }) { ) { endpoint = EModelEndpoint.openAI; } + + const iconKey = endpointsConfig?.[endpoint ?? '']?.type ? 'unknown' : endpoint ?? 'unknown'; + return (
{Header && Header}
- {icons[endpoint ?? 'unknown']({ size: 41, className: 'h-2/3 w-2/3' })} + {icons[iconKey]({ + size: 41, + context: 'landing', + className: 'h-2/3 w-2/3', + endpoint: endpoint as EModelEndpoint | string, + iconURL: endpointsConfig?.[endpoint ?? ''].iconURL, + })}
diff --git a/client/src/components/Chat/Menus/Endpoints/Icons.tsx b/client/src/components/Chat/Menus/Endpoints/Icons.tsx index 56aed93f1241..4bfb593d8373 100644 --- a/client/src/components/Chat/Menus/Endpoints/Icons.tsx +++ b/client/src/components/Chat/Menus/Endpoints/Icons.tsx @@ -6,8 +6,10 @@ import { AzureMinimalIcon, BingAIMinimalIcon, GoogleMinimalIcon, + CustomMinimalIcon, LightningIcon, } from '~/components/svg'; +import UnknownIcon from './UnknownIcon'; import { cn } from '~/utils'; export const icons = { @@ -18,6 +20,7 @@ export const icons = { [EModelEndpoint.chatGPTBrowser]: LightningIcon, [EModelEndpoint.google]: GoogleMinimalIcon, [EModelEndpoint.bingAI]: BingAIMinimalIcon, + [EModelEndpoint.custom]: CustomMinimalIcon, [EModelEndpoint.assistant]: ({ className = '' }) => ( ), - unknown: GPTIcon, + unknown: UnknownIcon, }; diff --git a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx index b555ad8d4447..fd516bbab3b2 100644 --- a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Settings } from 'lucide-react'; import { EModelEndpoint } from 'librechat-data-provider'; +import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { FC } from 'react'; import { useLocalize, useUserKey } from '~/hooks'; import { SetKeyDialog } from '~/components/Input/SetKeyDialog'; @@ -26,7 +27,8 @@ const MenuItem: FC = ({ userProvidesKey, ...rest }) => { - const Icon = icons[endpoint] ?? icons.unknown; + const { data: endpointsConfig } = useGetEndpointsQuery(); + const [isDialogOpen, setDialogOpen] = useState(false); const { newConversation } = useChatContext(); const { getExpiry } = useUserKey(endpoint); @@ -44,6 +46,10 @@ const MenuItem: FC = ({ } }; + const endpointType = endpointsConfig?.[endpoint ?? '']?.type; + const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown'; + const Icon = icons[iconKey]; + return ( <>
= ({
- {} + { + + }
{title}
{description}
@@ -128,7 +142,13 @@ const MenuItem: FC = ({
{userProvidesKey && ( - + )} ); diff --git a/client/src/components/Chat/Menus/Endpoints/UnknownIcon.tsx b/client/src/components/Chat/Menus/Endpoints/UnknownIcon.tsx new file mode 100644 index 000000000000..496f627acad6 --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/UnknownIcon.tsx @@ -0,0 +1,36 @@ +import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider'; +import { CustomMinimalIcon } from '~/components/svg'; + +export default function UnknownIcon({ + className = '', + endpoint, + iconURL, + context, +}: { + iconURL?: string; + className?: string; + endpoint: EModelEndpoint | string | null; + context?: 'landing' | 'menu-item' | 'nav' | 'message'; +}) { + if (!endpoint) { + return ; + } + + const currentEndpoint = endpoint.toLowerCase(); + + if (iconURL) { + return {`${endpoint}; + } else if (currentEndpoint === KnownEndpoints.mistral) { + return ( + Mistral AI Icon + ); + } else if (currentEndpoint === KnownEndpoints.openrouter) { + return OpenRouter Icon; + } + + return ; +} diff --git a/client/src/components/Chat/Menus/EndpointsMenu.tsx b/client/src/components/Chat/Menus/EndpointsMenu.tsx index a4b5ed439ae8..f7339ac2989f 100644 --- a/client/src/components/Chat/Menus/EndpointsMenu.tsx +++ b/client/src/components/Chat/Menus/EndpointsMenu.tsx @@ -21,7 +21,7 @@ const EndpointsMenu: FC = () => { } return ( - +
diff --git a/client/src/components/Chat/Menus/Presets/PresetItems.tsx b/client/src/components/Chat/Menus/Presets/PresetItems.tsx index bdbc7870bf26..3e30b6b2c5b8 100644 --- a/client/src/components/Chat/Menus/Presets/PresetItems.tsx +++ b/client/src/components/Chat/Menus/Presets/PresetItems.tsx @@ -2,6 +2,7 @@ import { Trash2 } from 'lucide-react'; import { useRecoilValue } from 'recoil'; import { Close } from '@radix-ui/react-popover'; import { Flipper, Flipped } from 'react-flip-toolkit'; +import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { FC } from 'react'; import type { TPreset } from 'librechat-data-provider'; import FileUpload from '~/components/Input/EndpointMenu/FileUpload'; @@ -31,6 +32,7 @@ const PresetItems: FC<{ clearAllPresets, onFileSelected, }) => { + const { data: endpointsConfig } = useGetEndpointsQuery(); const defaultPreset = useRecoilValue(store.defaultPreset); const localize = useLocalize(); return ( @@ -93,6 +95,10 @@ const PresetItems: FC<{ return null; } + const iconKey = endpointsConfig?.[preset.endpoint ?? '']?.type + ? 'unknown' + : preset.endpoint ?? 'unknown'; + return (
@@ -103,8 +109,11 @@ const PresetItems: FC<{ title={getPresetTitle(preset)} disableHover={true} onClick={() => onSelectPreset(preset)} - icon={icons[preset.endpoint ?? 'unknown']({ + icon={icons[iconKey]({ + context: 'menu-item', + iconURL: endpointsConfig?.[preset.endpoint ?? ''].iconURL, className: 'icon-md mr-1 dark:text-white', + endpoint: preset.endpoint, })} selected={false} data-testid={`preset-item-${preset}`} diff --git a/client/src/components/Chat/Menus/PresetsMenu.tsx b/client/src/components/Chat/Menus/PresetsMenu.tsx index bdd47895e92f..01a34a10bc99 100644 --- a/client/src/components/Chat/Menus/PresetsMenu.tsx +++ b/client/src/components/Chat/Menus/PresetsMenu.tsx @@ -3,6 +3,7 @@ import { BookCopy } from 'lucide-react'; import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover'; import { EditPresetDialog, PresetItems } from './Presets'; import { useLocalize, usePresets } from '~/hooks'; +import { useChatContext } from '~/Providers'; import { cn } from '~/utils'; const PresetsMenu: FC = () => { @@ -18,6 +19,7 @@ const PresetsMenu: FC = () => { submitPreset, exportPreset, } = usePresets(); + const { preset } = useChatContext(); const presets = presetsQuery.data || []; return ( @@ -64,7 +66,7 @@ const PresetsMenu: FC = () => {
- + {preset && } ); }; diff --git a/client/src/components/Chat/Messages/Content/EditMessage.tsx b/client/src/components/Chat/Messages/Content/EditMessage.tsx index 4b8b5c73af43..70d33aaac01e 100644 --- a/client/src/components/Chat/Messages/Content/EditMessage.tsx +++ b/client/src/components/Chat/Messages/Content/EditMessage.tsx @@ -19,7 +19,8 @@ const EditMessage = ({ const textEditor = useRef(null); const { conversationId, parentMessageId, messageId } = message; - const { endpoint } = conversation ?? { endpoint: null }; + const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null }; + const endpoint = endpointType ?? _endpoint; const updateMessageMutation = useUpdateMessageMutation(conversationId ?? ''); const localize = useLocalize(); diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index ba891c1371a7..2166c72ae8fe 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import type { TConversation, TMessage } from 'librechat-data-provider'; import { Clipboard, CheckMark, EditIcon, RegenerateIcon, ContinueIcon } from '~/components/svg'; -import { useGenerations, useLocalize } from '~/hooks'; +import { useGenerationsByLatest, useLocalize } from '~/hooks'; import { cn } from '~/utils'; type THoverButtons = { @@ -28,9 +28,10 @@ export default function HoverButtons({ latestMessage, }: THoverButtons) { const localize = useLocalize(); - const { endpoint } = conversation ?? {}; + const { endpoint: _endpoint, endpointType } = conversation ?? {}; + const endpoint = endpointType ?? _endpoint; const [isCopied, setIsCopied] = useState(false); - const { hideEditButton, regenerateEnabled, continueSupported } = useGenerations({ + const { hideEditButton, regenerateEnabled, continueSupported } = useGenerationsByLatest({ isEditing, isSubmitting, message, diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 5e3cd822e739..aa7d2cfd6595 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -1,7 +1,10 @@ import { useRecoilValue } from 'recoil'; import { useState, useRef } from 'react'; import { useParams } from 'react-router-dom'; -import { useUpdateConversationMutation } from 'librechat-data-provider/react-query'; +import { + useGetEndpointsQuery, + useUpdateConversationMutation, +} from 'librechat-data-provider/react-query'; import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; import { useConversations, useNavigateToConvo } from '~/hooks'; import { MinimalIcon } from '~/components/Endpoints'; @@ -15,8 +18,9 @@ type KeyEvent = KeyboardEvent; export default function Conversation({ conversation, retainView, toggleNav, i }) { const { conversationId: currentConvoId } = useParams(); - const activeConvos = useRecoilValue(store.allConversationsSelector); const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? ''); + const activeConvos = useRecoilValue(store.allConversationsSelector); + const { data: endpointsConfig } = useGetEndpointsQuery(); const { refreshConversations } = useConversations(); const { navigateToConvo } = useNavigateToConvo(); const { showToast } = useToastContext(); @@ -86,7 +90,9 @@ export default function Conversation({ conversation, retainView, toggleNav, i }) const icon = MinimalIcon({ size: 20, + iconURL: endpointsConfig?.[conversation.endpoint ?? '']?.iconURL, endpoint: conversation.endpoint, + endpointType: conversation.endpointType, model: conversation.model, error: false, className: 'mr-0', diff --git a/client/src/components/Endpoints/EndpointSettings.tsx b/client/src/components/Endpoints/EndpointSettings.tsx index 29b3e3dc30ba..3a3c04f069b6 100644 --- a/client/src/components/Endpoints/EndpointSettings.tsx +++ b/client/src/components/Endpoints/EndpointSettings.tsx @@ -17,8 +17,9 @@ export default function Settings({ } const { settings, multiViewSettings } = getSettings(isMultiChat); - const { endpoint } = conversation; - const models = modelsConfig?.[endpoint] ?? []; + const { endpoint: _endpoint, endpointType } = conversation; + const models = modelsConfig?.[_endpoint] ?? []; + const endpoint = endpointType ?? _endpoint; const OptionComponent = settings[endpoint]; if (OptionComponent) { diff --git a/client/src/components/Endpoints/Icon.tsx b/client/src/components/Endpoints/Icon.tsx index 23e2845eabf5..5d9d3fff7c18 100644 --- a/client/src/components/Endpoints/Icon.tsx +++ b/client/src/components/Endpoints/Icon.tsx @@ -1,9 +1,11 @@ import { EModelEndpoint } from 'librechat-data-provider'; +import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon'; import { Plugin, GPTIcon, AnthropicIcon, AzureMinimalIcon, + CustomMinimalIcon, PaLMIcon, CodeyIcon, GeminiIcon, @@ -13,9 +15,8 @@ import { IconProps } from '~/common'; import { cn } from '~/utils'; const Icon: React.FC = (props) => { - const { size = 30, isCreatedByUser, button, model = '', endpoint, error, jailbreak } = props; - const { user } = useAuthContext(); + const { size = 30, isCreatedByUser, button, model = '', endpoint, error, jailbreak } = props; if (isCreatedByUser) { const username = user?.name || 'User'; @@ -94,8 +95,22 @@ const Icon: React.FC = (props) => { : `rgba(0, 163, 255, ${button ? 0.75 : 1})`, name: 'ChatGPT', }, + [EModelEndpoint.custom]: { + icon: , + name: 'Custom', + }, null: { icon: , bg: 'grey', name: 'N/A' }, - default: { icon: , bg: 'grey', name: 'UNKNOWN' }, + default: { + icon: ( + + ), + name: endpoint, + }, }; const { icon, bg, name } = diff --git a/client/src/components/Endpoints/MinimalIcon.tsx b/client/src/components/Endpoints/MinimalIcon.tsx index 5af2661fd679..1499dec7ce18 100644 --- a/client/src/components/Endpoints/MinimalIcon.tsx +++ b/client/src/components/Endpoints/MinimalIcon.tsx @@ -1,4 +1,5 @@ import { EModelEndpoint } from 'librechat-data-provider'; +import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon'; import { AzureMinimalIcon, OpenAIMinimalIcon, @@ -6,6 +7,7 @@ import { PluginMinimalIcon, BingAIMinimalIcon, GoogleMinimalIcon, + CustomMinimalIcon, AnthropicIcon, } from '~/components/svg'; import { cn } from '~/utils'; @@ -32,9 +34,23 @@ const MinimalIcon: React.FC = (props) => { icon: , name: props.modelLabel || 'Claude', }, + [EModelEndpoint.custom]: { + icon: , + name: 'Custom', + }, [EModelEndpoint.bingAI]: { icon: , name: 'BingAI' }, [EModelEndpoint.chatGPTBrowser]: { icon: , name: 'ChatGPT' }, - default: { icon: , name: 'UNKNOWN' }, + default: { + icon: ( + + ), + name: endpoint, + }, }; const { icon, name } = endpointIcons[endpoint] ?? endpointIcons.default; diff --git a/client/src/components/Endpoints/Settings/OpenAI.tsx b/client/src/components/Endpoints/Settings/OpenAI.tsx index 062fd1e0bc95..77b224c8d37c 100644 --- a/client/src/components/Endpoints/Settings/OpenAI.tsx +++ b/client/src/components/Endpoints/Settings/OpenAI.tsx @@ -1,6 +1,4 @@ import TextareaAutosize from 'react-textarea-autosize'; -import type { TModelSelectProps } from '~/common'; -import { ESide } from '~/common'; import { SelectDropDown, Input, @@ -10,9 +8,11 @@ import { HoverCard, HoverCardTrigger, } from '~/components/ui'; -import OptionHover from './OptionHover'; import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/'; +import type { TModelSelectProps } from '~/common'; +import OptionHover from './OptionHover'; import { useLocalize } from '~/hooks'; +import { ESide } from '~/common'; export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) { const localize = useLocalize(); @@ -28,9 +28,6 @@ export default function Settings({ conversation, setOption, models, readonly }: frequency_penalty: freqP, presence_penalty: presP, } = conversation; - const endpoint = conversation.endpoint || 'openAI'; - const isOpenAI = endpoint === 'openAI' || endpoint === 'azureOpenAI'; - const setModel = setOption('model'); const setChatGptLabel = setOption('chatGptLabel'); const setPromptPrefix = setOption('promptPrefix'); @@ -52,47 +49,43 @@ export default function Settings({ conversation, setOption, models, readonly }: containerClassName="flex w-full resize-none" />
- {isOpenAI && ( - <> -
- - setChatGptLabel(e.target.value ?? null)} - placeholder={localize('com_endpoint_openai_custom_name_placeholder')} - className={cn( - defaultTextProps, - 'dark:bg-gray-700 dark:hover:bg-gray-700/60 dark:focus:bg-gray-700', - 'flex h-10 max-h-10 w-full resize-none px-3 py-2', - removeFocusOutlines, - )} - /> -
-
- - setPromptPrefix(e.target.value ?? null)} - placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')} - className={cn( - defaultTextProps, - 'dark:bg-gray-700 dark:hover:bg-gray-700/60 dark:focus:bg-gray-700', - 'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ', - )} - /> -
- - )} +
+ + setChatGptLabel(e.target.value ?? null)} + placeholder={localize('com_endpoint_openai_custom_name_placeholder')} + className={cn( + defaultTextProps, + 'dark:bg-gray-700 dark:hover:bg-gray-700/60 dark:focus:bg-gray-700', + 'flex h-10 max-h-10 w-full resize-none px-3 py-2', + removeFocusOutlines, + )} + /> +
+
+ + setPromptPrefix(e.target.value ?? null)} + placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')} + className={cn( + defaultTextProps, + 'dark:bg-gray-700 dark:hover:bg-gray-700/60 dark:focus:bg-gray-700', + 'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ', + )} + /> +
@@ -101,7 +94,7 @@ export default function Settings({ conversation, setOption, models, readonly }: } = { [EModelEndpoint.openAI]: OpenAISettings, + [EModelEndpoint.custom]: OpenAISettings, [EModelEndpoint.azureOpenAI]: OpenAISettings, [EModelEndpoint.bingAI]: BingAISettings, [EModelEndpoint.anthropic]: AnthropicSettings, diff --git a/client/src/components/Input/ModelSelect/ModelSelect.tsx b/client/src/components/Input/ModelSelect/ModelSelect.tsx index 3eef9d3c730d..aeb354008144 100644 --- a/client/src/components/Input/ModelSelect/ModelSelect.tsx +++ b/client/src/components/Input/ModelSelect/ModelSelect.tsx @@ -28,9 +28,11 @@ export default function ModelSelect({ return null; } - const { endpoint } = conversation; + const { endpoint: _endpoint, endpointType } = conversation; + const models = modelsConfig?.[_endpoint] ?? []; + const endpoint = endpointType ?? _endpoint; + const OptionComponent = isMultiChat ? multiChatOptions[endpoint] : options[endpoint]; - const models = modelsConfig?.[endpoint] ?? []; if (!OptionComponent) { return null; diff --git a/client/src/components/Input/ModelSelect/options.ts b/client/src/components/Input/ModelSelect/options.ts index 9863a4930e06..ce231e86371a 100644 --- a/client/src/components/Input/ModelSelect/options.ts +++ b/client/src/components/Input/ModelSelect/options.ts @@ -12,6 +12,7 @@ import PluginsByIndex from './PluginsByIndex'; export const options: { [key: string]: FC } = { [EModelEndpoint.openAI]: OpenAI, + [EModelEndpoint.custom]: OpenAI, [EModelEndpoint.azureOpenAI]: OpenAI, [EModelEndpoint.bingAI]: BingAI, [EModelEndpoint.google]: Google, diff --git a/client/src/components/Input/SetKeyDialog/CustomEndpoint.tsx b/client/src/components/Input/SetKeyDialog/CustomEndpoint.tsx new file mode 100644 index 000000000000..fd5aa5d74316 --- /dev/null +++ b/client/src/components/Input/SetKeyDialog/CustomEndpoint.tsx @@ -0,0 +1,46 @@ +import { EModelEndpoint } from 'librechat-data-provider'; +import { useFormContext, Controller } from 'react-hook-form'; +import InputWithLabel from './InputWithLabel'; + +const CustomEndpoint = ({ + endpoint, + userProvideURL, +}: { + endpoint: EModelEndpoint | string; + userProvideURL?: boolean | null; +}) => { + const { control } = useFormContext(); + return ( + + ( + + )} + /> + {userProvideURL && ( + ( + + )} + /> + )} + + ); +}; + +export default CustomEndpoint; diff --git a/client/src/components/Input/SetKeyDialog/InputWithLabel.tsx b/client/src/components/Input/SetKeyDialog/InputWithLabel.tsx index 270e954ea758..2266a7e460a0 100644 --- a/client/src/components/Input/SetKeyDialog/InputWithLabel.tsx +++ b/client/src/components/Input/SetKeyDialog/InputWithLabel.tsx @@ -1,21 +1,26 @@ -import React, { ChangeEvent, FC } from 'react'; +import { forwardRef } from 'react'; +import type { ChangeEvent, FC, Ref } from 'react'; import { cn, defaultTextPropsLabel, removeFocusOutlines } from '~/utils/'; import { Input, Label } from '~/components/ui'; import { useLocalize } from '~/hooks'; interface InputWithLabelProps { + id: string; value: string; - onChange: (event: ChangeEvent) => void; label: string; subLabel?: string; - id: string; + onChange: (event: ChangeEvent) => void; + labelClassName?: string; + inputClassName?: string; + ref?: Ref; } -const InputWithLabel: FC = ({ value, onChange, label, subLabel, id }) => { +const InputWithLabel: FC = forwardRef((props, ref) => { + const { id, value, label, subLabel, onChange, labelClassName = '', inputClassName = '' } = props; const localize = useLocalize(); return ( <> -
+
@@ -24,21 +29,22 @@ const InputWithLabel: FC = ({ value, onChange, label, subLa )}
- ); -}; +}); export default InputWithLabel; diff --git a/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx b/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx index bfb2d09d37df..ab259fc58c22 100644 --- a/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx +++ b/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx @@ -1,10 +1,13 @@ import React, { useState } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; import { EModelEndpoint, alternateName } from 'librechat-data-provider'; import type { TDialogProps } from '~/common'; import DialogTemplate from '~/components/ui/DialogTemplate'; import { RevokeKeysButton } from '~/components/Nav'; import { Dialog, Dropdown } from '~/components/ui'; import { useUserKey, useLocalize } from '~/hooks'; +import { useToastContext } from '~/Providers'; +import CustomConfig from './CustomEndpoint'; import GoogleConfig from './GoogleConfig'; import OpenAIConfig from './OpenAIConfig'; import OtherConfig from './OtherConfig'; @@ -13,6 +16,7 @@ import HelpText from './HelpText'; const endpointComponents = { [EModelEndpoint.google]: GoogleConfig, [EModelEndpoint.openAI]: OpenAIConfig, + [EModelEndpoint.custom]: CustomConfig, [EModelEndpoint.azureOpenAI]: OpenAIConfig, [EModelEndpoint.gptPlugins]: OpenAIConfig, default: OtherConfig, @@ -31,12 +35,28 @@ const SetKeyDialog = ({ open, onOpenChange, endpoint, + endpointType, + userProvideURL, }: Pick & { - endpoint: string; + endpoint: EModelEndpoint | string; + endpointType?: EModelEndpoint; + userProvideURL?: boolean | null; }) => { + const methods = useForm({ + defaultValues: { + apiKey: '', + baseURL: '', + // TODO: allow endpoint definitions from user + // name: '', + // TODO: add custom endpoint models defined by user + // models: '', + }, + }); + const [userKey, setUserKey] = useState(''); const [expiresAtLabel, setExpiresAtLabel] = useState(EXPIRY.TWELVE_HOURS.display); const { getExpiry, saveUserKey } = useUserKey(endpoint); + const { showToast } = useToastContext(); const localize = useLocalize(); const expirationOptions = Object.values(EXPIRY); @@ -48,12 +68,42 @@ const SetKeyDialog = ({ const submit = () => { const selectedOption = expirationOptions.find((option) => option.display === expiresAtLabel); const expiresAt = Date.now() + (selectedOption ? selectedOption.value : 0); - saveUserKey(userKey, expiresAt); - onOpenChange(false); + + const saveKey = (key: string) => { + saveUserKey(key, expiresAt); + onOpenChange(false); + }; + + if (endpoint === EModelEndpoint.custom || endpointType === EModelEndpoint.custom) { + // TODO: handle other user provided options besides baseURL and apiKey + methods.handleSubmit((data) => { + const emptyValues = Object.keys(data).filter((key) => { + if (key === 'baseURL' && !userProvideURL) { + return false; + } + return data[key] === ''; + }); + + if (emptyValues.length > 0) { + showToast({ + message: 'The following fields are required: ' + emptyValues.join(', '), + status: 'error', + }); + onOpenChange(true); + } else { + saveKey(JSON.stringify(data)); + methods.reset(); + } + })(); + return; + } + + saveKey(userKey); setUserKey(''); }; - const EndpointComponent = endpointComponents[endpoint] ?? endpointComponents['default']; + const EndpointComponent = + endpointComponents[endpointType ?? endpoint] ?? endpointComponents['default']; const expiryTime = getExpiry(); return ( @@ -77,7 +127,14 @@ const SetKeyDialog = ({ options={expirationOptions.map((option) => option.display)} width={185} /> - + + +
} diff --git a/client/src/components/svg/CustomMinimalIcon.tsx b/client/src/components/svg/CustomMinimalIcon.tsx new file mode 100644 index 000000000000..196dd779c2fc --- /dev/null +++ b/client/src/components/svg/CustomMinimalIcon.tsx @@ -0,0 +1,30 @@ +import { cn } from '~/utils'; +export default function CustomMinimalIcon({ + size = 25, + className = '', +}: { + size?: number; + className?: string; +}) { + return ( + + + + + + + + + ); +} diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index 3ad62c93eeac..5421233eaabf 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -8,6 +8,7 @@ export { default as Clipboard } from './Clipboard'; export { default as CheckMark } from './CheckMark'; export { default as CrossIcon } from './CrossIcon'; export { default as LogOutIcon } from './LogOutIcon'; +export { default as CustomMinimalIcon } from './CustomMinimalIcon'; export { default as LightningIcon } from './LightningIcon'; export { default as AttachmentIcon } from './AttachmentIcon'; export { default as MessagesSquared } from './MessagesSquared'; diff --git a/client/src/hooks/Conversations/index.ts b/client/src/hooks/Conversations/index.ts index 666341ddd642..5a84f39f3ab6 100644 --- a/client/src/hooks/Conversations/index.ts +++ b/client/src/hooks/Conversations/index.ts @@ -1 +1,2 @@ export { default as usePresets } from './usePresets'; +export { default as useGetSender } from './useGetSender'; diff --git a/client/src/hooks/Conversations/useGetSender.ts b/client/src/hooks/Conversations/useGetSender.ts new file mode 100644 index 000000000000..0b8ed9ffea36 --- /dev/null +++ b/client/src/hooks/Conversations/useGetSender.ts @@ -0,0 +1,15 @@ +import { useCallback } from 'react'; +import { getResponseSender } from 'librechat-data-provider'; +import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; +import type { TEndpointOption, TEndpointsConfig } from 'librechat-data-provider'; + +export default function useGetSender() { + const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); + return useCallback( + (endpointOption: TEndpointOption) => { + const { modelDisplayLabel } = endpointsConfig[endpointOption.endpoint ?? ''] ?? {}; + return getResponseSender({ ...endpointOption, modelDisplayLabel }); + }, + [endpointsConfig], + ); +} diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index 9d5e5cf81993..6705a8157b53 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -1,7 +1,8 @@ -import { useEffect, useRef } from 'react'; import debounce from 'lodash/debounce'; -import { TEndpointOption, getResponseSender } from 'librechat-data-provider'; +import { useEffect, useRef } from 'react'; +import { TEndpointOption } from 'librechat-data-provider'; import type { KeyboardEvent } from 'react'; +import useGetSender from '~/hooks/Conversations/useGetSender'; import { useChatContext } from '~/Providers/ChatContext'; import useFileHandling from '~/hooks/useFileHandling'; import useLocalize from '~/hooks/useLocalize'; @@ -14,6 +15,7 @@ export default function useTextarea({ setText, submitMessage, disabled = false } const isComposing = useRef(false); const inputRef = useRef(null); const { handleFiles } = useFileHandling(); + const getSender = useGetSender(); const localize = useLocalize(); const { conversationId, jailbreak } = conversation || {}; @@ -59,7 +61,7 @@ export default function useTextarea({ setText, submitMessage, disabled = false } return localize('com_endpoint_message_not_appendable'); } - const sender = getResponseSender(conversation as TEndpointOption); + const sender = getSender(conversation as TEndpointOption); return `${localize('com_endpoint_message')} ${sender ? sender : 'ChatGPT'}…`; }; @@ -82,7 +84,7 @@ export default function useTextarea({ setText, submitMessage, disabled = false } debouncedSetPlaceholder(); return () => debouncedSetPlaceholder.cancel(); - }, [conversation, disabled, latestMessage, isNotAppendable, localize]); + }, [conversation, disabled, latestMessage, isNotAppendable, localize, getSender]); const handleKeyDown = (e: KeyEvent) => { if (e.key === 'Enter' && isSubmitting) { diff --git a/client/src/hooks/Messages/useMessageHelpers.ts b/client/src/hooks/Messages/useMessageHelpers.ts index a6c03462509f..285d1a88deee 100644 --- a/client/src/hooks/Messages/useMessageHelpers.ts +++ b/client/src/hooks/Messages/useMessageHelpers.ts @@ -1,5 +1,6 @@ -import { useEffect, useRef } from 'react'; import copy from 'copy-to-clipboard'; +import { useEffect, useRef } from 'react'; +import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { TMessage } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; import Icon from '~/components/Endpoints/Icon'; @@ -7,6 +8,7 @@ import { useChatContext } from '~/Providers'; export default function useMessageHelpers(props: TMessageProps) { const latestText = useRef(''); + const { data: endpointsConfig } = useGetEndpointsQuery(); const { message, currentEditId, setCurrentEditId } = props; const { @@ -51,6 +53,7 @@ export default function useMessageHelpers(props: TMessageProps) { const icon = Icon({ ...conversation, ...(message as TMessage), + iconURL: endpointsConfig?.[conversation?.endpoint ?? '']?.iconURL, model: message?.model ?? conversation?.model, size: 28.8, }); diff --git a/client/src/hooks/useChatHelpers.ts b/client/src/hooks/useChatHelpers.ts index bb702cb98f88..1cf3c16fea60 100644 --- a/client/src/hooks/useChatHelpers.ts +++ b/client/src/hooks/useChatHelpers.ts @@ -1,18 +1,20 @@ import { v4 } from 'uuid'; import { useCallback, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import { QueryKeys, parseCompactConvo } from 'librechat-data-provider'; import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; -import { QueryKeys, parseCompactConvo, getResponseSender } from 'librechat-data-provider'; -import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query'; +import { useGetMessagesByConvoId, useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { TMessage, TSubmission, TEndpointOption, TConversation, + TEndpointsConfig, TGetConversationsResponse, } from 'librechat-data-provider'; import type { TAskFunction } from '~/common'; import useSetFilesToDelete from './useSetFilesToDelete'; +import useGetSender from './Conversations/useGetSender'; import { useAuthContext } from './AuthContext'; import useUserKey from './Input/useUserKey'; import useNewConvo from './useNewConvo'; @@ -20,10 +22,12 @@ import store from '~/store'; // this to be set somewhere else export default function useChatHelpers(index = 0, paramId: string | undefined) { + const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const [files, setFiles] = useRecoilState(store.filesByIndex(index)); const [showStopButton, setShowStopButton] = useState(true); const [filesLoading, setFilesLoading] = useState(false); const setFilesToDelete = useSetFilesToDelete(); + const getSender = useGetSender(); const queryClient = useQueryClient(); const { isAuthenticated } = useAuthContext(); @@ -31,7 +35,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) { const { newConversation } = useNewConvo(index); const { useCreateConversationAtom } = store; const { conversation, setConversation } = useCreateConversationAtom(index); - const { conversationId, endpoint } = conversation ?? {}; + const { conversationId, endpoint, endpointType } = conversation ?? {}; const queryParam = paramId === 'new' ? paramId : conversationId ?? paramId ?? ''; @@ -151,13 +155,21 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) { const isEditOrContinue = isEdited || isContinued; // set the endpoint option - const convo = parseCompactConvo(endpoint, conversation ?? {}); + const convo = parseCompactConvo({ + endpoint, + endpointType, + conversation: conversation ?? {}, + }); + + const { modelDisplayLabel } = endpointsConfig[endpoint ?? ''] ?? {}; const endpointOption = { ...convo, endpoint, + endpointType, + modelDisplayLabel, key: getExpiry(), } as TEndpointOption; - const responseSender = getResponseSender({ model: conversation?.model, ...endpointOption }); + const responseSender = getSender({ model: conversation?.model, ...endpointOption }); let currentMessages: TMessage[] | null = getMessages() ?? []; diff --git a/client/src/hooks/useConversation.ts b/client/src/hooks/useConversation.ts index 6ed2bf229bce..d16493c23332 100644 --- a/client/src/hooks/useConversation.ts +++ b/client/src/hooks/useConversation.ts @@ -7,6 +7,7 @@ import type { TSubmission, TPreset, TModelsConfig, + TEndpointsConfig, } from 'librechat-data-provider'; import { buildDefaultConvo, getDefaultEndpoint } from '~/utils'; import useOriginNavigate from './useOriginNavigate'; @@ -18,7 +19,7 @@ const useConversation = () => { const setMessages = useSetRecoilState(store.messages); const setSubmission = useSetRecoilState(store.submission); const resetLatestMessage = useResetRecoilState(store.latestMessage); - const { data: endpointsConfig = {} } = useGetEndpointsQuery(); + const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const switchToConversation = useRecoilCallback( ({ snapshot }) => @@ -37,6 +38,10 @@ const useConversation = () => { endpointsConfig, }); + if (!conversation.endpointType && endpointsConfig[defaultEndpoint]?.type) { + conversation.endpointType = endpointsConfig[defaultEndpoint]?.type; + } + const models = modelsConfig?.[defaultEndpoint] ?? []; conversation = buildDefaultConvo({ conversation, diff --git a/client/src/hooks/useDefaultConvo.ts b/client/src/hooks/useDefaultConvo.ts index f60f35a3dd83..59ff182d7a18 100644 --- a/client/src/hooks/useDefaultConvo.ts +++ b/client/src/hooks/useDefaultConvo.ts @@ -1,13 +1,13 @@ import { useRecoilValue } from 'recoil'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; -import type { TConversation, TPreset } from 'librechat-data-provider'; +import type { TConversation, TPreset, TEndpointsConfig } from 'librechat-data-provider'; import { getDefaultEndpoint, buildDefaultConvo } from '~/utils'; import store from '~/store'; type TDefaultConvo = { conversation: Partial; preset?: Partial | null }; const useDefaultConvo = () => { - const { data: endpointsConfig = {} } = useGetEndpointsQuery(); + const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const modelsConfig = useRecoilValue(store.modelsConfig); const getDefaultConversation = ({ conversation, preset }: TDefaultConvo) => { diff --git a/client/src/hooks/useGenerationsByLatest.ts b/client/src/hooks/useGenerationsByLatest.ts index acbb3baa337d..4fe91ca096ec 100644 --- a/client/src/hooks/useGenerationsByLatest.ts +++ b/client/src/hooks/useGenerationsByLatest.ts @@ -19,6 +19,7 @@ export default function useGenerationsByLatest({ const { error, messageId, searchResult, finish_reason, isCreatedByUser } = message ?? {}; const isEditableEndpoint = !![ EModelEndpoint.openAI, + EModelEndpoint.custom, EModelEndpoint.google, EModelEndpoint.assistant, EModelEndpoint.anthropic, @@ -39,6 +40,7 @@ export default function useGenerationsByLatest({ !![ EModelEndpoint.azureOpenAI, EModelEndpoint.openAI, + EModelEndpoint.custom, EModelEndpoint.chatGPTBrowser, EModelEndpoint.google, EModelEndpoint.bingAI, diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index e92150765e38..8d8fd3680384 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -7,7 +7,13 @@ import { useRecoilState, useRecoilValue, } from 'recoil'; -import type { TConversation, TSubmission, TPreset, TModelsConfig } from 'librechat-data-provider'; +import type { + TConversation, + TSubmission, + TPreset, + TModelsConfig, + TEndpointsConfig, +} from 'librechat-data-provider'; import { buildDefaultConvo, getDefaultEndpoint } from '~/utils'; import { useDeleteFilesMutation } from '~/data-provider'; import useOriginNavigate from './useOriginNavigate'; @@ -22,7 +28,7 @@ const useNewConvo = (index = 0) => { const [files, setFiles] = useRecoilState(store.filesByIndex(index)); const setSubmission = useSetRecoilState(store.submissionByIndex(index)); const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index)); - const { data: endpointsConfig = {} } = useGetEndpointsQuery(); + const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { mutateAsync } = useDeleteFilesMutation({ onSuccess: () => { @@ -62,6 +68,10 @@ const useNewConvo = (index = 0) => { endpointsConfig, }); + if (!conversation.endpointType && endpointsConfig[defaultEndpoint]?.type) { + conversation.endpointType = endpointsConfig[defaultEndpoint]?.type; + } + const models = modelsConfig?.[defaultEndpoint] ?? []; conversation = buildDefaultConvo({ conversation, diff --git a/client/src/hooks/useSSE.ts b/client/src/hooks/useSSE.ts index e5258511c7fe..64b209148461 100644 --- a/client/src/hooks/useSSE.ts +++ b/client/src/hooks/useSSE.ts @@ -1,3 +1,4 @@ +import { v4 } from 'uuid'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { @@ -5,7 +6,7 @@ import { SSE, createPayload, tMessageSchema, - tConversationSchema, + tConvoUpdateSchema, EModelEndpoint, removeNullishValues, } from 'librechat-data-provider'; @@ -152,10 +153,10 @@ export default function useSSE(submission: TSubmission | null, index = 0) { let update = {} as TConversation; setConversation((prevState) => { - update = tConversationSchema.parse({ + update = tConvoUpdateSchema.parse({ ...prevState, conversationId, - }); + }) as TConversation; setStorage(update); return update; @@ -207,10 +208,37 @@ export default function useSSE(submission: TSubmission | null, index = 0) { setIsSubmitting(false); }; - const errorHandler = (data: TResData, submission: TSubmission) => { + const errorHandler = ({ data, submission }: { data?: TResData; submission: TSubmission }) => { const { messages, message } = submission; - if (!data.conversationId) { + const conversationId = message?.conversationId ?? submission?.conversationId; + const parseErrorResponse = (data: TResData | Partial) => { + const metadata = data['responseMessage'] ?? data; + return tMessageSchema.parse({ + ...metadata, + error: true, + parentMessageId: message?.messageId, + }); + }; + + if (!data) { + const convoId = conversationId ?? v4(); + const errorResponse = parseErrorResponse({ + text: 'Error connecting to server', + ...submission, + conversationId: convoId, + }); + setMessages([...messages, message, errorResponse]); + newConversation({ template: { conversationId: convoId } }); + setIsSubmitting(false); + return; + } + + if (!conversationId && !data.conversationId) { + const convoId = v4(); + const errorResponse = parseErrorResponse(data); + setMessages([...messages, message, errorResponse]); + newConversation({ template: { conversationId: convoId } }); setIsSubmitting(false); return; } @@ -318,19 +346,20 @@ export default function useSSE(submission: TSubmission | null, index = 0) { abortConversation(message?.conversationId ?? submission?.conversationId, submission); events.onerror = function (e: MessageEvent) { - console.log('error in opening conn.'); + console.log('error in server stream.'); startupConfig?.checkBalance && balanceQuery.refetch(); events.close(); - let data = {} as TResData; + let data: TResData | undefined = undefined; try { - data = JSON.parse(e.data); + data = JSON.parse(e.data) as TResData; } catch (error) { console.error(error); console.log(e); } - errorHandler(data, { ...submission, message }); + errorHandler({ data, submission: { ...submission, message } }); + events.oncancel(); }; setIsSubmitting(true); diff --git a/client/src/hooks/useSetIndexOptions.ts b/client/src/hooks/useSetIndexOptions.ts index 3de9fa78597a..26d49556f8c9 100644 --- a/client/src/hooks/useSetIndexOptions.ts +++ b/client/src/hooks/useSetIndexOptions.ts @@ -1,5 +1,11 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { TPreset, TPlugin, tConversationSchema, EModelEndpoint } from 'librechat-data-provider'; +import { + TPreset, + TPlugin, + tConvoUpdateSchema, + EModelEndpoint, + TConversation, +} from 'librechat-data-provider'; import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common'; import usePresetIndexOptions from './usePresetIndexOptions'; import { useChatContext } from '~/Providers/ChatContext'; @@ -36,11 +42,12 @@ const useSetOptions: TUseSetOptions = (preset = false) => { setLastBingSettings({ ...lastBingSettings, jailbreak: newValue }); } - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - ...update, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + ...update, + }) as TConversation, ); }; @@ -51,11 +58,12 @@ const useSetOptions: TUseSetOptions = (preset = false) => { currentExample[type] = { content: newValue }; current[i] = currentExample; update['examples'] = current; - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - ...update, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + ...update, + }) as TConversation, ); }; @@ -64,11 +72,12 @@ const useSetOptions: TUseSetOptions = (preset = false) => { const current = conversation?.examples?.slice() || []; current.push({ input: { content: '' }, output: { content: '' } }); update['examples'] = current; - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - ...update, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + ...update, + }) as TConversation, ); }; @@ -77,21 +86,23 @@ const useSetOptions: TUseSetOptions = (preset = false) => { const current = conversation?.examples?.slice() || []; if (current.length <= 1) { update['examples'] = [{ input: { content: '' }, output: { content: '' } }]; - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - ...update, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + ...update, + }) as TConversation, ); return; } current.pop(); update['examples'] = current; - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - ...update, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + ...update, + }) as TConversation, ); }; @@ -113,11 +124,12 @@ const useSetOptions: TUseSetOptions = (preset = false) => { lastModelUpdate.secondaryModel = newValue; setLastModel(lastModelUpdate); } - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - agentOptions, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + agentOptions, + }) as TConversation, ); }; @@ -139,11 +151,12 @@ const useSetOptions: TUseSetOptions = (preset = false) => { } localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools'])); - setConversation((prevState) => - tConversationSchema.parse({ - ...prevState, - ...update, - }), + setConversation( + (prevState) => + tConvoUpdateSchema.parse({ + ...prevState, + ...update, + }) as TConversation, ); }; diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index 8bbb371e43e4..de4f066cef3d 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -9,6 +9,7 @@ import { import { useNewConvo, useConfigOverride } from '~/hooks'; import ChatView from '~/components/Chat/ChatView'; import useAuthRedirect from './useAuthRedirect'; +import { Spinner } from '~/components/svg'; import store from '~/store'; export default function ChatRoute() { @@ -51,6 +52,10 @@ export default function ChatRoute() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialConvoQuery.data, modelsQuery.data, endpointsQuery.data]); + if (endpointsQuery.isLoading || modelsQuery.isLoading) { + return ; + } + if (!isAuthenticated) { return null; } diff --git a/client/src/store/endpoints.ts b/client/src/store/endpoints.ts index e944ebb7a0d4..b92fca701cdd 100644 --- a/client/src/store/endpoints.ts +++ b/client/src/store/endpoints.ts @@ -11,6 +11,7 @@ const defaultConfig: TEndpointsConfig = { [EModelEndpoint.gptPlugins]: null, [EModelEndpoint.google]: null, [EModelEndpoint.anthropic]: null, + [EModelEndpoint.custom]: null, }; const endpointsConfig = atom({ @@ -55,6 +56,7 @@ const availableEndpoints = selector({ 'bingAI', 'google', 'anthropic', + 'custom', ]; const f = get(endpointsFilter); return endpoints.filter((endpoint) => f[endpoint]); diff --git a/client/src/utils/buildDefaultConvo.ts b/client/src/utils/buildDefaultConvo.ts index 2ea4d006b3a6..bf1032c9046f 100644 --- a/client/src/utils/buildDefaultConvo.ts +++ b/client/src/utils/buildDefaultConvo.ts @@ -15,10 +15,12 @@ const buildDefaultConvo = ({ }) => { const { lastSelectedModel, lastSelectedTools, lastBingSettings } = getLocalStorageItems(); const { jailbreak, toneStyle } = lastBingSettings; + const { endpointType } = conversation; if (!endpoint) { return { ...conversation, + endpointType, endpoint, }; } @@ -44,13 +46,20 @@ const buildDefaultConvo = ({ secondaryModels = [...availableModels]; } - const convo = parseConvo(endpoint, lastConversationSetup, { - models: possibleModels, - secondaryModels, + const convo = parseConvo({ + endpoint, + endpointType, + conversation: lastConversationSetup, + possibleValues: { + models: possibleModels, + secondaryModels, + }, }); + const defaultConvo = { ...conversation, ...convo, + endpointType, endpoint, }; diff --git a/client/src/utils/cleanupPreset.ts b/client/src/utils/cleanupPreset.ts index 22bd32734006..ffff879a601a 100644 --- a/client/src/utils/cleanupPreset.ts +++ b/client/src/utils/cleanupPreset.ts @@ -6,7 +6,7 @@ type TCleanupPreset = { }; const cleanupPreset = ({ preset: _preset }: TCleanupPreset): TPreset => { - const { endpoint } = _preset; + const { endpoint, endpointType } = _preset; if (!endpoint) { console.error(`Unknown endpoint ${endpoint}`, _preset); return { @@ -16,12 +16,13 @@ const cleanupPreset = ({ preset: _preset }: TCleanupPreset): TPreset => { }; } - const parsedPreset = parseConvo(endpoint, _preset); + const parsedPreset = parseConvo({ endpoint, endpointType, conversation: _preset }); return { presetId: _preset?.presetId ?? null, ...parsedPreset, endpoint, + endpointType, title: _preset?.title ?? 'New Preset', } as TPreset; }; diff --git a/client/src/utils/getDefaultEndpoint.ts b/client/src/utils/getDefaultEndpoint.ts index bdfb7b6470b1..a70b20c159e0 100644 --- a/client/src/utils/getDefaultEndpoint.ts +++ b/client/src/utils/getDefaultEndpoint.ts @@ -1,4 +1,9 @@ -import type { TConversation, TPreset, TEndpointsConfig } from 'librechat-data-provider'; +import type { + TConversation, + TPreset, + TEndpointsConfig, + EModelEndpoint, +} from 'librechat-data-provider'; import getLocalStorageItems from './getLocalStorageItems'; import mapEndpoints from './mapEndpoints'; @@ -42,7 +47,7 @@ const getDefinedEndpoint = (endpointsConfig: TEndpointsConfig) => { return endpoints.find((e) => Object.hasOwn(endpointsConfig ?? {}, e)); }; -const getDefaultEndpoint = ({ convoSetup, endpointsConfig }: TDefaultEndpoint) => { +const getDefaultEndpoint = ({ convoSetup, endpointsConfig }: TDefaultEndpoint): EModelEndpoint => { return ( getEndpointFromSetup(convoSetup, endpointsConfig) || getEndpointFromLocalStorage(endpointsConfig) || diff --git a/client/src/utils/mapEndpoints.ts b/client/src/utils/mapEndpoints.ts index 1902971a01d0..74460e0dfe9f 100644 --- a/client/src/utils/mapEndpoints.ts +++ b/client/src/utils/mapEndpoints.ts @@ -1,20 +1,37 @@ import { defaultEndpoints } from 'librechat-data-provider'; -import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { EModelEndpoint, TEndpointsConfig } from 'librechat-data-provider'; -const getEndpointsFilter = (config: TEndpointsConfig) => { +const getEndpointsFilter = (endpointsConfig: TEndpointsConfig) => { const filter: Record = {}; - for (const key of Object.keys(config)) { - filter[key] = !!config[key]; + for (const key of Object.keys(endpointsConfig)) { + filter[key] = !!endpointsConfig[key]; } return filter; }; -const getAvailableEndpoints = (filter: Record) => { - const endpoints = defaultEndpoints; - return endpoints.filter((endpoint) => filter[endpoint]); +const getAvailableEndpoints = ( + filter: Record, + endpointsConfig: TEndpointsConfig, +) => { + const defaultSet = new Set(defaultEndpoints); + const availableEndpoints: EModelEndpoint[] = []; + + for (const endpoint in endpointsConfig) { + // Check if endpoint is in the filter or its type is in defaultEndpoints + if ( + filter[endpoint] || + (endpointsConfig[endpoint]?.type && defaultSet.has(endpointsConfig[endpoint].type)) + ) { + availableEndpoints.push(endpoint as EModelEndpoint); + } + } + + return availableEndpoints; }; -export default function mapEndpoints(config: TEndpointsConfig) { - const filter = getEndpointsFilter(config); - return getAvailableEndpoints(filter).sort((a, b) => config[a].order - config[b].order); +export default function mapEndpoints(endpointsConfig: TEndpointsConfig) { + const filter = getEndpointsFilter(endpointsConfig); + return getAvailableEndpoints(filter, endpointsConfig).sort( + (a, b) => (endpointsConfig[a]?.order ?? 0) - (endpointsConfig[b]?.order ?? 0), + ); } diff --git a/client/src/utils/presets.ts b/client/src/utils/presets.ts index 512572526d60..ee86087d875b 100644 --- a/client/src/utils/presets.ts +++ b/client/src/utils/presets.ts @@ -26,7 +26,10 @@ export const getPresetTitle = (preset: TPreset) => { let modelInfo = model || ''; let label = ''; - if (endpoint && [EModelEndpoint.azureOpenAI, EModelEndpoint.openAI].includes(endpoint)) { + if ( + endpoint && + [EModelEndpoint.azureOpenAI, EModelEndpoint.openAI, EModelEndpoint.custom].includes(endpoint) + ) { label = chatGptLabel || ''; } else if (endpoint && [EModelEndpoint.google, EModelEndpoint.anthropic].includes(endpoint)) { label = modelLabel || ''; diff --git a/docs/install/configuration/ai_setup.md b/docs/install/configuration/ai_setup.md index 4743b2b73e3e..a1096d702f93 100644 --- a/docs/install/configuration/ai_setup.md +++ b/docs/install/configuration/ai_setup.md @@ -330,15 +330,21 @@ To use Azure with the Plugins endpoint, make sure the following environment vari > See their available models and pricing here: **[Supported Models](https://openrouter.ai/docs#models)** -OpenRouter is so great, I decided to integrate it to the project as a standalone feature. +OpenRouter is integrated to the LibreChat by overriding the OpenAI endpoint. -**Setup:** +**Important**: As of v0.6.6, you can use OpenRouter as its own standalone endpoint: + +![image](https://github.com/danny-avila/LibreChat/assets/110412045/4955bfa3-7b6b-4602-933f-daef89c9eab3) + +### [Review the Custom Config Guide (click here)](./custom_config.md) to add an `OpenRouter` Endpoint + +**Setup (legacy):** - Signup to **[OpenRouter](https://openrouter.ai/)** and create a key. You should name it and set a limit as well. - Set the environment variable `OPENROUTER_API_KEY` in your .env file to the key you just created. - Set something in the `OPENAI_API_KEY`, it can be anyting, but **do not** leave it blank or set to `user_provided` - Restart your LibreChat server and use the OpenAI or Plugins endpoints. -**Notes:** +**Notes:** - [TODO] **In the future, you will be able to set up OpenRouter from the frontend as well.** - This will override the official OpenAI API or your reverse proxy settings for both Plugins and OpenAI. - On initial setup, you may need to refresh your page twice to see all their supported models populate automatically. diff --git a/docs/install/configuration/custom_config.md b/docs/install/configuration/custom_config.md new file mode 100644 index 000000000000..f5d2febe91f8 --- /dev/null +++ b/docs/install/configuration/custom_config.md @@ -0,0 +1,221 @@ +# LibreChat Configuration Guide + +This document provides detailed instructions for configuring the `librechat.yaml` file used by LibreChat. + +In future updates, some of the configurations from [your `.env` file](./dotenv.md) will migrate here. + +Further customization of the current configurations are also planned. + +# Table of Contents + +1. [Intro](#librechat-configuration-guide) + - [Configuration Overview](#configuration-overview) + - [1. Version](#1-version) + - [2. Cache Settings](#2-cache-settings) + - [3. Endpoints](#3-endpoints) + - [Endpoint Object Structure](#endpoint-object-structure) + - [Additional Notes](#additional-notes) + - [Default Parameters](#default-parameters) + - [Breakdown of Default Params](#breakdown-of-default-params) + - [Example Config](#example-config) + +## Configuration Overview + + +The `librechat.yaml` file contains several key sections. + +**Note:** Fields not specifically mentioned as required are optional. + +### 1. Version +- **Key**: `version` +- **Type**: String +- **Description**: Specifies the version of the configuration file. +- **Example**: `version: 1.0.0` +- **Required** + +### 2. Cache Settings +- **Key**: `cache` +- **Type**: Boolean +- **Description**: Toggles caching on or off. Set to `true` to enable caching. +- **Example**: `cache: true` + +### 3. Endpoints +- **Key**: `endpoints` +- **Type**: Object +- **Description**: Defines custom API endpoints for the application. + - **Sub-Key**: `custom` + - **Type**: Array of Objects + - **Description**: Each object in the array represents a unique endpoint configuration. +- **Required** + +#### Endpoint Object Structure +Each endpoint in the `custom` array should have the following structure: + +- **name**: A unique name for the endpoint. + - Type: String + - Example: `name: "Mistral"` + - **Required** + - **Note**: Will be used as the "title" in the Endpoints Selector + +- **apiKey**: Your API key for the service. Can reference an environment variable, or allow user to provide the value. + - Type: String (apiKey | `"user_provided"`) + - **Example**: `apiKey: "${MISTRAL_API_KEY}"` | `apiKey: "your_api_key"` | `apiKey: "user_provided"` + - **Required** + - **Note**: It's highly recommended to use the env. variable reference for this field, i.e. `${YOUR_VARIABLE}` + +- **baseURL**: Base URL for the API. Can reference an environment variable, or allow user to provide the value. + - Type: String (baseURL | `"user_provided"`) + - **Example**: `baseURL: "https://api.mistral.ai/v1"` | `baseURL: "${MISTRAL_BASE_URL}"` | `baseURL: "user_provided"` + - **Required** + - **Note**: It's highly recommended to use the env. variable reference for this field, i.e. `${YOUR_VARIABLE}` + +- **iconURL**: The URL to use as the Endpoint Icon. + - Type: Boolean + - Example: `iconURL: https://github.com/danny-avila/LibreChat/raw/main/docs/assets/LibreChat.svg` + - **Note**: The following are "known endpoints" (case-insensitive), which have icons provided for them. If your endpoint `name` matches these, you should omit this field: + - "Mistral" + - "OpenRouter" + +- **models**: Configuration for models. +- **Required** + - **default**: An array of strings indicating the default models to use. At least one value is required. + - Type: Array of Strings + - Example: `default: ["mistral-tiny", "mistral-small", "mistral-medium"]` + - **Note**: If fetching models fails, these defaults are used as a fallback. + - **fetch**: When set to `true`, attempts to fetch a list of models from the API. + - Type: Boolean + - Example: `fetch: true` + - **Note**: May cause slowdowns during initial use of the app if the response is delayed. Defaults to `false`. + +- **titleConvo**: Enables title conversation when set to `true`. + - Type: Boolean + - Example: `titleConvo: true` + +- **titleMethod**: Chooses between "completion" or "functions" for title method. + - Type: String (`"completion"` | `"functions"`) + - Example: `titleMethod: "completion"` + - **Note**: Defaults to "completion" if omitted. + +- **titleModel**: Specifies the model to use for titles. + - Type: String + - Example: `titleModel: "mistral-tiny"` + - **Note**: Defaults to "gpt-3.5-turbo" if omitted. May cause issues if "gpt-3.5-turbo" is not available. + +- **summarize**: Enables summarization when set to `true`. + - Type: Boolean + - Example: `summarize: false` + - **Note**: This feature requires an OpenAI Functions compatible API + +- **summaryModel**: Specifies the model to use if summarization is enabled. + - Type: String + - Example: `summaryModel: "mistral-tiny"` + - **Note**: Defaults to "gpt-3.5-turbo" if omitted. May cause issues if "gpt-3.5-turbo" is not available. + +- **forcePrompt**: If `true`, sends a `prompt` parameter instead of `messages`. + - Type: Boolean + - Example: `forcePrompt: false` + - **Note**: This combines all messages into a single text payload, [following OpenAI format](https://github.com/pvicente/openai-python/blob/main/chatml.md), and uses the `/completions` endpoint of your baseURL rather than `/chat/completions`. + +- **modelDisplayLabel**: The label displayed in messages next to the Icon for the current AI model. + - Type: String + - Example: `modelDisplayLabel: "Mistral"` + - **Note**: The display order is: + - 1. Custom name set via preset (if available) + - 2. Label derived from the model name (if applicable) + - 3. This value, `modelDisplayLabel`, is used if the above are not specified. Defaults to "AI". + +- **addParams**: Adds additional parameters to requests. + - Type: Object/Dictionary + - **Description**: Adds/Overrides parameters. Useful for specifying API-specific options. + - **Example**: +```yaml + addParams: + safe_mode: true +``` + +- **dropParams**: Removes default parameters from requests. + - Type: Array/List of Strings + - **Description**: Excludes specified default parameters. Useful for APIs that do not accept or recognize certain parameters. + - **Example**: `dropParams: ["stop", "temperature", "top_p"]` + - **Note**: For a list of default parameters sent with every request, see the "Default Parameters" Section below. + +## Additional Notes +- Ensure that all URLs and keys are correctly specified to avoid connectivity issues. + +## Default Parameters + +Custom endpoints share logic with the OpenAI endpoint, and thus have default parameters tailored to the OpenAI API. + +```json +{ + "model": "your-selected-model", + "temperature": 1, + "top_p": 1, + "presence_penalty": 0, + "frequency_penalty": 0, + "stop": [ + "||>", + "\nUser:", + "<|diff_marker|>", + ], + "user": "LibreChat_User_ID", + "stream": true, + "messages": [ + { + "role": "user", + "content": "hi how are you", + }, + ], +} +``` +### Breakdown of Default Params +- `model`: The selected model from list of models. +- `temperature`: Defaults to `1` if not provided via preset, +- `top_p`: Defaults to `1` if not provided via preset, +- `presence_penalty`: Defaults to `0` if not provided via preset, +- `frequency_penalty`: Defaults to `0` if not provided via preset, +- `stop`: Sequences where the AI will stop generating further tokens. By default, uses the start token (`||>`), the user label (`\nUser:`), and end token (`<|diff_marker|>`). Up to 4 sequences can be provided to the [OpenAI API](https://platform.openai.com/docs/api-reference/chat/create#chat-create-stop) +- `user`: A unique identifier representing your end-user, which can help OpenAI to [monitor and detect abuse](https://platform.openai.com/docs/api-reference/chat/create#chat-create-user). +- `stream`: If set, partial message deltas will be sent, like in ChatGPT. Otherwise, generation will only be available when completed. +- `messages`: [OpenAI format for messages](https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages); the `name` field is added to messages with `system` and `assistant` roles when a custom name is specified via preset. + +**Note:** The `max_tokens` field is not sent to use the maximum amount of tokens available, which is default OpenAI API behavior. Some alternate APIs require this field, or it may default to a very low value and your responses may appear cut off; in this case, you should add it to `addParams` field as shown in the [Endpoint Object Structure](#endpoint-object-structure). + +## Example Config + +```yaml +version: 1.0.0 +cache: true +endpoints: + custom: + # Mistral AI API + - name: "Mistral" + apiKey: "your_api_key" + baseURL: "https://api.mistral.ai/v1" + models: + default: ["mistral-tiny", "mistral-small", "mistral-medium"] + titleConvo: true + titleModel: "mistral-tiny" + summarize: false + summaryModel: "mistral-tiny" + forcePrompt: false + modelDisplayLabel: "Mistral" + addParams: + safe_mode: true + dropParams: ["stop", "temperature", "top_p"] + + # OpenRouter.ai API + - name: "OpenRouter" + # Known issue: you should not use `OPENROUTER_API_KEY` as it will then override the `openAI` endpoint to use OpenRouter as well. + apiKey: "${OPENROUTER_KEY}" + baseURL: "https://openrouter.ai/api/v1" + models: + default: ["gpt-3.5-turbo"] + fetch: true + titleConvo: true + titleModel: "gpt-3.5-turbo" + summarize: false + summaryModel: "gpt-3.5-turbo" + forcePrompt: false + modelDisplayLabel: "OpenRouter" +``` diff --git a/docs/install/configuration/dotenv.md b/docs/install/configuration/dotenv.md index 5df5f8b88391..5ae3426c456e 100644 --- a/docs/install/configuration/dotenv.md +++ b/docs/install/configuration/dotenv.md @@ -302,12 +302,14 @@ OPENAI_TITLE_MODEL=gpt-3.5-turbo OPENAI_SUMMARIZE=true ``` -> **Not yet implemented**: this will be a conversation option enabled by default to save users on tokens. We are using the ConversationSummaryBufferMemory method to summarize messages. To learn more about this, see this article: [https://www.pinecone.io/learn/series/langchain/langchain-conversational-memory/](https://www.pinecone.io/learn/series/langchain/langchain-conversational-memory/) +> **Experimental**: We are using the ConversationSummaryBufferMemory method to summarize messages. To learn more about this, see this article: [https://www.pinecone.io/learn/series/langchain/langchain-conversational-memory/](https://www.pinecone.io/learn/series/langchain/langchain-conversational-memory/) - Reverse proxy settings for OpenAI: - see: [LiteLLM](./litellm.md) - see also: [Free AI APIs](./free_ai_apis.md#nagaai) +**Important**: As of v0.6.6, it's recommend you use the `librechat.yaml` [Configuration file (guide here)](./custom_config.md) to add Reverse Proxies as separate endpoints. + ```bash OPENAI_REVERSE_PROXY= ``` diff --git a/docs/install/configuration/free_ai_apis.md b/docs/install/configuration/free_ai_apis.md index 671ed9b6206b..bc3542dd037f 100644 --- a/docs/install/configuration/free_ai_apis.md +++ b/docs/install/configuration/free_ai_apis.md @@ -34,6 +34,8 @@ OPENAI_REVERSE_PROXY=https://api.naga.ac/v1/chat/completions # OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314,gpt-4-0613 ``` +**Important**: As of v0.6.6, it's recommend you use the `librechat.yaml` [Configuration file (guide here)](./custom_config.md) to add Reverse Proxies as separate endpoints. + **Note:** The `OPENAI_MODELS` variable is commented out so that the server can fetch nagaai/api/v1/models for all available models. Uncomment and adjust if you wish to specify which exact models you want to use. It's worth noting that not all models listed by their API will work, with or without this project. The exact URL may also change, just make sure you include `/v1/chat/completions` in the reverse proxy URL if it ever changes. diff --git a/docs/install/configuration/index.md b/docs/install/configuration/index.md index e577a8b3a564..502686a3ec6e 100644 --- a/docs/install/configuration/index.md +++ b/docs/install/configuration/index.md @@ -7,6 +7,7 @@ weight: 2 # Configuration * ⚙️ [Environment Variables](./dotenv.md) + * 🖥️ [Custom Config & Endpoints](./configuration/custom_config.md) * 🐋 [Docker Compose Override](./docker_override.md) --- * 🤖 [AI Setup](./ai_setup.md) diff --git a/docs/install/configuration/litellm.md b/docs/install/configuration/litellm.md index c9f86368993a..d4dfdd6db96e 100644 --- a/docs/install/configuration/litellm.md +++ b/docs/install/configuration/litellm.md @@ -62,6 +62,8 @@ git clone https://github.com/danny-avila/LibreChat.git OPENAI_REVERSE_PROXY=http://host.docker.internal:8000/v1/chat/completions ``` +**Important**: As of v0.6.6, it's recommend you use the `librechat.yaml` [Configuration file (guide here)](./custom_config.md) to add Reverse Proxies as separate endpoints. + #### 3. Save fake OpenAI key in Librechat's `.env` Copy Librechat's `.env.example` to `.env` and overwrite the default OPENAI_API_KEY (by default it requires the user to pass a key). diff --git a/docs/install/index.md b/docs/install/index.md index e6177b07b400..01786ee3f755 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -17,6 +17,7 @@ weight: 1 ## **[Configuration](./configuration/index.md)** * ⚙️ [Environment Variables](./configuration/dotenv.md) + * 🖥️ [Custom Config & Endpoints](./configuration/custom_config.md) * 🐋 [Docker Compose Override](./configuration/docker_override.md) * 🤖 [AI Setup](./configuration/ai_setup.md) * 🚅 [LiteLLM](./configuration/litellm.md) diff --git a/librechat.example.yaml b/librechat.example.yaml new file mode 100644 index 000000000000..b2a9bbaf9343 --- /dev/null +++ b/librechat.example.yaml @@ -0,0 +1,76 @@ +# Configuration version (required) +version: 1.0.0 + +# Cache settings: Set to true to enable caching +cache: true + +# Definition of custom endpoints +endpoints: + custom: + # Mistral AI API + - name: "Mistral" # Unique name for the endpoint + # For `apiKey` and `baseURL`, you can use environment variables that you define. + # recommended environment variables: + apiKey: "${MISTRAL_API_KEY}" + baseURL: "https://api.mistral.ai/v1" + + # Models configuration + models: + # List of default models to use. At least one value is required. + default: ["mistral-tiny", "mistral-small", "mistral-medium"] + # Fetch option: Set to true to fetch models from API. + fetch: true # Defaults to false. + + # Optional configurations + + # Title Conversation setting + titleConvo: true # Set to true to enable title conversation + + # Title Method: Choose between "completion" or "functions". + titleMethod: "completion" # Defaults to "completion" if omitted. + + # Title Model: Specify the model to use for titles. + titleModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted. + + # Summarize setting: Set to true to enable summarization. + summarize: false + + # Summary Model: Specify the model to use if summarization is enabled. + summaryModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted. + + # Force Prompt setting: If true, sends a `prompt` parameter instead of `messages`. + forcePrompt: false + + # The label displayed for the AI model in messages. + modelDisplayLabel: "Mistral" # Default is "AI" when not set. + + # Add additional parameters to the request. Default params will be overwritten. + addParams: + safe_mode: true # This field is specific to Mistral AI: https://docs.mistral.ai/api/ + + # Drop Default params parameters from the request. See default params in guide linked below. + dropParams: ["stop", "temperature", "top_p"] + # - stop # dropped since it's not recognized by Mistral AI API + # `temperature` and `top_p` are removed to allow Mistral AI API defaults to be used: + # - temperature + # - top_p + + # OpenRouter.ai Example + - name: "OpenRouter" + # For `apiKey` and `baseURL`, you can use environment variables that you define. + # recommended environment variables: + # Known issue: you should not use `OPENROUTER_API_KEY` as it will then override the `openAI` endpoint to use OpenRouter as well. + apiKey: "${OPENROUTER_KEY}" + baseURL: "https://openrouter.ai/api/v1" + models: + default: ["gpt-3.5-turbo"] + fetch: true + titleConvo: true + titleModel: "gpt-3.5-turbo" + summarize: false + summaryModel: "gpt-3.5-turbo" + forcePrompt: false + modelDisplayLabel: "OpenRouter" + +# See the Custom Configuration Guide for more information: +# https://docs.librechat.ai/install/configuration/custom_config.html diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts new file mode 100644 index 000000000000..7b31508cc5b5 --- /dev/null +++ b/packages/data-provider/src/config.ts @@ -0,0 +1,186 @@ +import { z } from 'zod'; +import { EModelEndpoint, eModelEndpointSchema } from './schemas'; + +export const endpointSchema = z.object({ + name: z.string().refine((value) => !eModelEndpointSchema.safeParse(value).success, { + message: `Value cannot be one of the default endpoint (EModelEndpoint) values: ${Object.values( + EModelEndpoint, + ).join(', ')}`, + }), + apiKey: z.string(), + baseURL: z.string(), + models: z.object({ + default: z.array(z.string()).min(1), + fetch: z.boolean().optional(), + }), + titleConvo: z.boolean().optional(), + titleMethod: z.union([z.literal('completion'), z.literal('functions')]).optional(), + titleModel: z.string().optional(), + summarize: z.boolean().optional(), + summaryModel: z.string().optional(), + forcePrompt: z.boolean().optional(), + modelDisplayLabel: z.string().optional(), +}); + +export const configSchema = z.object({ + version: z.string(), + cache: z.boolean(), + endpoints: z + .object({ + custom: z.array(endpointSchema.partial()), + }) + .strict(), +}); + +export enum KnownEndpoints { + mistral = 'mistral', + openrouter = 'openrouter', +} + +export const defaultEndpoints: EModelEndpoint[] = [ + EModelEndpoint.openAI, + EModelEndpoint.assistant, + EModelEndpoint.azureOpenAI, + EModelEndpoint.bingAI, + EModelEndpoint.chatGPTBrowser, + EModelEndpoint.gptPlugins, + EModelEndpoint.google, + EModelEndpoint.anthropic, + EModelEndpoint.custom, +]; + +export const alternateName = { + [EModelEndpoint.openAI]: 'OpenAI', + [EModelEndpoint.assistant]: 'Assistants', + [EModelEndpoint.azureOpenAI]: 'Azure OpenAI', + [EModelEndpoint.bingAI]: 'Bing', + [EModelEndpoint.chatGPTBrowser]: 'ChatGPT', + [EModelEndpoint.gptPlugins]: 'Plugins', + [EModelEndpoint.google]: 'Google', + [EModelEndpoint.anthropic]: 'Anthropic', + [EModelEndpoint.custom]: 'Custom', +}; + +export const defaultModels = { + [EModelEndpoint.google]: [ + 'gemini-pro', + 'gemini-pro-vision', + 'chat-bison', + 'chat-bison-32k', + 'codechat-bison', + 'codechat-bison-32k', + 'text-bison', + 'text-bison-32k', + 'text-unicorn', + 'code-gecko', + 'code-bison', + 'code-bison-32k', + ], + [EModelEndpoint.anthropic]: [ + 'claude-2.1', + 'claude-2', + 'claude-1.2', + 'claude-1', + 'claude-1-100k', + 'claude-instant-1', + 'claude-instant-1-100k', + ], + [EModelEndpoint.openAI]: [ + 'gpt-3.5-turbo-16k-0613', + 'gpt-3.5-turbo-16k', + 'gpt-4-1106-preview', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-1106', + 'gpt-4-vision-preview', + 'gpt-4', + 'gpt-3.5-turbo-instruct-0914', + 'gpt-3.5-turbo-0613', + 'gpt-3.5-turbo-0301', + 'gpt-3.5-turbo-instruct', + 'gpt-4-0613', + 'text-davinci-003', + 'gpt-4-0314', + ], +}; + +export const EndpointURLs: { [key in EModelEndpoint]: string } = { + [EModelEndpoint.openAI]: `/api/ask/${EModelEndpoint.openAI}`, + [EModelEndpoint.bingAI]: `/api/ask/${EModelEndpoint.bingAI}`, + [EModelEndpoint.google]: `/api/ask/${EModelEndpoint.google}`, + [EModelEndpoint.custom]: `/api/ask/${EModelEndpoint.custom}`, + [EModelEndpoint.anthropic]: `/api/ask/${EModelEndpoint.anthropic}`, + [EModelEndpoint.gptPlugins]: `/api/ask/${EModelEndpoint.gptPlugins}`, + [EModelEndpoint.azureOpenAI]: `/api/ask/${EModelEndpoint.azureOpenAI}`, + [EModelEndpoint.chatGPTBrowser]: `/api/ask/${EModelEndpoint.chatGPTBrowser}`, + [EModelEndpoint.assistant]: '/api/assistants/chat', +}; + +export const modularEndpoints = new Set([ + EModelEndpoint.gptPlugins, + EModelEndpoint.anthropic, + EModelEndpoint.google, + EModelEndpoint.openAI, + EModelEndpoint.azureOpenAI, + EModelEndpoint.custom, +]); + +export const supportsFiles = { + [EModelEndpoint.openAI]: true, + [EModelEndpoint.google]: true, + [EModelEndpoint.assistant]: true, + [EModelEndpoint.azureOpenAI]: true, + [EModelEndpoint.custom]: true, +}; + +export const supportsBalanceCheck = { + [EModelEndpoint.openAI]: true, + [EModelEndpoint.azureOpenAI]: true, + [EModelEndpoint.gptPlugins]: true, + [EModelEndpoint.custom]: true, +}; + +export const visionModels = ['gpt-4-vision', 'llava-13b', 'gemini-pro-vision']; + +/** + * Enum for cache keys. + */ +export enum CacheKeys { + /** + * Key for the config store namespace. + */ + CONFIG_STORE = 'configStore', + /** + * Key for the plugins cache. + */ + PLUGINS = 'plugins', + /** + * Key for the model config cache. + */ + MODELS_CONFIG = 'modelsConfig', + /** + * Key for the default endpoint config cache. + */ + ENDPOINT_CONFIG = 'endpointsConfig', + /** + * Key for the custom config cache. + */ + CUSTOM_CONFIG = 'customConfig', + /** + * Key for the override config cache. + */ + OVERRIDE_CONFIG = 'overrideConfig', +} + +/** + * Enum for authentication keys. + */ +export enum AuthKeys { + /** + * Key for the Service Account to use Vertex AI. + */ + GOOGLE_SERVICE_KEY = 'GOOGLE_SERVICE_KEY', + /** + * API key to use Google Generative AI. + */ + GOOGLE_API_KEY = 'GOOGLE_API_KEY', +} diff --git a/packages/data-provider/src/createPayload.ts b/packages/data-provider/src/createPayload.ts index fd652065f64e..998b5774b5f2 100644 --- a/packages/data-provider/src/createPayload.ts +++ b/packages/data-provider/src/createPayload.ts @@ -1,13 +1,16 @@ -import { tConversationSchema } from './schemas'; import type { TSubmission, TMessage, TEndpointOption } from './types'; -import { EModelEndpoint, EndpointURLs } from './types'; +import { tConvoUpdateSchema, EModelEndpoint } from './schemas'; +import { EndpointURLs } from './config'; export default function createPayload(submission: TSubmission) { const { conversation, message, messages, endpointOption, isEdited, isContinued } = submission; - const { conversationId } = tConversationSchema.parse(conversation); - const { endpoint } = endpointOption as { endpoint: EModelEndpoint }; + const { conversationId } = tConvoUpdateSchema.parse(conversation); + const { endpoint, endpointType } = endpointOption as { + endpoint: EModelEndpoint; + endpointType?: EModelEndpoint; + }; - let server = EndpointURLs[endpoint]; + let server = EndpointURLs[endpointType ?? endpoint]; if (isEdited && endpoint === EModelEndpoint.assistant) { server += '/modify'; diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index 4b45891dc924..318829adafa5 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -1,8 +1,13 @@ -/* types/schemas/schema helpers */ +/* config */ +export * from './config'; +/* schema helpers */ +export * from './parsers'; +/* types (exports schemas from `./types` as they contain needed in other defs) */ export * from './types'; export * from './types/assistants'; export * from './types/files'; export * from './types/mutations'; +/* query/mutation keys */ export * from './keys'; /* api call helpers */ export * from './headers-helpers'; diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts new file mode 100644 index 000000000000..89872b1b0a42 --- /dev/null +++ b/packages/data-provider/src/parsers.ts @@ -0,0 +1,225 @@ +import type { TConversation, TPreset } from './schemas'; +import type { TEndpointOption } from './types'; +import { + EModelEndpoint, + openAISchema, + googleSchema, + bingAISchema, + anthropicSchema, + chatGPTBrowserSchema, + gptPluginsSchema, + assistantSchema, + compactOpenAISchema, + compactGoogleSchema, + compactAnthropicSchema, + compactChatGPTSchema, + compactPluginsSchema, +} from './schemas'; +import { alternateName } from './config'; + +type EndpointSchema = + | typeof openAISchema + | typeof googleSchema + | typeof bingAISchema + | typeof anthropicSchema + | typeof chatGPTBrowserSchema + | typeof gptPluginsSchema + | typeof assistantSchema; + +const endpointSchemas: Record = { + [EModelEndpoint.openAI]: openAISchema, + [EModelEndpoint.azureOpenAI]: openAISchema, + [EModelEndpoint.custom]: openAISchema, + [EModelEndpoint.google]: googleSchema, + [EModelEndpoint.bingAI]: bingAISchema, + [EModelEndpoint.anthropic]: anthropicSchema, + [EModelEndpoint.chatGPTBrowser]: chatGPTBrowserSchema, + [EModelEndpoint.gptPlugins]: gptPluginsSchema, + [EModelEndpoint.assistant]: assistantSchema, +}; + +// const schemaCreators: Record EndpointSchema> = { +// [EModelEndpoint.google]: createGoogleSchema, +// }; + +export function getFirstDefinedValue(possibleValues: string[]) { + let returnValue; + for (const value of possibleValues) { + if (value) { + returnValue = value; + break; + } + } + return returnValue; +} + +export type TPossibleValues = { + models: string[]; + secondaryModels?: string[]; +}; + +export const parseConvo = ({ + endpoint, + endpointType, + conversation, + possibleValues, +}: { + endpoint: EModelEndpoint; + endpointType?: EModelEndpoint; + conversation: Partial; + possibleValues?: TPossibleValues; + // TODO: POC for default schema + // defaultSchema?: Partial, +}) => { + let schema = endpointSchemas[endpoint]; + + if (!schema && !endpointType) { + throw new Error(`Unknown endpoint: ${endpoint}`); + } else if (!schema && endpointType) { + schema = endpointSchemas[endpointType]; + } + + // if (defaultSchema && schemaCreators[endpoint]) { + // schema = schemaCreators[endpoint](defaultSchema); + // } + + const convo = schema.parse(conversation) as TConversation; + const { models, secondaryModels } = possibleValues ?? {}; + + if (models && convo) { + convo.model = getFirstDefinedValue(models) ?? convo.model; + } + + if (secondaryModels && convo.agentOptions) { + convo.agentOptions.model = getFirstDefinedValue(secondaryModels) ?? convo.agentOptions.model; + } + + return convo; +}; + +export const getResponseSender = (endpointOption: TEndpointOption): string => { + const { model, endpoint, endpointType, modelDisplayLabel, chatGptLabel, modelLabel, jailbreak } = + endpointOption; + + if ( + [ + EModelEndpoint.openAI, + EModelEndpoint.azureOpenAI, + EModelEndpoint.gptPlugins, + EModelEndpoint.chatGPTBrowser, + ].includes(endpoint) + ) { + if (chatGptLabel) { + return chatGptLabel; + } else if (model && model.includes('gpt-3')) { + return 'GPT-3.5'; + } else if (model && model.includes('gpt-4')) { + return 'GPT-4'; + } else if (model && model.includes('mistral')) { + return 'Mistral'; + } + return alternateName[endpoint] ?? 'ChatGPT'; + } + + if (endpoint === EModelEndpoint.bingAI) { + return jailbreak ? 'Sydney' : 'BingAI'; + } + + if (endpoint === EModelEndpoint.anthropic) { + return modelLabel ?? 'Claude'; + } + + if (endpoint === EModelEndpoint.google) { + if (modelLabel) { + return modelLabel; + } else if (model && model.includes('gemini')) { + return 'Gemini'; + } else if (model && model.includes('code')) { + return 'Codey'; + } + + return 'PaLM2'; + } + + if (endpoint === EModelEndpoint.custom || endpointType === EModelEndpoint.custom) { + if (modelLabel) { + return modelLabel; + } else if (chatGptLabel) { + return chatGptLabel; + } else if (model && model.includes('mistral')) { + return 'Mistral'; + } else if (model && model.includes('gpt-3')) { + return 'GPT-3.5'; + } else if (model && model.includes('gpt-4')) { + return 'GPT-4'; + } else if (modelDisplayLabel) { + return modelDisplayLabel; + } + + return 'AI'; + } + + return ''; +}; + +type CompactEndpointSchema = + | typeof compactOpenAISchema + | typeof assistantSchema + | typeof compactGoogleSchema + | typeof bingAISchema + | typeof compactAnthropicSchema + | typeof compactChatGPTSchema + | typeof compactPluginsSchema; + +const compactEndpointSchemas: Record = { + openAI: compactOpenAISchema, + azureOpenAI: compactOpenAISchema, + custom: compactOpenAISchema, + assistant: assistantSchema, + google: compactGoogleSchema, + /* BingAI needs all fields */ + bingAI: bingAISchema, + anthropic: compactAnthropicSchema, + chatGPTBrowser: compactChatGPTSchema, + gptPlugins: compactPluginsSchema, +}; + +export const parseCompactConvo = ({ + endpoint, + endpointType, + conversation, + possibleValues, +}: { + endpoint?: EModelEndpoint; + endpointType?: EModelEndpoint; + conversation: Partial; + possibleValues?: TPossibleValues; + // TODO: POC for default schema + // defaultSchema?: Partial, +}) => { + if (!endpoint) { + throw new Error(`undefined endpoint: ${endpoint}`); + } + + let schema = compactEndpointSchemas[endpoint]; + + if (!schema && !endpointType) { + throw new Error(`Unknown endpoint: ${endpoint}`); + } else if (!schema && endpointType) { + schema = compactEndpointSchemas[endpointType]; + } + + const convo = schema.parse(conversation) as TConversation; + // const { models, secondaryModels } = possibleValues ?? {}; + const { models } = possibleValues ?? {}; + + if (models && convo) { + convo.model = getFirstDefinedValue(models) ?? convo.model; + } + + // if (secondaryModels && convo.agentOptions) { + // convo.agentOptionmodel = getFirstDefinedValue(secondaryModels) ?? convo.agentOptionmodel; + // } + + return convo; +}; diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 4698ea6a556b..fb4979812ce7 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -9,75 +9,7 @@ export enum EModelEndpoint { gptPlugins = 'gptPlugins', anthropic = 'anthropic', assistant = 'assistant', -} - -export const defaultEndpoints: EModelEndpoint[] = [ - EModelEndpoint.openAI, - EModelEndpoint.assistant, - EModelEndpoint.azureOpenAI, - EModelEndpoint.bingAI, - EModelEndpoint.chatGPTBrowser, - EModelEndpoint.gptPlugins, - EModelEndpoint.google, - EModelEndpoint.anthropic, -]; - -export const defaultModels = { - [EModelEndpoint.google]: [ - 'gemini-pro', - 'gemini-pro-vision', - 'chat-bison', - 'chat-bison-32k', - 'codechat-bison', - 'codechat-bison-32k', - 'text-bison', - 'text-bison-32k', - 'text-unicorn', - 'code-gecko', - 'code-bison', - 'code-bison-32k', - ], - [EModelEndpoint.anthropic]: [ - 'claude-2.1', - 'claude-2', - 'claude-1.2', - 'claude-1', - 'claude-1-100k', - 'claude-instant-1', - 'claude-instant-1-100k', - ], - [EModelEndpoint.openAI]: [ - 'gpt-3.5-turbo-16k-0613', - 'gpt-3.5-turbo-16k', - 'gpt-4-1106-preview', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-1106', - 'gpt-4-vision-preview', - 'gpt-4', - 'gpt-3.5-turbo-instruct-0914', - 'gpt-3.5-turbo-0613', - 'gpt-3.5-turbo-0301', - 'gpt-3.5-turbo-instruct', - 'gpt-4-0613', - 'text-davinci-003', - 'gpt-4-0314', - ], -}; - -export const alternateName = { - [EModelEndpoint.openAI]: 'OpenAI', - [EModelEndpoint.assistant]: 'Assistants', - [EModelEndpoint.azureOpenAI]: 'Azure OpenAI', - [EModelEndpoint.bingAI]: 'Bing', - [EModelEndpoint.chatGPTBrowser]: 'ChatGPT', - [EModelEndpoint.gptPlugins]: 'Plugins', - [EModelEndpoint.google]: 'Google', - [EModelEndpoint.anthropic]: 'Anthropic', -}; - -export enum AuthKeys { - GOOGLE_SERVICE_KEY = 'GOOGLE_SERVICE_KEY', - GOOGLE_API_KEY = 'GOOGLE_API_KEY', + custom = 'custom', } export const endpointSettings = { @@ -116,41 +48,10 @@ export const endpointSettings = { const google = endpointSettings[EModelEndpoint.google]; -export const EndpointURLs: { [key in EModelEndpoint]: string } = { - [EModelEndpoint.azureOpenAI]: '/api/ask/azureOpenAI', - [EModelEndpoint.openAI]: '/api/ask/openAI', - [EModelEndpoint.bingAI]: '/api/ask/bingAI', - [EModelEndpoint.chatGPTBrowser]: '/api/ask/chatGPTBrowser', - [EModelEndpoint.google]: '/api/ask/google', - [EModelEndpoint.gptPlugins]: '/api/ask/gptPlugins', - [EModelEndpoint.anthropic]: '/api/ask/anthropic', - [EModelEndpoint.assistant]: '/api/assistants/chat', -}; - -export const modularEndpoints = new Set([ - EModelEndpoint.gptPlugins, - EModelEndpoint.anthropic, - EModelEndpoint.google, - EModelEndpoint.openAI, -]); - -export const supportsFiles = { - [EModelEndpoint.openAI]: true, - [EModelEndpoint.google]: true, - [EModelEndpoint.assistant]: true, - [EModelEndpoint.azureOpenAI]: true, -}; - -export const supportsBalanceCheck = { - [EModelEndpoint.openAI]: true, - [EModelEndpoint.azureOpenAI]: true, - [EModelEndpoint.gptPlugins]: true, -}; - -export const visionModels = ['gpt-4-vision', 'llava-13b', 'gemini-pro-vision']; - export const eModelEndpointSchema = z.nativeEnum(EModelEndpoint); +export const extendedModelEndpointSchema = z.union([eModelEndpointSchema, z.string()]); + export const tPluginAuthConfigSchema = z.object({ authField: z.string(), label: z.string(), @@ -253,6 +154,7 @@ export const tConversationSchema = z.object({ title: z.string().nullable().or(z.literal('New Chat')).default('New Chat'), user: z.string().optional(), endpoint: eModelEndpointSchema.nullable(), + endpointType: eModelEndpointSchema.optional(), suggestions: z.array(z.string()).optional(), messages: z.array(z.string()).optional(), tools: z.array(tPluginSchema).optional(), @@ -305,8 +207,22 @@ export const tPresetSchema = tConversationSchema }), ); +export const tConvoUpdateSchema = tConversationSchema.merge( + z.object({ + endpoint: extendedModelEndpointSchema.nullable(), + }), +); + +export const tPresetUpdateSchema = tConversationSchema.merge( + z.object({ + endpoint: extendedModelEndpointSchema.nullable(), + }), +); + export type TPreset = z.infer; +// type DefaultSchemaValues = Partial; + export const openAISchema = tConversationSchema .pick({ model: true, @@ -528,122 +444,6 @@ export const assistantSchema = tConversationSchema .transform(removeNullishValues) .catch(() => ({})); -type EndpointSchema = - | typeof openAISchema - | typeof googleSchema - | typeof bingAISchema - | typeof anthropicSchema - | typeof chatGPTBrowserSchema - | typeof gptPluginsSchema - | typeof assistantSchema; - -const endpointSchemas: Record = { - [EModelEndpoint.openAI]: openAISchema, - [EModelEndpoint.azureOpenAI]: openAISchema, - [EModelEndpoint.google]: googleSchema, - [EModelEndpoint.bingAI]: bingAISchema, - [EModelEndpoint.anthropic]: anthropicSchema, - [EModelEndpoint.chatGPTBrowser]: chatGPTBrowserSchema, - [EModelEndpoint.gptPlugins]: gptPluginsSchema, - [EModelEndpoint.assistant]: assistantSchema, -}; - -export function getFirstDefinedValue(possibleValues: string[]) { - let returnValue; - for (const value of possibleValues) { - if (value) { - returnValue = value; - break; - } - } - return returnValue; -} - -export type TPossibleValues = { - models: string[]; - secondaryModels?: string[]; -}; - -export const parseConvo = ( - endpoint: EModelEndpoint, - conversation: Partial, - possibleValues?: TPossibleValues, -) => { - const schema = endpointSchemas[endpoint]; - - if (!schema) { - throw new Error(`Unknown endpoint: ${endpoint}`); - } - - const convo = schema.parse(conversation) as TConversation; - const { models, secondaryModels } = possibleValues ?? {}; - - if (models && convo) { - convo.model = getFirstDefinedValue(models) ?? convo.model; - } - - if (secondaryModels && convo.agentOptions) { - convo.agentOptions.model = getFirstDefinedValue(secondaryModels) ?? convo.agentOptions.model; - } - - return convo; -}; - -export type TEndpointOption = { - endpoint: EModelEndpoint; - model?: string | null; - promptPrefix?: string; - temperature?: number; - chatGptLabel?: string | null; - modelLabel?: string | null; - jailbreak?: boolean; - key?: string | null; -}; - -export const getResponseSender = (endpointOption: TEndpointOption): string => { - const { model, endpoint, chatGptLabel, modelLabel, jailbreak } = endpointOption; - - if ( - [ - EModelEndpoint.openAI, - EModelEndpoint.azureOpenAI, - EModelEndpoint.gptPlugins, - EModelEndpoint.chatGPTBrowser, - ].includes(endpoint) - ) { - if (chatGptLabel) { - return chatGptLabel; - } else if (model && model.includes('gpt-3')) { - return 'GPT-3.5'; - } else if (model && model.includes('gpt-4')) { - return 'GPT-4'; - } - return alternateName[endpoint] ?? 'ChatGPT'; - } - - if (endpoint === EModelEndpoint.bingAI) { - return jailbreak ? 'Sydney' : 'BingAI'; - } - - if (endpoint === EModelEndpoint.anthropic) { - return modelLabel ?? 'Claude'; - } - - if (endpoint === EModelEndpoint.google) { - if (modelLabel) { - return modelLabel; - } else if (model && model.includes('gemini')) { - return 'Gemini'; - } else if (model && model.includes('code')) { - return 'Codey'; - } - - return 'PaLM2'; - } - - return ''; -}; - export const compactOpenAISchema = tConversationSchema .pick({ model: true, @@ -809,53 +609,52 @@ export const compactPluginsSchema = tConversationSchema }) .catch(() => ({})); -type CompactEndpointSchema = - | typeof compactOpenAISchema - | typeof assistantSchema - | typeof compactGoogleSchema - | typeof bingAISchema - | typeof compactAnthropicSchema - | typeof compactChatGPTSchema - | typeof compactPluginsSchema; - -const compactEndpointSchemas: Record = { - openAI: compactOpenAISchema, - azureOpenAI: compactOpenAISchema, - assistant: assistantSchema, - google: compactGoogleSchema, - /* BingAI needs all fields */ - bingAI: bingAISchema, - anthropic: compactAnthropicSchema, - chatGPTBrowser: compactChatGPTSchema, - gptPlugins: compactPluginsSchema, -}; - -export const parseCompactConvo = ( - endpoint: EModelEndpoint | undefined, - conversation: Partial, - possibleValues?: TPossibleValues, -) => { - if (!endpoint) { - throw new Error(`undefined endpoint: ${endpoint}`); - } - - const schema = compactEndpointSchemas[endpoint]; - - if (!schema) { - throw new Error(`Unknown endpoint: ${endpoint}`); - } - - const convo = schema.parse(conversation) as TConversation; - // const { models, secondaryModels } = possibleValues ?? {}; - const { models } = possibleValues ?? {}; - - if (models && convo) { - convo.model = getFirstDefinedValue(models) ?? convo.model; - } - - // if (secondaryModels && convo.agentOptions) { - // convo.agentOptionmodel = getFirstDefinedValue(secondaryModels) ?? convo.agentOptionmodel; - // } - - return convo; -}; +// const createGoogleSchema = (customGoogle: DefaultSchemaValues) => { +// const defaults = { ...google, ...customGoogle }; +// return tConversationSchema +// .pick({ +// model: true, +// modelLabel: true, +// promptPrefix: true, +// examples: true, +// temperature: true, +// maxOutputTokens: true, +// topP: true, +// topK: true, +// }) +// .transform((obj) => { +// const isGeminiPro = obj?.model?.toLowerCase()?.includes('gemini-pro'); + +// const maxOutputTokensMax = isGeminiPro +// ? defaults.maxOutputTokens.maxGeminiPro +// : defaults.maxOutputTokens.max; +// const maxOutputTokensDefault = isGeminiPro +// ? defaults.maxOutputTokens.defaultGeminiPro +// : defaults.maxOutputTokens.default; + +// let maxOutputTokens = obj.maxOutputTokens ?? maxOutputTokensDefault; +// maxOutputTokens = Math.min(maxOutputTokens, maxOutputTokensMax); + +// return { +// ...obj, +// model: obj.model ?? defaults.model.default, +// modelLabel: obj.modelLabel ?? null, +// promptPrefix: obj.promptPrefix ?? null, +// examples: obj.examples ?? [{ input: { content: '' }, output: { content: '' } }], +// temperature: obj.temperature ?? defaults.temperature.default, +// maxOutputTokens, +// topP: obj.topP ?? defaults.topP.default, +// topK: obj.topK ?? defaults.topK.default, +// }; +// }) +// .catch(() => ({ +// model: defaults.model.default, +// modelLabel: null, +// promptPrefix: null, +// examples: [{ input: { content: '' }, output: { content: '' } }], +// temperature: defaults.temperature.default, +// maxOutputTokens: defaults.maxOutputTokens.default, +// topP: defaults.topP.default, +// topK: defaults.topK.default, +// })); +// }; diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 0e66da603e8c..0921cbbe97a1 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import type { TResPlugin, TMessage, TConversation, TEndpointOption } from './schemas'; +import type { TResPlugin, TMessage, TConversation, EModelEndpoint } from './schemas'; export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam; export type TOpenAIFunction = OpenAI.Chat.ChatCompletionCreateParams.Function; @@ -11,6 +11,19 @@ export type TMessages = TMessage[]; export type TMessagesAtom = TMessages | null; +export type TEndpointOption = { + endpoint: EModelEndpoint; + endpointType?: EModelEndpoint; + modelDisplayLabel?: string; + model?: string | null; + promptPrefix?: string; + temperature?: number; + chatGptLabel?: string | null; + modelLabel?: string | null; + jailbreak?: boolean; + key?: string | null; +}; + export type TSubmission = { plugin?: TResPlugin; plugins?: TResPlugin[]; @@ -114,17 +127,21 @@ export type TSearchResults = { }; export type TConfig = { - availableModels?: []; - userProvide?: boolean | null; + order: number; + type?: EModelEndpoint; + azure?: boolean; availableTools?: []; plugins?: Record; - azure?: boolean; - order: number; + name?: string; + iconURL?: string; + modelDisplayLabel?: string; + userProvide?: boolean | null; + userProvideURL?: boolean | null; }; export type TModelsConfig = Record; -export type TEndpointsConfig = Record; +export type TEndpointsConfig = Record; export type TUpdateTokenCountResponse = { count: number; From ac9543a6737b7cc1fa4c67438e81cb528496380c Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 3 Jan 2024 10:07:36 -0500 Subject: [PATCH 21/42] =?UTF-8?q?=F0=9F=8E=A8=20style:=20Add=20Dynamic=20H?= =?UTF-8?q?eight=20to=20Endpoint/Model=20Menus=20(#1480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style(EndpointsMenu): add scrolling and dynamic height * style(SelectDropDownPop): add dynamic height --- client/src/components/Chat/Menus/EndpointsMenu.tsx | 2 +- client/src/components/ui/SelectDropDownPop.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/Chat/Menus/EndpointsMenu.tsx b/client/src/components/Chat/Menus/EndpointsMenu.tsx index f7339ac2989f..a7d4901210f7 100644 --- a/client/src/components/Chat/Menus/EndpointsMenu.tsx +++ b/client/src/components/Chat/Menus/EndpointsMenu.tsx @@ -36,7 +36,7 @@ const EndpointsMenu: FC = () => { diff --git a/client/src/components/ui/SelectDropDownPop.tsx b/client/src/components/ui/SelectDropDownPop.tsx index 0e40b70d583f..51c453fa6eaf 100644 --- a/client/src/components/ui/SelectDropDownPop.tsx +++ b/client/src/components/ui/SelectDropDownPop.tsx @@ -95,7 +95,7 @@ function SelectDropDownPop({ {availableValues.map((option) => { return ( @@ -105,7 +105,7 @@ function SelectDropDownPop({ value={option} selected={!!(value && value === option)} onClick={() => setValue(option)} - > + /> ); })} From d6d3d2ba139fb2d82c26488b73ca5273450f8e1b Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 3 Jan 2024 10:59:38 -0500 Subject: [PATCH 22/42] =?UTF-8?q?=F0=9F=94=A7=20fix:=20langchain=20package?= =?UTF-8?q?s=20mismatch,=20mount=20config=20file=20for=20`deploy-compose.y?= =?UTF-8?q?aml`,=20silence=20config=20not=20found=20error=20(#1481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(api): version mismatch between langchain packages `@langchain/google-genai` & `langchain` * chore(loadYaml): silence config file not found error * chore: improve firebase init message when not configured (generalized) * fix(deploy-compose.yml): mount `librechat.yaml` config file --- api/package.json | 4 +-- .../services/Files/Firebase/initialize.js | 7 ++--- api/utils/loadYaml.js | 2 +- deploy-compose.yml | 1 + package-lock.json | 26 +++++++++---------- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/api/package.json b/api/package.json index 56d1a7e59b90..38e63ba5c9d4 100644 --- a/api/package.json +++ b/api/package.json @@ -31,7 +31,7 @@ "@azure/search-documents": "^12.0.0", "@keyv/mongo": "^2.1.8", "@keyv/redis": "^2.8.1", - "@langchain/google-genai": "^0.0.2", + "@langchain/google-genai": "^0.0.7", "axios": "^1.3.4", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", @@ -54,7 +54,7 @@ "keyv": "^4.5.4", "keyv-file": "^0.2.0", "klona": "^2.0.6", - "langchain": "^0.0.213", + "langchain": "^0.0.214", "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.33.0", diff --git a/api/server/services/Files/Firebase/initialize.js b/api/server/services/Files/Firebase/initialize.js index 5dc1f9379157..67d923c44f88 100644 --- a/api/server/services/Files/Firebase/initialize.js +++ b/api/server/services/Files/Firebase/initialize.js @@ -21,16 +21,13 @@ const initializeFirebase = () => { }; if (Object.values(firebaseConfig).some((value) => !value)) { - i === 0 && - logger.info( - '[Optional] Firebase configuration missing or incomplete. Firebase will not be initialized.', - ); + i === 0 && logger.info('[Optional] CDN not initialized.'); i++; return null; } firebaseApp = firebase.initializeApp(firebaseConfig); - logger.info('Firebase initialized'); + logger.info('Firebase CDN initialized'); return firebaseApp; }; diff --git a/api/utils/loadYaml.js b/api/utils/loadYaml.js index eec7e2ec6ced..b7068e209f01 100644 --- a/api/utils/loadYaml.js +++ b/api/utils/loadYaml.js @@ -6,7 +6,7 @@ function loadYaml(filepath) { let fileContents = fs.readFileSync(filepath, 'utf8'); return yaml.load(fileContents); } catch (e) { - console.error(e); + // console.error(e); } } diff --git a/deploy-compose.yml b/deploy-compose.yml index e811578bdee9..4f432c31e053 100644 --- a/deploy-compose.yml +++ b/deploy-compose.yml @@ -23,6 +23,7 @@ services: - MEILI_HOST=http://meilisearch:7700 volumes: - ./images:/app/client/public/images + - ./librechat.yaml:/app/librechat.yaml client: build: context: . diff --git a/package-lock.json b/package-lock.json index 806b828ce55c..bd7b518ec4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "@azure/search-documents": "^12.0.0", "@keyv/mongo": "^2.1.8", "@keyv/redis": "^2.8.1", - "@langchain/google-genai": "^0.0.2", + "@langchain/google-genai": "^0.0.7", "axios": "^1.3.4", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", @@ -68,7 +68,7 @@ "keyv": "^4.5.4", "keyv-file": "^0.2.0", "klona": "^2.0.6", - "langchain": "^0.0.213", + "langchain": "^0.0.214", "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.33.0", @@ -131,9 +131,9 @@ } }, "api/node_modules/@langchain/community": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.0.12.tgz", - "integrity": "sha512-mcm6FxxnLxSx9PiYvehGGwvcHjsVR5WXfYOwymojf/6d0apyewjOLzKsR3xx0HJVtCs8pff7NZSdDoE+jj8OcA==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.0.13.tgz", + "integrity": "sha512-Dh7ri1ZySfX5t6Zz7+jR0kHp769j1+S4IHNyoDnzWlHfPFA5hfF9+zrRRmxV17KVCUawQNoOxgNmZnUjNMm5qg==", "dependencies": { "@langchain/core": "~0.1.5", "@langchain/openai": "~0.0.9", @@ -565,12 +565,12 @@ } }, "api/node_modules/langchain": { - "version": "0.0.213", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.0.213.tgz", - "integrity": "sha512-nQDOJXvtIAIuUzamCiF1AWyi2GH9FSDPR+3XulJUEpdU60aSFPZ9GBiWdu+dVHXeAmm8C0iCVi0+3GWLJrUoXA==", + "version": "0.0.214", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.0.214.tgz", + "integrity": "sha512-HWdgjuqsir7MYSX3OcKW+XUFZyqhma8yfn4NU/7K5MuIz649g0SejVjuHr3rexmsOvZyHeR3XlwIsqxEtOsF7A==", "dependencies": { "@anthropic-ai/sdk": "^0.9.1", - "@langchain/community": "~0.0.12", + "@langchain/community": "~0.0.13", "@langchain/core": "~0.1.5", "@langchain/openai": "~0.0.9", "binary-extensions": "^2.2.0", @@ -6501,12 +6501,12 @@ } }, "node_modules/@langchain/google-genai": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-0.0.2.tgz", - "integrity": "sha512-Q6zgVeZ0IzD976LGhwl86RwyTn6zpdwltVTYGEEag3AyT3zDzALPiyEfORFxublQjIVeIoTiyDJ9MT9nXb1xwg==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-0.0.7.tgz", + "integrity": "sha512-0VUrzVRS5PW/HhGVdTelDZd8DJVXdyLj2KqHpUNWdXNNMKaLVV6AY2nwqKLA3I4SV0VfOt1/XoZAgPIpQfn4Ow==", "dependencies": { "@google/generative-ai": "^0.1.0", - "@langchain/core": "~0.1.0" + "@langchain/core": "~0.1.5" }, "engines": { "node": ">=18" From 4befee829bb3b25ec653e25c164c2efba866d385 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:26:13 -0500 Subject: [PATCH 23/42] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix:=20Error=20Me?= =?UTF-8?q?ssage=20Parsing=20and=20ChatOpenAI=20credentials=20(#1482)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(createLLM): ensure ChatOpenAI class always uses client-defined openAIApiKey; move typedefs to main def file * refactor(useSSE): improve error message parsing in error handler --- api/app/clients/llm/createLLM.js | 33 +---------------------------- api/typedefs.js | 36 ++++++++++++++++++++++++++++++++ client/src/hooks/useSSE.ts | 13 +++++++++--- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/api/app/clients/llm/createLLM.js b/api/app/clients/llm/createLLM.js index 020fba65034f..de5fa18e77dc 100644 --- a/api/app/clients/llm/createLLM.js +++ b/api/app/clients/llm/createLLM.js @@ -2,38 +2,6 @@ const { ChatOpenAI } = require('langchain/chat_models/openai'); const { sanitizeModelName } = require('../../../utils'); const { isEnabled } = require('../../../server/utils'); -/** - * @typedef {Object} ModelOptions - * @property {string} modelName - The name of the model. - * @property {number} [temperature] - The temperature setting for the model. - * @property {number} [presence_penalty] - The presence penalty setting. - * @property {number} [frequency_penalty] - The frequency penalty setting. - * @property {number} [max_tokens] - The maximum number of tokens to generate. - */ - -/** - * @typedef {Object} ConfigOptions - * @property {string} [basePath] - The base path for the API requests. - * @property {Object} [baseOptions] - Base options for the API requests, including headers. - * @property {Object} [httpAgent] - The HTTP agent for the request. - * @property {Object} [httpsAgent] - The HTTPS agent for the request. - */ - -/** - * @typedef {Object} Callbacks - * @property {Function} [handleChatModelStart] - A callback function for handleChatModelStart - * @property {Function} [handleLLMEnd] - A callback function for handleLLMEnd - * @property {Function} [handleLLMError] - A callback function for handleLLMError - */ - -/** - * @typedef {Object} AzureOptions - * @property {string} [azureOpenAIApiKey] - The Azure OpenAI API key. - * @property {string} [azureOpenAIApiInstanceName] - The Azure OpenAI API instance name. - * @property {string} [azureOpenAIApiDeploymentName] - The Azure OpenAI API deployment name. - * @property {string} [azureOpenAIApiVersion] - The Azure OpenAI API version. - */ - /** * Creates a new instance of a language model (LLM) for chat interactions. * @@ -96,6 +64,7 @@ function createLLM({ configuration, ...azureOptions, ...modelOptions, + ...credentials, callbacks, }, configOptions, diff --git a/api/typedefs.js b/api/typedefs.js index e96d7dba2924..e40a09763492 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -337,3 +337,39 @@ * @property {number} order - The order of the endpoint. * @memberof typedefs */ + +/** + * @typedef {Object} ModelOptions + * @property {string} modelName - The name of the model. + * @property {number} [temperature] - The temperature setting for the model. + * @property {number} [presence_penalty] - The presence penalty setting. + * @property {number} [frequency_penalty] - The frequency penalty setting. + * @property {number} [max_tokens] - The maximum number of tokens to generate. + * @memberof typedefs + */ + +/** + * @typedef {Object} ConfigOptions + * @property {string} [basePath] - The base path for the API requests. + * @property {Object} [baseOptions] - Base options for the API requests, including headers. + * @property {Object} [httpAgent] - The HTTP agent for the request. + * @property {Object} [httpsAgent] - The HTTPS agent for the request. + * @memberof typedefs + */ + +/** + * @typedef {Object} Callbacks + * @property {Function} [handleChatModelStart] - A callback function for handleChatModelStart + * @property {Function} [handleLLMEnd] - A callback function for handleLLMEnd + * @property {Function} [handleLLMError] - A callback function for handleLLMError + * @memberof typedefs + */ + +/** + * @typedef {Object} AzureOptions + * @property {string} [azureOpenAIApiKey] - The Azure OpenAI API key. + * @property {string} [azureOpenAIApiInstanceName] - The Azure OpenAI API instance name. + * @property {string} [azureOpenAIApiDeploymentName] - The Azure OpenAI API deployment name. + * @property {string} [azureOpenAIApiVersion] - The Azure OpenAI API version. + * @memberof typedefs + */ diff --git a/client/src/hooks/useSSE.ts b/client/src/hooks/useSSE.ts index 64b209148461..0cc7957f5b37 100644 --- a/client/src/hooks/useSSE.ts +++ b/client/src/hooks/useSSE.ts @@ -209,16 +209,23 @@ export default function useSSE(submission: TSubmission | null, index = 0) { }; const errorHandler = ({ data, submission }: { data?: TResData; submission: TSubmission }) => { - const { messages, message } = submission; + const { messages, message, initialResponse } = submission; const conversationId = message?.conversationId ?? submission?.conversationId; const parseErrorResponse = (data: TResData | Partial) => { const metadata = data['responseMessage'] ?? data; - return tMessageSchema.parse({ + const errorMessage = { + ...initialResponse, ...metadata, error: true, parentMessageId: message?.messageId, - }); + }; + + if (!errorMessage.messageId) { + errorMessage.messageId = v4(); + } + + return tMessageSchema.parse(errorMessage); }; if (!data) { From e1a529b5aeed9505acc2e41ad05ba07c28758a3f Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 3 Jan 2024 19:17:42 -0500 Subject: [PATCH 24/42] =?UTF-8?q?=F0=9F=A7=AA=20feat:=20Experimental:=20En?= =?UTF-8?q?able=20Switching=20Endpoints=20Mid-Conversation=20(#1483)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: load all existing conversation settings on refresh * refactor(buildDefaultConvo): use `lastConversationSetup.endpointType` before `conversation.endpointType` * refactor(TMessage/messageSchema): add `endpoint` field to messages to differentiate generation origin * feat(useNewConvo): `keepLatestMessage` param to prevent reseting the `latestMessage` mid-conversation * style(Settings): adjust height styling to allow more space in dialog for additional settings * feat: Modular Chat: experimental setting to Enable switching Endpoints mid-conversation * fix(ChatRoute): fix potential parsing issue with tPresetSchema --- api/app/clients/BaseClient.js | 2 +- api/models/Message.js | 2 ++ api/models/schema/messageSchema.js | 5 +-- api/server/controllers/AskController.js | 2 ++ .../Chat/Menus/Endpoints/MenuItem.tsx | 28 ++++++++++++--- client/src/components/Nav/Settings.tsx | 6 ++-- .../Nav/SettingsTabs/General/General.tsx | 6 +++- .../Nav/SettingsTabs/General/ModularChat.tsx | 35 +++++++++++++++++++ client/src/hooks/Conversations/usePresets.ts | 27 ++++++++------ client/src/hooks/useChatHelpers.ts | 1 + client/src/hooks/useNewConvo.ts | 9 +++-- client/src/localization/languages/Eng.tsx | 2 ++ client/src/routes/ChatRoute.tsx | 3 ++ client/src/store/settings.ts | 20 +++++++++++ client/src/utils/buildDefaultConvo.ts | 2 +- packages/data-provider/src/schemas.ts | 1 + 16 files changed, 127 insertions(+), 24 deletions(-) create mode 100644 client/src/components/Nav/SettingsTabs/General/ModularChat.tsx diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 17edf685574f..5b7ea103c2d6 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -516,7 +516,7 @@ class BaseClient { } async saveMessageToDatabase(message, endpointOptions, user = null) { - await saveMessage({ ...message, user, unfinished: false }); + await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false }); await saveConvo(user, { conversationId: message.conversationId, endpoint: this.options.endpoint, diff --git a/api/models/Message.js b/api/models/Message.js index 7accf9285a8b..fe615f3283f0 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -9,6 +9,7 @@ module.exports = { async saveMessage({ user, + endpoint, messageId, newMessageId, conversationId, @@ -34,6 +35,7 @@ module.exports = { const update = { user, + endpoint, messageId: newMessageId || messageId, conversationId, parentMessageId, diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index 8e0b688d40f3..06da19e476de 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -23,9 +23,11 @@ const messageSchema = mongoose.Schema( type: String, default: null, }, + endpoint: { + type: String, + }, conversationSignature: { type: String, - // required: true }, clientId: { type: String, @@ -35,7 +37,6 @@ const messageSchema = mongoose.Schema( }, parentMessageId: { type: String, - // required: true }, tokenCount: { type: Number, diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index 6dc3949966a0..67d7c67e9f75 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -118,6 +118,8 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { response = { ...response, ...metadata }; } + response.endpoint = endpointOption.endpoint; + if (client.options.attachments) { userMessage.files = client.options.attachments; delete userMessage.image_urls; diff --git a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx index fd516bbab3b2..7e201786801e 100644 --- a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx @@ -1,11 +1,14 @@ import { useState } from 'react'; import { Settings } from 'lucide-react'; +import { useRecoilValue } from 'recoil'; import { EModelEndpoint } from 'librechat-data-provider'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { FC } from 'react'; -import { useLocalize, useUserKey } from '~/hooks'; +import type { TPreset } from 'librechat-data-provider'; +import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks'; import { SetKeyDialog } from '~/components/Input/SetKeyDialog'; import { useChatContext } from '~/Providers'; +import store from '~/store'; import { icons } from './Icons'; import { cn } from '~/utils'; @@ -27,10 +30,12 @@ const MenuItem: FC = ({ userProvidesKey, ...rest }) => { + const modularChat = useRecoilValue(store.modularChat); + const [isDialogOpen, setDialogOpen] = useState(false); const { data: endpointsConfig } = useGetEndpointsQuery(); + const { conversation, newConversation } = useChatContext(); + const getDefaultConversation = useDefaultConvo(); - const [isDialogOpen, setDialogOpen] = useState(false); - const { newConversation } = useChatContext(); const { getExpiry } = useUserKey(endpoint); const localize = useLocalize(); const expiryTime = getExpiry(); @@ -42,7 +47,22 @@ const MenuItem: FC = ({ if (!expiryTime) { setDialogOpen(true); } - newConversation({ template: { endpoint: newEndpoint, conversationId: 'new' } }); + const template: Partial = { endpoint: newEndpoint, conversationId: 'new' }; + const { conversationId } = conversation ?? {}; + if (modularChat && conversationId && conversationId !== 'new') { + template.endpointType = endpointsConfig?.[newEndpoint]?.type; + + const currentConvo = getDefaultConversation({ + /* target endpointType is necessary to avoid endpoint mixing */ + conversation: { ...(conversation ?? {}), endpointType: template.endpointType }, + preset: template, + }); + + /* We don't reset the latest message, only when changing settings mid-converstion */ + newConversation({ template: currentConvo, keepLatestMessage: true }); + return; + } + newConversation({ template }); } }; diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index da3fcefa7cf8..01196e1cf7df 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -1,9 +1,9 @@ import * as Tabs from '@radix-ui/react-tabs'; +import type { TDialogProps } from '~/common'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; import { GearIcon, DataIcon, UserIcon } from '~/components/svg'; -import { useMediaQuery, useLocalize } from '~/hooks'; -import type { TDialogProps } from '~/common'; import { General, Data, Account } from './SettingsTabs'; +import { useMediaQuery, useLocalize } from '~/hooks'; import { cn } from '~/utils'; export default function Settings({ open, onOpenChange }: TDialogProps) { @@ -13,7 +13,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { return ( diff --git a/client/src/components/Nav/SettingsTabs/General/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx index 48ecd6be006e..27e0db04a9c6 100644 --- a/client/src/components/Nav/SettingsTabs/General/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -12,9 +12,10 @@ import { } from '~/hooks'; import type { TDangerButtonProps } from '~/common'; import AutoScrollSwitch from './AutoScrollSwitch'; +import { Dropdown } from '~/components/ui'; import DangerButton from '../DangerButton'; +import ModularChat from './ModularChat'; import store from '~/store'; -import { Dropdown } from '~/components/ui'; export const ThemeSelector = ({ theme, @@ -188,6 +189,9 @@ function General() {
+
+ +
); diff --git a/client/src/components/Nav/SettingsTabs/General/ModularChat.tsx b/client/src/components/Nav/SettingsTabs/General/ModularChat.tsx new file mode 100644 index 000000000000..757c96bdde00 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/General/ModularChat.tsx @@ -0,0 +1,35 @@ +import { useRecoilState } from 'recoil'; +import { Switch } from '~/components/ui'; +import { useLocalize } from '~/hooks'; +import store from '~/store'; + +export default function ModularChatSwitch({ + onCheckedChange, +}: { + onCheckedChange?: (value: boolean) => void; +}) { + const [modularChat, setModularChat] = useRecoilState(store.modularChat); + const localize = useLocalize(); + + const handleCheckedChange = (value: boolean) => { + setModularChat(value); + if (onCheckedChange) { + onCheckedChange(value); + } + }; + + return ( +
+
+ {`[${localize('com_ui_experimental')}]`} {localize('com_nav_modular_chat')}{' '} +
+ +
+ ); +} diff --git a/client/src/hooks/Conversations/usePresets.ts b/client/src/hooks/Conversations/usePresets.ts index b697ae7e3bf2..140572712162 100644 --- a/client/src/hooks/Conversations/usePresets.ts +++ b/client/src/hooks/Conversations/usePresets.ts @@ -1,11 +1,11 @@ -import { QueryKeys, modularEndpoints } from 'librechat-data-provider'; -import { useCreatePresetMutation } from 'librechat-data-provider/react-query'; import filenamify from 'filenamify'; -import { useCallback, useEffect, useRef } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; import exportFromJSON from 'export-from-json'; +import { useCallback, useEffect, useRef } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import type { TPreset } from 'librechat-data-provider'; +import { QueryKeys, modularEndpoints } from 'librechat-data-provider'; +import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil'; +import { useCreatePresetMutation } from 'librechat-data-provider/react-query'; +import type { TPreset, TEndpointsConfig } from 'librechat-data-provider'; import { useUpdatePresetMutation, useDeletePresetMutation, @@ -27,6 +27,7 @@ export default function usePresets() { const { showToast } = useToastContext(); const { user, isAuthenticated } = useAuthContext(); + const modularChat = useRecoilValue(store.modularChat); const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset); const setPresetModalVisible = useSetRecoilState(store.presetModalVisible); const { preset, conversation, newConversation, setPreset } = useChatContext(); @@ -159,14 +160,20 @@ export default function usePresets() { duration: 750, }); + const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); + + const currentEndpointType = endpointsConfig?.[endpoint ?? '']?.type ?? ''; + const endpointType = endpointsConfig?.[newPreset?.endpoint ?? '']?.type; + if ( - modularEndpoints.has(endpoint ?? '') && - modularEndpoints.has(newPreset?.endpoint ?? '') && - endpoint === newPreset?.endpoint + (modularEndpoints.has(endpoint ?? '') || modularEndpoints.has(currentEndpointType)) && + (modularEndpoints.has(newPreset?.endpoint ?? '') || modularEndpoints.has(endpointType)) && + (endpoint === newPreset?.endpoint || modularChat) ) { const currentConvo = getDefaultConversation({ - conversation: conversation ?? {}, - preset: newPreset, + /* target endpointType is necessary to avoid endpoint mixing */ + conversation: { ...(conversation ?? {}), endpointType }, + preset: { ...newPreset, endpointType }, }); /* We don't reset the latest message, only when changing settings mid-converstion */ diff --git a/client/src/hooks/useChatHelpers.ts b/client/src/hooks/useChatHelpers.ts index 1cf3c16fea60..2afd61227779 100644 --- a/client/src/hooks/useChatHelpers.ts +++ b/client/src/hooks/useChatHelpers.ts @@ -225,6 +225,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) { const initialResponse: TMessage = { sender: responseSender, text: responseText, + endpoint: endpoint ?? '', parentMessageId: isRegenerate ? messageId : fakeMessageId, messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`, conversationId, diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index 8d8fd3680384..08ebef33b1c0 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -46,6 +46,7 @@ const useNewConvo = (index = 0) => { preset: TPreset | null = null, modelsData?: TModelsConfig, buildDefault?: boolean, + keepLatestMessage?: boolean, ) => { const modelsConfig = modelsData ?? snapshot.getLoadable(store.modelsConfig).contents; const { endpoint = null } = conversation; @@ -84,7 +85,9 @@ const useNewConvo = (index = 0) => { setStorage(conversation); setConversation(conversation); setSubmission({} as TSubmission); - resetLatestMessage(); + if (!keepLatestMessage) { + resetLatestMessage(); + } if (conversation.conversationId === 'new' && !modelsData) { navigate('new'); @@ -99,11 +102,13 @@ const useNewConvo = (index = 0) => { preset, modelsData, buildDefault = true, + keepLatestMessage = false, }: { template?: Partial; preset?: TPreset; modelsData?: TModelsConfig; buildDefault?: boolean; + keepLatestMessage?: boolean; } = {}) => { const conversation = { conversationId: 'new', @@ -130,7 +135,7 @@ const useNewConvo = (index = 0) => { } } - switchToConversation(conversation, preset, modelsData, buildDefault); + switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage); }, [switchToConversation, files, mutateAsync, setFiles], ); diff --git a/client/src/localization/languages/Eng.tsx b/client/src/localization/languages/Eng.tsx index 8a431707bcb0..eb255aaaa9e0 100644 --- a/client/src/localization/languages/Eng.tsx +++ b/client/src/localization/languages/Eng.tsx @@ -15,6 +15,7 @@ export default { com_ui_limitation_harmful_biased: 'May occasionally produce harmful instructions or biased content', com_ui_limitation_limited_2021: 'Limited knowledge of world and events after 2021', + com_ui_experimental: 'Experimental', com_ui_input: 'Input', com_ui_close: 'Close', com_ui_model: 'Model', @@ -257,6 +258,7 @@ export default { 'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.', com_nav_welcome_message: 'How can I help you today?', com_nav_auto_scroll: 'Auto-scroll to Newest on Open', + com_nav_modular_chat: 'Enable switching Endpoints mid-conversation', com_nav_profile_picture: 'Profile Picture', com_nav_change_picture: 'Change picture', com_nav_plugin_store: 'Plugin store', diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index de4f066cef3d..8cca53233b21 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -6,6 +6,7 @@ import { useGetModelsQuery, useGetEndpointsQuery, } from 'librechat-data-provider/react-query'; +import { TPreset } from 'librechat-data-provider'; import { useNewConvo, useConfigOverride } from '~/hooks'; import ChatView from '~/components/Chat/ChatView'; import useAuthRedirect from './useAuthRedirect'; @@ -45,6 +46,8 @@ export default function ChatRoute() { ) { newConversation({ template: initialConvoQuery.data, + /* this is necessary to load all existing settings */ + preset: initialConvoQuery.data as TPreset, modelsData: modelsQuery.data, }); hasSetConversation.current = true; diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 9b98ea661e3a..d580e4bf3b6d 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -50,6 +50,25 @@ const autoScroll = atom({ ] as const, }); +const modularChat = atom({ + key: 'modularChat', + default: localStorage.getItem('modularChat') === 'true', + effects: [ + ({ setSelf, onSet }) => { + const savedValue = localStorage.getItem('modularChat'); + if (savedValue != null) { + setSelf(savedValue === 'true'); + } + + onSet((newValue: unknown) => { + if (typeof newValue === 'boolean') { + localStorage.setItem('modularChat', newValue.toString()); + } + }); + }, + ] as const, +}); + export default { abortScroll, optionSettings, @@ -58,4 +77,5 @@ export default { showBingToneSetting, showPopover, autoScroll, + modularChat, }; diff --git a/client/src/utils/buildDefaultConvo.ts b/client/src/utils/buildDefaultConvo.ts index bf1032c9046f..4b30f035505d 100644 --- a/client/src/utils/buildDefaultConvo.ts +++ b/client/src/utils/buildDefaultConvo.ts @@ -15,7 +15,7 @@ const buildDefaultConvo = ({ }) => { const { lastSelectedModel, lastSelectedTools, lastBingSettings } = getLocalStorageItems(); const { jailbreak, toneStyle } = lastBingSettings; - const { endpointType } = conversation; + const endpointType = lastConversationSetup?.endpointType ?? conversation?.endpointType; if (!endpoint) { return { diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index fb4979812ce7..950cc18e4ce3 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -106,6 +106,7 @@ export const tAgentOptionsSchema = z.object({ export const tMessageSchema = z.object({ messageId: z.string(), + endpoint: z.string().optional(), clientId: z.string().nullable().optional(), conversationId: z.string().nullable(), parentMessageId: z.string().nullable(), From 42f23535099cd3f4fd76744129a47c05fcf9f4c1 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 3 Jan 2024 19:34:41 -0500 Subject: [PATCH 25/42] =?UTF-8?q?=F0=9F=97=A8=EF=B8=8F=20refactor:=20Open?= =?UTF-8?q?=20New=20Tab=20for=20Ctrl+Click=20or=20Button=20Combo=20in=20Ne?= =?UTF-8?q?wChat=20(#1484)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Nav/NewChat.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx index 873aab2e1ca8..b0ebb195dcef 100644 --- a/client/src/components/Nav/NewChat.tsx +++ b/client/src/components/Nav/NewChat.tsx @@ -6,15 +6,19 @@ export default function NewChat({ toggleNav }: { toggleNav: () => void }) { const navigate = useOriginNavigate(); const localize = useLocalize(); - const clickHandler = () => { - newConvo(); - newConversation(); - navigate('new'); - toggleNav(); + const clickHandler = (event: React.MouseEvent) => { + if (event.button === 0 && !event.ctrlKey) { + event.preventDefault(); + newConvo(); + newConversation(); + navigate('new'); + toggleNav(); + } }; return ( Date: Thu, 4 Jan 2024 10:17:15 -0500 Subject: [PATCH 26/42] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Improve=20Endpoint?= =?UTF-8?q?=20Handling=20and=20Address=20Edge=20Cases=20(#1486)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(TEndpointsConfig): resolve property access issues with typesafe helper function * fix: undefined or null endpoint edge case * refactor(mapEndpoints -> endpoints): renamed module to be more general for endpoint handling, wrote unit tests, export all helpers --- client/src/components/Chat/Input/ChatForm.tsx | 22 +++-- client/src/components/Chat/Landing.tsx | 20 ++-- .../Chat/Menus/Endpoints/MenuItem.tsx | 12 +-- .../Chat/Menus/Endpoints/MenuItems.tsx | 7 +- .../Chat/Menus/Presets/PresetItems.tsx | 6 +- client/src/components/Conversations/Convo.tsx | 6 +- .../Input/EndpointMenu/EndpointItem.tsx | 8 +- client/src/components/Input/TextChat.tsx | 11 ++- .../src/hooks/Conversations/useGetSender.ts | 2 +- client/src/hooks/Conversations/usePresets.ts | 11 ++- client/src/hooks/Input/useRequiresKey.ts | 7 +- client/src/hooks/Input/useUserKey.ts | 2 +- .../src/hooks/Messages/useMessageHelpers.ts | 3 +- client/src/hooks/useChatHelpers.ts | 2 +- client/src/hooks/useConversation.ts | 7 +- client/src/hooks/useNavigateToConvo.tsx | 37 +++++++- client/src/hooks/useNewConvo.ts | 7 +- client/src/utils/buildDefaultConvo.ts | 1 + client/src/utils/endpoints.spec.ts | 94 +++++++++++++++++++ client/src/utils/endpoints.ts | 58 ++++++++++++ client/src/utils/getDefaultEndpoint.ts | 6 +- client/src/utils/index.ts | 2 +- client/src/utils/mapEndpoints.ts | 37 -------- packages/data-provider/src/types.ts | 6 +- 24 files changed, 275 insertions(+), 99 deletions(-) create mode 100644 client/src/utils/endpoints.spec.ts create mode 100644 client/src/utils/endpoints.ts delete mode 100644 client/src/utils/mapEndpoints.ts diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index efd6285d299a..f52ff943e1fe 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -45,19 +45,23 @@ export default function ChatForm({ index = 0 }) {
-