diff --git a/.env.example b/.env.example
index a5e1af8a40a..a42ed5d683e 100644
--- a/.env.example
+++ b/.env.example
@@ -2,11 +2,9 @@
# LibreChat Configuration #
#=====================================================================#
# Please refer to the reference documentation for assistance #
-# with configuring your LibreChat environment. The guide is #
-# available both online and within your local LibreChat #
-# directory: #
-# Online: https://docs.librechat.ai/install/configuration/dotenv.html #
-# Locally: ./docs/install/configuration/dotenv.md #
+# with configuring your LibreChat environment. #
+# #
+# https://www.librechat.ai/docs/configuration/dotenv #
#=====================================================================#
#==================================================#
@@ -62,15 +60,17 @@ PROXY=
#===================================#
# Known Endpoints - librechat.yaml #
#===================================#
-# https://docs.librechat.ai/install/configuration/ai_endpoints.html
+# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints
-# GROQ_API_KEY=
-# SHUTTLEAI_KEY=
-# OPENROUTER_KEY=
-# MISTRAL_API_KEY=
# ANYSCALE_API_KEY=
+# APIPIE_API_KEY=
# FIREWORKS_API_KEY=
+# GROQ_API_KEY=
+# HUGGINGFACE_TOKEN=
+# MISTRAL_API_KEY=
+# OPENROUTER_KEY=
# PERPLEXITY_API_KEY=
+# SHUTTLEAI_API_KEY=
# TOGETHERAI_API_KEY=
#============#
@@ -113,15 +113,34 @@ BINGAI_TOKEN=user_provided
#============#
GOOGLE_KEY=user_provided
-# GOOGLE_MODELS=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
# GOOGLE_REVERSE_PROXY=
+# Gemini API
+# GOOGLE_MODELS=gemini-1.5-flash-latest,gemini-1.0-pro,gemini-1.0-pro-001,gemini-1.0-pro-latest,gemini-1.0-pro-vision-latest,gemini-1.5-pro-latest,gemini-pro,gemini-pro-vision
+
+# Vertex AI
+# GOOGLE_MODELS=gemini-1.5-flash-preview-0514,gemini-1.5-pro-preview-0409,gemini-1.0-pro-vision-001,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
+
+# Google Gemini Safety Settings
+# NOTE (Vertex AI): You do not have access to the BLOCK_NONE setting by default.
+# To use this restricted HarmBlockThreshold setting, you will need to either:
+#
+# (a) Get access through an allowlist via your Google account team
+# (b) Switch your account type to monthly invoiced billing following this instruction:
+# https://cloud.google.com/billing/docs/how-to/invoiced-billing
+#
+# GOOGLE_SAFETY_SEXUALLY_EXPLICIT=BLOCK_ONLY_HIGH
+# GOOGLE_SAFETY_HATE_SPEECH=BLOCK_ONLY_HIGH
+# GOOGLE_SAFETY_HARASSMENT=BLOCK_ONLY_HIGH
+# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH
+
+
#============#
# OpenAI #
#============#
OPENAI_API_KEY=user_provided
-# OPENAI_MODELS=gpt-3.5-turbo-0125,gpt-3.5-turbo-0301,gpt-3.5-turbo,gpt-4,gpt-4-0613,gpt-4-vision-preview,gpt-3.5-turbo-0613,gpt-3.5-turbo-16k-0613,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview,gpt-3.5-turbo-1106,gpt-3.5-turbo-instruct,gpt-3.5-turbo-instruct-0914,gpt-3.5-turbo-16k
+# OPENAI_MODELS=gpt-4o,gpt-3.5-turbo-0125,gpt-3.5-turbo-0301,gpt-3.5-turbo,gpt-4,gpt-4-0613,gpt-4-vision-preview,gpt-3.5-turbo-0613,gpt-3.5-turbo-16k-0613,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview,gpt-3.5-turbo-1106,gpt-3.5-turbo-instruct,gpt-3.5-turbo-instruct-0914,gpt-3.5-turbo-16k
DEBUG_OPENAI=false
@@ -143,19 +162,19 @@ DEBUG_OPENAI=false
ASSISTANTS_API_KEY=user_provided
# ASSISTANTS_BASE_URL=
-# ASSISTANTS_MODELS=gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview
+# ASSISTANTS_MODELS=gpt-4o,gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview
#============#
# OpenRouter #
#============#
-
+# !!!Warning: Use the variable above instead of this one. Using this one will override the OpenAI endpoint
# OPENROUTER_API_KEY=
#============#
# Plugins #
#============#
-# PLUGIN_MODELS=gpt-4,gpt-4-turbo-preview,gpt-4-0125-preview,gpt-4-1106-preview,gpt-4-0613,gpt-3.5-turbo,gpt-3.5-turbo-0125,gpt-3.5-turbo-1106,gpt-3.5-turbo-0613
+# PLUGIN_MODELS=gpt-4o,gpt-4,gpt-4-turbo-preview,gpt-4-0125-preview,gpt-4-1106-preview,gpt-4-0613,gpt-3.5-turbo,gpt-3.5-turbo-0125,gpt-3.5-turbo-1106,gpt-3.5-turbo-0613
DEBUG_PLUGINS=true
@@ -192,7 +211,7 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
# Google
#-----------------
-GOOGLE_API_KEY=
+GOOGLE_SEARCH_API_KEY=
GOOGLE_CSE_ID=
# SerpAPI
diff --git a/.eslintrc.js b/.eslintrc.js
index e85e0d768ca..58ee6d20a23 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -132,6 +132,13 @@ module.exports = {
},
],
},
+ {
+ files: './config/translations/**/*.ts',
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: './config/translations/tsconfig.json',
+ },
+ },
{
files: ['./packages/data-provider/specs/**/*.ts'],
parserOptions: {
diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
index 26155bdc685..d85957fd22e 100644
--- a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
+++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
@@ -43,7 +43,7 @@ body:
id: terms
attributes:
label: Code of Conduct
- description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml
index 8a0cbf5535b..0669fd67244 100644
--- a/.github/ISSUE_TEMPLATE/QUESTION.yml
+++ b/.github/ISSUE_TEMPLATE/QUESTION.yml
@@ -44,7 +44,7 @@ body:
id: terms
attributes:
label: Code of Conduct
- description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index a1542cb76e4..cb637787f12 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,7 +1,10 @@
# Pull Request Template
+⚠️ Before Submitting a PR, Please Review:
+- Please ensure that you have thoroughly read and understood the [Contributing Docs](https://github.com/danny-avila/LibreChat/blob/main/.github/CONTRIBUTING.md) before submitting your Pull Request.
-### ⚠️ Before Submitting a PR, read the [Contributing Docs](https://github.com/danny-avila/LibreChat/blob/main/.github/CONTRIBUTING.md) in full!
+⚠️ Documentation Updates Notice:
+- Kindly note that documentation updates are managed in this repository: [librechat.ai](https://github.com/LibreChat-AI/librechat.ai)
## Summary
@@ -16,8 +19,6 @@ Please delete any irrelevant options.
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Translation update
-- [ ] Documentation update
-
## Testing
@@ -37,4 +38,4 @@ Please delete any irrelevant options.
- [ ] I have written tests demonstrating that my changes are effective or that my feature works
- [ ] Local unit tests pass with my changes
- [ ] Any changes dependent on mine have been merged and published in downstream modules.
-- [ ] New documents have been locally validated with mkdocs
+- [ ] A pull request for updating the documentation has been submitted.
diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml
index 9f8bd7b5302..52560009a97 100644
--- a/.github/workflows/backend-review.yml
+++ b/.github/workflows/backend-review.yml
@@ -51,6 +51,9 @@ jobs:
exit 1
fi
+ - name: Prepare .env.test file
+ run: cp api/test/.env.test.example api/test/.env.test
+
- name: Run unit tests
run: cd api && npm run test:ci
@@ -60,4 +63,4 @@ jobs:
- name: Run linters
uses: wearerequired/lint-action@v2
with:
- eslint: true
\ No newline at end of file
+ eslint: true
diff --git a/.github/workflows/frontend-review.yml b/.github/workflows/frontend-review.yml
index 9f479e1b7a0..c8ba609a72c 100644
--- a/.github/workflows/frontend-review.yml
+++ b/.github/workflows/frontend-review.yml
@@ -1,11 +1,6 @@
-#github action to run unit tests for frontend with jest
name: Frontend Unit Tests
+
on:
- # push:
- # branches:
- # - main
- # - dev
- # - release/*
pull_request:
branches:
- main
@@ -14,9 +9,10 @@ on:
paths:
- 'client/**'
- 'packages/**'
+
jobs:
- tests_frontend:
- name: Run frontend unit tests
+ tests_frontend_ubuntu:
+ name: Run frontend unit tests on Ubuntu
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
@@ -35,4 +31,26 @@ jobs:
- name: Run unit tests
run: npm run test:ci --verbose
- working-directory: client
\ No newline at end of file
+ working-directory: client
+
+ tests_frontend_windows:
+ name: Run frontend unit tests on Windows
+ timeout-minutes: 60
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build Client
+ run: npm run frontend:ci
+
+ - name: Run unit tests
+ run: npm run test:ci --verbose
+ working-directory: client
diff --git a/.github/workflows/mkdocs.yaml b/.github/workflows/mkdocs.yaml
deleted file mode 100644
index 3b2878fa2a7..00000000000
--- a/.github/workflows/mkdocs.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: mkdocs
-on:
- push:
- branches:
- - main
-permissions:
- contents: write
-jobs:
- deploy:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v4
- with:
- python-version: 3.x
- - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- - uses: actions/cache@v3
- with:
- key: mkdocs-material-${{ env.cache_id }}
- path: .cache
- restore-keys: |
- 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/.gitignore b/.gitignore
index c55115988b9..3a01fc80176 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,10 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
+# translation services
+config/translations/stores/*
+client/src/localization/languages/*_missing_keys.json
+
# Compiled Dirs (http://nodejs.org/api/addons.html)
build/
dist/
@@ -69,6 +73,8 @@ src/style - official.css
/playwright/.cache/
.DS_Store
*.code-workspace
+.idx
+monospace.json
.idea
*.iml
*.pem
@@ -76,6 +82,7 @@ config.local.ts
**/storageState.json
junit.xml
**/.venv/
+**/venv/
# docker override file
docker-compose.override.yaml
diff --git a/Dockerfile b/Dockerfile
index fd087eae39d..6c70a6d7f24 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,8 @@
-# v0.7.0
+# v0.7.2
# Base node image
-FROM node:18-alpine3.18 AS node
+FROM node:20-alpine AS node
-RUN apk add g++ make py3-pip
-RUN npm install -g node-gyp
RUN apk --no-cache add curl
RUN mkdir -p /app && chown node:node /app
@@ -14,20 +12,20 @@ USER node
COPY --chown=node:node . .
-# Allow mounting of these files, which have no default
-# values.
-RUN touch .env
-RUN npm config set fetch-retry-maxtimeout 600000
-RUN npm config set fetch-retries 5
-RUN npm config set fetch-retry-mintimeout 15000
-RUN npm install --no-audit
+RUN \
+ # Allow mounting of these files, which have no default
+ touch .env ; \
+ # Create directories for the volumes to inherit the correct permissions
+ mkdir -p /app/client/public/images /app/api/logs ; \
+ npm config set fetch-retry-maxtimeout 600000 ; \
+ npm config set fetch-retries 5 ; \
+ npm config set fetch-retry-mintimeout 15000 ; \
+ npm install --no-audit; \
+ # React client build
+ NODE_OPTIONS="--max-old-space-size=2048" npm run frontend; \
+ npm prune --production; \
+ npm cache clean --force
-# React client build
-ENV NODE_OPTIONS="--max-old-space-size=2048"
-RUN npm run frontend
-
-# Create directories for the volumes to inherit
-# the correct permissions
RUN mkdir -p /app/client/public/images /app/api/logs
# Node API setup
diff --git a/Dockerfile.multi b/Dockerfile.multi
index 00ed37e3ef8..41e8825b810 100644
--- a/Dockerfile.multi
+++ b/Dockerfile.multi
@@ -1,4 +1,4 @@
-# v0.7.0
+# v0.7.2
# Build API, Client and Data Provider
FROM node:20-alpine AS base
@@ -7,32 +7,31 @@ FROM node:20-alpine AS base
FROM base AS data-provider-build
WORKDIR /app/packages/data-provider
COPY ./packages/data-provider ./
-RUN npm install
+RUN npm install; npm cache clean --force
RUN npm run build
+RUN npm prune --production
# React client build
-FROM data-provider-build AS client-build
+FROM base AS client-build
WORKDIR /app/client
COPY ./client/package*.json ./
# Copy data-provider to client's node_modules
-RUN mkdir -p /app/client/node_modules/librechat-data-provider/
-RUN cp -R /app/packages/data-provider/* /app/client/node_modules/librechat-data-provider/
-RUN npm install
+COPY --from=data-provider-build /app/packages/data-provider/ /app/client/node_modules/librechat-data-provider/
+RUN npm install; npm cache clean --force
COPY ./client/ ./
ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm run build
# Node API setup
-FROM data-provider-build AS api-build
+FROM base AS api-build
WORKDIR /app/api
COPY api/package*.json ./
COPY api/ ./
# Copy helper scripts
COPY config/ ./
# Copy data-provider to API's node_modules
-RUN mkdir -p /app/api/node_modules/librechat-data-provider/
-RUN cp -R /app/packages/data-provider/* /app/api/node_modules/librechat-data-provider/
-RUN npm install
+COPY --from=data-provider-build /app/packages/data-provider/ /app/api/node_modules/librechat-data-provider/
+RUN npm install --include prod; npm cache clean --force
COPY --from=client-build /app/client/dist /app/client/dist
EXPOSE 3080
ENV HOST=0.0.0.0
diff --git a/README.md b/README.md
index 901ddbc7c14..2cb0b593fc8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
-
+
@@ -41,8 +41,16 @@
# 📃 Features
- 🖥️ UI matching ChatGPT, including Dark mode, Streaming, and latest updates
+- 🤖 AI model selection:
+ - OpenAI, Azure OpenAI, BingAI, ChatGPT, Google Vertex AI, Anthropic (Claude), Plugins, Assistants API (including Azure Assistants)
+- ✅ Compatible across both **[Remote & Local AI services](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints):**
+ - groq, Ollama, Cohere, Mistral AI, Apple MLX, koboldcpp, OpenRouter, together.ai, Perplexity, ShuttleAI, and more
+- 💾 Create, Save, & Share Custom Presets
+- 🔀 Switch between AI Endpoints and Presets, mid-chat
+- 🔄 Edit, Resubmit, and Continue Messages with Conversation branching
+- 🌿 Fork Messages & Conversations for Advanced Context control
- 💬 Multimodal Chat:
- - Upload and analyze images with Claude 3, GPT-4, and Gemini Vision 📸
+ - Upload and analyze images with Claude 3, GPT-4 (including `gpt-4o`), and Gemini Vision 📸
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, & Google. 🗃️
- Advanced Agents with Files, Code Interpreter, Tools, and API Actions 🔦
- Available through the [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview) 🌤️
@@ -50,18 +58,18 @@
- 🌎 Multilingual UI:
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro,
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
-- 🤖 AI model selection: OpenAI, Azure OpenAI, BingAI, ChatGPT, Google Vertex AI, Anthropic (Claude), Plugins, Assistants API (including Azure Assistants)
-- 💾 Create, Save, & Share Custom Presets
-- 🔄 Edit, Resubmit, and Continue messages with conversation branching
+- 🎨 Customizable Dropdown & Interface: Adapts to both power users and newcomers.
+- 📥 Import Conversations from LibreChat, ChatGPT, Chatbot UI
- 📤 Export conversations as screenshots, markdown, text, json.
- 🔍 Search all messages/conversations
- 🔌 Plugins, including web access, image generation with DALL-E-3 and more
- 👥 Multi-User, Secure Authentication with Moderation and Token spend tools
-- ⚙️ Configure Proxy, Reverse Proxy, Docker, & many Deployment options
+- ⚙️ Configure Proxy, Reverse Proxy, Docker, & many Deployment options:
+ - Use completely local or deploy on the cloud
- 📖 Completely Open-Source & Built in Public
- 🧑🤝🧑 Community-driven development, support, and feedback
-[For a thorough review of our features, see our docs here](https://docs.librechat.ai/features/plugins/introduction.html) 📚
+[For a thorough review of our features, see our docs here](https://docs.librechat.ai/) 📚
## 🪶 All-In-One AI Conversations with LibreChat
@@ -69,37 +77,49 @@ LibreChat brings together the future of assistant AIs with the revolutionary tec
With LibreChat, you no longer need to opt for ChatGPT Plus and can instead use free or pay-per-call APIs. We welcome contributions, cloning, and forking to enhance the capabilities of this advanced chatbot platform.
-
-
-[![Watch the video](https://img.youtube.com/vi/pNIOs1ovsXw/maxresdefault.jpg)](https://youtu.be/pNIOs1ovsXw)
+[![Watch the video](https://img.youtube.com/vi/YLVUW5UP9N0/maxresdefault.jpg)](https://www.youtube.com/watch?v=YLVUW5UP9N0)
Click on the thumbnail to open the video☝️
---
-## 📚 Documentation
+## 🌐 Resources
-For more information on how to use our advanced features, install and configure our software, and access our guidelines and tutorials, please check out our documentation at [docs.librechat.ai](https://docs.librechat.ai)
+**GitHub Repo:**
+ - **RAG API:** [github.com/danny-avila/rag_api](https://github.com/danny-avila/rag_api)
+ - **Website:** [github.com/LibreChat-AI/librechat.ai](https://github.com/LibreChat-AI/librechat.ai)
+
+**Other:**
+ - **Website:** [librechat.ai](https://librechat.ai)
+ - **Documentation:** [docs.librechat.ai](https://docs.librechat.ai)
+ - **Blog:** [blog.librechat.ai](https://docs.librechat.ai)
---
## 📝 Changelog
-Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
+Keep up with the latest updates by visiting the releases page and notes:
+- [Releases](https://github.com/danny-avila/LibreChat/releases)
+- [Changelog](https://www.librechat.ai/changelog)
-**⚠️ [Breaking Changes](docs/general_info/breaking_changes.md)**
-Please consult the breaking changes before updating.
+**⚠️ Please consult the [changelog](https://www.librechat.ai/changelog) for breaking changes before updating.**
---
## ⭐ Star History
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
---
diff --git a/api/app/bingai.js b/api/app/bingai.js
index f7ecf4462d4..ecb7cf33667 100644
--- a/api/app/bingai.js
+++ b/api/app/bingai.js
@@ -1,5 +1,6 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
+const { EModelEndpoint } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { logger } = require('~/config');
@@ -23,10 +24,7 @@ const askBing = async ({
let key = null;
if (expiresAt && isUserProvided) {
- checkUserKeyExpiry(
- expiresAt,
- 'Your BingAI Cookies have expired. Please provide your cookies again.',
- );
+ checkUserKeyExpiry(expiresAt, EModelEndpoint.bingAI);
key = await getUserKey({ userId, name: 'bingAI' });
}
diff --git a/api/app/chatgpt-browser.js b/api/app/chatgpt-browser.js
index 818661555dc..f3444d0e781 100644
--- a/api/app/chatgpt-browser.js
+++ b/api/app/chatgpt-browser.js
@@ -1,6 +1,6 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
-const { Constants } = require('librechat-data-provider');
+const { Constants, EModelEndpoint } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('../server/services/UserService');
const browserClient = async ({
@@ -18,10 +18,7 @@ const browserClient = async ({
let key = null;
if (expiresAt && isUserProvided) {
- checkUserKeyExpiry(
- expiresAt,
- 'Your ChatGPT Access Token has expired. Please provide your token again.',
- );
+ checkUserKeyExpiry(expiresAt, EModelEndpoint.chatGPTBrowser);
key = await getUserKey({ userId, name: 'chatGPTBrowser' });
}
diff --git a/api/app/clients/AnthropicClient.js b/api/app/clients/AnthropicClient.js
index 6d478defab0..aa5f913367c 100644
--- a/api/app/clients/AnthropicClient.js
+++ b/api/app/clients/AnthropicClient.js
@@ -7,10 +7,10 @@ const {
} = require('librechat-data-provider');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const {
- titleFunctionPrompt,
- parseTitleFromPrompt,
truncateText,
formatMessage,
+ titleFunctionPrompt,
+ parseParamFromPrompt,
createContextHandlers,
} = require('./prompts');
const spendTokens = require('~/models/spendTokens');
@@ -75,7 +75,9 @@ class AnthropicClient extends BaseClient {
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
this.maxContextTokens =
- getModelMaxTokens(this.modelOptions.model, EModelEndpoint.anthropic) ?? 100000;
+ this.options.maxContextTokens ??
+ getModelMaxTokens(this.modelOptions.model, EModelEndpoint.anthropic) ??
+ 100000;
this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1500;
this.maxPromptTokens =
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
@@ -652,9 +654,13 @@ class AnthropicClient extends BaseClient {
getSaveOptions() {
return {
+ maxContextTokens: this.options.maxContextTokens,
promptPrefix: this.options.promptPrefix,
modelLabel: this.options.modelLabel,
resendFiles: this.options.resendFiles,
+ iconURL: this.options.iconURL,
+ greeting: this.options.greeting,
+ spec: this.options.spec,
...this.modelOptions,
};
}
@@ -742,7 +748,7 @@ class AnthropicClient extends BaseClient {
context: 'title',
});
const text = response.content[0].text;
- title = parseTitleFromPrompt(text);
+ title = parseParamFromPrompt(text, 'title');
} catch (e) {
logger.error('[AnthropicClient] There was an issue generating the title', e);
}
diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js
index f7ed3b9cf18..7a4d345d74d 100644
--- a/api/app/clients/BaseClient.js
+++ b/api/app/clients/BaseClient.js
@@ -456,6 +456,8 @@ class BaseClient {
sender: this.sender,
text: addSpaceIfNeeded(generation) + completion,
promptTokens,
+ iconURL: this.options.iconURL,
+ endpoint: this.options.endpoint,
...(this.metadata ?? {}),
};
@@ -525,8 +527,19 @@ class BaseClient {
return _messages;
}
+ /**
+ * Save a message to the database.
+ * @param {TMessage} message
+ * @param {Partial} endpointOptions
+ * @param {string | null} user
+ */
async saveMessageToDatabase(message, endpointOptions, user = null) {
- await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false });
+ await saveMessage({
+ ...message,
+ endpoint: this.options.endpoint,
+ unfinished: false,
+ user,
+ });
await saveConvo(user, {
conversationId: message.conversationId,
endpoint: this.options.endpoint,
@@ -556,11 +569,11 @@ class BaseClient {
* the message is considered a root message.
*
* @param {Object} options - The options for the function.
- * @param {Array} options.messages - An array of message objects. Each object should have either an 'id' or 'messageId' property, and may have a 'parentMessageId' property.
+ * @param {TMessage[]} options.messages - An array of message objects. Each object should have either an 'id' or 'messageId' property, and may have a 'parentMessageId' property.
* @param {string} options.parentMessageId - The ID of the parent message to start the traversal from.
* @param {Function} [options.mapMethod] - An optional function to map over the ordered messages. If provided, it will be applied to each message in the resulting array.
* @param {boolean} [options.summary=false] - If set to true, the traversal modifies messages with 'summary' and 'summaryTokenCount' properties and stops at the message with a 'summary' property.
- * @returns {Array} An array containing the messages in the order they should be displayed, starting with the most recent message with a 'summary' property if the 'summary' option is true, and ending with the message identified by 'parentMessageId'.
+ * @returns {TMessage[]} An array containing the messages in the order they should be displayed, starting with the most recent message with a 'summary' property if the 'summary' option is true, and ending with the message identified by 'parentMessageId'.
*/
static getMessagesForConversation({
messages,
diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js
index c5edcb275a8..6852410efb9 100644
--- a/api/app/clients/GoogleClient.js
+++ b/api/app/clients/GoogleClient.js
@@ -1,7 +1,9 @@
const { google } = require('googleapis');
const { Agent, ProxyAgent } = require('undici');
-const { GoogleVertexAI } = require('langchain/llms/googlevertexai');
+const { ChatVertexAI } = require('@langchain/google-vertexai');
const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
+const { GoogleGenerativeAI: GenAI } = require('@google/generative-ai');
+const { GoogleVertexAI } = require('@langchain/community/llms/googlevertexai');
const { ChatGoogleVertexAI } = require('langchain/chat_models/googlevertexai');
const { AIMessage, HumanMessage, SystemMessage } = require('langchain/schema');
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
@@ -10,6 +12,7 @@ const {
getResponseSender,
endpointSettings,
EModelEndpoint,
+ VisionModes,
AuthKeys,
} = require('librechat-data-provider');
const { encodeAndFormat } = require('~/server/services/Files/images');
@@ -126,7 +129,7 @@ class GoogleClient extends BaseClient {
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
- // TODO: as of 12/14/23, only gemini models are "Generative AI" models provided by Google
+ /** @type {boolean} Whether using a "GenerativeAI" Model */
this.isGenerativeModel = this.modelOptions.model.includes('gemini');
const { isGenerativeModel } = this;
this.isChatModel = !isGenerativeModel && this.modelOptions.model.includes('chat');
@@ -135,7 +138,10 @@ class GoogleClient extends BaseClient {
!isGenerativeModel && !isChatModel && /code|text/.test(this.modelOptions.model);
const { isTextModel } = this;
- this.maxContextTokens = getModelMaxTokens(this.modelOptions.model, EModelEndpoint.google);
+ this.maxContextTokens =
+ this.options.maxContextTokens ??
+ getModelMaxTokens(this.modelOptions.model, EModelEndpoint.google);
+
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
// Earlier messages will be dropped until the prompt is within the limit.
this.maxResponseTokens = this.modelOptions.maxOutputTokens || settings.maxOutputTokens.default;
@@ -234,7 +240,7 @@ class GoogleClient extends BaseClient {
this.isVisionModel = true;
}
- if (this.isVisionModel && !attachments) {
+ if (this.isVisionModel && !attachments && this.modelOptions.model.includes('gemini-pro')) {
this.modelOptions.model = 'gemini-pro';
this.isVisionModel = false;
}
@@ -247,6 +253,40 @@ class GoogleClient extends BaseClient {
})).bind(this);
}
+ /**
+ * Formats messages for generative AI
+ * @param {TMessage[]} messages
+ * @returns
+ */
+ async formatGenerativeMessages(messages) {
+ const formattedMessages = [];
+ const attachments = await this.options.attachments;
+ const latestMessage = { ...messages[messages.length - 1] };
+ const files = await this.addImageURLs(latestMessage, attachments, VisionModes.generative);
+ this.options.attachments = files;
+ messages[messages.length - 1] = latestMessage;
+
+ for (const _message of messages) {
+ const role = _message.isCreatedByUser ? this.userLabel : this.modelLabel;
+ const parts = [];
+ parts.push({ text: _message.text });
+ if (!_message.image_urls?.length) {
+ formattedMessages.push({ role, parts });
+ continue;
+ }
+
+ for (const images of _message.image_urls) {
+ if (images.inlineData) {
+ parts.push({ inlineData: images.inlineData });
+ }
+ }
+
+ formattedMessages.push({ role, parts });
+ }
+
+ return formattedMessages;
+ }
+
/**
*
* Adds image URLs to the message object and returns the files
@@ -255,17 +295,23 @@ class GoogleClient extends BaseClient {
* @param {MongoFile[]} files
* @returns {Promise}
*/
- async addImageURLs(message, attachments) {
+ async addImageURLs(message, attachments, mode = '') {
const { files, image_urls } = await encodeAndFormat(
this.options.req,
attachments,
EModelEndpoint.google,
+ mode,
);
message.image_urls = image_urls.length ? image_urls : undefined;
return files;
}
- async buildVisionMessages(messages = [], parentMessageId) {
+ /**
+ * Builds the augmented prompt for attachments
+ * TODO: Add File API Support
+ * @param {TMessage[]} messages
+ */
+ async buildAugmentedPrompt(messages = []) {
const attachments = await this.options.attachments;
const latestMessage = { ...messages[messages.length - 1] };
this.contextHandlers = createContextHandlers(this.options.req, latestMessage.text);
@@ -281,6 +327,12 @@ class GoogleClient extends BaseClient {
this.augmentedPrompt = await this.contextHandlers.createContext();
this.options.promptPrefix = this.augmentedPrompt + this.options.promptPrefix;
}
+ }
+
+ async buildVisionMessages(messages = [], parentMessageId) {
+ const attachments = await this.options.attachments;
+ const latestMessage = { ...messages[messages.length - 1] };
+ await this.buildAugmentedPrompt(messages);
const { prompt } = await this.buildMessagesPrompt(messages, parentMessageId);
@@ -301,15 +353,26 @@ class GoogleClient extends BaseClient {
return { prompt: payload };
}
+ /** @param {TMessage[]} [messages=[]] */
+ async buildGenerativeMessages(messages = []) {
+ this.userLabel = 'user';
+ this.modelLabel = 'model';
+ const promises = [];
+ promises.push(await this.formatGenerativeMessages(messages));
+ promises.push(this.buildAugmentedPrompt(messages));
+ const [formattedMessages] = await Promise.all(promises);
+ return { prompt: formattedMessages };
+ }
+
async buildMessages(messages = [], parentMessageId) {
if (!this.isGenerativeModel && !this.project_id) {
throw new Error(
'[GoogleClient] a Service Account JSON Key is required for PaLM 2 and Codey models (Vertex AI)',
);
- } else if (this.isGenerativeModel && (!this.apiKey || this.apiKey === 'user_provided')) {
- throw new Error(
- '[GoogleClient] an API Key is required for Gemini models (Generative Language API)',
- );
+ }
+
+ if (!this.project_id && this.modelOptions.model.includes('1.5')) {
+ return await this.buildGenerativeMessages(messages);
}
if (this.options.attachments && this.isGenerativeModel) {
@@ -526,13 +589,24 @@ class GoogleClient extends BaseClient {
}
createLLM(clientOptions) {
- if (this.isGenerativeModel) {
- return new ChatGoogleGenerativeAI({ ...clientOptions, apiKey: this.apiKey });
+ const model = clientOptions.modelName ?? clientOptions.model;
+ if (this.project_id && this.isTextModel) {
+ return new GoogleVertexAI(clientOptions);
+ } else if (this.project_id && this.isChatModel) {
+ return new ChatGoogleVertexAI(clientOptions);
+ } else if (this.project_id) {
+ return new ChatVertexAI(clientOptions);
+ } else if (model.includes('1.5')) {
+ return new GenAI(this.apiKey).getGenerativeModel(
+ {
+ ...clientOptions,
+ model,
+ },
+ { apiVersion: 'v1beta' },
+ );
}
- return this.isTextModel
- ? new GoogleVertexAI(clientOptions)
- : new ChatGoogleVertexAI(clientOptions);
+ return new ChatGoogleGenerativeAI({ ...clientOptions, apiKey: this.apiKey });
}
async getCompletion(_payload, options = {}) {
@@ -544,7 +618,7 @@ class GoogleClient extends BaseClient {
let clientOptions = { ...parameters, maxRetries: 2 };
- if (!this.isGenerativeModel) {
+ if (this.project_id) {
clientOptions['authOptions'] = {
credentials: {
...this.serviceKey,
@@ -557,7 +631,7 @@ class GoogleClient extends BaseClient {
clientOptions = { ...clientOptions, ...this.modelOptions };
}
- if (this.isGenerativeModel) {
+ if (this.isGenerativeModel && !this.project_id) {
clientOptions.modelName = clientOptions.model;
delete clientOptions.model;
}
@@ -588,16 +662,56 @@ class GoogleClient extends BaseClient {
messages.unshift(new SystemMessage(context));
}
+ const modelName = clientOptions.modelName ?? clientOptions.model ?? '';
+ if (modelName?.includes('1.5') && !this.project_id) {
+ /** @type {GenerativeModel} */
+ const client = model;
+ const requestOptions = {
+ contents: _payload,
+ };
+
+ if (this.options?.promptPrefix?.length) {
+ requestOptions.systemInstruction = {
+ parts: [
+ {
+ text: this.options.promptPrefix,
+ },
+ ],
+ };
+ }
+
+ const safetySettings = _payload.safetySettings;
+ requestOptions.safetySettings = safetySettings;
+
+ const delay = modelName.includes('flash') ? 8 : 14;
+ const result = await client.generateContentStream(requestOptions);
+ for await (const chunk of result.stream) {
+ const chunkText = chunk.text();
+ await this.generateTextStream(chunkText, onProgress, {
+ delay,
+ });
+ reply += chunkText;
+ }
+ return reply;
+ }
+
+ const safetySettings = _payload.safetySettings;
const stream = await model.stream(messages, {
signal: abortController.signal,
timeout: 7000,
+ safetySettings: safetySettings,
});
+ let delay = this.isGenerativeModel ? 12 : 8;
+ if (modelName.includes('flash')) {
+ delay = 5;
+ }
for await (const chunk of stream) {
- await this.generateTextStream(chunk?.content ?? chunk, onProgress, {
- delay: this.isGenerativeModel ? 12 : 8,
+ const chunkText = chunk?.content ?? chunk;
+ await this.generateTextStream(chunkText, onProgress, {
+ delay,
});
- reply += chunk?.content ?? chunk;
+ reply += chunkText;
}
return reply;
@@ -607,6 +721,9 @@ class GoogleClient extends BaseClient {
return {
promptPrefix: this.options.promptPrefix,
modelLabel: this.options.modelLabel,
+ iconURL: this.options.iconURL,
+ greeting: this.options.greeting,
+ spec: this.options.spec,
...this.modelOptions,
};
}
@@ -616,6 +733,33 @@ class GoogleClient extends BaseClient {
}
async sendCompletion(payload, opts = {}) {
+ const modelName = payload.parameters?.model;
+
+ if (modelName && modelName.toLowerCase().includes('gemini')) {
+ const safetySettings = [
+ {
+ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
+ threshold:
+ process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
+ },
+ {
+ category: 'HARM_CATEGORY_HATE_SPEECH',
+ threshold: process.env.GOOGLE_SAFETY_HATE_SPEECH || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
+ },
+ {
+ category: 'HARM_CATEGORY_HARASSMENT',
+ threshold: process.env.GOOGLE_SAFETY_HARASSMENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
+ },
+ {
+ category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
+ threshold:
+ process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
+ },
+ ];
+
+ payload.safetySettings = safetySettings;
+ }
+
let reply = '';
reply = await this.getCompletion(payload, opts);
return reply.trim();
diff --git a/api/app/clients/OllamaClient.js b/api/app/clients/OllamaClient.js
new file mode 100644
index 00000000000..57bc8754fb9
--- /dev/null
+++ b/api/app/clients/OllamaClient.js
@@ -0,0 +1,154 @@
+const { z } = require('zod');
+const axios = require('axios');
+const { Ollama } = require('ollama');
+const { deriveBaseURL } = require('~/utils');
+const { logger } = require('~/config');
+
+const ollamaPayloadSchema = z.object({
+ mirostat: z.number().optional(),
+ mirostat_eta: z.number().optional(),
+ mirostat_tau: z.number().optional(),
+ num_ctx: z.number().optional(),
+ repeat_last_n: z.number().optional(),
+ repeat_penalty: z.number().optional(),
+ temperature: z.number().optional(),
+ seed: z.number().nullable().optional(),
+ stop: z.array(z.string()).optional(),
+ tfs_z: z.number().optional(),
+ num_predict: z.number().optional(),
+ top_k: z.number().optional(),
+ top_p: z.number().optional(),
+ stream: z.optional(z.boolean()),
+ model: z.string(),
+});
+
+/**
+ * @param {string} imageUrl
+ * @returns {string}
+ * @throws {Error}
+ */
+const getValidBase64 = (imageUrl) => {
+ const parts = imageUrl.split(';base64,');
+
+ if (parts.length === 2) {
+ return parts[1];
+ } else {
+ logger.error('Invalid or no Base64 string found in URL.');
+ }
+};
+
+class OllamaClient {
+ constructor(options = {}) {
+ const host = deriveBaseURL(options.baseURL ?? 'http://localhost:11434');
+ /** @type {Ollama} */
+ this.client = new Ollama({ host });
+ }
+
+ /**
+ * Fetches Ollama models from the specified base API path.
+ * @param {string} baseURL
+ * @returns {Promise} The Ollama models.
+ */
+ static async fetchModels(baseURL) {
+ let models = [];
+ if (!baseURL) {
+ return models;
+ }
+ try {
+ const ollamaEndpoint = deriveBaseURL(baseURL);
+ /** @type {Promise>} */
+ const response = await axios.get(`${ollamaEndpoint}/api/tags`);
+ models = response.data.models.map((tag) => tag.name);
+ return models;
+ } catch (error) {
+ const logMessage =
+ 'Failed to fetch models from Ollama API. If you are not using Ollama directly, and instead, through some aggregator or reverse proxy that handles fetching via OpenAI spec, ensure the name of the endpoint doesn\'t start with `ollama` (case-insensitive).';
+ logger.error(logMessage, error);
+ return [];
+ }
+ }
+
+ /**
+ * @param {ChatCompletionMessage[]} messages
+ * @returns {OllamaMessage[]}
+ */
+ static formatOpenAIMessages(messages) {
+ const ollamaMessages = [];
+
+ for (const message of messages) {
+ if (typeof message.content === 'string') {
+ ollamaMessages.push({
+ role: message.role,
+ content: message.content,
+ });
+ continue;
+ }
+
+ let aggregatedText = '';
+ let imageUrls = [];
+
+ for (const content of message.content) {
+ if (content.type === 'text') {
+ aggregatedText += content.text + ' ';
+ } else if (content.type === 'image_url') {
+ imageUrls.push(getValidBase64(content.image_url.url));
+ }
+ }
+
+ const ollamaMessage = {
+ role: message.role,
+ content: aggregatedText.trim(),
+ };
+
+ if (imageUrls.length > 0) {
+ ollamaMessage.images = imageUrls;
+ }
+
+ ollamaMessages.push(ollamaMessage);
+ }
+
+ return ollamaMessages;
+ }
+
+ /***
+ * @param {Object} params
+ * @param {ChatCompletionPayload} params.payload
+ * @param {onTokenProgress} params.onProgress
+ * @param {AbortController} params.abortController
+ */
+ async chatCompletion({ payload, onProgress, abortController = null }) {
+ let intermediateReply = '';
+
+ const parameters = ollamaPayloadSchema.parse(payload);
+ const messages = OllamaClient.formatOpenAIMessages(payload.messages);
+
+ if (parameters.stream) {
+ const stream = await this.client.chat({
+ messages,
+ ...parameters,
+ });
+
+ for await (const chunk of stream) {
+ const token = chunk.message.content;
+ intermediateReply += token;
+ onProgress(token);
+ if (abortController.signal.aborted) {
+ stream.controller.abort();
+ break;
+ }
+ }
+ }
+ // TODO: regular completion
+ else {
+ // const generation = await this.client.generate(payload);
+ }
+
+ return intermediateReply;
+ }
+ catch(err) {
+ logger.error('[OllamaClient.chatCompletion]', err);
+ throw err;
+ }
+}
+
+module.exports = { OllamaClient, ollamaPayloadSchema };
diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js
index f66afda4abd..b4a50bc05c6 100644
--- a/api/app/clients/OpenAIClient.js
+++ b/api/app/clients/OpenAIClient.js
@@ -1,6 +1,8 @@
const OpenAI = require('openai');
+const { OllamaClient } = require('./OllamaClient');
const { HttpsProxyAgent } = require('https-proxy-agent');
const {
+ Constants,
ImageDetail,
EModelEndpoint,
resolveHeaders,
@@ -20,16 +22,16 @@ const {
const {
truncateText,
formatMessage,
- createContextHandlers,
CUT_OFF_PROMPT,
titleInstruction,
+ createContextHandlers,
} = require('./prompts');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
+const { isEnabled, sleep } = require('~/server/utils');
const { handleOpenAIErrors } = require('./tools/util');
const spendTokens = require('~/models/spendTokens');
const { createLLM, RunManager } = require('./llm');
const ChatGPTClient = require('./ChatGPTClient');
-const { isEnabled } = require('~/server/utils');
const { summaryBuffer } = require('./memory');
const { runTitleChain } = require('./chains');
const { tokenSplit } = require('./document');
@@ -127,6 +129,10 @@ class OpenAIClient extends BaseClient {
this.useOpenRouter = true;
}
+ if (this.options.endpoint?.toLowerCase() === 'ollama') {
+ this.isOllama = true;
+ }
+
this.FORCE_PROMPT =
isEnabled(OPENAI_FORCE_PROMPT) ||
(reverseProxy && reverseProxy.includes('completions') && !reverseProxy.includes('chat'));
@@ -159,11 +165,13 @@ class OpenAIClient extends BaseClient {
model.startsWith('text-chat') || model.startsWith('text-davinci-002-render');
this.maxContextTokens =
+ this.options.maxContextTokens ??
getModelMaxTokens(
model,
this.options.endpointType ?? this.options.endpoint,
this.options.endpointTokenConfig,
- ) ?? 4095; // 1 less than maximum
+ ) ??
+ 4095; // 1 less than maximum
if (this.shouldSummarize) {
this.maxContextTokens = Math.floor(this.maxContextTokens / 2);
@@ -200,16 +208,6 @@ class OpenAIClient extends BaseClient {
this.setupTokens();
- if (!this.modelOptions.stop && !this.isVisionModel) {
- const stopTokens = [this.startToken];
- if (this.endToken && this.endToken !== this.startToken) {
- stopTokens.push(this.endToken);
- }
- stopTokens.push(`\n${this.userLabel}:`);
- stopTokens.push('<|diff_marker|>');
- this.modelOptions.stop = stopTokens;
- }
-
if (reverseProxy) {
this.completionsUrl = reverseProxy;
this.langchainProxy = extractBaseURL(reverseProxy);
@@ -243,23 +241,52 @@ class OpenAIClient extends BaseClient {
* @param {MongoFile[]} attachments
*/
checkVisionRequest(attachments) {
+ if (!attachments) {
+ return;
+ }
+
const availableModels = this.options.modelsConfig?.[this.options.endpoint];
- this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
+ if (!availableModels) {
+ return;
+ }
- const visionModelAvailable = availableModels?.includes(this.defaultVisionModel);
- if (
- attachments &&
- attachments.some((file) => file?.type && file?.type?.includes('image')) &&
- visionModelAvailable &&
- !this.isVisionModel
- ) {
- this.modelOptions.model = this.defaultVisionModel;
- this.isVisionModel = true;
+ let visionRequestDetected = false;
+ for (const file of attachments) {
+ if (file?.type?.includes('image')) {
+ visionRequestDetected = true;
+ break;
+ }
+ }
+ if (!visionRequestDetected) {
+ return;
}
+ this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
if (this.isVisionModel) {
delete this.modelOptions.stop;
+ return;
}
+
+ for (const model of availableModels) {
+ if (!validateVisionModel({ model, availableModels })) {
+ continue;
+ }
+ this.modelOptions.model = model;
+ this.isVisionModel = true;
+ delete this.modelOptions.stop;
+ return;
+ }
+
+ if (!availableModels.includes(this.defaultVisionModel)) {
+ return;
+ }
+ if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) {
+ return;
+ }
+
+ this.modelOptions.model = this.defaultVisionModel;
+ this.isVisionModel = true;
+ delete this.modelOptions.stop;
}
setupTokens() {
@@ -281,7 +308,7 @@ class OpenAIClient extends BaseClient {
let tokenizer;
this.encoding = 'text-davinci-003';
if (this.isChatCompletion) {
- this.encoding = 'cl100k_base';
+ this.encoding = this.modelOptions.model.includes('gpt-4o') ? 'o200k_base' : 'cl100k_base';
tokenizer = this.constructor.getTokenizer(this.encoding);
} else if (this.isUnofficialChatGptModel) {
const extendSpecialTokens = {
@@ -386,10 +413,14 @@ class OpenAIClient extends BaseClient {
getSaveOptions() {
return {
+ maxContextTokens: this.options.maxContextTokens,
chatGptLabel: this.options.chatGptLabel,
promptPrefix: this.options.promptPrefix,
resendFiles: this.options.resendFiles,
imageDetail: this.options.imageDetail,
+ iconURL: this.options.iconURL,
+ greeting: this.options.greeting,
+ spec: this.options.spec,
...this.modelOptions,
};
}
@@ -411,7 +442,11 @@ class OpenAIClient extends BaseClient {
* @returns {Promise}
*/
async addImageURLs(message, attachments) {
- const { files, image_urls } = await encodeAndFormat(this.options.req, attachments);
+ const { files, image_urls } = await encodeAndFormat(
+ this.options.req,
+ attachments,
+ this.options.endpoint,
+ );
message.image_urls = image_urls.length ? image_urls : undefined;
return files;
}
@@ -721,6 +756,10 @@ class OpenAIClient extends BaseClient {
* In case of failure, it will return the default title, "New Chat".
*/
async titleConvo({ text, conversationId, responseText = '' }) {
+ if (this.options.attachments) {
+ delete this.options.attachments;
+ }
+
let title = 'New Chat';
const convo = `||>User:
"${truncateText(text)}"
@@ -729,7 +768,10 @@ class OpenAIClient extends BaseClient {
const { OPENAI_TITLE_MODEL } = process.env ?? {};
- const model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo';
+ let model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo';
+ if (model === Constants.CURRENT_MODEL) {
+ model = this.modelOptions.model;
+ }
const modelOptions = {
// TODO: remove the gpt fallback and make it specific to endpoint
@@ -851,7 +893,11 @@ ${convo}
// TODO: remove the gpt fallback and make it specific to endpoint
const { OPENAI_SUMMARY_MODEL = 'gpt-3.5-turbo' } = process.env ?? {};
- const model = this.options.summaryModel ?? OPENAI_SUMMARY_MODEL;
+ let model = this.options.summaryModel ?? OPENAI_SUMMARY_MODEL;
+ if (model === Constants.CURRENT_MODEL) {
+ model = this.modelOptions.model;
+ }
+
const maxContextTokens =
getModelMaxTokens(
model,
@@ -1078,11 +1124,8 @@ ${convo}
...opts,
});
- /* hacky fixes for Mistral AI API:
- - Re-orders system message to the top of the messages payload, as not allowed anywhere else
- - If there is only one message and it's a system message, change the role to user
- */
- if (opts.baseURL.includes('https://api.mistral.ai/v1') && modelOptions.messages) {
+ /* Re-orders system message to the top of the messages payload, as not allowed anywhere else */
+ if (modelOptions.messages && (opts.baseURL.includes('api.mistral.ai') || this.isOllama)) {
const { messages } = modelOptions;
const systemMessageIndex = messages.findIndex((msg) => msg.role === 'system');
@@ -1093,10 +1136,16 @@ ${convo}
}
modelOptions.messages = messages;
+ }
- if (messages.length === 1 && messages[0].role === 'system') {
- modelOptions.messages[0].role = 'user';
- }
+ /* If there is only one message and it's a system message, change the role to user */
+ if (
+ (opts.baseURL.includes('api.mistral.ai') || opts.baseURL.includes('api.perplexity.ai')) &&
+ modelOptions.messages &&
+ modelOptions.messages.length === 1 &&
+ modelOptions.messages[0]?.role === 'system'
+ ) {
+ modelOptions.messages[0].role = 'user';
}
if (this.options.addParams && typeof this.options.addParams === 'object') {
@@ -1120,6 +1169,15 @@ ${convo}
});
}
+ if (this.message_file_map && this.isOllama) {
+ const ollamaClient = new OllamaClient({ baseURL });
+ return await ollamaClient.chatCompletion({
+ payload: modelOptions,
+ onProgress,
+ abortController,
+ });
+ }
+
let UnexpectedRoleError = false;
if (modelOptions.stream) {
const stream = await openai.beta.chat.completions
@@ -1150,6 +1208,7 @@ ${convo}
}
});
+ const azureDelay = this.modelOptions.model?.includes('gpt-4') ? 30 : 17;
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content || '';
intermediateReply += token;
@@ -1158,6 +1217,10 @@ ${convo}
stream.controller.abort();
break;
}
+
+ if (this.azure) {
+ await sleep(azureDelay);
+ }
}
if (!UnexpectedRoleError) {
diff --git a/api/app/clients/PluginsClient.js b/api/app/clients/PluginsClient.js
index 033c122664f..9c57ee2e9cd 100644
--- a/api/app/clients/PluginsClient.js
+++ b/api/app/clients/PluginsClient.js
@@ -42,8 +42,12 @@ class PluginsClient extends OpenAIClient {
return {
chatGptLabel: this.options.chatGptLabel,
promptPrefix: this.options.promptPrefix,
+ tools: this.options.tools,
...this.modelOptions,
agentOptions: this.agentOptions,
+ iconURL: this.options.iconURL,
+ greeting: this.options.greeting,
+ spec: this.options.spec,
};
}
@@ -144,9 +148,11 @@ class PluginsClient extends OpenAIClient {
signal,
pastMessages,
tools: this.tools,
- currentDateString: this.currentDateString,
verbose: this.options.debug,
returnIntermediateSteps: true,
+ customName: this.options.chatGptLabel,
+ currentDateString: this.currentDateString,
+ customInstructions: this.options.promptPrefix,
callbackManager: CallbackManager.fromHandlers({
async handleAgentAction(action, runId) {
handleAction(action, runId, onAgentAction);
@@ -244,7 +250,7 @@ class PluginsClient extends OpenAIClient {
this.setOptions(opts);
return super.sendMessage(message, opts);
}
- logger.debug('[PluginsClient] sendMessage', { message, opts });
+ logger.debug('[PluginsClient] sendMessage', { userMessageText: message, opts });
const {
user,
isEdited,
@@ -304,6 +310,8 @@ class PluginsClient extends OpenAIClient {
}
const responseMessage = {
+ endpoint: EModelEndpoint.gptPlugins,
+ iconURL: this.options.iconURL,
messageId: responseMessageId,
conversationId,
parentMessageId: userMessage.messageId,
diff --git a/api/app/clients/agents/CustomAgent/initializeCustomAgent.js b/api/app/clients/agents/CustomAgent/initializeCustomAgent.js
index 2a7813eea60..3d45e5be834 100644
--- a/api/app/clients/agents/CustomAgent/initializeCustomAgent.js
+++ b/api/app/clients/agents/CustomAgent/initializeCustomAgent.js
@@ -13,10 +13,18 @@ const initializeCustomAgent = async ({
tools,
model,
pastMessages,
+ customName,
+ customInstructions,
currentDateString,
...rest
}) => {
let prompt = CustomAgent.createPrompt(tools, { currentDateString, model: model.modelName });
+ if (customName) {
+ prompt = `You are "${customName}".\n${prompt}`;
+ }
+ if (customInstructions) {
+ prompt = `${prompt}\n${customInstructions}`;
+ }
const chatPrompt = ChatPromptTemplate.fromMessages([
new SystemMessagePromptTemplate(prompt),
diff --git a/api/app/clients/agents/Functions/initializeFunctionsAgent.js b/api/app/clients/agents/Functions/initializeFunctionsAgent.js
index 3d1a1704ea0..3e813bdbcca 100644
--- a/api/app/clients/agents/Functions/initializeFunctionsAgent.js
+++ b/api/app/clients/agents/Functions/initializeFunctionsAgent.js
@@ -10,6 +10,8 @@ const initializeFunctionsAgent = async ({
tools,
model,
pastMessages,
+ customName,
+ customInstructions,
currentDateString,
...rest
}) => {
@@ -24,7 +26,13 @@ const initializeFunctionsAgent = async ({
returnMessages: true,
});
- const prefix = addToolDescriptions(`Current Date: ${currentDateString}\n${PREFIX}`, tools);
+ let prefix = addToolDescriptions(`Current Date: ${currentDateString}\n${PREFIX}`, tools);
+ if (customName) {
+ prefix = `You are "${customName}".\n${prefix}`;
+ }
+ if (customInstructions) {
+ prefix = `${prefix}\n${customInstructions}`;
+ }
return await initializeAgentExecutorWithOptions(tools, model, {
agentType: 'openai-functions',
diff --git a/api/app/clients/prompts/index.js b/api/app/clients/prompts/index.js
index 36bb6f7e283..9477fb30ca6 100644
--- a/api/app/clients/prompts/index.js
+++ b/api/app/clients/prompts/index.js
@@ -13,7 +13,7 @@ module.exports = {
...handleInputs,
...instructions,
...titlePrompts,
- truncateText,
+ ...truncateText,
createVisionPrompt,
createContextHandlers,
};
diff --git a/api/app/clients/prompts/titlePrompts.js b/api/app/clients/prompts/titlePrompts.js
index 83d8909f3a7..4b3ec354c6b 100644
--- a/api/app/clients/prompts/titlePrompts.js
+++ b/api/app/clients/prompts/titlePrompts.js
@@ -59,25 +59,57 @@ Submit a brief title in the conversation's language, following the parameter des
`;
-/**
- * Parses titles from title functions based on the provided prompt.
- * @param {string} prompt - The prompt containing the title function.
- * @returns {string} The parsed title. "New Chat" if no title is found.
- */
-function parseTitleFromPrompt(prompt) {
- const titleRegex = /(.+?)<\/title>/;
- const titleMatch = prompt.match(titleRegex);
+const genTranslationPrompt = (
+ translationPrompt,
+) => `In this environment you have access to a set of tools you can use to translate text.
+
+You may call them like this:
+
+
+$TOOL_NAME
+
+<$PARAMETER_NAME>$PARAMETER_VALUE$PARAMETER_NAME>
+...
+
+
+
- if (titleMatch && titleMatch[1]) {
- const title = titleMatch[1].trim();
+Here are the tools available:
+
+
+submit_translation
+
+Submit a translation in the target language, following the parameter description and its language closely.
+
+
+
+translation
+string
+${translationPrompt}
+ONLY include the generated translation without quotations, nor its related key
+
+
+
+`;
- // // Capitalize the first letter of each word; Note: unnecessary due to title case prompting
- // const capitalizedTitle = title.replace(/\b\w/g, (char) => char.toUpperCase());
+/**
+ * Parses specified parameter from the provided prompt.
+ * @param {string} prompt - The prompt containing the desired parameter.
+ * @param {string} paramName - The name of the parameter to extract.
+ * @returns {string} The parsed parameter's value or a default value if not found.
+ */
+function parseParamFromPrompt(prompt, paramName) {
+ const paramRegex = new RegExp(`<${paramName}>([\\s\\S]+?)${paramName}>`);
+ const paramMatch = prompt.match(paramRegex);
- return title;
+ if (paramMatch && paramMatch[1]) {
+ return paramMatch[1].trim();
}
- return 'New Chat';
+ if (prompt && prompt.length) {
+ return `NO TOOL INVOCATION: ${prompt}`;
+ }
+ return `No ${paramName} provided`;
}
module.exports = {
@@ -85,5 +117,6 @@ module.exports = {
titleInstruction,
createTitlePrompt,
titleFunctionPrompt,
- parseTitleFromPrompt,
+ parseParamFromPrompt,
+ genTranslationPrompt,
};
diff --git a/api/app/clients/prompts/truncateText.js b/api/app/clients/prompts/truncateText.js
index 003b1bc9af5..e744b40daad 100644
--- a/api/app/clients/prompts/truncateText.js
+++ b/api/app/clients/prompts/truncateText.js
@@ -1,10 +1,40 @@
const MAX_CHAR = 255;
-function truncateText(text) {
- if (text.length > MAX_CHAR) {
- return `${text.slice(0, MAX_CHAR)}... [text truncated for brevity]`;
+/**
+ * Truncates a given text to a specified maximum length, appending ellipsis and a notification
+ * if the original text exceeds the maximum length.
+ *
+ * @param {string} text - The text to be truncated.
+ * @param {number} [maxLength=MAX_CHAR] - The maximum length of the text after truncation. Defaults to MAX_CHAR.
+ * @returns {string} The truncated text if the original text length exceeds maxLength, otherwise returns the original text.
+ */
+function truncateText(text, maxLength = MAX_CHAR) {
+ if (text.length > maxLength) {
+ return `${text.slice(0, maxLength)}... [text truncated for brevity]`;
}
return text;
}
-module.exports = truncateText;
+/**
+ * Truncates a given text to a specified maximum length by showing the first half and the last half of the text,
+ * separated by ellipsis. This method ensures the output does not exceed the maximum length, including the addition
+ * of ellipsis and notification if the original text exceeds the maximum length.
+ *
+ * @param {string} text - The text to be truncated.
+ * @param {number} [maxLength=MAX_CHAR] - The maximum length of the output text after truncation. Defaults to MAX_CHAR.
+ * @returns {string} The truncated text showing the first half and the last half, or the original text if it does not exceed maxLength.
+ */
+function smartTruncateText(text, maxLength = MAX_CHAR) {
+ const ellipsis = '...';
+ const notification = ' [text truncated for brevity]';
+ const halfMaxLength = Math.floor((maxLength - ellipsis.length - notification.length) / 2);
+
+ if (text.length > maxLength) {
+ const startLastHalf = text.length - halfMaxLength;
+ return `${text.slice(0, halfMaxLength)}${ellipsis}${text.slice(startLastHalf)}${notification}`;
+ }
+
+ return text;
+}
+
+module.exports = { truncateText, smartTruncateText };
diff --git a/api/app/clients/specs/FakeClient.js b/api/app/clients/specs/FakeClient.js
index a5915adcf24..7f4b75e1db9 100644
--- a/api/app/clients/specs/FakeClient.js
+++ b/api/app/clients/specs/FakeClient.js
@@ -40,7 +40,8 @@ class FakeClient extends BaseClient {
};
}
- this.maxContextTokens = getModelMaxTokens(this.modelOptions.model) ?? 4097;
+ this.maxContextTokens =
+ this.options.maxContextTokens ?? getModelMaxTokens(this.modelOptions.model) ?? 4097;
}
buildMessages() {}
getTokenCount(str) {
diff --git a/api/app/clients/specs/OpenAIClient.test.js b/api/app/clients/specs/OpenAIClient.test.js
index 8c2226215c1..7ef4fdcae51 100644
--- a/api/app/clients/specs/OpenAIClient.test.js
+++ b/api/app/clients/specs/OpenAIClient.test.js
@@ -157,12 +157,19 @@ describe('OpenAIClient', () => {
azureOpenAIApiVersion: '2020-07-01-preview',
};
+ let originalWarn;
+
beforeAll(() => {
- jest.spyOn(console, 'warn').mockImplementation(() => {});
+ originalWarn = console.warn;
+ console.warn = jest.fn();
});
afterAll(() => {
- console.warn.mockRestore();
+ console.warn = originalWarn;
+ });
+
+ beforeEach(() => {
+ console.warn.mockClear();
});
beforeEach(() => {
@@ -662,4 +669,35 @@ describe('OpenAIClient', () => {
expect(constructorArgs.baseURL).toBe(expectedURL);
});
});
+
+ describe('checkVisionRequest functionality', () => {
+ let client;
+ const attachments = [{ type: 'image/png' }];
+
+ beforeEach(() => {
+ client = new OpenAIClient('test-api-key', {
+ endpoint: 'ollama',
+ modelOptions: {
+ model: 'initial-model',
+ },
+ modelsConfig: {
+ ollama: ['initial-model', 'llava', 'other-model'],
+ },
+ });
+
+ client.defaultVisionModel = 'non-valid-default-model';
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should set "llava" as the model if it is the first valid model when default validation fails', () => {
+ client.checkVisionRequest(attachments);
+
+ expect(client.modelOptions.model).toBe('llava');
+ expect(client.isVisionModel).toBeTruthy();
+ expect(client.modelOptions.stop).toBeUndefined();
+ });
+ });
});
diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json
index 3daaf9dd3bc..c8beed976fe 100644
--- a/api/app/clients/tools/manifest.json
+++ b/api/app/clients/tools/manifest.json
@@ -24,7 +24,7 @@
"description": "This is your Google Custom Search Engine ID. For instructions on how to obtain this, see Our Docs."
},
{
- "authField": "GOOGLE_API_KEY",
+ "authField": "GOOGLE_SEARCH_API_KEY",
"label": "Google API Key",
"description": "This is your Google Custom Search API Key. For instructions on how to obtain this, see Our Docs."
}
@@ -60,7 +60,7 @@
"name": "CodeSherpa",
"pluginKey": "codesherpa_tools",
"description": "[Experimental] A REPL for your chat. Requires https://github.com/iamgreggarcia/codesherpa",
- "icon": "https://github.com/iamgreggarcia/codesherpa/blob/main/localserver/_logo.png",
+ "icon": "https://raw.githubusercontent.com/iamgreggarcia/codesherpa/main/localserver/_logo.png",
"authConfig": [
{
"authField": "CODESHERPA_SERVER_URL",
diff --git a/api/app/clients/tools/structured/GoogleSearch.js b/api/app/clients/tools/structured/GoogleSearch.js
index 92d33272c83..bae1a458e0d 100644
--- a/api/app/clients/tools/structured/GoogleSearch.js
+++ b/api/app/clients/tools/structured/GoogleSearch.js
@@ -9,7 +9,7 @@ class GoogleSearchResults extends Tool {
constructor(fields = {}) {
super(fields);
- this.envVarApiKey = 'GOOGLE_API_KEY';
+ this.envVarApiKey = 'GOOGLE_SEARCH_API_KEY';
this.envVarSearchEngineId = 'GOOGLE_CSE_ID';
this.override = fields.override ?? false;
this.apiKey = fields.apiKey ?? getEnvironmentVariable(this.envVarApiKey);
diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js
index 3d67e578722..1d86007638b 100644
--- a/api/cache/banViolation.js
+++ b/api/cache/banViolation.js
@@ -1,6 +1,7 @@
-const Session = require('~/models/Session');
-const getLogStores = require('./getLogStores');
+const { ViolationTypes } = require('librechat-data-provider');
const { isEnabled, math, removePorts } = require('~/server/utils');
+const getLogStores = require('./getLogStores');
+const Session = require('~/models/Session');
const { logger } = require('~/config');
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
@@ -48,7 +49,7 @@ const banViolation = async (req, res, errorMessage) => {
await Session.deleteAllUserSessions(user_id);
res.clearCookie('refreshToken');
- const banLogs = getLogStores('ban');
+ const banLogs = getLogStores(ViolationTypes.BAN);
const duration = errorMessage.duration || banLogs.opts.ttl;
if (duration <= 0) {
diff --git a/api/cache/banViolation.spec.js b/api/cache/banViolation.spec.js
index ba8e78a1edc..8fef16920f8 100644
--- a/api/cache/banViolation.spec.js
+++ b/api/cache/banViolation.spec.js
@@ -6,6 +6,7 @@ jest.mock('../models/Session');
jest.mock('./getLogStores', () => {
return jest.fn().mockImplementation(() => {
const EventEmitter = require('events');
+ const { CacheKeys } = require('librechat-data-provider');
const math = require('../server/utils/math');
const mockGet = jest.fn();
const mockSet = jest.fn();
@@ -33,7 +34,7 @@ jest.mock('./getLogStores', () => {
}
return new KeyvMongo('', {
- namespace: 'bans',
+ namespace: CacheKeys.BANS,
ttl: math(process.env.BAN_DURATION, 7200000),
});
});
diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js
index 786bb1f1f74..0d9b662e4e1 100644
--- a/api/cache/getLogStores.js
+++ b/api/cache/getLogStores.js
@@ -6,6 +6,7 @@ const keyvRedis = require('./keyvRedis');
const keyvMongo = require('./keyvMongo');
const { BAN_DURATION, USE_REDIS } = process.env ?? {};
+const THIRTY_MINUTES = 1800000;
const duration = math(BAN_DURATION, 7200000);
@@ -24,8 +25,8 @@ const config = isEnabled(USE_REDIS)
: new Keyv({ namespace: CacheKeys.CONFIG_STORE });
const tokenConfig = isEnabled(USE_REDIS) // ttl: 30 minutes
- ? new Keyv({ store: keyvRedis, ttl: 1800000 })
- : new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: 1800000 });
+ ? new Keyv({ store: keyvRedis, ttl: THIRTY_MINUTES })
+ : new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: THIRTY_MINUTES });
const genTitle = isEnabled(USE_REDIS) // ttl: 2 minutes
? new Keyv({ store: keyvRedis, ttl: 120000 })
@@ -42,7 +43,12 @@ const abortKeys = isEnabled(USE_REDIS)
const namespaces = {
[CacheKeys.CONFIG_STORE]: config,
pending_req,
- ban: new Keyv({ store: keyvMongo, namespace: 'bans', ttl: duration }),
+ [ViolationTypes.BAN]: new Keyv({ store: keyvMongo, namespace: CacheKeys.BANS, ttl: duration }),
+ [CacheKeys.ENCODED_DOMAINS]: new Keyv({
+ store: keyvMongo,
+ namespace: CacheKeys.ENCODED_DOMAINS,
+ ttl: 0,
+ }),
general: new Keyv({ store: logFile, namespace: 'violations' }),
concurrent: createViolationInstance('concurrent'),
non_browser: createViolationInstance('non_browser'),
diff --git a/api/lib/db/indexSync.js b/api/lib/db/indexSync.js
index 53ac3d3a266..86c909419d6 100644
--- a/api/lib/db/indexSync.js
+++ b/api/lib/db/indexSync.js
@@ -1,11 +1,28 @@
const { MeiliSearch } = require('meilisearch');
-const Message = require('~/models/schema/messageSchema');
const Conversation = require('~/models/schema/convoSchema');
+const Message = require('~/models/schema/messageSchema');
const { logger } = require('~/config');
const searchEnabled = process.env?.SEARCH?.toLowerCase() === 'true';
let currentTimeout = null;
+class MeiliSearchClient {
+ static instance = null;
+
+ static getInstance() {
+ if (!MeiliSearchClient.instance) {
+ if (!process.env.MEILI_HOST || !process.env.MEILI_MASTER_KEY) {
+ throw new Error('Meilisearch configuration is missing.');
+ }
+ MeiliSearchClient.instance = new MeiliSearch({
+ host: process.env.MEILI_HOST,
+ apiKey: process.env.MEILI_MASTER_KEY,
+ });
+ }
+ return MeiliSearchClient.instance;
+ }
+}
+
// eslint-disable-next-line no-unused-vars
async function indexSync(req, res, next) {
if (!searchEnabled) {
@@ -13,20 +30,10 @@ async function indexSync(req, res, next) {
}
try {
- if (!process.env.MEILI_HOST || !process.env.MEILI_MASTER_KEY || !searchEnabled) {
- throw new Error('Meilisearch not configured, search will be disabled.');
- }
-
- const client = new MeiliSearch({
- host: process.env.MEILI_HOST,
- apiKey: process.env.MEILI_MASTER_KEY,
- });
+ const client = MeiliSearchClient.getInstance();
const { status } = await client.health();
- // logger.debug(`[indexSync] Meilisearch: ${status}`);
- const result = status === 'available' && !!process.env.SEARCH;
-
- if (!result) {
+ if (status !== 'available' || !process.env.SEARCH) {
throw new Error('Meilisearch not available');
}
@@ -37,12 +44,8 @@ async function indexSync(req, res, next) {
const messagesIndexed = messages.numberOfDocuments;
const convosIndexed = convos.numberOfDocuments;
- logger.debug(
- `[indexSync] There are ${messageCount} messages in the database, ${messagesIndexed} indexed`,
- );
- logger.debug(
- `[indexSync] There are ${convoCount} convos in the database, ${convosIndexed} indexed`,
- );
+ logger.debug(`[indexSync] There are ${messageCount} messages and ${messagesIndexed} indexed`);
+ logger.debug(`[indexSync] There are ${convoCount} convos and ${convosIndexed} indexed`);
if (messageCount !== messagesIndexed) {
logger.debug('[indexSync] Messages out of sync, indexing');
@@ -54,7 +57,6 @@ async function indexSync(req, res, next) {
Conversation.syncWithMeili();
}
} catch (err) {
- // logger.debug('[indexSync] in index sync');
if (err.message.includes('not found')) {
logger.debug('[indexSync] Creating indices...');
currentTimeout = setTimeout(async () => {
diff --git a/api/models/Action.js b/api/models/Action.js
index 5141569c103..9acac078b9f 100644
--- a/api/models/Action.js
+++ b/api/models/Action.js
@@ -5,19 +5,18 @@ const Action = mongoose.model('action', actionSchema);
/**
* Update an action with new data without overwriting existing properties,
- * or create a new action if it doesn't exist.
+ * or create a new action if it doesn't exist, within a transaction session if provided.
*
* @param {Object} searchParams - The search parameters to find the action to update.
* @param {string} searchParams.action_id - The ID of the action to update.
* @param {string} searchParams.user - The user ID of the action's author.
* @param {Object} updateData - An object containing the properties to update.
+ * @param {mongoose.ClientSession} [session] - The transaction session to use.
* @returns {Promise