Compare commits
302 Commits
v0.6.10
...
v2-assista
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c5e827ce9 | ||
|
|
84f68f9a15 | ||
|
|
fb6e87c36d | ||
|
|
bce0584c67 | ||
|
|
5c15b60601 | ||
|
|
e55a89270e | ||
|
|
3eabbd572e | ||
|
|
6082e26716 | ||
|
|
bc46ccdcad | ||
|
|
2bdbff5141 | ||
|
|
f0e8cca5df | ||
|
|
38ad36c1c5 | ||
|
|
53fe2f6453 | ||
|
|
31479d6a48 | ||
|
|
612a58737d | ||
|
|
8a7f36f581 | ||
|
|
4a5d06a774 | ||
|
|
fc9368e0e7 | ||
|
|
64bf0800a0 | ||
|
|
94eeec354e | ||
|
|
e42709bd1f | ||
|
|
638ac5bba6 | ||
|
|
5920672a8c | ||
|
|
a0d1e2a5f8 | ||
|
|
4ffc1414a8 | ||
|
|
df6183db0f | ||
|
|
3816219936 | ||
|
|
6fc664e4a3 | ||
|
|
89899164ed | ||
|
|
c83d9d61d4 | ||
|
|
bcdddaed72 | ||
|
|
a4de635719 | ||
|
|
4a32d7466a | ||
|
|
2ec821ea4c | ||
|
|
978009787c | ||
|
|
27e7621b6a | ||
|
|
2b37a44b8d | ||
|
|
98c96cd020 | ||
|
|
8f20fb28e5 | ||
|
|
d73ea8e1f2 | ||
|
|
83bae9e9d9 | ||
|
|
6ba7f60eec | ||
|
|
5293b73b6d | ||
|
|
b6d1f5fa53 | ||
|
|
c94278be85 | ||
|
|
3c5fa40435 | ||
|
|
b6d6343f54 | ||
|
|
89b1e33be0 | ||
|
|
436f7195b5 | ||
|
|
2aec4a6250 | ||
|
|
b77bd19092 | ||
|
|
446ffe0417 | ||
|
|
b9bcaee656 | ||
|
|
110c0535fb | ||
|
|
25fceb78b7 | ||
|
|
c8baceac76 | ||
|
|
a0288f1c5c | ||
|
|
5d3c90be26 | ||
|
|
ab6fbe48f1 | ||
|
|
3b44741cf9 | ||
|
|
d21a05606e | ||
|
|
0e50c07e3f | ||
|
|
a5cac03fa4 | ||
|
|
ba4fa6150e | ||
|
|
463ca5d613 | ||
|
|
039c7ae880 | ||
|
|
63ef15ab63 | ||
|
|
8a78500fe2 | ||
|
|
144fd5f6aa | ||
|
|
2720327aa1 | ||
|
|
4d0806d3e8 | ||
|
|
5b5f9b950b | ||
|
|
3ccff19821 | ||
|
|
11d5e232b3 | ||
|
|
099aa9dead | ||
|
|
4121818124 | ||
|
|
ca9a0fe629 | ||
|
|
bde6bb0152 | ||
|
|
667f5f91fe | ||
|
|
75da75be08 | ||
|
|
cdab1e9cda | ||
|
|
3df4fac118 | ||
|
|
0ae98ff011 | ||
|
|
4d05e5b79a | ||
|
|
199f9f32e6 | ||
|
|
f94a782b4f | ||
|
|
738207de50 | ||
|
|
c96f067689 | ||
|
|
3bfd185cab | ||
|
|
c937b8cd07 | ||
|
|
6db91978ca | ||
|
|
8c22bb1d3d | ||
|
|
5d642d0187 | ||
|
|
4196a86fa9 | ||
|
|
e6310c806a | ||
|
|
3d1dec62a4 | ||
|
|
de3987cbaf | ||
|
|
f406a85633 | ||
|
|
692ce3b346 | ||
|
|
26ea990045 | ||
|
|
265abbc1c8 | ||
|
|
0b7da72be6 | ||
|
|
3c184e9410 | ||
|
|
bf4e64ce63 | ||
|
|
9d854dac07 | ||
|
|
fce7246ac1 | ||
|
|
2cc580ba52 | ||
|
|
d2d9ac0280 | ||
|
|
f380f261a5 | ||
|
|
9d137ce42f | ||
|
|
25f92dd1c3 | ||
|
|
9277e2a0c5 | ||
|
|
c19dfddd0f | ||
|
|
0fe47cf1f8 | ||
|
|
8e5f1ad575 | ||
|
|
f64a2cb0b0 | ||
|
|
e4c07eb895 | ||
|
|
2240fee44a | ||
|
|
cb64b84846 | ||
|
|
cc71125fa1 | ||
|
|
6f0eb35365 | ||
|
|
3411d7a543 | ||
|
|
caabab4489 | ||
|
|
0b165260f7 | ||
|
|
334b603247 | ||
|
|
476767355b | ||
|
|
e80debb704 | ||
|
|
549026f677 | ||
|
|
f6a84887e1 | ||
|
|
fb80af05be | ||
|
|
cd7f3a51e1 | ||
|
|
daa5f43ac6 | ||
|
|
d0d8e47ec8 | ||
|
|
09cd1a7e74 | ||
|
|
94950b6e8b | ||
|
|
e418edd3dc | ||
|
|
e3c236ba3b | ||
|
|
7bd03a6e70 | ||
|
|
f146db5c59 | ||
|
|
9922baf7d1 | ||
|
|
09da05afa1 | ||
|
|
e66aa280c0 | ||
|
|
ed17e17a73 | ||
|
|
30d084e696 | ||
|
|
93af814596 | ||
|
|
1bafe80e78 | ||
|
|
49753a35e5 | ||
|
|
1605ef3793 | ||
|
|
8b3f80fe24 | ||
|
|
038063d4d1 | ||
|
|
5c8b16fbaf | ||
|
|
aff219c655 | ||
|
|
d07396d308 | ||
|
|
cc92597f14 | ||
|
|
4854b39f41 | ||
|
|
bb8a40dd98 | ||
|
|
56ea0f9ae7 | ||
|
|
6a6b2e79b0 | ||
|
|
bc2a628902 | ||
|
|
dec7879cc1 | ||
|
|
0a8118deed | ||
|
|
59a8165379 | ||
|
|
3a1d07136c | ||
|
|
a00756c469 | ||
|
|
7945fea0f9 | ||
|
|
84656b9812 | ||
|
|
b5d25f5e4f | ||
|
|
d4b0af3dba | ||
|
|
57d1f12574 | ||
|
|
182c9f7080 | ||
|
|
5df0ec06ea | ||
|
|
ea54cf03e9 | ||
|
|
7f83a060a0 | ||
|
|
2259bf8b03 | ||
|
|
5c3c28009f | ||
|
|
f55bd3d0e9 | ||
|
|
718572b7c8 | ||
|
|
cb62847838 | ||
|
|
3ef46132eb | ||
|
|
8fc52348e8 | ||
|
|
a4f4ec85f8 | ||
|
|
f86d80de59 | ||
|
|
798e8763d0 | ||
|
|
1f0fb497f8 | ||
|
|
8e7816468d | ||
|
|
45a95acec2 | ||
|
|
f427ad792a | ||
|
|
ed64c76053 | ||
|
|
25a0487ce5 | ||
|
|
3f77fe18b7 | ||
|
|
09de9a2b42 | ||
|
|
a673f62831 | ||
|
|
e0dd0381b2 | ||
|
|
1ee2c32a67 | ||
|
|
f521040784 | ||
|
|
30f6d90cfe | ||
|
|
e95c0aaaed | ||
|
|
9bab595204 | ||
|
|
4f17d97eb2 | ||
|
|
e4ac58012f | ||
|
|
f7761df52c | ||
|
|
af347cccde | ||
|
|
86db0a1043 | ||
|
|
1796821888 | ||
|
|
d8304ec1bb | ||
|
|
382b303963 | ||
|
|
f51ac74e12 | ||
|
|
7cddd943d0 | ||
|
|
89f6b35e6c | ||
|
|
a8cdd3460c | ||
|
|
2f90c8764a | ||
|
|
39042f8761 | ||
|
|
a9d2d3fe40 | ||
|
|
f848d752e0 | ||
|
|
8881346889 | ||
|
|
f769077ab4 | ||
|
|
5cd5c3bef8 | ||
|
|
1b243c6f8c | ||
|
|
d4190c9320 | ||
|
|
cba135d456 | ||
|
|
f27e7c720f | ||
|
|
1b8c0f0bfd | ||
|
|
0f417aaec0 | ||
|
|
d1c37e8bde | ||
|
|
0bd8c2ba00 | ||
|
|
ebcca16b94 | ||
|
|
f5a754c8be | ||
|
|
2e77813952 | ||
|
|
f307488dd4 | ||
|
|
f489aee518 | ||
|
|
2f88c5cb8a | ||
|
|
6fcaeaafe2 | ||
|
|
db870e55c3 | ||
|
|
5d0d02f5f7 | ||
|
|
40e884b3ec | ||
|
|
18edd2660b | ||
|
|
d4fe8fc82d | ||
|
|
a5f4292d2d | ||
|
|
fbdf1d17ea | ||
|
|
11bca134e7 | ||
|
|
ab66747e97 | ||
|
|
b2ab6fd19d | ||
|
|
ab263c7a50 | ||
|
|
911babd3e0 | ||
|
|
2733c5ebe7 | ||
|
|
959d6153f6 | ||
|
|
14dd3dd240 | ||
|
|
8263ddda3f | ||
|
|
b023c5683d | ||
|
|
a33db54b81 | ||
|
|
7a6a41a72e | ||
|
|
2ea6e8c18a | ||
|
|
7c85b35af0 | ||
|
|
eccf7bbbde | ||
|
|
8bef084bfc | ||
|
|
62834e18fb | ||
|
|
2da0a7661d | ||
|
|
7d633f4018 | ||
|
|
78f52859c4 | ||
|
|
b2ef75e009 | ||
|
|
ef86b25dae | ||
|
|
c52ea9490b | ||
|
|
de0cee3f56 | ||
|
|
1caa31b035 | ||
|
|
ed7d7c2fda | ||
|
|
93803323cf | ||
|
|
388dc1789b | ||
|
|
057fcf6274 | ||
|
|
2f92b54787 | ||
|
|
53ae2d7bfb | ||
|
|
156abe2fca | ||
|
|
c37d5568bf | ||
|
|
5d887492ea | ||
|
|
04eeb59d47 | ||
|
|
08d4b3cc8a | ||
|
|
6d6b3c9c1d | ||
|
|
49744d1af9 | ||
|
|
b4dc8cc2ad | ||
|
|
097a978e5b | ||
|
|
7a55132e42 | ||
|
|
c1a4733d50 | ||
|
|
f431c8fb00 | ||
|
|
5445d55af2 | ||
|
|
6a25dd38a4 | ||
|
|
ece5d9f588 | ||
|
|
5f6d1f3db0 | ||
|
|
4012dea4ab | ||
|
|
128446601a | ||
|
|
dd8038b375 | ||
|
|
542494fad6 | ||
|
|
64e81392f2 | ||
|
|
a8a19c6caa | ||
|
|
d8038e3b19 | ||
|
|
ee97179edb | ||
|
|
63a5039fae | ||
|
|
7442955a1d | ||
|
|
5291d18f38 | ||
|
|
d1eb7fcfc7 | ||
|
|
ce1cdea3de | ||
|
|
0da30b9481 | ||
|
|
b7aebf6c51 | ||
|
|
29ee4423a6 |
171
.env.example
171
.env.example
@@ -2,20 +2,15 @@
|
||||
# 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 #
|
||||
#=====================================================================#
|
||||
|
||||
#==================================================#
|
||||
# Server Configuration #
|
||||
#==================================================#
|
||||
|
||||
APP_TITLE=LibreChat
|
||||
# CUSTOM_FOOTER="My custom footer"
|
||||
|
||||
HOST=localhost
|
||||
PORT=3080
|
||||
|
||||
@@ -26,6 +21,13 @@ DOMAIN_SERVER=http://localhost:3080
|
||||
|
||||
NO_INDEX=true
|
||||
|
||||
#===============#
|
||||
# JSON Logging #
|
||||
#===============#
|
||||
|
||||
# Use when process console logs in cloud deployment like GCP/AWS
|
||||
CONSOLE_JSON=false
|
||||
|
||||
#===============#
|
||||
# Debug Logging #
|
||||
#===============#
|
||||
@@ -40,38 +42,64 @@ DEBUG_CONSOLE=false
|
||||
# UID=1000
|
||||
# GID=1000
|
||||
|
||||
#===============#
|
||||
# Configuration #
|
||||
#===============#
|
||||
# Use an absolute path, a relative path, or a URL
|
||||
|
||||
# CONFIG_PATH="/alternative/path/to/librechat.yaml"
|
||||
|
||||
#===================================================#
|
||||
# Endpoints #
|
||||
#===================================================#
|
||||
|
||||
# ENDPOINTS=openAI,assistants,azureOpenAI,bingAI,chatGPTBrowser,google,gptPlugins,anthropic
|
||||
# ENDPOINTS=openAI,assistants,azureOpenAI,bingAI,google,gptPlugins,anthropic
|
||||
|
||||
PROXY=
|
||||
|
||||
#===================================#
|
||||
# Known Endpoints - librechat.yaml #
|
||||
#===================================#
|
||||
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints
|
||||
|
||||
# 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=
|
||||
|
||||
#============#
|
||||
# Anthropic #
|
||||
#============#
|
||||
|
||||
ANTHROPIC_API_KEY=user_provided
|
||||
ANTHROPIC_MODELS=claude-1,claude-instant-1,claude-2
|
||||
# ANTHROPIC_MODELS=claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307,claude-2.1,claude-2,claude-1.2,claude-1,claude-1-100k,claude-instant-1,claude-instant-1-100k
|
||||
# ANTHROPIC_REVERSE_PROXY=
|
||||
|
||||
#============#
|
||||
# Azure #
|
||||
#============#
|
||||
|
||||
# AZURE_API_KEY=
|
||||
AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4
|
||||
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo
|
||||
# PLUGINS_USE_AZURE="true"
|
||||
|
||||
AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE
|
||||
# Note: these variables are DEPRECATED
|
||||
# Use the `librechat.yaml` configuration for `azureOpenAI` instead
|
||||
# You may also continue to use them if you opt out of using the `librechat.yaml` configuration
|
||||
|
||||
# AZURE_OPENAI_API_INSTANCE_NAME=
|
||||
# AZURE_OPENAI_API_DEPLOYMENT_NAME=
|
||||
# AZURE_OPENAI_API_VERSION=
|
||||
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME=
|
||||
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME=
|
||||
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo # Deprecated
|
||||
# AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4 # Deprecated
|
||||
# AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE # Deprecated
|
||||
# AZURE_API_KEY= # Deprecated
|
||||
# AZURE_OPENAI_API_INSTANCE_NAME= # Deprecated
|
||||
# AZURE_OPENAI_API_DEPLOYMENT_NAME= # Deprecated
|
||||
# AZURE_OPENAI_API_VERSION= # Deprecated
|
||||
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated
|
||||
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated
|
||||
# PLUGINS_USE_AZURE="true" # Deprecated
|
||||
|
||||
#============#
|
||||
# BingAI #
|
||||
@@ -80,28 +108,39 @@ AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE
|
||||
BINGAI_TOKEN=user_provided
|
||||
# BINGAI_HOST=https://cn.bing.com
|
||||
|
||||
#============#
|
||||
# ChatGPT #
|
||||
#============#
|
||||
|
||||
CHATGPT_TOKEN=
|
||||
CHATGPT_MODELS=text-davinci-002-render-sha
|
||||
# CHATGPT_REVERSE_PROXY=
|
||||
|
||||
#============#
|
||||
# Google #
|
||||
#============#
|
||||
|
||||
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
|
||||
|
||||
@@ -115,27 +154,37 @@ DEBUG_OPENAI=false
|
||||
|
||||
# OPENAI_REVERSE_PROXY=
|
||||
|
||||
# OPENAI_ORGANIZATION=
|
||||
# OPENAI_ORGANIZATION=
|
||||
|
||||
#====================#
|
||||
# Assistants API #
|
||||
#====================#
|
||||
|
||||
# ASSISTANTS_API_KEY=
|
||||
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
|
||||
|
||||
#==========================#
|
||||
# Azure Assistants API #
|
||||
#==========================#
|
||||
|
||||
# Note: You should map your credentials with custom variables according to your Azure OpenAI Configuration
|
||||
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.
|
||||
|
||||
# More info, including how to enable use of Assistants with Azure here:
|
||||
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure
|
||||
|
||||
#============#
|
||||
# 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
|
||||
|
||||
@@ -172,7 +221,7 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
|
||||
|
||||
# Google
|
||||
#-----------------
|
||||
GOOGLE_API_KEY=
|
||||
GOOGLE_SEARCH_API_KEY=
|
||||
GOOGLE_CSE_ID=
|
||||
|
||||
# SerpAPI
|
||||
@@ -183,6 +232,14 @@ SERPAPI_API_KEY=
|
||||
#-----------------
|
||||
SD_WEBUI_URL=http://host.docker.internal:7860
|
||||
|
||||
# Tavily
|
||||
#-----------------
|
||||
TAVILY_API_KEY=
|
||||
|
||||
# Traversaal
|
||||
#-----------------
|
||||
TRAVERSAAL_API_KEY=
|
||||
|
||||
# WolframAlpha
|
||||
#-----------------
|
||||
WOLFRAM_APP_ID=
|
||||
@@ -238,6 +295,8 @@ LIMIT_MESSAGE_USER=false
|
||||
MESSAGE_USER_MAX=40
|
||||
MESSAGE_USER_WINDOW=1
|
||||
|
||||
ILLEGAL_MODEL_REQ_SCORE=5
|
||||
|
||||
#========================#
|
||||
# Balance #
|
||||
#========================#
|
||||
@@ -286,6 +345,9 @@ OPENID_ISSUER=
|
||||
OPENID_SESSION_SECRET=
|
||||
OPENID_SCOPE="openid profile email"
|
||||
OPENID_CALLBACK_URL=/oauth/openid/callback
|
||||
OPENID_REQUIRED_ROLE=
|
||||
OPENID_REQUIRED_ROLE_TOKEN_KIND=
|
||||
OPENID_REQUIRED_ROLE_PARAMETER_PATH=
|
||||
|
||||
OPENID_BUTTON_LABEL=
|
||||
OPENID_IMAGE_URL=
|
||||
@@ -294,15 +356,15 @@ OPENID_IMAGE_URL=
|
||||
# Email Password Reset #
|
||||
#========================#
|
||||
|
||||
EMAIL_SERVICE=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_ENCRYPTION=
|
||||
EMAIL_ENCRYPTION_HOSTNAME=
|
||||
EMAIL_ALLOW_SELFSIGNED=
|
||||
EMAIL_USERNAME=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_FROM_NAME=
|
||||
EMAIL_SERVICE=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_ENCRYPTION=
|
||||
EMAIL_ENCRYPTION_HOSTNAME=
|
||||
EMAIL_ALLOW_SELFSIGNED=
|
||||
EMAIL_USERNAME=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_FROM_NAME=
|
||||
EMAIL_FROM=noreply@librechat.ai
|
||||
|
||||
#========================#
|
||||
@@ -316,6 +378,16 @@ FIREBASE_STORAGE_BUCKET=
|
||||
FIREBASE_MESSAGING_SENDER_ID=
|
||||
FIREBASE_APP_ID=
|
||||
|
||||
#===================================================#
|
||||
# UI #
|
||||
#===================================================#
|
||||
|
||||
APP_TITLE=LibreChat
|
||||
# CUSTOM_FOOTER="My custom footer"
|
||||
HELP_AND_FAQ_URL=https://librechat.ai
|
||||
|
||||
# SHOW_BIRTHDAY_ICON=true
|
||||
|
||||
#==================================================#
|
||||
# Others #
|
||||
#==================================================#
|
||||
@@ -323,15 +395,8 @@ FIREBASE_APP_ID=
|
||||
|
||||
# NODE_ENV=
|
||||
|
||||
# If using Redis, you should flush the cache after changing any LibreChat settings
|
||||
# REDIS_URI=
|
||||
# USE_REDIS=
|
||||
|
||||
# Give the AI Icon a Birthday Hat :)
|
||||
# Will show automatically on February 11th (LibreChat's birthday)
|
||||
# Set this to false to disable the birthday hat
|
||||
# Set to true to enable all the time.
|
||||
# SHOW_BIRTHDAY_ICON=true
|
||||
|
||||
# E2E_USER_EMAIL=
|
||||
# E2E_USER_PASSWORD=
|
||||
# E2E_USER_PASSWORD=
|
||||
|
||||
@@ -19,6 +19,7 @@ module.exports = {
|
||||
'e2e/playwright-report/**/*',
|
||||
'packages/data-provider/types/**/*',
|
||||
'packages/data-provider/dist/**/*',
|
||||
'packages/data-provider/test_bundle/**/*',
|
||||
'data-node/**/*',
|
||||
'meili_data/**/*',
|
||||
'node_modules/**/*',
|
||||
@@ -131,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: {
|
||||
|
||||
2
.github/CODE_OF_CONDUCT.md
vendored
2
.github/CODE_OF_CONDUCT.md
vendored
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement here on GitHub or
|
||||
on the official [Discord Server](https://discord.gg/uDyZ5Tzhct).
|
||||
on the official [Discord Server](https://discord.librechat.ai).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -8,7 +8,7 @@ If the feature you would like to contribute has not already received prior appro
|
||||
|
||||
Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected. We appreciate your understanding and cooperation.
|
||||
|
||||
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.gg/uDyZ5Tzhct), where you can engage with other contributors and seek guidance from the community.
|
||||
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.librechat.ai), where you can engage with other contributors and seek guidance from the community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -50,7 +50,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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
@@ -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
|
||||
|
||||
6
.github/SECURITY.md
vendored
6
.github/SECURITY.md
vendored
@@ -12,7 +12,7 @@ When reporting a security vulnerability, you have the following options to reach
|
||||
|
||||
- **Option 2: GitHub Issues**: You can initiate first contact via GitHub Issues. However, please note that initial contact through GitHub Issues should not include any sensitive details.
|
||||
|
||||
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/5rbRxn4uME) and initiate first contact in the `#issues` channel. However, please ensure that initial contact through Discord does not include any sensitive details.
|
||||
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.librechat.ai) and initiate first contact in the `#issues` channel. However, please ensure that initial contact through Discord does not include any sensitive details.
|
||||
|
||||
_After the initial contact, we will establish a private communication channel for further discussion._
|
||||
|
||||
@@ -39,11 +39,11 @@ Please note that as a security-conscious community, we may not always disclose d
|
||||
|
||||
This security policy applies to the following GitHub repository:
|
||||
|
||||
- Repository: [LibreChat](https://github.com/danny-avila/LibreChat)
|
||||
- Repository: [LibreChat](https://github.librechat.ai)
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.gg/NGaa9RPCft) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/danny-avila/LibreChat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
|
||||
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.librechat.ai) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/danny-avila/LibreChat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@@ -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
|
||||
|
||||
@@ -15,7 +18,6 @@ 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
|
||||
- [ ] Translation update
|
||||
|
||||
## Testing
|
||||
@@ -26,6 +28,8 @@ Please describe your test process and include instructions so that we can reprod
|
||||
|
||||
## Checklist
|
||||
|
||||
Please delete any irrelevant options.
|
||||
|
||||
- [ ] My code adheres to this project's style guidelines
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented in any complex areas of my code
|
||||
@@ -34,3 +38,4 @@ Please describe your test process and include instructions so that we can reprod
|
||||
- [ ] 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.
|
||||
- [ ] A pull request for updating the documentation has been submitted.
|
||||
|
||||
20
.github/workflows/backend-review.yml
vendored
20
.github/workflows/backend-review.yml
vendored
@@ -35,6 +35,24 @@ jobs:
|
||||
|
||||
- name: Install Data Provider
|
||||
run: npm run build:data-provider
|
||||
|
||||
- name: Create empty auth.json file
|
||||
run: |
|
||||
mkdir -p api/data
|
||||
echo '{}' > api/data/auth.json
|
||||
|
||||
- name: Check for Circular dependency in rollup
|
||||
working-directory: ./packages/data-provider
|
||||
run: |
|
||||
output=$(npm run rollup:api)
|
||||
echo "$output"
|
||||
if echo "$output" | grep -q "Circular dependency"; then
|
||||
echo "Error: Circular dependency detected!"
|
||||
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
|
||||
@@ -45,4 +63,4 @@ jobs:
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v2
|
||||
with:
|
||||
eslint: true
|
||||
eslint: true
|
||||
|
||||
83
.github/workflows/container.yml
vendored
83
.github/workflows/container.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: Docker Compose Build on Tag
|
||||
|
||||
# The workflow is triggered when a tag is pushed
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Set up QEMU for cross-platform builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Prepare Docker Build
|
||||
- name: Build Docker images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Tag and push librechat-api
|
||||
- name: Docker metadata for librechat-api
|
||||
id: meta-librechat-api
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/librechat-api
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and librechat-api
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile.multi
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat-api.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: api-build
|
||||
|
||||
# Tag and push librechat
|
||||
- name: Docker metadata for librechat
|
||||
id: meta-librechat
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/librechat
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and librechat
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: node
|
||||
62
.github/workflows/dev-images.yml
vendored
62
.github/workflows/dev-images.yml
vendored
@@ -13,14 +13,27 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-dev-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat-dev
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
@@ -38,35 +51,22 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Build Docker images
|
||||
- name: Build Docker images
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker build -f Dockerfile.multi --target api-build -t librechat-dev-api .
|
||||
docker build -f Dockerfile -t librechat-dev .
|
||||
|
||||
# Tag and push the images to GitHub Container Registry
|
||||
- name: Tag and push images to GHCR
|
||||
run: |
|
||||
docker tag librechat-dev-api:latest ghcr.io/${{ github.repository_owner }}/librechat-dev-api:${{ github.sha }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev-api:${{ github.sha }}
|
||||
docker tag librechat-dev-api:latest ghcr.io/${{ github.repository_owner }}/librechat-dev-api:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev-api:latest
|
||||
|
||||
docker tag librechat-dev:latest ghcr.io/${{ github.repository_owner }}/librechat-dev:${{ github.sha }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev:${{ github.sha }}
|
||||
docker tag librechat-dev:latest ghcr.io/${{ github.repository_owner }}/librechat-dev:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev:latest
|
||||
|
||||
# Tag and push the images to Docker Hub
|
||||
- name: Tag and push images to Docker Hub
|
||||
run: |
|
||||
docker tag librechat-dev-api:latest ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev-api:${{ github.sha }}
|
||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev-api:${{ github.sha }}
|
||||
docker tag librechat-dev-api:latest ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev-api:latest
|
||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev-api:latest
|
||||
|
||||
docker tag librechat-dev:latest ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev:${{ github.sha }}
|
||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev:${{ github.sha }}
|
||||
docker tag librechat-dev:latest ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev:latest
|
||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/librechat-dev:latest
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
36
.github/workflows/frontend-review.yml
vendored
36
.github/workflows/frontend-review.yml
vendored
@@ -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
|
||||
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
|
||||
|
||||
20
.github/workflows/generate_embeddings.yml
vendored
Normal file
20
.github/workflows/generate_embeddings.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: 'generate_embeddings'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: supabase/embeddings-generator@v0.0.5
|
||||
with:
|
||||
supabase-url: ${{ secrets.SUPABASE_URL }}
|
||||
supabase-service-role-key: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||
openai-key: ${{ secrets.OPENAI_DOC_EMBEDDINGS_KEY }}
|
||||
docs-root-path: 'docs'
|
||||
88
.github/workflows/latest-images-main.yml
vendored
88
.github/workflows/latest-images-main.yml
vendored
@@ -1,88 +0,0 @@
|
||||
name: Docker Compose Build Latest Tag (Manual Dispatch)
|
||||
|
||||
# The workflow is manually triggered
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Fetch all tags and set the latest tag
|
||||
- name: Fetch tags and set the latest tag
|
||||
run: |
|
||||
git fetch --tags
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Prepare Docker Build
|
||||
- name: Build Docker images
|
||||
run: cp .env.example .env
|
||||
|
||||
# Docker metadata for librechat-api
|
||||
- name: Docker metadata for librechat-api
|
||||
id: meta-librechat-api
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/librechat-api
|
||||
tags: |
|
||||
type=raw,value=${{ env.LATEST_TAG }},enable=true
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
# Build and push librechat-api
|
||||
- name: Build and push librechat-api
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile.multi
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat-api.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: api-build
|
||||
|
||||
# Docker metadata for librechat
|
||||
- name: Docker metadata for librechat
|
||||
id: meta-librechat
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/librechat
|
||||
tags: |
|
||||
type=raw,value=${{ env.LATEST_TAG }},enable=true
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
# Build and push librechat
|
||||
- name: Build and push librechat
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: node
|
||||
69
.github/workflows/main-image-workflow.yml
vendored
Normal file
69
.github/workflows/main-image-workflow.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Docker Compose Build Latest Main Image Tag (Manual Dispatch)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch tags and set the latest tag
|
||||
run: |
|
||||
git fetch --tags
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
27
.github/workflows/mkdocs.yaml
vendored
27
.github/workflows/mkdocs.yaml
vendored
@@ -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
|
||||
67
.github/workflows/tag-images.yml
vendored
Normal file
67
.github/workflows/tag-images.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Docker Images Build on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.ref_name }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.ref_name }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -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/
|
||||
@@ -50,6 +54,7 @@ bower_components/
|
||||
|
||||
#config file
|
||||
librechat.yaml
|
||||
librechat.yml
|
||||
|
||||
# Environment
|
||||
.npmrc
|
||||
@@ -68,12 +73,16 @@ src/style - official.css
|
||||
/playwright/.cache/
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
.idx
|
||||
monospace.json
|
||||
.idea
|
||||
*.iml
|
||||
*.pem
|
||||
config.local.ts
|
||||
**/storageState.json
|
||||
junit.xml
|
||||
**/.venv/
|
||||
**/venv/
|
||||
|
||||
# docker override file
|
||||
docker-compose.override.yaml
|
||||
@@ -91,4 +100,7 @@ auth.json
|
||||
!client/src/components/Nav/SettingsTabs/Data/
|
||||
|
||||
# User uploads
|
||||
uploads/
|
||||
uploads/
|
||||
|
||||
# owner
|
||||
release/
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
[ -n "$CI" ] && exit 0
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,19 +1,32 @@
|
||||
# Base node image
|
||||
FROM node:18-alpine AS node
|
||||
# v0.7.2
|
||||
|
||||
COPY . /app
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
RUN apk --no-cache add curl
|
||||
|
||||
RUN mkdir -p /app && chown node:node /app
|
||||
WORKDIR /app
|
||||
|
||||
# Allow mounting of these files, which have no default
|
||||
# values.
|
||||
RUN touch .env
|
||||
# Install call deps - Install curl for health check
|
||||
RUN apk --no-cache add curl && \
|
||||
npm ci
|
||||
USER node
|
||||
|
||||
# React client build
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
RUN npm run frontend
|
||||
COPY --chown=node:node . .
|
||||
|
||||
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
|
||||
|
||||
RUN mkdir -p /app/client/public/images /app/api/logs
|
||||
|
||||
# Node API setup
|
||||
EXPOSE 3080
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# v0.7.2
|
||||
|
||||
# Build API, Client and Data Provider
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
@@ -5,29 +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/ ./
|
||||
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
|
||||
|
||||
107
README.md
107
README.md
@@ -1,10 +1,10 @@
|
||||
<p align="center">
|
||||
<a href="https://docs.librechat.ai">
|
||||
<img src="docs/assets/LibreChat.svg" height="256">
|
||||
</a>
|
||||
<a href="https://docs.librechat.ai">
|
||||
<h1 align="center">LibreChat</h1>
|
||||
<a href="https://librechat.ai">
|
||||
<img src="client/public/assets/logo.svg" height="256">
|
||||
</a>
|
||||
<h1 align="center">
|
||||
<a href="https://librechat.ai">LibreChat</a>
|
||||
</h1>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -27,7 +27,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://railway.app/template/b5k2mn?referralCode=HI9hWz">
|
||||
<a href="https://railway.app/template/b5k2mn?referralCode=myKrVZ">
|
||||
<img src="https://railway.app/button.svg" alt="Deploy on Railway" height="30">
|
||||
</a>
|
||||
<a href="https://zeabur.com/templates/0X2ZY8">
|
||||
@@ -39,71 +39,100 @@
|
||||
</p>
|
||||
|
||||
# 📃 Features
|
||||
- 🖥️ UI matching ChatGPT, including Dark mode, Streaming, and 11-2023 updates
|
||||
- 💬 Multimodal Chat:
|
||||
- Upload and analyze images with GPT-4 and Gemini Vision 📸
|
||||
- More filetypes and Assistants API integration in Active Development 🚧
|
||||
- 🌎 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 API, Azure, BingAI, ChatGPT, Google Vertex AI, Anthropic (Claude), Plugins
|
||||
- 💾 Create, Save, & Share Custom Presets
|
||||
- 🔄 Edit, Resubmit, and Continue messages with conversation branching
|
||||
- 📤 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, and completely Open-Source
|
||||
|
||||
[For a thorough review of our features, see our docs here](https://docs.librechat.ai/features/plugins/introduction.html) 📚
|
||||
- 🖥️ 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 (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) 🌤️
|
||||
- Non-OpenAI Agents in Active Development 🚧
|
||||
- 🌎 Multilingual UI:
|
||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro,
|
||||
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
|
||||
- 🎨 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:
|
||||
- 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/) 📚
|
||||
|
||||
## 🪶 All-In-One AI Conversations with LibreChat
|
||||
|
||||
LibreChat brings together the future of assistant AIs with the revolutionary technology of OpenAI's ChatGPT. Celebrating the original styling, LibreChat gives you the ability to integrate multiple AI models. It also integrates and enhances original client features such as conversation and message search, prompt templates and plugins.
|
||||
|
||||
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.
|
||||
|
||||
<!-- https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59 -->
|
||||
|
||||
[](https://youtu.be/pNIOs1ovsXw)
|
||||
[](https://www.youtube.com/watch?v=YLVUW5UP9N0)
|
||||
Click on the thumbnail to open the video☝️
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
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)
|
||||
## 🌐 Resources
|
||||
|
||||
**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)
|
||||
## 📝 Changelog
|
||||
|
||||
**⚠️ [Breaking Changes](docs/general_info/breaking_changes.md)**
|
||||
Please consult the breaking changes before updating.
|
||||
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)
|
||||
|
||||
**⚠️ Please consult the [changelog](https://www.librechat.ai/changelog) for breaking changes before updating.**
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/4685" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4685" alt="danny-avila%2FLibreChat | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://star-history.com/#danny-avila/LibreChat&Date">
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date&theme=dark" onerror="this.src='https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date'" />
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/4685" target="_blank" style="padding: 10px;">
|
||||
<img src="https://trendshift.io/api/badge/repositories/4685" alt="danny-avila%2FLibreChat | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
<a href="https://runacap.com/ross-index/q1-24/" target="_blank" rel="noopener" style="margin-left: 20px;">
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2024/04/ROSS_badge_white_Q1_2024.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q1 2024 | Runa Capital" width="260" height="56"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a href="https://star-history.com/#danny-avila/LibreChat&Date">
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date&theme=dark" onerror="this.src='https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date'" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Contributions
|
||||
|
||||
Contributions, suggestions, bug reports and fixes are welcome!
|
||||
|
||||
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
||||
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
||||
|
||||
---
|
||||
|
||||
💖 This project exists in its current state thanks to all the people who contribute
|
||||
---
|
||||
## 💖 This project exists in its current state thanks to all the people who contribute
|
||||
|
||||
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
||||
</a>
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
const Anthropic = require('@anthropic-ai/sdk');
|
||||
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
|
||||
const { getResponseSender, EModelEndpoint } = require('librechat-data-provider');
|
||||
const {
|
||||
getResponseSender,
|
||||
EModelEndpoint,
|
||||
validateVisionModel,
|
||||
} = require('librechat-data-provider');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const {
|
||||
truncateText,
|
||||
formatMessage,
|
||||
titleFunctionPrompt,
|
||||
parseParamFromPrompt,
|
||||
createContextHandlers,
|
||||
} = require('./prompts');
|
||||
const spendTokens = require('~/models/spendTokens');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const { logger } = require('~/config');
|
||||
@@ -10,12 +23,20 @@ const AI_PROMPT = '\n\nAssistant:';
|
||||
|
||||
const tokenizersCache = {};
|
||||
|
||||
/** Helper function to introduce a delay before retrying */
|
||||
function delayBeforeRetry(attempts, baseDelay = 1000) {
|
||||
return new Promise((resolve) => setTimeout(resolve, baseDelay * attempts));
|
||||
}
|
||||
|
||||
class AnthropicClient extends BaseClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
super(apiKey, options);
|
||||
this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY;
|
||||
this.userLabel = HUMAN_PROMPT;
|
||||
this.assistantLabel = AI_PROMPT;
|
||||
this.contextStrategy = options.contextStrategy
|
||||
? options.contextStrategy.toLowerCase()
|
||||
: 'discard';
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
@@ -47,8 +68,16 @@ class AnthropicClient extends BaseClient {
|
||||
stop: modelOptions.stop, // no stop method for now
|
||||
};
|
||||
|
||||
this.isClaude3 = this.modelOptions.model.includes('claude-3');
|
||||
this.useMessages = this.isClaude3 || !!this.options.attachments;
|
||||
|
||||
this.defaultVisionModel = this.options.visionModel ?? 'claude-3-sonnet-20240229';
|
||||
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;
|
||||
@@ -87,7 +116,12 @@ class AnthropicClient extends BaseClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initialized Anthropic client.
|
||||
* @returns {Anthropic} The Anthropic client instance.
|
||||
*/
|
||||
getClient() {
|
||||
/** @type {Anthropic.default.RequestOptions} */
|
||||
const options = {
|
||||
apiKey: this.apiKey,
|
||||
};
|
||||
@@ -99,6 +133,75 @@ class AnthropicClient extends BaseClient {
|
||||
return new Anthropic(options);
|
||||
}
|
||||
|
||||
getTokenCountForResponse(response) {
|
||||
return this.getTokenCountForMessage({
|
||||
role: 'assistant',
|
||||
content: response.text,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks if the model is a vision model based on request attachments and sets the appropriate options:
|
||||
* - Sets `this.modelOptions.model` to `gpt-4-vision-preview` if the request is a vision request.
|
||||
* - Sets `this.isVisionModel` to `true` if vision request.
|
||||
* - Deletes `this.modelOptions.stop` if vision request.
|
||||
* @param {MongoFile[]} attachments
|
||||
*/
|
||||
checkVisionRequest(attachments) {
|
||||
const availableModels = this.options.modelsConfig?.[EModelEndpoint.anthropic];
|
||||
this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the token cost in tokens for an image based on its dimensions and detail level.
|
||||
*
|
||||
* For reference, see: https://docs.anthropic.com/claude/docs/vision#image-costs
|
||||
*
|
||||
* @param {Object} image - The image object.
|
||||
* @param {number} image.width - The width of the image.
|
||||
* @param {number} image.height - The height of the image.
|
||||
* @returns {number} The calculated token cost measured by tokens.
|
||||
*
|
||||
*/
|
||||
calculateImageTokenCost({ width, height }) {
|
||||
return Math.ceil((width * height) / 750);
|
||||
}
|
||||
|
||||
async addImageURLs(message, attachments) {
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
this.options.req,
|
||||
attachments,
|
||||
EModelEndpoint.anthropic,
|
||||
);
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
return files;
|
||||
}
|
||||
|
||||
async recordTokenUsage({ promptTokens, completionTokens, model, context = 'message' }) {
|
||||
await spendTokens(
|
||||
{
|
||||
context,
|
||||
user: this.user,
|
||||
conversationId: this.conversationId,
|
||||
model: model ?? this.modelOptions.model,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
{ promptTokens, completionTokens },
|
||||
);
|
||||
}
|
||||
|
||||
async buildMessages(messages, parentMessageId) {
|
||||
const orderedMessages = this.constructor.getMessagesForConversation({
|
||||
messages,
|
||||
@@ -107,28 +210,145 @@ class AnthropicClient extends BaseClient {
|
||||
|
||||
logger.debug('[AnthropicClient] orderedMessages', { orderedMessages, parentMessageId });
|
||||
|
||||
const formattedMessages = orderedMessages.map((message) => ({
|
||||
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
||||
content: message?.content ?? message.text,
|
||||
}));
|
||||
if (this.options.attachments) {
|
||||
const attachments = await this.options.attachments;
|
||||
const images = attachments.filter((file) => file.type.includes('image'));
|
||||
|
||||
if (images.length && !this.isVisionModel) {
|
||||
throw new Error('Images are only supported with the Claude 3 family of models');
|
||||
}
|
||||
|
||||
const latestMessage = orderedMessages[orderedMessages.length - 1];
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.message_file_map[latestMessage.messageId] = attachments;
|
||||
} else {
|
||||
this.message_file_map = {
|
||||
[latestMessage.messageId]: attachments,
|
||||
};
|
||||
}
|
||||
|
||||
const files = await this.addImageURLs(latestMessage, attachments);
|
||||
|
||||
this.options.attachments = files;
|
||||
}
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.contextHandlers = createContextHandlers(
|
||||
this.options.req,
|
||||
orderedMessages[orderedMessages.length - 1].text,
|
||||
);
|
||||
}
|
||||
|
||||
const formattedMessages = orderedMessages.map((message, i) => {
|
||||
const formattedMessage = this.useMessages
|
||||
? formatMessage({
|
||||
message,
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
})
|
||||
: {
|
||||
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
||||
content: message?.content ?? message.text,
|
||||
};
|
||||
|
||||
const needsTokenCount = this.contextStrategy && !orderedMessages[i].tokenCount;
|
||||
/* If tokens were never counted, or, is a Vision request and the message has files, count again */
|
||||
if (needsTokenCount || (this.isVisionModel && (message.image_urls || message.files))) {
|
||||
orderedMessages[i].tokenCount = this.getTokenCountForMessage(formattedMessage);
|
||||
}
|
||||
|
||||
/* If message has files, calculate image token cost */
|
||||
if (this.message_file_map && this.message_file_map[message.messageId]) {
|
||||
const attachments = this.message_file_map[message.messageId];
|
||||
for (const file of attachments) {
|
||||
if (file.embedded) {
|
||||
this.contextHandlers?.processFile(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
orderedMessages[i].tokenCount += this.calculateImageTokenCost({
|
||||
width: file.width,
|
||||
height: file.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formattedMessage.tokenCount = orderedMessages[i].tokenCount;
|
||||
return formattedMessage;
|
||||
});
|
||||
|
||||
if (this.contextHandlers) {
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
this.options.promptPrefix = this.augmentedPrompt + (this.options.promptPrefix ?? '');
|
||||
}
|
||||
|
||||
let { context: messagesInWindow, remainingContextTokens } =
|
||||
await this.getMessagesWithinTokenLimit(formattedMessages);
|
||||
|
||||
const tokenCountMap = orderedMessages
|
||||
.slice(orderedMessages.length - messagesInWindow.length)
|
||||
.reduce((map, message, index) => {
|
||||
const { messageId } = message;
|
||||
if (!messageId) {
|
||||
return map;
|
||||
}
|
||||
|
||||
map[messageId] = orderedMessages[index].tokenCount;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
logger.debug('[AnthropicClient]', {
|
||||
messagesInWindow: messagesInWindow.length,
|
||||
remainingContextTokens,
|
||||
});
|
||||
|
||||
let lastAuthor = '';
|
||||
let groupedMessages = [];
|
||||
|
||||
for (let message of formattedMessages) {
|
||||
for (let i = 0; i < messagesInWindow.length; i++) {
|
||||
const message = messagesInWindow[i];
|
||||
const author = message.role ?? message.author;
|
||||
// If last author is not same as current author, add to new group
|
||||
if (lastAuthor !== message.author) {
|
||||
groupedMessages.push({
|
||||
author: message.author,
|
||||
if (lastAuthor !== author) {
|
||||
const newMessage = {
|
||||
content: [message.content],
|
||||
});
|
||||
lastAuthor = message.author;
|
||||
};
|
||||
|
||||
if (message.role) {
|
||||
newMessage.role = message.role;
|
||||
} else {
|
||||
newMessage.author = message.author;
|
||||
}
|
||||
|
||||
groupedMessages.push(newMessage);
|
||||
lastAuthor = author;
|
||||
// If same author, append content to the last group
|
||||
} else {
|
||||
groupedMessages[groupedMessages.length - 1].content.push(message.content);
|
||||
}
|
||||
}
|
||||
|
||||
groupedMessages = groupedMessages.map((msg, i) => {
|
||||
const isLast = i === groupedMessages.length - 1;
|
||||
if (msg.content.length === 1) {
|
||||
const content = msg.content[0];
|
||||
return {
|
||||
...msg,
|
||||
// reason: final assistant content cannot end with trailing whitespace
|
||||
content:
|
||||
isLast && this.useMessages && msg.role === 'assistant' && typeof content === 'string'
|
||||
? content?.trim()
|
||||
: content,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.useMessages && msg.tokenCount) {
|
||||
delete msg.tokenCount;
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
let identityPrefix = '';
|
||||
if (this.options.userLabel) {
|
||||
identityPrefix = `\nHuman's name: ${this.options.userLabel}`;
|
||||
@@ -154,9 +374,10 @@ class AnthropicClient extends BaseClient {
|
||||
// Prompt AI to respond, empty if last message was from AI
|
||||
let isEdited = lastAuthor === this.assistantLabel;
|
||||
const promptSuffix = isEdited ? '' : `${promptPrefix}${this.assistantLabel}\n`;
|
||||
let currentTokenCount = isEdited
|
||||
? this.getTokenCount(promptPrefix)
|
||||
: this.getTokenCount(promptSuffix);
|
||||
let currentTokenCount =
|
||||
isEdited || this.useMessages
|
||||
? this.getTokenCount(promptPrefix)
|
||||
: this.getTokenCount(promptSuffix);
|
||||
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
@@ -224,7 +445,69 @@ class AnthropicClient extends BaseClient {
|
||||
return true;
|
||||
};
|
||||
|
||||
await buildPromptBody();
|
||||
const messagesPayload = [];
|
||||
const buildMessagesPayload = async () => {
|
||||
let canContinue = true;
|
||||
|
||||
if (promptPrefix) {
|
||||
this.systemMessage = promptPrefix;
|
||||
}
|
||||
|
||||
while (currentTokenCount < maxTokenCount && groupedMessages.length > 0 && canContinue) {
|
||||
const message = groupedMessages.pop();
|
||||
|
||||
let tokenCountForMessage = message.tokenCount ?? this.getTokenCountForMessage(message);
|
||||
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
const exceededMaxCount = newTokenCount > maxTokenCount;
|
||||
|
||||
if (exceededMaxCount && messagesPayload.length === 0) {
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
} else if (exceededMaxCount) {
|
||||
canContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
delete message.tokenCount;
|
||||
messagesPayload.unshift(message);
|
||||
currentTokenCount = newTokenCount;
|
||||
|
||||
// Switch off isEdited after using it once
|
||||
if (isEdited && message.role === 'assistant') {
|
||||
isEdited = false;
|
||||
}
|
||||
|
||||
// Wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
};
|
||||
|
||||
const processTokens = () => {
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.maxOutputTokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
};
|
||||
|
||||
if (this.modelOptions.model.startsWith('claude-3')) {
|
||||
await buildMessagesPayload();
|
||||
processTokens();
|
||||
return {
|
||||
prompt: messagesPayload,
|
||||
context: messagesInWindow,
|
||||
promptTokens: currentTokenCount,
|
||||
tokenCountMap,
|
||||
};
|
||||
} else {
|
||||
await buildPromptBody();
|
||||
processTokens();
|
||||
}
|
||||
|
||||
if (nextMessage.remove) {
|
||||
promptBody = promptBody.replace(nextMessage.messageString, '');
|
||||
@@ -234,22 +517,26 @@ class AnthropicClient extends BaseClient {
|
||||
|
||||
let prompt = `${promptBody}${promptSuffix}`;
|
||||
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.maxOutputTokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
|
||||
return { prompt, context };
|
||||
return { prompt, context, promptTokens: currentTokenCount, tokenCountMap };
|
||||
}
|
||||
|
||||
getCompletion() {
|
||||
logger.debug('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message or completion response using the Anthropic client.
|
||||
* @param {Anthropic} client - The Anthropic client instance.
|
||||
* @param {Anthropic.default.MessageCreateParams | Anthropic.default.CompletionCreateParams} options - The options for the message or completion.
|
||||
* @param {boolean} useMessages - Whether to use messages or completions. Defaults to `this.useMessages`.
|
||||
* @returns {Promise<Anthropic.default.Message | Anthropic.default.Completion>} The response from the Anthropic client.
|
||||
*/
|
||||
async createResponse(client, options, useMessages) {
|
||||
return useMessages ?? this.useMessages
|
||||
? await client.messages.create(options)
|
||||
: await client.completions.create(options);
|
||||
}
|
||||
|
||||
async sendCompletion(payload, { onProgress, abortController }) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
@@ -279,44 +566,101 @@ class AnthropicClient extends BaseClient {
|
||||
topP: top_p,
|
||||
topK: top_k,
|
||||
} = this.modelOptions;
|
||||
|
||||
const requestOptions = {
|
||||
prompt: payload,
|
||||
model,
|
||||
stream: stream || true,
|
||||
max_tokens_to_sample: maxOutputTokens || 1500,
|
||||
stop_sequences,
|
||||
temperature,
|
||||
metadata,
|
||||
top_p,
|
||||
top_k,
|
||||
};
|
||||
logger.debug('[AnthropicClient]', { ...requestOptions });
|
||||
const response = await client.completions.create(requestOptions);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
logger.debug('[AnthropicClient] message aborted!');
|
||||
response.controller.abort();
|
||||
});
|
||||
|
||||
for await (const completion of response) {
|
||||
// Uncomment to debug message stream
|
||||
// logger.debug(completion);
|
||||
text += completion.completion;
|
||||
onProgress(completion.completion);
|
||||
if (this.useMessages) {
|
||||
requestOptions.messages = payload;
|
||||
requestOptions.max_tokens = maxOutputTokens || 1500;
|
||||
} else {
|
||||
requestOptions.prompt = payload;
|
||||
requestOptions.max_tokens_to_sample = maxOutputTokens || 1500;
|
||||
}
|
||||
|
||||
signal.removeEventListener('abort', () => {
|
||||
logger.debug('[AnthropicClient] message aborted!');
|
||||
response.controller.abort();
|
||||
});
|
||||
if (this.systemMessage) {
|
||||
requestOptions.system = this.systemMessage;
|
||||
}
|
||||
|
||||
logger.debug('[AnthropicClient]', { ...requestOptions });
|
||||
|
||||
const handleChunk = (currentChunk) => {
|
||||
if (currentChunk) {
|
||||
text += currentChunk;
|
||||
onProgress(currentChunk);
|
||||
}
|
||||
};
|
||||
|
||||
const maxRetries = 3;
|
||||
async function processResponse() {
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxRetries) {
|
||||
let response;
|
||||
try {
|
||||
response = await this.createResponse(client, requestOptions);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
logger.debug('[AnthropicClient] message aborted!');
|
||||
if (response.controller?.abort) {
|
||||
response.controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
for await (const completion of response) {
|
||||
// Handle each completion as before
|
||||
if (completion?.delta?.text) {
|
||||
handleChunk(completion.delta.text);
|
||||
} else if (completion.completion) {
|
||||
handleChunk(completion.completion);
|
||||
}
|
||||
}
|
||||
|
||||
// Successful processing, exit loop
|
||||
break;
|
||||
} catch (error) {
|
||||
attempts += 1;
|
||||
logger.warn(
|
||||
`User: ${this.user} | Anthropic Request ${attempts} failed: ${error.message}`,
|
||||
);
|
||||
|
||||
if (attempts < maxRetries) {
|
||||
await delayBeforeRetry(attempts, 350);
|
||||
} else {
|
||||
throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener('abort', () => {
|
||||
logger.debug('[AnthropicClient] message aborted!');
|
||||
if (response.controller?.abort) {
|
||||
response.controller.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await processResponse.bind(this)();
|
||||
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -342,6 +686,78 @@ class AnthropicClient extends BaseClient {
|
||||
getTokenCount(text) {
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a concise title for a conversation based on the user's input text and response.
|
||||
* Involves sending a chat completion request with specific instructions for title generation.
|
||||
*
|
||||
* This function capitlizes on [Anthropic's function calling training](https://docs.anthropic.com/claude/docs/functions-external-tools).
|
||||
*
|
||||
* @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<string | 'New Chat'>} 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 = `<initial_message>
|
||||
${truncateText(text)}
|
||||
</initial_message>
|
||||
<response>
|
||||
${JSON.stringify(truncateText(responseText))}
|
||||
</response>`;
|
||||
|
||||
const { ANTHROPIC_TITLE_MODEL } = process.env ?? {};
|
||||
const model = this.options.titleModel ?? ANTHROPIC_TITLE_MODEL ?? 'claude-3-haiku-20240307';
|
||||
const system = titleFunctionPrompt;
|
||||
|
||||
const titleChatCompletion = async () => {
|
||||
const content = `<conversation_context>
|
||||
${convo}
|
||||
</conversation_context>
|
||||
|
||||
Please generate a title for this conversation.`;
|
||||
|
||||
const titleMessage = { role: 'user', content };
|
||||
const requestOptions = {
|
||||
model,
|
||||
temperature: 0.3,
|
||||
max_tokens: 1024,
|
||||
system,
|
||||
stop_sequences: ['\n\nHuman:', '\n\nAssistant', '</function_calls>'],
|
||||
messages: [titleMessage],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.createResponse(this.getClient(), requestOptions, true);
|
||||
let promptTokens = response?.usage?.input_tokens;
|
||||
let completionTokens = response?.usage?.output_tokens;
|
||||
if (!promptTokens) {
|
||||
promptTokens = this.getTokenCountForMessage(titleMessage);
|
||||
promptTokens += this.getTokenCountForMessage({ role: 'system', content: system });
|
||||
}
|
||||
if (!completionTokens) {
|
||||
completionTokens = this.getTokenCountForMessage(response.content[0]);
|
||||
}
|
||||
await this.recordTokenUsage({
|
||||
model,
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
context: 'title',
|
||||
});
|
||||
const text = response.content[0].text;
|
||||
title = parseParamFromPrompt(text, 'title');
|
||||
} catch (e) {
|
||||
logger.error('[AnthropicClient] There was an issue generating the title', e);
|
||||
}
|
||||
};
|
||||
|
||||
await titleChatCompletion();
|
||||
logger.debug('[AnthropicClient] Convo Title: ' + title);
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnthropicClient;
|
||||
|
||||
@@ -3,6 +3,7 @@ const { supportsBalanceCheck, Constants } = require('librechat-data-provider');
|
||||
const { getConvo, getMessages, saveMessage, updateMessage, saveConvo } = require('~/models');
|
||||
const { addSpaceIfNeeded, isEnabled } = require('~/server/utils');
|
||||
const checkBalance = require('~/models/checkBalance');
|
||||
const { getFiles } = require('~/models/File');
|
||||
const TextStream = require('./TextStream');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
@@ -22,7 +23,7 @@ class BaseClient {
|
||||
throw new Error('Method \'setOptions\' must be implemented.');
|
||||
}
|
||||
|
||||
getCompletion() {
|
||||
async getCompletion() {
|
||||
throw new Error('Method \'getCompletion\' must be implemented.');
|
||||
}
|
||||
|
||||
@@ -46,10 +47,6 @@ class BaseClient {
|
||||
logger.debug('`[BaseClient] recordTokenUsage` not implemented.', response);
|
||||
}
|
||||
|
||||
async addPreviousAttachments(messages) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
async recordTokenUsage({ promptTokens, completionTokens }) {
|
||||
logger.debug('`[BaseClient] recordTokenUsage` not implemented.', {
|
||||
promptTokens,
|
||||
@@ -447,6 +444,8 @@ class BaseClient {
|
||||
}
|
||||
|
||||
const completion = await this.sendCompletion(payload, opts);
|
||||
this.abortController.requestCompleted = true;
|
||||
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
@@ -457,6 +456,9 @@ class BaseClient {
|
||||
sender: this.sender,
|
||||
text: addSpaceIfNeeded(generation) + completion,
|
||||
promptTokens,
|
||||
iconURL: this.options.iconURL,
|
||||
endpoint: this.options.endpoint,
|
||||
...(this.metadata ?? {}),
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -525,8 +527,19 @@ class BaseClient {
|
||||
return _messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a message to the database.
|
||||
* @param {TMessage} message
|
||||
* @param {Partial<TConversation>} 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,
|
||||
@@ -681,6 +694,54 @@ class BaseClient {
|
||||
|
||||
return await this.sendCompletion(payload, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TMessage[]} _messages
|
||||
* @returns {Promise<TMessage[]>}
|
||||
*/
|
||||
async addPreviousAttachments(_messages) {
|
||||
if (!this.options.resendFiles) {
|
||||
return _messages;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TMessage} message
|
||||
*/
|
||||
const processMessage = async (message) => {
|
||||
if (!this.message_file_map) {
|
||||
/** @type {Record<string, MongoFile[]> */
|
||||
this.message_file_map = {};
|
||||
}
|
||||
|
||||
const fileIds = message.files.map((file) => file.file_id);
|
||||
const files = await getFiles({
|
||||
file_id: { $in: fileIds },
|
||||
});
|
||||
|
||||
await this.addImageURLs(message, files);
|
||||
|
||||
this.message_file_map[message.messageId] = files;
|
||||
return message;
|
||||
};
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const message of _messages) {
|
||||
if (!message.files) {
|
||||
promises.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
promises.push(processMessage(message));
|
||||
}
|
||||
|
||||
const messages = await Promise.all(promises);
|
||||
|
||||
this.checkVisionRequest(Object.values(this.message_file_map ?? {}).flat());
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseClient;
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
const crypto = require('crypto');
|
||||
const Keyv = require('keyv');
|
||||
const crypto = require('crypto');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
resolveHeaders,
|
||||
CohereConstants,
|
||||
mapModelToAzureConfig,
|
||||
} = require('librechat-data-provider');
|
||||
const { CohereClient } = require('cohere-ai');
|
||||
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
|
||||
const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source');
|
||||
const { createCoherePayload } = require('./llm');
|
||||
const { Agent, ProxyAgent } = require('undici');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const { logger } = require('~/config');
|
||||
const { extractBaseURL, constructAzureURL, genAzureChatCompletion } = require('~/utils');
|
||||
|
||||
const CHATGPT_MODEL = 'gpt-3.5-turbo';
|
||||
const tokenizersCache = {};
|
||||
@@ -140,11 +150,13 @@ class ChatGPTClient extends BaseClient {
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
async getCompletion(input, onProgress, abortController = null) {
|
||||
/** @type {getCompletion} */
|
||||
async getCompletion(input, onProgress, onTokenProgress, abortController = null) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
const modelOptions = { ...this.modelOptions };
|
||||
|
||||
let modelOptions = { ...this.modelOptions };
|
||||
if (typeof onProgress === 'function') {
|
||||
modelOptions.stream = true;
|
||||
}
|
||||
@@ -159,56 +171,176 @@ class ChatGPTClient extends BaseClient {
|
||||
}
|
||||
|
||||
const { debug } = this.options;
|
||||
const url = this.completionsUrl;
|
||||
let baseURL = this.completionsUrl;
|
||||
if (debug) {
|
||||
console.debug();
|
||||
console.debug(url);
|
||||
console.debug(baseURL);
|
||||
console.debug(modelOptions);
|
||||
console.debug();
|
||||
}
|
||||
|
||||
if (this.azure || this.options.azure) {
|
||||
// Azure does not accept `model` in the body, so we need to remove it.
|
||||
delete modelOptions.model;
|
||||
}
|
||||
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(modelOptions),
|
||||
dispatcher: new Agent({
|
||||
bodyTimeout: 0,
|
||||
headersTimeout: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
if (this.apiKey && this.options.azure) {
|
||||
opts.headers['api-key'] = this.apiKey;
|
||||
if (this.isVisionModel) {
|
||||
modelOptions.max_tokens = 4000;
|
||||
}
|
||||
|
||||
/** @type {TAzureConfig | undefined} */
|
||||
const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
|
||||
|
||||
const isAzure = this.azure || this.options.azure;
|
||||
if (
|
||||
(isAzure && this.isVisionModel && azureConfig) ||
|
||||
(azureConfig && this.isVisionModel && this.options.endpoint === EModelEndpoint.azureOpenAI)
|
||||
) {
|
||||
const { modelGroupMap, groupMap } = azureConfig;
|
||||
const {
|
||||
azureOptions,
|
||||
baseURL,
|
||||
headers = {},
|
||||
serverless,
|
||||
} = mapModelToAzureConfig({
|
||||
modelName: modelOptions.model,
|
||||
modelGroupMap,
|
||||
groupMap,
|
||||
});
|
||||
opts.headers = resolveHeaders(headers);
|
||||
this.langchainProxy = extractBaseURL(baseURL);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
||||
const groupName = modelGroupMap[modelOptions.model].group;
|
||||
this.options.addParams = azureConfig.groupMap[groupName].addParams;
|
||||
this.options.dropParams = azureConfig.groupMap[groupName].dropParams;
|
||||
// Note: `forcePrompt` not re-assigned as only chat models are vision models
|
||||
|
||||
this.azure = !serverless && azureOptions;
|
||||
this.azureEndpoint =
|
||||
!serverless && genAzureChatCompletion(this.azure, modelOptions.model, this);
|
||||
}
|
||||
|
||||
if (this.options.headers) {
|
||||
opts.headers = { ...opts.headers, ...this.options.headers };
|
||||
}
|
||||
|
||||
if (isAzure) {
|
||||
// Azure does not accept `model` in the body, so we need to remove it.
|
||||
delete modelOptions.model;
|
||||
|
||||
baseURL = this.langchainProxy
|
||||
? constructAzureURL({
|
||||
baseURL: this.langchainProxy,
|
||||
azureOptions: this.azure,
|
||||
})
|
||||
: this.azureEndpoint.split(/(?<!\/)\/(chat|completion)\//)[0];
|
||||
|
||||
if (this.options.forcePrompt) {
|
||||
baseURL += '/completions';
|
||||
} else {
|
||||
baseURL += '/chat/completions';
|
||||
}
|
||||
|
||||
opts.defaultQuery = { 'api-version': this.azure.azureOpenAIApiVersion };
|
||||
opts.headers = { ...opts.headers, 'api-key': this.apiKey };
|
||||
} else if (this.apiKey) {
|
||||
opts.headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
if (process.env.OPENAI_ORGANIZATION) {
|
||||
opts.headers['OpenAI-Organization'] = process.env.OPENAI_ORGANIZATION;
|
||||
}
|
||||
|
||||
if (this.useOpenRouter) {
|
||||
opts.headers['HTTP-Referer'] = 'https://librechat.ai';
|
||||
opts.headers['X-Title'] = 'LibreChat';
|
||||
}
|
||||
|
||||
if (this.options.headers) {
|
||||
opts.headers = { ...opts.headers, ...this.options.headers };
|
||||
}
|
||||
|
||||
if (this.options.proxy) {
|
||||
opts.dispatcher = new ProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
||||
/* 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 (baseURL.includes('https://api.mistral.ai/v1') && modelOptions.messages) {
|
||||
const { messages } = modelOptions;
|
||||
|
||||
const systemMessageIndex = messages.findIndex((msg) => msg.role === 'system');
|
||||
|
||||
if (systemMessageIndex > 0) {
|
||||
const [systemMessage] = messages.splice(systemMessageIndex, 1);
|
||||
messages.unshift(systemMessage);
|
||||
}
|
||||
|
||||
modelOptions.messages = messages;
|
||||
|
||||
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,
|
||||
};
|
||||
logger.debug('[ChatGPTClient] chatCompletion: added params', {
|
||||
addParams: this.options.addParams,
|
||||
modelOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.dropParams && Array.isArray(this.options.dropParams)) {
|
||||
this.options.dropParams.forEach((param) => {
|
||||
delete modelOptions[param];
|
||||
});
|
||||
logger.debug('[ChatGPTClient] chatCompletion: dropped params', {
|
||||
dropParams: this.options.dropParams,
|
||||
modelOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (baseURL.startsWith(CohereConstants.API_URL)) {
|
||||
const payload = createCoherePayload({ modelOptions });
|
||||
return await this.cohereChatCompletion({ payload, onTokenProgress });
|
||||
}
|
||||
|
||||
if (baseURL.includes('v1') && !baseURL.includes('/completions') && !this.isChatCompletion) {
|
||||
baseURL = baseURL.split('v1')[0] + 'v1/completions';
|
||||
} else if (
|
||||
baseURL.includes('v1') &&
|
||||
!baseURL.includes('/chat/completions') &&
|
||||
this.isChatCompletion
|
||||
) {
|
||||
baseURL = baseURL.split('v1')[0] + 'v1/chat/completions';
|
||||
}
|
||||
|
||||
const BASE_URL = new URL(baseURL);
|
||||
if (opts.defaultQuery) {
|
||||
Object.entries(opts.defaultQuery).forEach(([key, value]) => {
|
||||
BASE_URL.searchParams.append(key, value);
|
||||
});
|
||||
delete opts.defaultQuery;
|
||||
}
|
||||
|
||||
const completionsURL = BASE_URL.toString();
|
||||
opts.body = JSON.stringify(modelOptions);
|
||||
|
||||
if (modelOptions.stream) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let done = false;
|
||||
await fetchEventSource(url, {
|
||||
await fetchEventSource(completionsURL, {
|
||||
...opts,
|
||||
signal: abortController.signal,
|
||||
async onopen(response) {
|
||||
@@ -236,7 +368,6 @@ class ChatGPTClient extends BaseClient {
|
||||
// workaround for private API not sending [DONE] event
|
||||
if (!done) {
|
||||
onProgress('[DONE]');
|
||||
abortController.abort();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
@@ -249,14 +380,13 @@ class ChatGPTClient extends BaseClient {
|
||||
},
|
||||
onmessage(message) {
|
||||
if (debug) {
|
||||
// console.debug(message);
|
||||
console.debug(message);
|
||||
}
|
||||
if (!message.data || message.event === 'ping') {
|
||||
return;
|
||||
}
|
||||
if (message.data === '[DONE]') {
|
||||
onProgress('[DONE]');
|
||||
abortController.abort();
|
||||
resolve();
|
||||
done = true;
|
||||
return;
|
||||
@@ -269,7 +399,7 @@ class ChatGPTClient extends BaseClient {
|
||||
}
|
||||
});
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
const response = await fetch(completionsURL, {
|
||||
...opts,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
@@ -287,6 +417,35 @@ class ChatGPTClient extends BaseClient {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/** @type {cohereChatCompletion} */
|
||||
async cohereChatCompletion({ payload, onTokenProgress }) {
|
||||
const cohere = new CohereClient({
|
||||
token: this.apiKey,
|
||||
environment: this.completionsUrl,
|
||||
});
|
||||
|
||||
if (!payload.stream) {
|
||||
const chatResponse = await cohere.chat(payload);
|
||||
return chatResponse.text;
|
||||
}
|
||||
|
||||
const chatStream = await cohere.chatStream(payload);
|
||||
let reply = '';
|
||||
for await (const message of chatStream) {
|
||||
if (!message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.eventType === 'text-generation' && message.text) {
|
||||
onTokenProgress(message.text);
|
||||
} else if (message.eventType === 'stream-end' && message.response) {
|
||||
reply = message.response.text;
|
||||
}
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
async generateTitle(userMessage, botMessage) {
|
||||
const instructionsPayload = {
|
||||
role: 'system',
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
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 { encodeAndFormat } = require('~/server/services/Files/images');
|
||||
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
|
||||
const {
|
||||
validateVisionModel,
|
||||
getResponseSender,
|
||||
endpointSettings,
|
||||
EModelEndpoint,
|
||||
VisionModes,
|
||||
AuthKeys,
|
||||
} = require('librechat-data-provider');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images');
|
||||
const { formatMessage, createContextHandlers } = require('./prompts');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { formatMessage } = require('./prompts');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
@@ -124,25 +127,21 @@ class GoogleClient extends BaseClient {
|
||||
// stop: modelOptions.stop // no stop method for now
|
||||
};
|
||||
|
||||
if (this.options.attachments) {
|
||||
this.modelOptions.model = 'gemini-pro-vision';
|
||||
}
|
||||
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');
|
||||
this.isVisionModel = validateVisionModel(this.modelOptions.model);
|
||||
const { isGenerativeModel } = this;
|
||||
if (this.isVisionModel && !this.options.attachments) {
|
||||
this.modelOptions.model = 'gemini-pro';
|
||||
this.isVisionModel = false;
|
||||
}
|
||||
this.isChatModel = !isGenerativeModel && this.modelOptions.model.includes('chat');
|
||||
const { isChatModel } = this;
|
||||
this.isTextModel =
|
||||
!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;
|
||||
@@ -220,6 +219,33 @@ class GoogleClient extends BaseClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks if the model is a vision model based on request attachments and sets the appropriate options:
|
||||
* @param {MongoFile[]} attachments
|
||||
*/
|
||||
checkVisionRequest(attachments) {
|
||||
/* Validation vision request */
|
||||
this.defaultVisionModel = this.options.visionModel ?? 'gemini-pro-vision';
|
||||
const availableModels = this.options.modelsConfig?.[EModelEndpoint.google];
|
||||
this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
||||
|
||||
if (
|
||||
attachments &&
|
||||
attachments.some((file) => file?.type && file?.type?.includes('image')) &&
|
||||
availableModels?.includes(this.defaultVisionModel) &&
|
||||
!this.isVisionModel
|
||||
) {
|
||||
this.modelOptions.model = this.defaultVisionModel;
|
||||
this.isVisionModel = true;
|
||||
}
|
||||
|
||||
if (this.isVisionModel && !attachments && this.modelOptions.model.includes('gemini-pro')) {
|
||||
this.modelOptions.model = 'gemini-pro';
|
||||
this.isVisionModel = false;
|
||||
}
|
||||
}
|
||||
|
||||
formatMessages() {
|
||||
return ((message) => ({
|
||||
author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel),
|
||||
@@ -227,18 +253,91 @@ class GoogleClient extends BaseClient {
|
||||
})).bind(this);
|
||||
}
|
||||
|
||||
async buildVisionMessages(messages = [], parentMessageId) {
|
||||
const { prompt } = await this.buildMessagesPrompt(messages, parentMessageId);
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {TMessage[]} messages
|
||||
* @param {MongoFile[]} files
|
||||
* @returns {Promise<MongoFile[]>}
|
||||
*/
|
||||
async addImageURLs(message, attachments, mode = '') {
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
this.options.req,
|
||||
attachments.filter((file) => file.type.includes('image')),
|
||||
attachments,
|
||||
EModelEndpoint.google,
|
||||
mode,
|
||||
);
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (this.contextHandlers) {
|
||||
for (const file of attachments) {
|
||||
if (file.embedded) {
|
||||
this.contextHandlers?.processFile(file);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const files = await this.addImageURLs(latestMessage, attachments);
|
||||
|
||||
latestMessage.image_urls = image_urls;
|
||||
this.options.attachments = files;
|
||||
|
||||
latestMessage.text = prompt;
|
||||
@@ -254,18 +353,29 @@ 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.options.attachments) {
|
||||
if (!this.project_id && this.modelOptions.model.includes('1.5')) {
|
||||
return await this.buildGenerativeMessages(messages);
|
||||
}
|
||||
|
||||
if (this.options.attachments && this.isGenerativeModel) {
|
||||
return this.buildVisionMessages(messages, parentMessageId);
|
||||
}
|
||||
|
||||
@@ -479,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 = {}) {
|
||||
@@ -497,7 +618,7 @@ class GoogleClient extends BaseClient {
|
||||
|
||||
let clientOptions = { ...parameters, maxRetries: 2 };
|
||||
|
||||
if (!this.isGenerativeModel) {
|
||||
if (this.project_id) {
|
||||
clientOptions['authOptions'] = {
|
||||
credentials: {
|
||||
...this.serviceKey,
|
||||
@@ -510,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;
|
||||
}
|
||||
@@ -541,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;
|
||||
@@ -560,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,
|
||||
};
|
||||
}
|
||||
@@ -569,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();
|
||||
|
||||
154
api/app/clients/OllamaClient.js
Normal file
154
api/app/clients/OllamaClient.js
Normal file
@@ -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<string[]>} The Ollama models.
|
||||
*/
|
||||
static async fetchModels(baseURL) {
|
||||
let models = [];
|
||||
if (!baseURL) {
|
||||
return models;
|
||||
}
|
||||
try {
|
||||
const ollamaEndpoint = deriveBaseURL(baseURL);
|
||||
/** @type {Promise<AxiosResponse<OllamaListResponse>>} */
|
||||
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 };
|
||||
@@ -1,10 +1,16 @@
|
||||
const OpenAI = require('openai');
|
||||
const { OllamaClient } = require('./OllamaClient');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const {
|
||||
Constants,
|
||||
ImageDetail,
|
||||
EModelEndpoint,
|
||||
resolveHeaders,
|
||||
ImageDetailCost,
|
||||
CohereConstants,
|
||||
getResponseSender,
|
||||
validateVisionModel,
|
||||
ImageDetailCost,
|
||||
ImageDetail,
|
||||
mapModelToAzureConfig,
|
||||
} = require('librechat-data-provider');
|
||||
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
|
||||
const {
|
||||
@@ -13,14 +19,19 @@ const {
|
||||
getModelMaxTokens,
|
||||
genAzureChatCompletion,
|
||||
} = require('~/utils');
|
||||
const {
|
||||
truncateText,
|
||||
formatMessage,
|
||||
CUT_OFF_PROMPT,
|
||||
titleInstruction,
|
||||
createContextHandlers,
|
||||
} = require('./prompts');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const { truncateText, formatMessage, CUT_OFF_PROMPT } = require('./prompts');
|
||||
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 { getFiles } = require('~/models/File');
|
||||
const { summaryBuffer } = require('./memory');
|
||||
const { runTitleChain } = require('./chains');
|
||||
const { tokenSplit } = require('./document');
|
||||
@@ -37,7 +48,10 @@ class OpenAIClient extends BaseClient {
|
||||
super(apiKey, options);
|
||||
this.ChatGPTClient = new ChatGPTClient();
|
||||
this.buildPrompt = this.ChatGPTClient.buildPrompt.bind(this);
|
||||
/** @type {getCompletion} */
|
||||
this.getCompletion = this.ChatGPTClient.getCompletion.bind(this);
|
||||
/** @type {cohereChatCompletion} */
|
||||
this.cohereChatCompletion = this.ChatGPTClient.cohereChatCompletion.bind(this);
|
||||
this.contextStrategy = options.contextStrategy
|
||||
? options.contextStrategy.toLowerCase()
|
||||
: 'discard';
|
||||
@@ -45,6 +59,10 @@ class OpenAIClient extends BaseClient {
|
||||
/** @type {AzureOptions} */
|
||||
this.azure = options.azure || false;
|
||||
this.setOptions(options);
|
||||
this.metadata = {};
|
||||
|
||||
/** @type {string | undefined} - The API Completions URL */
|
||||
this.completionsUrl;
|
||||
}
|
||||
|
||||
// TODO: PluginsClient calls this 3x, unneeded
|
||||
@@ -88,7 +106,12 @@ class OpenAIClient extends BaseClient {
|
||||
};
|
||||
}
|
||||
|
||||
this.checkVisionRequest(this.options.attachments);
|
||||
this.defaultVisionModel = this.options.visionModel ?? 'gpt-4-vision-preview';
|
||||
if (typeof this.options.attachments?.then === 'function') {
|
||||
this.options.attachments.then((attachments) => this.checkVisionRequest(attachments));
|
||||
} else {
|
||||
this.checkVisionRequest(this.options.attachments);
|
||||
}
|
||||
|
||||
const { OPENROUTER_API_KEY, OPENAI_FORCE_PROMPT } = process.env ?? {};
|
||||
if (OPENROUTER_API_KEY && !this.azure) {
|
||||
@@ -106,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'));
|
||||
@@ -138,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);
|
||||
@@ -179,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);
|
||||
@@ -219,19 +238,55 @@ class OpenAIClient extends BaseClient {
|
||||
* - Sets `this.modelOptions.model` to `gpt-4-vision-preview` if the request is a vision request.
|
||||
* - Sets `this.isVisionModel` to `true` if vision request.
|
||||
* - Deletes `this.modelOptions.stop` if vision request.
|
||||
* @param {Array<Promise<MongoFile[]> | MongoFile[]> | Record<string, MongoFile[]>} attachments
|
||||
* @param {MongoFile[]} attachments
|
||||
*/
|
||||
checkVisionRequest(attachments) {
|
||||
this.isVisionModel = validateVisionModel(this.modelOptions.model);
|
||||
|
||||
if (attachments && !this.isVisionModel) {
|
||||
this.modelOptions.model = 'gpt-4-vision-preview';
|
||||
this.isVisionModel = true;
|
||||
if (!attachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableModels = this.options.modelsConfig?.[this.options.endpoint];
|
||||
if (!availableModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -253,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 = {
|
||||
@@ -358,10 +413,14 @@ class OpenAIClient extends BaseClient {
|
||||
|
||||
getSaveOptions() {
|
||||
return {
|
||||
maxContextTokens: this.options.maxContextTokens,
|
||||
chatGptLabel: this.options.chatGptLabel,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
resendImages: this.options.resendImages,
|
||||
resendFiles: this.options.resendFiles,
|
||||
imageDetail: this.options.imageDetail,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
@@ -374,54 +433,6 @@ class OpenAIClient extends BaseClient {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TMessage[]} _messages
|
||||
* @returns {TMessage[]}
|
||||
*/
|
||||
async addPreviousAttachments(_messages) {
|
||||
if (!this.options.resendImages) {
|
||||
return _messages;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TMessage} message
|
||||
*/
|
||||
const processMessage = async (message) => {
|
||||
if (!this.message_file_map) {
|
||||
/** @type {Record<string, MongoFile[]> */
|
||||
this.message_file_map = {};
|
||||
}
|
||||
|
||||
const fileIds = message.files.map((file) => file.file_id);
|
||||
const files = await getFiles({
|
||||
file_id: { $in: fileIds },
|
||||
});
|
||||
|
||||
await this.addImageURLs(message, files);
|
||||
|
||||
this.message_file_map[message.messageId] = files;
|
||||
return message;
|
||||
};
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const message of _messages) {
|
||||
if (!message.files) {
|
||||
promises.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
promises.push(processMessage(message));
|
||||
}
|
||||
|
||||
const messages = await Promise.all(promises);
|
||||
|
||||
this.checkVisionRequest(this.message_file_map);
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds image URLs to the message object and returns the files
|
||||
@@ -431,9 +442,12 @@ class OpenAIClient extends BaseClient {
|
||||
* @returns {Promise<MongoFile[]>}
|
||||
*/
|
||||
async addImageURLs(message, attachments) {
|
||||
const { files, image_urls } = await encodeAndFormat(this.options.req, attachments);
|
||||
|
||||
message.image_urls = image_urls;
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
this.options.req,
|
||||
attachments,
|
||||
this.options.endpoint,
|
||||
);
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -461,23 +475,9 @@ class OpenAIClient extends BaseClient {
|
||||
let promptTokens;
|
||||
|
||||
promptPrefix = (promptPrefix || this.options.promptPrefix || '').trim();
|
||||
if (promptPrefix) {
|
||||
promptPrefix = `Instructions:\n${promptPrefix}`;
|
||||
instructions = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: promptPrefix,
|
||||
};
|
||||
|
||||
if (this.contextStrategy) {
|
||||
instructions.tokenCount = this.getTokenCountForMessage(instructions);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.attachments) {
|
||||
const attachments = (await this.options.attachments).filter((file) =>
|
||||
file.type.includes('image'),
|
||||
);
|
||||
const attachments = await this.options.attachments;
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.message_file_map[orderedMessages[orderedMessages.length - 1].messageId] = attachments;
|
||||
@@ -495,6 +495,13 @@ class OpenAIClient extends BaseClient {
|
||||
this.options.attachments = files;
|
||||
}
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.contextHandlers = createContextHandlers(
|
||||
this.options.req,
|
||||
orderedMessages[orderedMessages.length - 1].text,
|
||||
);
|
||||
}
|
||||
|
||||
const formattedMessages = orderedMessages.map((message, i) => {
|
||||
const formattedMessage = formatMessage({
|
||||
message,
|
||||
@@ -513,6 +520,11 @@ class OpenAIClient extends BaseClient {
|
||||
if (this.message_file_map && this.message_file_map[message.messageId]) {
|
||||
const attachments = this.message_file_map[message.messageId];
|
||||
for (const file of attachments) {
|
||||
if (file.embedded) {
|
||||
this.contextHandlers?.processFile(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
orderedMessages[i].tokenCount += this.calculateImageTokenCost({
|
||||
width: file.width,
|
||||
height: file.height,
|
||||
@@ -524,6 +536,24 @@ class OpenAIClient extends BaseClient {
|
||||
return formattedMessage;
|
||||
});
|
||||
|
||||
if (this.contextHandlers) {
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
promptPrefix = this.augmentedPrompt + promptPrefix;
|
||||
}
|
||||
|
||||
if (promptPrefix) {
|
||||
promptPrefix = `Instructions:\n${promptPrefix.trim()}`;
|
||||
instructions = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: promptPrefix,
|
||||
};
|
||||
|
||||
if (this.contextStrategy) {
|
||||
instructions.tokenCount = this.getTokenCountForMessage(instructions);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: need to handle interleaving instructions better
|
||||
if (this.contextStrategy) {
|
||||
({ payload, tokenCountMap, promptTokens, messages } = await this.handleContextStrategy({
|
||||
@@ -551,15 +581,16 @@ class OpenAIClient extends BaseClient {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @type {sendCompletion} */
|
||||
async sendCompletion(payload, opts = {}) {
|
||||
let reply = '';
|
||||
let result = null;
|
||||
let streamResult = null;
|
||||
this.modelOptions.user = this.user;
|
||||
const invalidBaseUrl = this.completionsUrl && extractBaseURL(this.completionsUrl) === null;
|
||||
const useOldMethod = !!(invalidBaseUrl || !this.isChatCompletion);
|
||||
const useOldMethod = !!(invalidBaseUrl || !this.isChatCompletion || typeof Bun !== 'undefined');
|
||||
if (typeof opts.onProgress === 'function' && useOldMethod) {
|
||||
await this.getCompletion(
|
||||
const completionResult = await this.getCompletion(
|
||||
payload,
|
||||
(progressMessage) => {
|
||||
if (progressMessage === '[DONE]') {
|
||||
@@ -592,12 +623,16 @@ class OpenAIClient extends BaseClient {
|
||||
opts.onProgress(token);
|
||||
reply += token;
|
||||
},
|
||||
opts.onProgress,
|
||||
opts.abortController || new AbortController(),
|
||||
);
|
||||
|
||||
if (completionResult && typeof completionResult === 'string') {
|
||||
reply = completionResult;
|
||||
}
|
||||
} else if (typeof opts.onProgress === 'function' || this.options.useChatCompletion) {
|
||||
reply = await this.chatCompletion({
|
||||
payload,
|
||||
clientOptions: opts,
|
||||
onProgress: opts.onProgress,
|
||||
abortController: opts.abortController,
|
||||
});
|
||||
@@ -605,9 +640,14 @@ class OpenAIClient extends BaseClient {
|
||||
result = await this.getCompletion(
|
||||
payload,
|
||||
null,
|
||||
opts.onProgress,
|
||||
opts.abortController || new AbortController(),
|
||||
);
|
||||
|
||||
if (result && typeof result === 'string') {
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
logger.debug('[OpenAIClient] sendCompletion: result', result);
|
||||
|
||||
if (this.isChatCompletion) {
|
||||
@@ -617,11 +657,11 @@ class OpenAIClient extends BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
if (streamResult && typeof opts.addMetadata === 'function') {
|
||||
if (streamResult) {
|
||||
const { finish_reason } = streamResult.choices[0];
|
||||
opts.addMetadata({ finish_reason });
|
||||
this.metadata = { finish_reason };
|
||||
}
|
||||
return reply.trim();
|
||||
return (reply ?? '').trim();
|
||||
}
|
||||
|
||||
initializeLLM({
|
||||
@@ -665,6 +705,16 @@ class OpenAIClient extends BaseClient {
|
||||
};
|
||||
}
|
||||
|
||||
const { headers } = this.options;
|
||||
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
||||
configOptions.baseOptions = {
|
||||
headers: resolveHeaders({
|
||||
...headers,
|
||||
...configOptions?.baseOptions?.headers,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.options.proxy) {
|
||||
configOptions.httpAgent = new HttpsProxyAgent(this.options.proxy);
|
||||
configOptions.httpsAgent = new HttpsProxyAgent(this.options.proxy);
|
||||
@@ -706,6 +756,12 @@ class OpenAIClient extends BaseClient {
|
||||
* In case of failure, it will return the default title, "New Chat".
|
||||
*/
|
||||
async titleConvo({ text, conversationId, responseText = '' }) {
|
||||
this.conversationId = conversationId;
|
||||
|
||||
if (this.options.attachments) {
|
||||
delete this.options.attachments;
|
||||
}
|
||||
|
||||
let title = 'New Chat';
|
||||
const convo = `||>User:
|
||||
"${truncateText(text)}"
|
||||
@@ -714,7 +770,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
|
||||
@@ -725,6 +784,39 @@ class OpenAIClient extends BaseClient {
|
||||
max_tokens: 16,
|
||||
};
|
||||
|
||||
/** @type {TAzureConfig | undefined} */
|
||||
const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
|
||||
|
||||
const resetTitleOptions = !!(
|
||||
(this.azure && azureConfig) ||
|
||||
(azureConfig && this.options.endpoint === EModelEndpoint.azureOpenAI)
|
||||
);
|
||||
|
||||
if (resetTitleOptions) {
|
||||
const { modelGroupMap, groupMap } = azureConfig;
|
||||
const {
|
||||
azureOptions,
|
||||
baseURL,
|
||||
headers = {},
|
||||
serverless,
|
||||
} = mapModelToAzureConfig({
|
||||
modelName: modelOptions.model,
|
||||
modelGroupMap,
|
||||
groupMap,
|
||||
});
|
||||
|
||||
this.options.headers = resolveHeaders(headers);
|
||||
this.options.reverseProxyUrl = baseURL ?? null;
|
||||
this.langchainProxy = extractBaseURL(this.options.reverseProxyUrl);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
||||
const groupName = modelGroupMap[modelOptions.model].group;
|
||||
this.options.addParams = azureConfig.groupMap[groupName].addParams;
|
||||
this.options.dropParams = azureConfig.groupMap[groupName].dropParams;
|
||||
this.options.forcePrompt = azureConfig.groupMap[groupName].forcePrompt;
|
||||
this.azure = !serverless && azureOptions;
|
||||
}
|
||||
|
||||
const titleChatCompletion = async () => {
|
||||
modelOptions.model = model;
|
||||
|
||||
@@ -736,8 +828,7 @@ class OpenAIClient extends BaseClient {
|
||||
const instructionsPayload = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `Detect user language and write in the same language an extremely concise title for this conversation, which you must accurately detect.
|
||||
Write in the detected language. Title in 5 Words or Less. No Punctuation or Quotation. Do not mention the language. All first letters of every word should be capitalized and write the title in User Language only.
|
||||
content: `Please generate ${titleInstruction}
|
||||
|
||||
${convo}
|
||||
|
||||
@@ -745,10 +836,22 @@ ${convo}
|
||||
},
|
||||
];
|
||||
|
||||
const promptTokens = this.getTokenCountForMessage(instructionsPayload[0]);
|
||||
|
||||
try {
|
||||
let useChatCompletion = true;
|
||||
|
||||
if (this.options.reverseProxyUrl === CohereConstants.API_URL) {
|
||||
useChatCompletion = false;
|
||||
}
|
||||
|
||||
title = (
|
||||
await this.sendPayload(instructionsPayload, { modelOptions, useChatCompletion: true })
|
||||
await this.sendPayload(instructionsPayload, { modelOptions, useChatCompletion })
|
||||
).replaceAll('"', '');
|
||||
|
||||
const completionTokens = this.getTokenCount(title);
|
||||
|
||||
this.recordTokenUsage({ promptTokens, completionTokens, context: 'title' });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'[OpenAIClient] There was an issue generating the title with the completion method',
|
||||
@@ -771,6 +874,7 @@ ${convo}
|
||||
context: 'title',
|
||||
tokenBuffer: 150,
|
||||
});
|
||||
|
||||
title = await runTitleChain({ llm, text, convo, signal: this.abortController.signal });
|
||||
} catch (e) {
|
||||
if (e?.message?.toLowerCase()?.includes('abort')) {
|
||||
@@ -796,7 +900,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,
|
||||
@@ -900,14 +1008,13 @@ ${convo}
|
||||
}
|
||||
}
|
||||
|
||||
async recordTokenUsage({ promptTokens, completionTokens }) {
|
||||
logger.debug('[OpenAIClient] recordTokenUsage:', { promptTokens, completionTokens });
|
||||
async recordTokenUsage({ promptTokens, completionTokens, context = 'message' }) {
|
||||
await spendTokens(
|
||||
{
|
||||
user: this.user,
|
||||
context,
|
||||
model: this.modelOptions.model,
|
||||
context: 'message',
|
||||
conversationId: this.conversationId,
|
||||
user: this.user ?? this.options.req.user?.id,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
{ promptTokens, completionTokens },
|
||||
@@ -921,7 +1028,7 @@ ${convo}
|
||||
});
|
||||
}
|
||||
|
||||
async chatCompletion({ payload, onProgress, clientOptions, abortController = null }) {
|
||||
async chatCompletion({ payload, onProgress, abortController = null }) {
|
||||
let error = null;
|
||||
const errorCallback = (err) => (error = err);
|
||||
let intermediateReply = '';
|
||||
@@ -942,15 +1049,6 @@ ${convo}
|
||||
}
|
||||
|
||||
const baseURL = extractBaseURL(this.completionsUrl);
|
||||
// let { messages: _msgsToLog, ...modelOptionsToLog } = modelOptions;
|
||||
// if (modelOptionsToLog.messages) {
|
||||
// _msgsToLog = modelOptionsToLog.messages.map((msg) => {
|
||||
// let { content, ...rest } = msg;
|
||||
|
||||
// if (content)
|
||||
// return { ...rest, content: truncateText(content) };
|
||||
// });
|
||||
// }
|
||||
logger.debug('[OpenAIClient] chatCompletion', { baseURL, modelOptions });
|
||||
const opts = {
|
||||
baseURL,
|
||||
@@ -975,6 +1073,38 @@ ${convo}
|
||||
modelOptions.max_tokens = 4000;
|
||||
}
|
||||
|
||||
/** @type {TAzureConfig | undefined} */
|
||||
const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
|
||||
|
||||
if (
|
||||
(this.azure && this.isVisionModel && azureConfig) ||
|
||||
(azureConfig && this.isVisionModel && this.options.endpoint === EModelEndpoint.azureOpenAI)
|
||||
) {
|
||||
const { modelGroupMap, groupMap } = azureConfig;
|
||||
const {
|
||||
azureOptions,
|
||||
baseURL,
|
||||
headers = {},
|
||||
serverless,
|
||||
} = mapModelToAzureConfig({
|
||||
modelName: modelOptions.model,
|
||||
modelGroupMap,
|
||||
groupMap,
|
||||
});
|
||||
opts.defaultHeaders = resolveHeaders(headers);
|
||||
this.langchainProxy = extractBaseURL(baseURL);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
||||
const groupName = modelGroupMap[modelOptions.model].group;
|
||||
this.options.addParams = azureConfig.groupMap[groupName].addParams;
|
||||
this.options.dropParams = azureConfig.groupMap[groupName].dropParams;
|
||||
// Note: `forcePrompt` not re-assigned as only chat models are vision models
|
||||
|
||||
this.azure = !serverless && azureOptions;
|
||||
this.azureEndpoint =
|
||||
!serverless && genAzureChatCompletion(this.azure, modelOptions.model, this);
|
||||
}
|
||||
|
||||
if (this.azure || this.options.azure) {
|
||||
// Azure does not accept `model` in the body, so we need to remove it.
|
||||
delete modelOptions.model;
|
||||
@@ -982,9 +1112,10 @@ ${convo}
|
||||
opts.baseURL = this.langchainProxy
|
||||
? constructAzureURL({
|
||||
baseURL: this.langchainProxy,
|
||||
azure: this.azure,
|
||||
azureOptions: this.azure,
|
||||
})
|
||||
: this.azureEndpoint.split(/\/(chat|completion)/)[0];
|
||||
: this.azureEndpoint.split(/(?<!\/)\/(chat|completion)\//)[0];
|
||||
|
||||
opts.defaultQuery = { 'api-version': this.azure.azureOpenAIApiVersion };
|
||||
opts.defaultHeaders = { ...opts.defaultHeaders, 'api-key': this.apiKey };
|
||||
}
|
||||
@@ -994,16 +1125,14 @@ ${convo}
|
||||
}
|
||||
|
||||
let chatCompletion;
|
||||
/** @type {OpenAI} */
|
||||
const openai = new OpenAI({
|
||||
apiKey: this.apiKey,
|
||||
...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');
|
||||
@@ -1014,10 +1143,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') {
|
||||
@@ -1025,12 +1160,29 @@ ${convo}
|
||||
...modelOptions,
|
||||
...this.options.addParams,
|
||||
};
|
||||
logger.debug('[OpenAIClient] chatCompletion: added params', {
|
||||
addParams: this.options.addParams,
|
||||
modelOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.dropParams && Array.isArray(this.options.dropParams)) {
|
||||
this.options.dropParams.forEach((param) => {
|
||||
delete modelOptions[param];
|
||||
});
|
||||
logger.debug('[OpenAIClient] chatCompletion: dropped params', {
|
||||
dropParams: this.options.dropParams,
|
||||
modelOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.message_file_map && this.isOllama) {
|
||||
const ollamaClient = new OllamaClient({ baseURL });
|
||||
return await ollamaClient.chatCompletion({
|
||||
payload: modelOptions,
|
||||
onProgress,
|
||||
abortController,
|
||||
});
|
||||
}
|
||||
|
||||
let UnexpectedRoleError = false;
|
||||
@@ -1046,6 +1198,16 @@ ${convo}
|
||||
.on('error', (err) => {
|
||||
handleOpenAIErrors(err, errorCallback, 'stream');
|
||||
})
|
||||
.on('finalChatCompletion', (finalChatCompletion) => {
|
||||
const finalMessage = finalChatCompletion?.choices?.[0]?.message;
|
||||
if (finalMessage && finalMessage?.role !== 'assistant') {
|
||||
finalChatCompletion.choices[0].message.role = 'assistant';
|
||||
}
|
||||
|
||||
if (finalMessage && !finalMessage?.content?.trim()) {
|
||||
finalChatCompletion.choices[0].message.content = intermediateReply;
|
||||
}
|
||||
})
|
||||
.on('finalMessage', (message) => {
|
||||
if (message?.role !== 'assistant') {
|
||||
stream.messages.push({ role: 'assistant', content: intermediateReply });
|
||||
@@ -1053,6 +1215,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;
|
||||
@@ -1061,6 +1224,10 @@ ${convo}
|
||||
stream.controller.abort();
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.azure) {
|
||||
await sleep(azureDelay);
|
||||
}
|
||||
}
|
||||
|
||||
if (!UnexpectedRoleError) {
|
||||
@@ -1091,12 +1258,20 @@ ${convo}
|
||||
}
|
||||
|
||||
const { message, finish_reason } = chatCompletion.choices[0];
|
||||
if (chatCompletion && typeof clientOptions.addMetadata === 'function') {
|
||||
clientOptions.addMetadata({ finish_reason });
|
||||
if (chatCompletion) {
|
||||
this.metadata = { finish_reason };
|
||||
}
|
||||
|
||||
logger.debug('[OpenAIClient] chatCompletion response', chatCompletion);
|
||||
|
||||
if (!message?.content?.trim() && intermediateReply.length) {
|
||||
logger.debug(
|
||||
'[OpenAIClient] chatCompletion: using intermediateReply due to empty message.content',
|
||||
{ intermediateReply },
|
||||
);
|
||||
return intermediateReply;
|
||||
}
|
||||
|
||||
return message.content;
|
||||
} catch (err) {
|
||||
if (
|
||||
@@ -1109,6 +1284,9 @@ ${convo}
|
||||
err?.message?.includes(
|
||||
'OpenAI error: Invalid final message: OpenAI expects final message to include role=assistant',
|
||||
) ||
|
||||
err?.message?.includes(
|
||||
'stream ended without producing a ChatCompletionMessage with role=assistant',
|
||||
) ||
|
||||
err?.message?.includes('The server had an error processing your request') ||
|
||||
err?.message?.includes('missing finish_reason') ||
|
||||
err?.message?.includes('missing role') ||
|
||||
|
||||
@@ -31,10 +31,6 @@ class PluginsClient extends OpenAIClient {
|
||||
|
||||
super.setOptions(options);
|
||||
|
||||
if (this.functionsAgent && this.agentOptions.model && !this.useOpenRouter && !this.azure) {
|
||||
this.agentOptions.model = this.getFunctionModelName(this.agentOptions.model);
|
||||
}
|
||||
|
||||
this.isGpt3 = this.modelOptions?.model?.includes('gpt-3');
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
@@ -46,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,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);
|
||||
@@ -248,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,
|
||||
@@ -308,6 +310,8 @@ class PluginsClient extends OpenAIClient {
|
||||
}
|
||||
|
||||
const responseMessage = {
|
||||
endpoint: EModelEndpoint.gptPlugins,
|
||||
iconURL: this.options.iconURL,
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
85
api/app/clients/llm/createCoherePayload.js
Normal file
85
api/app/clients/llm/createCoherePayload.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const { CohereConstants } = require('librechat-data-provider');
|
||||
const { titleInstruction } = require('../prompts/titlePrompts');
|
||||
|
||||
// Mapping OpenAI roles to Cohere roles
|
||||
const roleMap = {
|
||||
user: CohereConstants.ROLE_USER,
|
||||
assistant: CohereConstants.ROLE_CHATBOT,
|
||||
system: CohereConstants.ROLE_SYSTEM, // Recognize and map the system role explicitly
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts an OpenAI ChatCompletionPayload to conform with Cohere's expected chat payload format.
|
||||
* Now includes handling for "system" roles explicitly mentioned.
|
||||
*
|
||||
* @param {Object} options - Object containing the model options.
|
||||
* @param {ChatCompletionPayload} options.modelOptions - The OpenAI model payload options.
|
||||
* @returns {CohereChatStreamRequest} Cohere-compatible chat API payload.
|
||||
*/
|
||||
function createCoherePayload({ modelOptions }) {
|
||||
/** @type {string | undefined} */
|
||||
let preamble;
|
||||
let latestUserMessageContent = '';
|
||||
const {
|
||||
stream,
|
||||
stop,
|
||||
top_p,
|
||||
temperature,
|
||||
frequency_penalty,
|
||||
presence_penalty,
|
||||
max_tokens,
|
||||
messages,
|
||||
model,
|
||||
...rest
|
||||
} = modelOptions;
|
||||
|
||||
// Filter out the latest user message and transform remaining messages to Cohere's chat_history format
|
||||
let chatHistory = messages.reduce((acc, message, index, arr) => {
|
||||
const isLastUserMessage = index === arr.length - 1 && message.role === 'user';
|
||||
|
||||
const messageContent =
|
||||
typeof message.content === 'string'
|
||||
? message.content
|
||||
: message.content.map((part) => (part.type === 'text' ? part.text : '')).join(' ');
|
||||
|
||||
if (isLastUserMessage) {
|
||||
latestUserMessageContent = messageContent;
|
||||
} else {
|
||||
acc.push({
|
||||
role: roleMap[message.role] || CohereConstants.ROLE_USER,
|
||||
message: messageContent,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (
|
||||
chatHistory.length === 1 &&
|
||||
chatHistory[0].role === CohereConstants.ROLE_SYSTEM &&
|
||||
!latestUserMessageContent.length
|
||||
) {
|
||||
const message = chatHistory[0].message;
|
||||
latestUserMessageContent = message.includes(titleInstruction)
|
||||
? CohereConstants.TITLE_MESSAGE
|
||||
: '.';
|
||||
preamble = message;
|
||||
}
|
||||
|
||||
return {
|
||||
message: latestUserMessageContent,
|
||||
model: model,
|
||||
chatHistory,
|
||||
stream: stream ?? false,
|
||||
temperature: temperature,
|
||||
frequencyPenalty: frequency_penalty,
|
||||
presencePenalty: presence_penalty,
|
||||
maxTokens: max_tokens,
|
||||
stopSequences: stop,
|
||||
preamble,
|
||||
p: top_p,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createCoherePayload;
|
||||
@@ -55,16 +55,18 @@ function createLLM({
|
||||
}
|
||||
|
||||
if (azure && configOptions.basePath) {
|
||||
configOptions.basePath = constructAzureURL({
|
||||
const azureURL = constructAzureURL({
|
||||
baseURL: configOptions.basePath,
|
||||
azure: azureOptions,
|
||||
azureOptions,
|
||||
});
|
||||
azureOptions.azureOpenAIBasePath = azureURL.split(
|
||||
`/${azureOptions.azureOpenAIApiDeploymentName}`,
|
||||
)[0];
|
||||
}
|
||||
|
||||
return new ChatOpenAI(
|
||||
{
|
||||
streaming,
|
||||
verbose: true,
|
||||
credentials,
|
||||
configuration,
|
||||
...azureOptions,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const createLLM = require('./createLLM');
|
||||
const RunManager = require('./RunManager');
|
||||
const createCoherePayload = require('./createCoherePayload');
|
||||
|
||||
module.exports = {
|
||||
createLLM,
|
||||
RunManager,
|
||||
createCoherePayload,
|
||||
};
|
||||
|
||||
159
api/app/clients/prompts/createContextHandlers.js
Normal file
159
api/app/clients/prompts/createContextHandlers.js
Normal file
@@ -0,0 +1,159 @@
|
||||
const axios = require('axios');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const footer = `Use the context as your learned knowledge to better answer the user.
|
||||
|
||||
In your response, remember to follow these guidelines:
|
||||
- If you don't know the answer, simply say that you don't know.
|
||||
- If you are unsure how to answer, ask for clarification.
|
||||
- Avoid mentioning that you obtained the information from the context.
|
||||
|
||||
Answer appropriately in the user's language.
|
||||
`;
|
||||
|
||||
function createContextHandlers(req, userMessageContent) {
|
||||
if (!process.env.RAG_API_URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryPromises = [];
|
||||
const processedFiles = [];
|
||||
const processedIds = new Set();
|
||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
||||
const useFullContext = isEnabled(process.env.RAG_USE_FULL_CONTEXT);
|
||||
|
||||
const query = async (file) => {
|
||||
if (useFullContext) {
|
||||
return axios.get(`${process.env.RAG_API_URL}/documents/${file.file_id}/context`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return axios.post(
|
||||
`${process.env.RAG_API_URL}/query`,
|
||||
{
|
||||
file_id: file.file_id,
|
||||
query: userMessageContent,
|
||||
k: 4,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const processFile = async (file) => {
|
||||
if (file.embedded && !processedIds.has(file.file_id)) {
|
||||
try {
|
||||
const promise = query(file);
|
||||
queryPromises.push(promise);
|
||||
processedFiles.push(file);
|
||||
processedIds.add(file.file_id);
|
||||
} catch (error) {
|
||||
logger.error(`Error processing file ${file.filename}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createContext = async () => {
|
||||
try {
|
||||
if (!queryPromises.length || !processedFiles.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const oneFile = processedFiles.length === 1;
|
||||
const header = `The user has attached ${oneFile ? 'a' : processedFiles.length} file${
|
||||
!oneFile ? 's' : ''
|
||||
} to the conversation:`;
|
||||
|
||||
const files = `${
|
||||
oneFile
|
||||
? ''
|
||||
: `
|
||||
<files>`
|
||||
}${processedFiles
|
||||
.map(
|
||||
(file) => `
|
||||
<file>
|
||||
<filename>${file.filename}</filename>
|
||||
<type>${file.type}</type>
|
||||
</file>`,
|
||||
)
|
||||
.join('')}${
|
||||
oneFile
|
||||
? ''
|
||||
: `
|
||||
</files>`
|
||||
}`;
|
||||
|
||||
const resolvedQueries = await Promise.all(queryPromises);
|
||||
|
||||
const context = resolvedQueries
|
||||
.map((queryResult, index) => {
|
||||
const file = processedFiles[index];
|
||||
let contextItems = queryResult.data;
|
||||
|
||||
const generateContext = (currentContext) =>
|
||||
`
|
||||
<file>
|
||||
<filename>${file.filename}</filename>
|
||||
<context>${currentContext}
|
||||
</context>
|
||||
</file>`;
|
||||
|
||||
if (useFullContext) {
|
||||
return generateContext(`\n${contextItems}`);
|
||||
}
|
||||
|
||||
contextItems = queryResult.data
|
||||
.map((item) => {
|
||||
const pageContent = item[0].page_content;
|
||||
return `
|
||||
<contextItem>
|
||||
<![CDATA[${pageContent?.trim()}]]>
|
||||
</contextItem>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return generateContext(contextItems);
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (useFullContext) {
|
||||
const prompt = `${header}
|
||||
${context}
|
||||
${footer}`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
const prompt = `${header}
|
||||
${files}
|
||||
|
||||
A semantic search was executed with the user's message as the query, retrieving the following context inside <context></context> XML tags.
|
||||
|
||||
<context>${context}
|
||||
</context>
|
||||
|
||||
${footer}`;
|
||||
|
||||
return prompt;
|
||||
} catch (error) {
|
||||
logger.error('Error creating context:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
processFile,
|
||||
createContext,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createContextHandlers;
|
||||
34
api/app/clients/prompts/createVisionPrompt.js
Normal file
34
api/app/clients/prompts/createVisionPrompt.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Generates a prompt instructing the user to describe an image in detail, tailored to different types of visual content.
|
||||
* @param {boolean} pluralized - Whether to pluralize the prompt for multiple images.
|
||||
* @returns {string} - The generated vision prompt.
|
||||
*/
|
||||
const createVisionPrompt = (pluralized = false) => {
|
||||
return `Please describe the image${
|
||||
pluralized ? 's' : ''
|
||||
} in detail, covering relevant aspects such as:
|
||||
|
||||
For photographs, illustrations, or artwork:
|
||||
- The main subject(s) and their appearance, positioning, and actions
|
||||
- The setting, background, and any notable objects or elements
|
||||
- Colors, lighting, and overall mood or atmosphere
|
||||
- Any interesting details, textures, or patterns
|
||||
- The style, technique, or medium used (if discernible)
|
||||
|
||||
For screenshots or images containing text:
|
||||
- The content and purpose of the text
|
||||
- The layout, formatting, and organization of the information
|
||||
- Any notable visual elements, such as logos, icons, or graphics
|
||||
- The overall context or message conveyed by the screenshot
|
||||
|
||||
For graphs, charts, or data visualizations:
|
||||
- The type of graph or chart (e.g., bar graph, line chart, pie chart)
|
||||
- The variables being compared or analyzed
|
||||
- Any trends, patterns, or outliers in the data
|
||||
- The axis labels, scales, and units of measurement
|
||||
- The title, legend, and any additional context provided
|
||||
|
||||
Be as specific and descriptive as possible while maintaining clarity and concision.`;
|
||||
};
|
||||
|
||||
module.exports = createVisionPrompt;
|
||||
@@ -1,3 +1,4 @@
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
|
||||
|
||||
/**
|
||||
@@ -7,10 +8,16 @@ const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
|
||||
* @param {Object} params.message - The message object to format.
|
||||
* @param {string} [params.message.role] - The role of the message sender (must be 'user').
|
||||
* @param {string} [params.message.content] - The text content of the message.
|
||||
* @param {EModelEndpoint} [params.endpoint] - Identifier for specific endpoint handling
|
||||
* @param {Array<string>} [params.image_urls] - The image_urls to attach to the message.
|
||||
* @returns {(Object)} - The formatted message.
|
||||
*/
|
||||
const formatVisionMessage = ({ message, image_urls }) => {
|
||||
const formatVisionMessage = ({ message, image_urls, endpoint }) => {
|
||||
if (endpoint === EModelEndpoint.anthropic) {
|
||||
message.content = [...image_urls, { type: 'text', text: message.content }];
|
||||
return message;
|
||||
}
|
||||
|
||||
message.content = [{ type: 'text', text: message.content }, ...image_urls];
|
||||
|
||||
return message;
|
||||
@@ -29,10 +36,11 @@ const formatVisionMessage = ({ message, image_urls }) => {
|
||||
* @param {Array<string>} [params.message.image_urls] - The image_urls attached to the message for Vision API.
|
||||
* @param {string} [params.userName] - The name of the user.
|
||||
* @param {string} [params.assistantName] - The name of the assistant.
|
||||
* @param {string} [params.endpoint] - Identifier for specific endpoint handling
|
||||
* @param {boolean} [params.langChain=false] - Whether to return a LangChain message object.
|
||||
* @returns {(Object|HumanMessage|AIMessage|SystemMessage)} - The formatted message.
|
||||
*/
|
||||
const formatMessage = ({ message, userName, assistantName, langChain = false }) => {
|
||||
const formatMessage = ({ message, userName, assistantName, endpoint, langChain = false }) => {
|
||||
let { role: _role, _name, sender, text, content: _content, lc_id } = message;
|
||||
if (lc_id && lc_id[2] && !langChain) {
|
||||
const roleMapping = {
|
||||
@@ -51,7 +59,11 @@ const formatMessage = ({ message, userName, assistantName, langChain = false })
|
||||
|
||||
const { image_urls } = message;
|
||||
if (Array.isArray(image_urls) && image_urls.length > 0 && role === 'user') {
|
||||
return formatVisionMessage({ message: formattedMessage, image_urls: message.image_urls });
|
||||
return formatVisionMessage({
|
||||
message: formattedMessage,
|
||||
image_urls: message.image_urls,
|
||||
endpoint,
|
||||
});
|
||||
}
|
||||
|
||||
if (_name) {
|
||||
|
||||
@@ -4,6 +4,8 @@ const handleInputs = require('./handleInputs');
|
||||
const instructions = require('./instructions');
|
||||
const titlePrompts = require('./titlePrompts');
|
||||
const truncateText = require('./truncateText');
|
||||
const createVisionPrompt = require('./createVisionPrompt');
|
||||
const createContextHandlers = require('./createContextHandlers');
|
||||
|
||||
module.exports = {
|
||||
...formatMessages,
|
||||
@@ -11,5 +13,7 @@ module.exports = {
|
||||
...handleInputs,
|
||||
...instructions,
|
||||
...titlePrompts,
|
||||
truncateText,
|
||||
...truncateText,
|
||||
createVisionPrompt,
|
||||
createContextHandlers,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,96 @@ ${convo}`,
|
||||
return titlePrompt;
|
||||
};
|
||||
|
||||
const titleInstruction =
|
||||
'a concise, 5-word-or-less title for the conversation, using its same language, with no punctuation. Apply title case conventions appropriate for the language. For English, use AP Stylebook Title Case. Never directly mention the language name or the word "title"';
|
||||
const titleFunctionPrompt = `In this environment you have access to a set of tools you can use to generate the conversation title.
|
||||
|
||||
You may call them like this:
|
||||
<function_calls>
|
||||
<invoke>
|
||||
<tool_name>$TOOL_NAME</tool_name>
|
||||
<parameters>
|
||||
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
|
||||
...
|
||||
</parameters>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
|
||||
Here are the tools available:
|
||||
<tools>
|
||||
<tool_description>
|
||||
<tool_name>submit_title</tool_name>
|
||||
<description>
|
||||
Submit a brief title in the conversation's language, following the parameter description closely.
|
||||
</description>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>title</name>
|
||||
<type>string</type>
|
||||
<description>${titleInstruction}</description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
</tool_description>
|
||||
</tools>`;
|
||||
|
||||
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:
|
||||
<function_calls>
|
||||
<invoke>
|
||||
<tool_name>$TOOL_NAME</tool_name>
|
||||
<parameters>
|
||||
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
|
||||
...
|
||||
</parameters>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
|
||||
Here are the tools available:
|
||||
<tools>
|
||||
<tool_description>
|
||||
<tool_name>submit_translation</tool_name>
|
||||
<description>
|
||||
Submit a translation in the target language, following the parameter description and its language closely.
|
||||
</description>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>translation</name>
|
||||
<type>string</type>
|
||||
<description>${translationPrompt}
|
||||
ONLY include the generated translation without quotations, nor its related key</description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
</tool_description>
|
||||
</tools>`;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (paramMatch && paramMatch[1]) {
|
||||
return paramMatch[1].trim();
|
||||
}
|
||||
|
||||
if (prompt && prompt.length) {
|
||||
return `NO TOOL INVOCATION: ${prompt}`;
|
||||
}
|
||||
return `No ${paramName} provided`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
langPrompt,
|
||||
titleInstruction,
|
||||
createTitlePrompt,
|
||||
titleFunctionPrompt,
|
||||
parseParamFromPrompt,
|
||||
genTranslationPrompt,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -144,6 +144,7 @@ describe('OpenAIClient', () => {
|
||||
|
||||
const defaultOptions = {
|
||||
// debug: true,
|
||||
req: {},
|
||||
openaiApiKey: 'new-api-key',
|
||||
modelOptions: {
|
||||
model,
|
||||
@@ -157,12 +158,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 +670,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
const { google } = require('googleapis');
|
||||
const { Tool } = require('langchain/tools');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Represents a tool that allows an agent to use the Google Custom Search API.
|
||||
* @extends Tool
|
||||
*/
|
||||
class GoogleSearchAPI extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
this.cx = fields.GOOGLE_CSE_ID || this.getCx();
|
||||
this.apiKey = fields.GOOGLE_API_KEY || this.getApiKey();
|
||||
this.customSearch = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the tool.
|
||||
* @type {string}
|
||||
*/
|
||||
name = 'google';
|
||||
|
||||
/**
|
||||
* A description for the agent to use
|
||||
* @type {string}
|
||||
*/
|
||||
description =
|
||||
'Use the \'google\' tool to retrieve internet search results relevant to your input. The results will return links and snippets of text from the webpages';
|
||||
description_for_model =
|
||||
'Use the \'google\' tool to retrieve internet search results relevant to your input. The results will return links and snippets of text from the webpages';
|
||||
|
||||
getCx() {
|
||||
const cx = process.env.GOOGLE_CSE_ID || '';
|
||||
if (!cx) {
|
||||
throw new Error('Missing GOOGLE_CSE_ID environment variable.');
|
||||
}
|
||||
return cx;
|
||||
}
|
||||
|
||||
getApiKey() {
|
||||
const apiKey = process.env.GOOGLE_API_KEY || '';
|
||||
if (!apiKey) {
|
||||
throw new Error('Missing GOOGLE_API_KEY environment variable.');
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
getCustomSearch() {
|
||||
if (!this.customSearch) {
|
||||
const version = 'v1';
|
||||
this.customSearch = google.customsearch(version);
|
||||
}
|
||||
return this.customSearch;
|
||||
}
|
||||
|
||||
resultsToReadableFormat(results) {
|
||||
let output = 'Results:\n';
|
||||
|
||||
results.forEach((resultObj, index) => {
|
||||
output += `Title: ${resultObj.title}\n`;
|
||||
output += `Link: ${resultObj.link}\n`;
|
||||
if (resultObj.snippet) {
|
||||
output += `Snippet: ${resultObj.snippet}\n`;
|
||||
}
|
||||
|
||||
if (index < results.length - 1) {
|
||||
output += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the tool with the provided input and returns a promise that resolves with a response from the Google Custom Search API.
|
||||
* @param {string} input - The input to provide to the API.
|
||||
* @returns {Promise<String>} A promise that resolves with a response from the Google Custom Search API.
|
||||
*/
|
||||
async _call(input) {
|
||||
try {
|
||||
const metadataResults = [];
|
||||
const response = await this.getCustomSearch().cse.list({
|
||||
q: input,
|
||||
cx: this.cx,
|
||||
auth: this.apiKey,
|
||||
num: 5, // Limit the number of results to 5
|
||||
});
|
||||
|
||||
// return response.data;
|
||||
// logger.debug(response.data);
|
||||
|
||||
if (!response.data.items || response.data.items.length === 0) {
|
||||
return this.resultsToReadableFormat([
|
||||
{ title: 'No good Google Search Result was found', link: '' },
|
||||
]);
|
||||
}
|
||||
|
||||
// const results = response.items.slice(0, numResults);
|
||||
const results = response.data.items;
|
||||
|
||||
for (const result of results) {
|
||||
const metadataResult = {
|
||||
title: result.title || '',
|
||||
link: result.link || '',
|
||||
};
|
||||
if (result.snippet) {
|
||||
metadataResult.snippet = result.snippet;
|
||||
}
|
||||
metadataResults.push(metadataResult);
|
||||
}
|
||||
|
||||
return this.resultsToReadableFormat(metadataResults);
|
||||
} catch (error) {
|
||||
logger.error('[GoogleSearchAPI]', error);
|
||||
// throw error;
|
||||
return 'There was an error searching Google.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleSearchAPI;
|
||||
@@ -1,7 +1,6 @@
|
||||
const availableTools = require('./manifest.json');
|
||||
// Basic Tools
|
||||
const CodeBrew = require('./CodeBrew');
|
||||
const GoogleSearchAPI = require('./GoogleSearch');
|
||||
const WolframAlphaAPI = require('./Wolfram');
|
||||
const AzureAiSearch = require('./AzureAiSearch');
|
||||
const OpenAICreateImage = require('./DALL-E');
|
||||
@@ -16,8 +15,10 @@ const CodeSherpa = require('./structured/CodeSherpa');
|
||||
const StructuredSD = require('./structured/StableDiffusion');
|
||||
const StructuredACS = require('./structured/AzureAISearch');
|
||||
const CodeSherpaTools = require('./structured/CodeSherpaTools');
|
||||
const GoogleSearchAPI = require('./structured/GoogleSearch');
|
||||
const StructuredWolfram = require('./structured/Wolfram');
|
||||
const TavilySearchResults = require('./structured/TavilySearchResults');
|
||||
const TraversaalSearch = require('./structured/TraversaalSearch');
|
||||
|
||||
module.exports = {
|
||||
availableTools,
|
||||
@@ -39,4 +40,5 @@ module.exports = {
|
||||
CodeSherpaTools,
|
||||
StructuredWolfram,
|
||||
TavilySearchResults,
|
||||
TraversaalSearch,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "Traversaal",
|
||||
"pluginKey": "traversaal_search",
|
||||
"description": "Traversaal is a robust search API tailored for LLM Agents. Get an API key here: https://api.traversaal.ai",
|
||||
"icon": "https://traversaal.ai/favicon.ico",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "TRAVERSAAL_API_KEY",
|
||||
"label": "Traversaal API Key",
|
||||
"description": "Get your API key here: <a href=\"https://api.traversaal.ai\" target=\"_blank\">https://api.traversaal.ai</a>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Google",
|
||||
"pluginKey": "google",
|
||||
@@ -11,7 +24,7 @@
|
||||
"description": "This is your Google Custom Search Engine ID. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
},
|
||||
{
|
||||
"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 <a href='https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
}
|
||||
@@ -47,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",
|
||||
@@ -111,7 +124,7 @@
|
||||
{
|
||||
"name": "Tavily Search",
|
||||
"pluginKey": "tavily_search_results_json",
|
||||
"description": "Tavily Search is a robust search API tailored specifically for LLM Agents. It seamlessly integrates with diverse data sources to ensure a superior, relevant search experience.",
|
||||
"description": "Tavily Search is a robust search API tailored for LLM Agents. It seamlessly integrates with diverse data sources to ensure a superior, relevant search experience.",
|
||||
"icon": "https://tavily.com/favicon.ico",
|
||||
"authConfig": [
|
||||
{
|
||||
|
||||
@@ -12,14 +12,15 @@ const { logger } = require('~/config');
|
||||
class DALLE3 extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
/* Used to initialize the Tool without necessary variables. */
|
||||
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
||||
this.override = fields.override ?? false;
|
||||
/* Necessary for output to contain all image metadata. */
|
||||
/** @type {boolean} Necessary for output to contain all image metadata. */
|
||||
this.returnMetadata = fields.returnMetadata ?? false;
|
||||
|
||||
this.userId = fields.userId;
|
||||
this.fileStrategy = fields.fileStrategy;
|
||||
if (fields.processFileURL) {
|
||||
/** @type {processFileURL} Necessary for output to contain all image metadata. */
|
||||
this.processFileURL = fields.processFileURL.bind(this);
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ class DALLE3 extends Tool {
|
||||
config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
}
|
||||
|
||||
/** @type {OpenAI} */
|
||||
this.openai = new OpenAI(config);
|
||||
this.name = 'dalle';
|
||||
this.description = `Use DALLE to create images from text descriptions.
|
||||
@@ -164,13 +166,7 @@ Error Message: ${error.message}`;
|
||||
});
|
||||
|
||||
if (this.returnMetadata) {
|
||||
this.result = {
|
||||
file_id: result.file_id,
|
||||
filename: result.filename,
|
||||
filepath: result.filepath,
|
||||
height: result.height,
|
||||
width: result.width,
|
||||
};
|
||||
this.result = result;
|
||||
} else {
|
||||
this.result = this.wrapInMarkdown(result.filepath);
|
||||
}
|
||||
|
||||
65
api/app/clients/tools/structured/GoogleSearch.js
Normal file
65
api/app/clients/tools/structured/GoogleSearch.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { z } = require('zod');
|
||||
const { Tool } = require('@langchain/core/tools');
|
||||
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
|
||||
|
||||
class GoogleSearchResults extends Tool {
|
||||
static lc_name() {
|
||||
return 'GoogleSearchResults';
|
||||
}
|
||||
|
||||
constructor(fields = {}) {
|
||||
super(fields);
|
||||
this.envVarApiKey = 'GOOGLE_SEARCH_API_KEY';
|
||||
this.envVarSearchEngineId = 'GOOGLE_CSE_ID';
|
||||
this.override = fields.override ?? false;
|
||||
this.apiKey = fields.apiKey ?? getEnvironmentVariable(this.envVarApiKey);
|
||||
this.searchEngineId =
|
||||
fields.searchEngineId ?? getEnvironmentVariable(this.envVarSearchEngineId);
|
||||
|
||||
this.kwargs = fields?.kwargs ?? {};
|
||||
this.name = 'google';
|
||||
this.description =
|
||||
'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.';
|
||||
|
||||
this.schema = z.object({
|
||||
query: z.string().min(1).describe('The search query string.'),
|
||||
max_results: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(10)
|
||||
.optional()
|
||||
.describe('The maximum number of search results to return. Defaults to 10.'),
|
||||
// Note: Google API has its own parameters for search customization, adjust as needed.
|
||||
});
|
||||
}
|
||||
|
||||
async _call(input) {
|
||||
const validationResult = this.schema.safeParse(input);
|
||||
if (!validationResult.success) {
|
||||
throw new Error(`Validation failed: ${JSON.stringify(validationResult.error.issues)}`);
|
||||
}
|
||||
|
||||
const { query, max_results = 5 } = validationResult.data;
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/customsearch/v1?key=${this.apiKey}&cx=${
|
||||
this.searchEngineId
|
||||
}&q=${encodeURIComponent(query)}&num=${max_results}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status ${response.status}: ${json.error.message}`);
|
||||
}
|
||||
|
||||
return JSON.stringify(json);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleSearchResults;
|
||||
@@ -4,14 +4,27 @@ const { z } = require('zod');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const sharp = require('sharp');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { StructuredTool } = require('langchain/tools');
|
||||
const { FileContext } = require('librechat-data-provider');
|
||||
const paths = require('~/config/paths');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
class StableDiffusionAPI extends StructuredTool {
|
||||
constructor(fields) {
|
||||
super();
|
||||
/* Used to initialize the Tool without necessary variables. */
|
||||
/** @type {string} User ID */
|
||||
this.userId = fields.userId;
|
||||
/** @type {Express.Request | undefined} Express Request object, only provided by ToolService */
|
||||
this.req = fields.req;
|
||||
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
||||
this.override = fields.override ?? false;
|
||||
/** @type {boolean} Necessary for output to contain all image metadata. */
|
||||
this.returnMetadata = fields.returnMetadata ?? false;
|
||||
if (fields.uploadImageBuffer) {
|
||||
/** @type {uploadImageBuffer} Necessary for output to contain all image metadata. */
|
||||
this.uploadImageBuffer = fields.uploadImageBuffer.bind(this);
|
||||
}
|
||||
|
||||
this.name = 'stable-diffusion';
|
||||
this.url = fields.SD_WEBUI_URL || this.getServerURL();
|
||||
@@ -47,7 +60,7 @@ class StableDiffusionAPI extends StructuredTool {
|
||||
|
||||
getMarkdownImageUrl(imageName) {
|
||||
const imageUrl = path
|
||||
.join(this.relativeImageUrl, imageName)
|
||||
.join(this.relativePath, this.userId, imageName)
|
||||
.replace(/\\/g, '/')
|
||||
.replace('public/', '');
|
||||
return ``;
|
||||
@@ -73,46 +86,67 @@ class StableDiffusionAPI extends StructuredTool {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const response = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
|
||||
const image = response.data.images[0];
|
||||
const pngPayload = { image: `data:image/png;base64,${image}` };
|
||||
const response2 = await axios.post(`${url}/sdapi/v1/png-info`, pngPayload);
|
||||
const info = response2.data.info;
|
||||
const generationResponse = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
|
||||
const image = generationResponse.data.images[0];
|
||||
|
||||
// Generate unique name
|
||||
const imageName = `${Date.now()}.png`;
|
||||
this.outputPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'client',
|
||||
'public',
|
||||
'images',
|
||||
);
|
||||
const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client');
|
||||
this.relativeImageUrl = path.relative(appRoot, this.outputPath);
|
||||
/** @type {{ height: number, width: number, seed: number, infotexts: string[] }} */
|
||||
let info = {};
|
||||
try {
|
||||
info = JSON.parse(generationResponse.data.info);
|
||||
} catch (error) {
|
||||
logger.error('[StableDiffusion] Error while getting image metadata:', error);
|
||||
}
|
||||
|
||||
// Check if directory exists, if not create it
|
||||
if (!fs.existsSync(this.outputPath)) {
|
||||
fs.mkdirSync(this.outputPath, { recursive: true });
|
||||
const file_id = uuidv4();
|
||||
const imageName = `${file_id}.png`;
|
||||
const { imageOutput: imageOutputPath, clientPath } = paths;
|
||||
const filepath = path.join(imageOutputPath, this.userId, imageName);
|
||||
this.relativePath = path.relative(clientPath, imageOutputPath);
|
||||
|
||||
if (!fs.existsSync(path.join(imageOutputPath, this.userId))) {
|
||||
fs.mkdirSync(path.join(imageOutputPath, this.userId), { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(image.split(',', 1)[0], 'base64');
|
||||
if (this.returnMetadata && this.uploadImageBuffer && this.req) {
|
||||
const file = await this.uploadImageBuffer({
|
||||
req: this.req,
|
||||
context: FileContext.image_generation,
|
||||
resize: false,
|
||||
metadata: {
|
||||
buffer,
|
||||
height: info.height,
|
||||
width: info.width,
|
||||
bytes: Buffer.byteLength(buffer),
|
||||
filename: imageName,
|
||||
type: 'image/png',
|
||||
file_id,
|
||||
},
|
||||
});
|
||||
|
||||
const generationInfo = info.infotexts[0].split('\n').pop();
|
||||
return {
|
||||
...file,
|
||||
prompt,
|
||||
metadata: {
|
||||
negative_prompt,
|
||||
seed: info.seed,
|
||||
info: generationInfo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await sharp(buffer)
|
||||
.withMetadata({
|
||||
iptcpng: {
|
||||
parameters: info,
|
||||
parameters: info.infotexts[0],
|
||||
},
|
||||
})
|
||||
.toFile(this.outputPath + '/' + imageName);
|
||||
.toFile(filepath);
|
||||
this.result = this.getMarkdownImageUrl(imageName);
|
||||
} catch (error) {
|
||||
logger.error('[StableDiffusion] Error while saving the image:', error);
|
||||
// this.result = theImageUrl;
|
||||
}
|
||||
|
||||
return this.result;
|
||||
|
||||
89
api/app/clients/tools/structured/TraversaalSearch.js
Normal file
89
api/app/clients/tools/structured/TraversaalSearch.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { z } = require('zod');
|
||||
const { Tool } = require('@langchain/core/tools');
|
||||
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Tool for the Traversaal AI search API, Ares.
|
||||
*/
|
||||
class TraversaalSearch extends Tool {
|
||||
static lc_name() {
|
||||
return 'TraversaalSearch';
|
||||
}
|
||||
constructor(fields) {
|
||||
super(fields);
|
||||
this.name = 'traversaal_search';
|
||||
this.description = `An AI search engine optimized for comprehensive, accurate, and trusted results.
|
||||
Useful for when you need to answer questions about current events. Input should be a search query.`;
|
||||
this.description_for_model =
|
||||
'\'Please create a specific sentence for the AI to understand and use as a query to search the web based on the user\'s request. For example, "Find information about the highest mountains in the world." or "Show me the latest news articles about climate change and its impact on polar ice caps."\'';
|
||||
this.schema = z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
'A properly written sentence to be interpreted by an AI to search the web according to the user\'s request.',
|
||||
),
|
||||
});
|
||||
|
||||
this.apiKey = fields?.TRAVERSAAL_API_KEY ?? this.getApiKey();
|
||||
}
|
||||
|
||||
getApiKey() {
|
||||
const apiKey = getEnvironmentVariable('TRAVERSAAL_API_KEY');
|
||||
if (!apiKey && this.override) {
|
||||
throw new Error(
|
||||
'No Traversaal API key found. Either set an environment variable named "TRAVERSAAL_API_KEY" or pass an API key as "apiKey".',
|
||||
);
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async _call({ query }, _runManager) {
|
||||
const body = {
|
||||
query: [query],
|
||||
};
|
||||
try {
|
||||
const response = await fetch('https://api-ares.traversaal.ai/live/predict', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
},
|
||||
body: JSON.stringify({ ...body }),
|
||||
});
|
||||
const json = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Request failed with status code ${response.status}: ${json.error ?? json.message}`,
|
||||
);
|
||||
}
|
||||
if (!json.data) {
|
||||
throw new Error('Could not parse Traversaal API results. Please try again.');
|
||||
}
|
||||
|
||||
const baseText = json.data?.response_text ?? '';
|
||||
const sources = json.data?.web_url;
|
||||
const noResponse = 'No response found in Traversaal API results';
|
||||
|
||||
if (!baseText && !sources) {
|
||||
return noResponse;
|
||||
}
|
||||
|
||||
const sourcesText = sources?.length ? '\n\nSources:\n - ' + sources.join('\n - ') : '';
|
||||
|
||||
const result = baseText + sourcesText;
|
||||
|
||||
if (!result) {
|
||||
return noResponse;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Traversaal API request failed', error);
|
||||
return `Traversaal API request failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TraversaalSearch;
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
StructuredSD,
|
||||
StructuredACS,
|
||||
CodeSherpaTools,
|
||||
TraversaalSearch,
|
||||
StructuredWolfram,
|
||||
TavilySearchResults,
|
||||
} = require('../');
|
||||
@@ -165,6 +166,7 @@ const loadTools = async ({
|
||||
'stable-diffusion': functions ? StructuredSD : StableDiffusionAPI,
|
||||
'azure-ai-search': functions ? StructuredACS : AzureAISearch,
|
||||
CodeBrew: CodeBrew,
|
||||
traversaal_search: TraversaalSearch,
|
||||
};
|
||||
|
||||
const openAIApiKey = await getOpenAIKey(options, user);
|
||||
@@ -235,9 +237,11 @@ const loadTools = async ({
|
||||
}
|
||||
|
||||
const imageGenOptions = {
|
||||
req: options.req,
|
||||
fileStrategy: options.fileStrategy,
|
||||
processFileURL: options.processFileURL,
|
||||
returnMetadata: options.returnMetadata,
|
||||
uploadImageBuffer: options.uploadImageBuffer,
|
||||
};
|
||||
|
||||
const toolOptions = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
const { availableTools } = require('../');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Loads a suite of tools with authentication values for a given user, supporting alternate authentication fields.
|
||||
@@ -30,7 +31,7 @@ const loadToolSuite = async ({ pluginKey, tools, user, options = {} }) => {
|
||||
return value;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching plugin auth value for ${field}: ${err.message}`);
|
||||
logger.error(`Error fetching plugin auth value for ${field}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -41,7 +42,7 @@ const loadToolSuite = async ({ pluginKey, tools, user, options = {} }) => {
|
||||
if (authValue !== null) {
|
||||
authValues[auth.authField] = authValue;
|
||||
} else {
|
||||
console.warn(`No auth value found for ${auth.authField}`);
|
||||
logger.warn(`[loadToolSuite] No auth value found for ${auth.authField}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
api/cache/banViolation.js
vendored
7
api/cache/banViolation.js
vendored
@@ -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) {
|
||||
|
||||
3
api/cache/banViolation.spec.js
vendored
3
api/cache/banViolation.spec.js
vendored
@@ -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),
|
||||
});
|
||||
});
|
||||
|
||||
23
api/cache/getLogStores.js
vendored
23
api/cache/getLogStores.js
vendored
@@ -1,11 +1,12 @@
|
||||
const Keyv = require('keyv');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const { CacheKeys, ViolationTypes } = 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 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 })
|
||||
@@ -37,19 +38,27 @@ const modelQueries = isEnabled(process.env.USE_REDIS)
|
||||
|
||||
const abortKeys = isEnabled(USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.ABORT_KEYS });
|
||||
: new Keyv({ namespace: CacheKeys.ABORT_KEYS, ttl: 600000 });
|
||||
|
||||
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'),
|
||||
message_limit: createViolationInstance('message_limit'),
|
||||
token_balance: createViolationInstance('token_balance'),
|
||||
token_balance: createViolationInstance(ViolationTypes.TOKEN_BALANCE),
|
||||
registrations: createViolationInstance('registrations'),
|
||||
[CacheKeys.FILE_UPLOAD_LIMIT]: createViolationInstance(CacheKeys.FILE_UPLOAD_LIMIT),
|
||||
[ViolationTypes.FILE_UPLOAD_LIMIT]: createViolationInstance(ViolationTypes.FILE_UPLOAD_LIMIT),
|
||||
[ViolationTypes.ILLEGAL_MODEL_REQUEST]: createViolationInstance(
|
||||
ViolationTypes.ILLEGAL_MODEL_REQUEST,
|
||||
),
|
||||
logins: createViolationInstance('logins'),
|
||||
[CacheKeys.ABORT_KEYS]: abortKeys,
|
||||
[CacheKeys.TOKEN_CONFIG]: tokenConfig,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
root: path.resolve(__dirname, '..', '..'),
|
||||
uploads: path.resolve(__dirname, '..', '..', 'uploads'),
|
||||
clientPath: path.resolve(__dirname, '..', '..', 'client'),
|
||||
dist: path.resolve(__dirname, '..', '..', 'client', 'dist'),
|
||||
publicPath: path.resolve(__dirname, '..', '..', 'client', 'public'),
|
||||
fonts: path.resolve(__dirname, '..', '..', 'client', 'public', 'fonts'),
|
||||
assets: path.resolve(__dirname, '..', '..', 'client', 'public', 'assets'),
|
||||
imageOutput: path.resolve(__dirname, '..', '..', 'client', 'public', 'images'),
|
||||
structuredTools: path.resolve(__dirname, '..', 'app', 'clients', 'tools', 'structured'),
|
||||
pluginManifest: path.resolve(__dirname, '..', 'app', 'clients', 'tools', 'manifest.json'),
|
||||
|
||||
@@ -5,7 +5,15 @@ const { redactFormat, redactMessage, debugTraverse } = require('./parsers');
|
||||
|
||||
const logDir = path.join(__dirname, '..', 'logs');
|
||||
|
||||
const { NODE_ENV, DEBUG_LOGGING = true, DEBUG_CONSOLE = false } = process.env;
|
||||
const { NODE_ENV, DEBUG_LOGGING = true, DEBUG_CONSOLE = false, CONSOLE_JSON = false } = process.env;
|
||||
|
||||
const useConsoleJson =
|
||||
(typeof CONSOLE_JSON === 'string' && CONSOLE_JSON?.toLowerCase() === 'true') ||
|
||||
CONSOLE_JSON === true;
|
||||
|
||||
const useDebugConsole =
|
||||
(typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE?.toLowerCase() === 'true') ||
|
||||
DEBUG_CONSOLE === true;
|
||||
|
||||
const levels = {
|
||||
error: 0,
|
||||
@@ -33,7 +41,7 @@ const level = () => {
|
||||
|
||||
const fileFormat = winston.format.combine(
|
||||
redactFormat(),
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.timestamp({ format: () => new Date().toISOString() }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
// redactErrors(),
|
||||
@@ -99,14 +107,20 @@ const consoleFormat = winston.format.combine(
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
(typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE?.toLowerCase() === 'true') ||
|
||||
DEBUG_CONSOLE === true
|
||||
) {
|
||||
if (useDebugConsole) {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: winston.format.combine(consoleFormat, debugTraverse),
|
||||
format: useConsoleJson
|
||||
? winston.format.combine(fileFormat, debugTraverse, winston.format.json())
|
||||
: winston.format.combine(fileFormat, debugTraverse),
|
||||
}),
|
||||
);
|
||||
} else if (useConsoleJson) {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: 'info',
|
||||
format: winston.format.combine(fileFormat, winston.format.json()),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<Object>} The updated or newly created action document as a plain object.
|
||||
*/
|
||||
const updateAction = async (searchParams, updateData) => {
|
||||
return await Action.findOneAndUpdate(searchParams, updateData, {
|
||||
new: true,
|
||||
upsert: true,
|
||||
}).lean();
|
||||
const updateAction = async (searchParams, updateData, session = null) => {
|
||||
const options = { new: true, upsert: true, session };
|
||||
return await Action.findOneAndUpdate(searchParams, updateData, options).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,19 +49,37 @@ const getActions = async (searchParams, includeSensitive = false) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an action by its ID.
|
||||
* Deletes an action by params, 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 {Object} searchParams - The search parameters to find the action to delete.
|
||||
* @param {string} searchParams.action_id - The ID of the action to delete.
|
||||
* @param {string} searchParams.user - The user ID of the action's author.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
|
||||
* @returns {Promise<Object>} A promise that resolves to the deleted action document as a plain object, or null if no document was found.
|
||||
*/
|
||||
const deleteAction = async (searchParams) => {
|
||||
return await Action.findOneAndDelete(searchParams).lean();
|
||||
const deleteAction = async (searchParams, session = null) => {
|
||||
const options = session ? { session } : {};
|
||||
return await Action.findOneAndDelete(searchParams, options).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes actions by params, within a transaction session if provided.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the actions to delete.
|
||||
* @param {string} searchParams.action_id - The ID of the action(s) to delete.
|
||||
* @param {string} searchParams.user - The user ID of the action's author.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
|
||||
* @returns {Promise<Number>} A promise that resolves to the number of deleted action documents.
|
||||
*/
|
||||
const deleteActions = async (searchParams, session = null) => {
|
||||
const options = session ? { session } : {};
|
||||
const result = await Action.deleteMany(searchParams, options);
|
||||
return result.deletedCount;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
updateAction,
|
||||
getActions,
|
||||
updateAction,
|
||||
deleteAction,
|
||||
deleteActions,
|
||||
};
|
||||
|
||||
@@ -5,19 +5,18 @@ const Assistant = mongoose.model('assistant', assistantSchema);
|
||||
|
||||
/**
|
||||
* Update an assistant with new data without overwriting existing properties,
|
||||
* or create a new assistant if it doesn't exist.
|
||||
* or create a new assistant if it doesn't exist, within a transaction session if provided.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the assistant to update.
|
||||
* @param {string} searchParams.assistant_id - The ID of the assistant to update.
|
||||
* @param {string} searchParams.user - The user ID of the assistant's author.
|
||||
* @param {Object} updateData - An object containing the properties to update.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
|
||||
* @returns {Promise<Object>} The updated or newly created assistant document as a plain object.
|
||||
*/
|
||||
const updateAssistant = async (searchParams, updateData) => {
|
||||
return await Assistant.findOneAndUpdate(searchParams, updateData, {
|
||||
new: true,
|
||||
upsert: true,
|
||||
}).lean();
|
||||
const updateAssistant = async (searchParams, updateData, session = null) => {
|
||||
const options = { new: true, upsert: true, session };
|
||||
return await Assistant.findOneAndUpdate(searchParams, updateData, options).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,8 +39,21 @@ const getAssistants = async (searchParams) => {
|
||||
return await Assistant.find(searchParams).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an assistant based on the provided ID.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the assistant to delete.
|
||||
* @param {string} searchParams.assistant_id - The ID of the assistant to delete.
|
||||
* @param {string} searchParams.user - The user ID of the assistant's author.
|
||||
* @returns {Promise<void>} Resolves when the assistant has been successfully deleted.
|
||||
*/
|
||||
const deleteAssistant = async (searchParams) => {
|
||||
return await Assistant.findOneAndDelete(searchParams);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
updateAssistant,
|
||||
deleteAssistant,
|
||||
getAssistants,
|
||||
getAssistant,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,12 @@ const Conversation = require('./schema/convoSchema');
|
||||
const { getMessages, deleteMessages } = require('./Message');
|
||||
const logger = require('~/config/winston');
|
||||
|
||||
/**
|
||||
* Retrieves a single conversation for a given user and conversation ID.
|
||||
* @param {string} user - The user's ID.
|
||||
* @param {string} conversationId - The conversation's ID.
|
||||
* @returns {Promise<TConversation>} The conversation object.
|
||||
*/
|
||||
const getConvo = async (user, conversationId) => {
|
||||
try {
|
||||
return await Conversation.findOne({ user, conversationId }).lean();
|
||||
@@ -30,11 +36,35 @@ module.exports = {
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
},
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 25) => {
|
||||
bulkSaveConvos: async (conversations) => {
|
||||
try {
|
||||
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
|
||||
const bulkOps = conversations.map((convo) => ({
|
||||
updateOne: {
|
||||
filter: { conversationId: convo.conversationId, user: convo.user },
|
||||
update: convo,
|
||||
upsert: true,
|
||||
timestamps: false,
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await Conversation.bulkWrite(bulkOps);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[saveBulkConversations] Error saving conversations in bulk', error);
|
||||
throw new Error('Failed to save conversations in bulk.');
|
||||
}
|
||||
},
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 25, isArchived = false) => {
|
||||
const query = { user };
|
||||
if (isArchived) {
|
||||
query.isArchived = true;
|
||||
} else {
|
||||
query.$or = [{ isArchived: false }, { isArchived: { $exists: false } }];
|
||||
}
|
||||
try {
|
||||
const totalConvos = (await Conversation.countDocuments(query)) || 1;
|
||||
const totalPages = Math.ceil(totalConvos / pageSize);
|
||||
const convos = await Conversation.find({ user })
|
||||
const convos = await Conversation.find(query)
|
||||
.sort({ updatedAt: -1 })
|
||||
.skip((pageNumber - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
|
||||
@@ -69,7 +69,7 @@ const updateFileUsage = async (data) => {
|
||||
const { file_id, inc = 1 } = data;
|
||||
const updateOperation = {
|
||||
$inc: { usage: inc },
|
||||
$unset: { expiresAt: '' },
|
||||
$unset: { expiresAt: '', temp_file_id: '' },
|
||||
};
|
||||
return await File.findOneAndUpdate({ file_id }, updateOperation, { new: true }).lean();
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ module.exports = {
|
||||
async saveMessage({
|
||||
user,
|
||||
endpoint,
|
||||
iconURL,
|
||||
messageId,
|
||||
newMessageId,
|
||||
conversationId,
|
||||
@@ -35,6 +36,7 @@ module.exports = {
|
||||
|
||||
const update = {
|
||||
user,
|
||||
iconURL,
|
||||
endpoint,
|
||||
messageId: newMessageId || messageId,
|
||||
conversationId,
|
||||
@@ -72,6 +74,25 @@ module.exports = {
|
||||
throw new Error('Failed to save message.');
|
||||
}
|
||||
},
|
||||
|
||||
async bulkSaveMessages(messages) {
|
||||
try {
|
||||
const bulkOps = messages.map((message) => ({
|
||||
updateOne: {
|
||||
filter: { messageId: message.messageId },
|
||||
update: message,
|
||||
upsert: true,
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await Message.bulkWrite(bulkOps);
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger.error('Error saving messages in bulk:', err);
|
||||
throw new Error('Failed to save messages in bulk.');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Records a message in the database.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,12 @@ module.exports = {
|
||||
try {
|
||||
const setter = { $set: {} };
|
||||
const update = { presetId, ...preset };
|
||||
if (preset.tools && Array.isArray(preset.tools)) {
|
||||
update.tools =
|
||||
preset.tools
|
||||
.map((tool) => tool?.pluginKey ?? tool)
|
||||
.filter((toolName) => typeof toolName === 'string') ?? [];
|
||||
}
|
||||
if (newPresetId) {
|
||||
update.presetId = newPresetId;
|
||||
}
|
||||
|
||||
89
api/models/Share.js
Normal file
89
api/models/Share.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const crypto = require('crypto');
|
||||
const { getMessages } = require('./Message');
|
||||
const SharedLink = require('./schema/shareSchema');
|
||||
const logger = require('~/config/winston');
|
||||
|
||||
module.exports = {
|
||||
SharedLink,
|
||||
getSharedMessages: async (shareId) => {
|
||||
try {
|
||||
const share = await SharedLink.findOne({ shareId })
|
||||
.populate({
|
||||
path: 'messages',
|
||||
select: '-_id -__v -user',
|
||||
})
|
||||
.select('-_id -__v -user')
|
||||
.lean();
|
||||
|
||||
if (!share || !share.conversationId || !share.isPublic) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return share;
|
||||
} catch (error) {
|
||||
logger.error('[getShare] Error getting share link', error);
|
||||
return { message: 'Error getting share link' };
|
||||
}
|
||||
},
|
||||
|
||||
getSharedLinks: async (user, pageNumber = 1, pageSize = 25, isPublic = true) => {
|
||||
const query = { user, isPublic };
|
||||
try {
|
||||
const totalConvos = (await SharedLink.countDocuments(query)) || 1;
|
||||
const totalPages = Math.ceil(totalConvos / pageSize);
|
||||
const shares = await SharedLink.find(query)
|
||||
.sort({ updatedAt: -1 })
|
||||
.skip((pageNumber - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.select('-_id -__v -user')
|
||||
.lean();
|
||||
|
||||
return { sharedLinks: shares, pages: totalPages, pageNumber, pageSize };
|
||||
} catch (error) {
|
||||
logger.error('[getShareByPage] Error getting shares', error);
|
||||
return { message: 'Error getting shares' };
|
||||
}
|
||||
},
|
||||
|
||||
createSharedLink: async (user, { conversationId, ...shareData }) => {
|
||||
const share = await SharedLink.findOne({ conversationId }).select('-_id -__v -user').lean();
|
||||
if (share) {
|
||||
return share;
|
||||
}
|
||||
|
||||
try {
|
||||
const shareId = crypto.randomUUID();
|
||||
const messages = await getMessages({ conversationId });
|
||||
const update = { ...shareData, shareId, messages, user };
|
||||
return await SharedLink.findOneAndUpdate({ conversationId: conversationId, user }, update, {
|
||||
new: true,
|
||||
upsert: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[saveShareMessage] Error saving conversation', error);
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
},
|
||||
|
||||
updateSharedLink: async (user, { conversationId, ...shareData }) => {
|
||||
const share = await SharedLink.findOne({ conversationId }).select('-_id -__v -user').lean();
|
||||
if (!share) {
|
||||
return { message: 'Share not found' };
|
||||
}
|
||||
// update messages to the latest
|
||||
const messages = await getMessages({ conversationId });
|
||||
const update = { ...shareData, messages, user };
|
||||
return await SharedLink.findOneAndUpdate({ conversationId: conversationId, user }, update, {
|
||||
new: true,
|
||||
upsert: false,
|
||||
});
|
||||
},
|
||||
|
||||
deleteSharedLink: async (user, { shareId }) => {
|
||||
const share = await SharedLink.findOne({ shareId, user });
|
||||
if (!share) {
|
||||
return { message: 'Share not found' };
|
||||
}
|
||||
return await SharedLink.findOneAndDelete({ shareId, user });
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@ const mongoose = require('mongoose');
|
||||
const { isEnabled } = require('../server/utils/handleText');
|
||||
const transactionSchema = require('./schema/transaction');
|
||||
const { getMultiplier } = require('./tx');
|
||||
const { logger } = require('~/config');
|
||||
const Balance = require('./Balance');
|
||||
const cancelRate = 1.15;
|
||||
|
||||
@@ -11,7 +12,7 @@ transactionSchema.methods.calculateTokenValue = function () {
|
||||
this.tokenValue = this.rawAmount;
|
||||
}
|
||||
const { valueKey, tokenType, model, endpointTokenConfig } = this;
|
||||
const multiplier = getMultiplier({ valueKey, tokenType, model, endpointTokenConfig });
|
||||
const multiplier = Math.abs(getMultiplier({ valueKey, tokenType, model, endpointTokenConfig }));
|
||||
this.rate = multiplier;
|
||||
this.tokenValue = this.rawAmount * multiplier;
|
||||
if (this.context && this.tokenType === 'completion' && this.context === 'incomplete') {
|
||||
@@ -35,12 +36,44 @@ transactionSchema.statics.create = async function (transactionData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust the user's balance
|
||||
return await Balance.findOneAndUpdate(
|
||||
let balance = await Balance.findOne({ user: transaction.user }).lean();
|
||||
let incrementValue = transaction.tokenValue;
|
||||
|
||||
if (balance && balance?.tokenCredits + incrementValue < 0) {
|
||||
incrementValue = -balance.tokenCredits;
|
||||
}
|
||||
|
||||
balance = await Balance.findOneAndUpdate(
|
||||
{ user: transaction.user },
|
||||
{ $inc: { tokenCredits: transaction.tokenValue } },
|
||||
{ $inc: { tokenCredits: incrementValue } },
|
||||
{ upsert: true, new: true },
|
||||
).lean();
|
||||
|
||||
return {
|
||||
rate: transaction.rate,
|
||||
user: transaction.user.toString(),
|
||||
balance: balance.tokenCredits,
|
||||
[transaction.tokenType]: incrementValue,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Transaction', transactionSchema);
|
||||
const Transaction = mongoose.model('Transaction', transactionSchema);
|
||||
|
||||
/**
|
||||
* Queries and retrieves transactions based on a given filter.
|
||||
* @async
|
||||
* @function getTransactions
|
||||
* @param {Object} filter - MongoDB filter object to apply when querying transactions.
|
||||
* @returns {Promise<Array>} A promise that resolves to an array of matched transactions.
|
||||
* @throws {Error} Throws an error if querying the database fails.
|
||||
*/
|
||||
async function getTransactions(filter) {
|
||||
try {
|
||||
return await Transaction.find(filter).lean();
|
||||
} catch (error) {
|
||||
logger.error('Error querying transactions:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Transaction, getTransactions };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { logViolation } = require('~/cache');
|
||||
const Balance = require('./Balance');
|
||||
const { logViolation } = require('../cache');
|
||||
/**
|
||||
* Checks the balance for a user and determines if they can spend a certain amount.
|
||||
* If the user cannot spend the amount, it logs a violation and denies the request.
|
||||
@@ -25,7 +26,7 @@ const checkBalance = async ({ req, res, txData }) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
const type = 'token_balance';
|
||||
const type = ViolationTypes.TOKEN_BALANCE;
|
||||
const errorMessage = {
|
||||
type,
|
||||
balance,
|
||||
|
||||
@@ -22,14 +22,12 @@ const Key = require('./Key');
|
||||
const User = require('./User');
|
||||
const Session = require('./Session');
|
||||
const Balance = require('./Balance');
|
||||
const Transaction = require('./Transaction');
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Key,
|
||||
Session,
|
||||
Balance,
|
||||
Transaction,
|
||||
|
||||
hashPassword,
|
||||
updateUser,
|
||||
|
||||
@@ -155,7 +155,7 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
function (results, value, key) {
|
||||
return { ...results, [key]: 1 };
|
||||
},
|
||||
{ _id: 1 },
|
||||
{ _id: 1, __v: 1 },
|
||||
),
|
||||
).lean();
|
||||
|
||||
@@ -348,7 +348,7 @@ module.exports = function mongoMeili(schema, options) {
|
||||
try {
|
||||
meiliDoc = await client.index('convos').getDocument(doc.conversationId);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
logger.debug(
|
||||
'[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' +
|
||||
doc.conversationId,
|
||||
error,
|
||||
|
||||
@@ -45,7 +45,6 @@ const actionSchema = new Schema({
|
||||
auth: AuthSchema,
|
||||
domain: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
},
|
||||
// json_schema: Schema.Types.Mixed,
|
||||
|
||||
@@ -9,7 +9,6 @@ const assistantSchema = mongoose.Schema(
|
||||
},
|
||||
assistant_id: {
|
||||
type: String,
|
||||
unique: true,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -70,10 +70,14 @@ const conversationPreset = {
|
||||
type: String,
|
||||
},
|
||||
file_ids: { type: [{ type: String }], default: undefined },
|
||||
// vision
|
||||
// deprecated
|
||||
resendImages: {
|
||||
type: Boolean,
|
||||
},
|
||||
// files
|
||||
resendFiles: {
|
||||
type: Boolean,
|
||||
},
|
||||
imageDetail: {
|
||||
type: String,
|
||||
},
|
||||
@@ -84,6 +88,28 @@ const conversationPreset = {
|
||||
instructions: {
|
||||
type: String,
|
||||
},
|
||||
stop: { type: [{ type: String }], default: undefined },
|
||||
isArchived: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/* UI Components */
|
||||
iconURL: {
|
||||
type: String,
|
||||
},
|
||||
greeting: {
|
||||
type: String,
|
||||
},
|
||||
spec: {
|
||||
type: String,
|
||||
},
|
||||
tools: { type: [{ type: String }], default: undefined },
|
||||
maxContextTokens: {
|
||||
type: Number,
|
||||
},
|
||||
max_tokens: {
|
||||
type: Number,
|
||||
},
|
||||
};
|
||||
|
||||
const agentOptions = {
|
||||
|
||||
@@ -15,6 +15,9 @@ const mongoose = require('mongoose');
|
||||
* @property {'file'} object - Type of object, always 'file'
|
||||
* @property {string} type - Type of file
|
||||
* @property {number} usage - Number of uses of the file
|
||||
* @property {string} [context] - Context of the file origin
|
||||
* @property {boolean} [embedded] - Whether or not the file is embedded in vector db
|
||||
* @property {string} [model] - The model to identify the group region of the file (for Azure OpenAI hosting)
|
||||
* @property {string} [source] - The source of the file
|
||||
* @property {number} [width] - Optional width of the file
|
||||
* @property {number} [height] - Optional height of the file
|
||||
@@ -61,6 +64,9 @@ const fileSchema = mongoose.Schema(
|
||||
required: true,
|
||||
default: 'file',
|
||||
},
|
||||
embedded: {
|
||||
type: Boolean,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -78,6 +84,9 @@ const fileSchema = mongoose.Schema(
|
||||
type: String,
|
||||
default: FileSources.local,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
expiresAt: {
|
||||
@@ -90,4 +99,6 @@ const fileSchema = mongoose.Schema(
|
||||
},
|
||||
);
|
||||
|
||||
fileSchema.index({ createdAt: 1, updatedAt: 1 });
|
||||
|
||||
module.exports = fileSchema;
|
||||
|
||||
@@ -110,6 +110,10 @@ const messageSchema = mongoose.Schema(
|
||||
thread_id: {
|
||||
type: String,
|
||||
},
|
||||
/* frontend components */
|
||||
iconURL: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
38
api/models/schema/shareSchema.js
Normal file
38
api/models/schema/shareSchema.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const shareSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
|
||||
shareId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
isPublic: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAnonymous: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
module.exports = mongoose.model('SharedLink', shareSchema);
|
||||
@@ -1,4 +1,4 @@
|
||||
const Transaction = require('./Transaction');
|
||||
const { Transaction } = require('./Transaction');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
@@ -21,10 +21,15 @@ const { logger } = require('~/config');
|
||||
*/
|
||||
const spendTokens = async (txData, tokenUsage) => {
|
||||
const { promptTokens, completionTokens } = tokenUsage;
|
||||
logger.debug(`[spendTokens] conversationId: ${txData.conversationId} | Token usage: `, {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
});
|
||||
logger.debug(
|
||||
`[spendTokens] conversationId: ${txData.conversationId}${
|
||||
txData?.context ? ` | Context: ${txData?.context}` : ''
|
||||
} | Token usage: `,
|
||||
{
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
},
|
||||
);
|
||||
let prompt, completion;
|
||||
try {
|
||||
if (promptTokens >= 0) {
|
||||
@@ -35,7 +40,7 @@ const spendTokens = async (txData, tokenUsage) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!completionTokens) {
|
||||
if (!completionTokens && isNaN(completionTokens)) {
|
||||
logger.debug('[spendTokens] !completionTokens', { prompt, completion });
|
||||
return;
|
||||
}
|
||||
@@ -49,8 +54,12 @@ const spendTokens = async (txData, tokenUsage) => {
|
||||
prompt &&
|
||||
completion &&
|
||||
logger.debug('[spendTokens] Transaction data record against balance:', {
|
||||
prompt,
|
||||
completion,
|
||||
user: txData.user,
|
||||
prompt: prompt.prompt,
|
||||
promptRate: prompt.rate,
|
||||
completion: completion.completion,
|
||||
completionRate: completion.rate,
|
||||
balance: completion.balance,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('[spendTokens]', err);
|
||||
|
||||
@@ -3,6 +3,7 @@ const defaultRate = 6;
|
||||
|
||||
/**
|
||||
* Mapping of model token sizes to their respective multipliers for prompt and completion.
|
||||
* The rates are 1 USD per 1M tokens.
|
||||
* @type {Object.<string, {prompt: number, completion: number}>}
|
||||
*/
|
||||
const tokenValues = {
|
||||
@@ -11,8 +12,24 @@ const tokenValues = {
|
||||
'4k': { prompt: 1.5, completion: 2 },
|
||||
'16k': { prompt: 3, completion: 4 },
|
||||
'gpt-3.5-turbo-1106': { prompt: 1, completion: 2 },
|
||||
'gpt-4o': { prompt: 5, completion: 15 },
|
||||
'gpt-4-1106': { prompt: 10, completion: 30 },
|
||||
'gpt-3.5-turbo-0125': { prompt: 0.5, completion: 1.5 },
|
||||
'claude-3-opus': { prompt: 15, completion: 75 },
|
||||
'claude-3-sonnet': { prompt: 3, completion: 15 },
|
||||
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
|
||||
'claude-2.1': { prompt: 8, completion: 24 },
|
||||
'claude-2': { prompt: 8, completion: 24 },
|
||||
'claude-': { prompt: 0.8, completion: 2.4 },
|
||||
'command-r-plus': { prompt: 3, completion: 15 },
|
||||
'command-r': { prompt: 0.5, completion: 1.5 },
|
||||
/* cohere doesn't have rates for the older command models,
|
||||
so this was from https://artificialanalysis.ai/models/command-light/providers */
|
||||
command: { prompt: 0.38, completion: 0.38 },
|
||||
// 'gemini-1.5': { prompt: 7, completion: 21 }, // May 2nd, 2024 pricing
|
||||
// 'gemini': { prompt: 0.5, completion: 1.5 }, // May 2nd, 2024 pricing
|
||||
'gemini-1.5': { prompt: 0, completion: 0 }, // currently free
|
||||
gemini: { prompt: 0, completion: 0 }, // currently free
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -36,6 +53,10 @@ const getValueKey = (model, endpoint) => {
|
||||
return 'gpt-3.5-turbo-1106';
|
||||
} else if (modelName.includes('gpt-3.5')) {
|
||||
return '4k';
|
||||
} else if (modelName.includes('gpt-4o')) {
|
||||
return 'gpt-4o';
|
||||
} else if (modelName.includes('gpt-4-vision')) {
|
||||
return 'gpt-4-1106';
|
||||
} else if (modelName.includes('gpt-4-1106')) {
|
||||
return 'gpt-4-1106';
|
||||
} else if (modelName.includes('gpt-4-0125')) {
|
||||
@@ -46,6 +67,8 @@ const getValueKey = (model, endpoint) => {
|
||||
return '32k';
|
||||
} else if (modelName.includes('gpt-4')) {
|
||||
return '8k';
|
||||
} else if (tokenValues[modelName]) {
|
||||
return modelName;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -34,6 +34,20 @@ describe('getValueKey', () => {
|
||||
expect(getValueKey('openai/gpt-4-1106')).toBe('gpt-4-1106');
|
||||
expect(getValueKey('gpt-4-1106/openai/')).toBe('gpt-4-1106');
|
||||
});
|
||||
|
||||
it('should return "gpt-4-1106" for model type of "gpt-4-1106"', () => {
|
||||
expect(getValueKey('gpt-4-vision-preview')).toBe('gpt-4-1106');
|
||||
expect(getValueKey('openai/gpt-4-1106')).toBe('gpt-4-1106');
|
||||
expect(getValueKey('gpt-4-turbo')).toBe('gpt-4-1106');
|
||||
expect(getValueKey('gpt-4-0125')).toBe('gpt-4-1106');
|
||||
});
|
||||
|
||||
it('should return "gpt-4o" for model type of "gpt-4o"', () => {
|
||||
expect(getValueKey('gpt-4o-2024-05-13')).toBe('gpt-4o');
|
||||
expect(getValueKey('openai/gpt-4o')).toBe('gpt-4o');
|
||||
expect(getValueKey('gpt-4o-turbo')).toBe('gpt-4o');
|
||||
expect(getValueKey('gpt-4o-0125')).toBe('gpt-4o');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMultiplier', () => {
|
||||
@@ -77,6 +91,17 @@ describe('getMultiplier', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct multiplier for gpt-4o', () => {
|
||||
const valueKey = getValueKey('gpt-4o-2024-05-13');
|
||||
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4o'].prompt);
|
||||
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-4o'].completion,
|
||||
);
|
||||
expect(getMultiplier({ valueKey, tokenType: 'completion' })).not.toBe(
|
||||
tokenValues['gpt-4-1106'].completion,
|
||||
);
|
||||
});
|
||||
|
||||
it('should derive the valueKey from the model if not provided for new models', () => {
|
||||
expect(
|
||||
getMultiplier({ tokenType: 'prompt', model: 'gpt-3.5-turbo-1106-some-other-info' }),
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.2",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
"server-dev": "echo 'please run this from the root directory'",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"b:test": "NODE_ENV=test bun jest",
|
||||
"test:ci": "jest --ci"
|
||||
"test:ci": "jest --ci",
|
||||
"add-balance": "node ./add-balance.js",
|
||||
"list-balances": "node ./list-balances.js",
|
||||
"user-stats": "node ./user-stats.js",
|
||||
"create-user": "node ./create-user.js",
|
||||
"ban-user": "node ./ban-user.js",
|
||||
"delete-user": "node ./delete-user.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,18 +31,21 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/danny-avila/LibreChat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/danny-avila/LibreChat#readme",
|
||||
"homepage": "https://librechat.ai",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.5.4",
|
||||
"@anthropic-ai/sdk": "^0.16.1",
|
||||
"@azure/search-documents": "^12.0.0",
|
||||
"@google/generative-ai": "^0.5.0",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.1",
|
||||
"@langchain/community": "^0.0.17",
|
||||
"@langchain/google-genai": "^0.0.8",
|
||||
"@langchain/community": "^0.0.46",
|
||||
"@langchain/google-genai": "^0.0.11",
|
||||
"@langchain/google-vertexai": "^0.0.5",
|
||||
"agenda": "^5.0.0",
|
||||
"axios": "^1.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cohere-ai": "^6.0.0",
|
||||
"cohere-ai": "^7.9.1",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -59,14 +68,15 @@
|
||||
"langchain": "^0.0.214",
|
||||
"librechat-data-provider": "*",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.33.0",
|
||||
"meilisearch": "^0.38.0",
|
||||
"mime": "^3.0.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mongoose": "^7.1.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodejs-gpt": "^1.37.4",
|
||||
"nodemailer": "^6.9.4",
|
||||
"openai": "^4.20.1",
|
||||
"ollama": "^0.5.0",
|
||||
"openai": "^4.47.1",
|
||||
"openai-chat-tokens": "^0.2.8",
|
||||
"openid-client": "^5.4.2",
|
||||
"passport": "^0.6.0",
|
||||
@@ -79,7 +89,7 @@
|
||||
"passport-local": "^1.0.0",
|
||||
"pino": "^8.12.1",
|
||||
"sharp": "^0.32.6",
|
||||
"tiktoken": "^1.0.10",
|
||||
"tiktoken": "^1.0.15",
|
||||
"traverse": "^0.6.7",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"winston": "^3.11.0",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { getResponseSender, Constants } = require('librechat-data-provider');
|
||||
const { sendMessage, createOnProgress } = require('~/server/utils');
|
||||
const { saveMessage, getConvoTitle, getConvo } = require('~/models');
|
||||
const throttle = require('lodash/throttle');
|
||||
const { getResponseSender, Constants, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { createAbortController, handleAbortError } = require('~/server/middleware');
|
||||
const { sendMessage, createOnProgress } = require('~/server/utils');
|
||||
const { saveMessage, getConvo } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
@@ -16,13 +17,10 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
|
||||
logger.debug('[AskController]', { text, conversationId, ...endpointOption });
|
||||
|
||||
let metadata;
|
||||
let userMessage;
|
||||
let promptTokens;
|
||||
let userMessageId;
|
||||
let responseMessageId;
|
||||
let lastSavedTimestamp = 0;
|
||||
let saveDelay = 100;
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.modelOptions.model,
|
||||
@@ -31,8 +29,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
const newConvo = !conversationId;
|
||||
const user = req.user.id;
|
||||
|
||||
const addMetadata = (data) => (metadata = data);
|
||||
|
||||
const getReqData = (data = {}) => {
|
||||
for (let key in data) {
|
||||
if (key === 'userMessage') {
|
||||
@@ -52,13 +48,10 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
|
||||
try {
|
||||
const { client } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
const unfinished = endpointOption.endpoint === EModelEndpoint.google ? false : true;
|
||||
const { onProgress: progressCallback, getPartialText } = createOnProgress({
|
||||
onProgress: ({ text: partialText }) => {
|
||||
const currentTimestamp = Date.now();
|
||||
|
||||
if (currentTimestamp - lastSavedTimestamp > saveDelay) {
|
||||
lastSavedTimestamp = currentTimestamp;
|
||||
onProgress: throttle(
|
||||
({ text: partialText }) => {
|
||||
saveMessage({
|
||||
messageId: responseMessageId,
|
||||
sender,
|
||||
@@ -66,16 +59,14 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId,
|
||||
text: partialText,
|
||||
model: client.modelOptions.model,
|
||||
unfinished: true,
|
||||
unfinished,
|
||||
error: false,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
if (saveDelay < 500) {
|
||||
saveDelay = 500;
|
||||
}
|
||||
},
|
||||
},
|
||||
3000,
|
||||
{ trailing: false },
|
||||
),
|
||||
});
|
||||
|
||||
getText = getPartialText;
|
||||
@@ -92,6 +83,20 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
||||
|
||||
res.on('close', () => {
|
||||
logger.debug('[AskController] Request closed');
|
||||
if (!abortController) {
|
||||
return;
|
||||
} else if (abortController.signal.aborted) {
|
||||
return;
|
||||
} else if (abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
logger.debug('[AskController] Request aborted on close');
|
||||
});
|
||||
|
||||
const messageOptions = {
|
||||
user,
|
||||
parentMessageId,
|
||||
@@ -99,7 +104,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
overrideParentMessageId,
|
||||
getReqData,
|
||||
onStart,
|
||||
addMetadata,
|
||||
abortController,
|
||||
onProgress: progressCallback.call(null, {
|
||||
res,
|
||||
@@ -114,22 +118,23 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
response.parentMessageId = overrideParentMessageId;
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
response = { ...response, ...metadata };
|
||||
}
|
||||
|
||||
response.endpoint = endpointOption.endpoint;
|
||||
|
||||
const conversation = await getConvo(user, conversationId);
|
||||
conversation.title =
|
||||
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
||||
|
||||
if (client.options.attachments) {
|
||||
userMessage.files = client.options.attachments;
|
||||
conversation.model = endpointOption.modelOptions.model;
|
||||
delete userMessage.image_urls;
|
||||
}
|
||||
|
||||
if (!abortController.signal.aborted) {
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(user, conversationId),
|
||||
final: true,
|
||||
conversation: await getConvo(user, conversationId),
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response,
|
||||
});
|
||||
|
||||
@@ -76,14 +76,14 @@ const refreshController = async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
let payload;
|
||||
payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
const userId = payload.id;
|
||||
const user = await User.findOne({ _id: userId });
|
||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
const user = await User.findOne({ _id: payload.id });
|
||||
if (!user) {
|
||||
return res.status(401).redirect('/login');
|
||||
}
|
||||
|
||||
const userId = payload.id;
|
||||
|
||||
if (process.env.NODE_ENV === 'CI') {
|
||||
const token = await setAuthTokens(userId, res);
|
||||
const userObj = user.toJSON();
|
||||
@@ -118,6 +118,6 @@ module.exports = {
|
||||
getUserController,
|
||||
refreshController,
|
||||
registrationController,
|
||||
resetPasswordRequestController,
|
||||
resetPasswordController,
|
||||
resetPasswordRequestController,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { getResponseSender } = require('librechat-data-provider');
|
||||
const { sendMessage, createOnProgress } = require('~/server/utils');
|
||||
const { saveMessage, getConvoTitle, getConvo } = require('~/models');
|
||||
const throttle = require('lodash/throttle');
|
||||
const { getResponseSender, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { createAbortController, handleAbortError } = require('~/server/middleware');
|
||||
const { sendMessage, createOnProgress } = require('~/server/utils');
|
||||
const { saveMessage, getConvo } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const EditController = async (req, res, next, initializeClient) => {
|
||||
@@ -25,11 +26,8 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
...endpointOption,
|
||||
});
|
||||
|
||||
let metadata;
|
||||
let userMessage;
|
||||
let promptTokens;
|
||||
let lastSavedTimestamp = 0;
|
||||
let saveDelay = 100;
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.modelOptions.model,
|
||||
@@ -38,7 +36,6 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
const userMessageId = parentMessageId;
|
||||
const user = req.user.id;
|
||||
|
||||
const addMetadata = (data) => (metadata = data);
|
||||
const getReqData = (data = {}) => {
|
||||
for (let key in data) {
|
||||
if (key === 'userMessage') {
|
||||
@@ -51,13 +48,11 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unfinished = endpointOption.endpoint === EModelEndpoint.google ? false : true;
|
||||
const { onProgress: progressCallback, getPartialText } = createOnProgress({
|
||||
generation,
|
||||
onProgress: ({ text: partialText }) => {
|
||||
const currentTimestamp = Date.now();
|
||||
|
||||
if (currentTimestamp - lastSavedTimestamp > saveDelay) {
|
||||
lastSavedTimestamp = currentTimestamp;
|
||||
onProgress: throttle(
|
||||
({ text: partialText }) => {
|
||||
saveMessage({
|
||||
messageId: responseMessageId,
|
||||
sender,
|
||||
@@ -65,17 +60,15 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId,
|
||||
text: partialText,
|
||||
model: endpointOption.modelOptions.model,
|
||||
unfinished: true,
|
||||
unfinished,
|
||||
isEdited: true,
|
||||
error: false,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
if (saveDelay < 500) {
|
||||
saveDelay = 500;
|
||||
}
|
||||
},
|
||||
},
|
||||
3000,
|
||||
{ trailing: false },
|
||||
),
|
||||
});
|
||||
|
||||
const getAbortData = () => ({
|
||||
@@ -90,6 +83,20 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
||||
|
||||
res.on('close', () => {
|
||||
logger.debug('[EditController] Request closed');
|
||||
if (!abortController) {
|
||||
return;
|
||||
} else if (abortController.signal.aborted) {
|
||||
return;
|
||||
} else if (abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
logger.debug('[EditController] Request aborted on close');
|
||||
});
|
||||
|
||||
try {
|
||||
const { client } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
@@ -104,7 +111,6 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
overrideParentMessageId,
|
||||
getReqData,
|
||||
onStart,
|
||||
addMetadata,
|
||||
abortController,
|
||||
onProgress: progressCallback.call(null, {
|
||||
res,
|
||||
@@ -113,15 +119,19 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
}),
|
||||
});
|
||||
|
||||
if (metadata) {
|
||||
response = { ...response, ...metadata };
|
||||
const conversation = await getConvo(user, conversationId);
|
||||
conversation.title =
|
||||
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
||||
|
||||
if (client.options.attachments) {
|
||||
conversation.model = endpointOption.modelOptions.model;
|
||||
}
|
||||
|
||||
if (!abortController.signal.aborted) {
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(user, conversationId),
|
||||
final: true,
|
||||
conversation: await getConvo(user, conversationId),
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { CacheKeys, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { CacheKeys, EModelEndpoint, orderEndpointsConfig } = require('librechat-data-provider');
|
||||
const { loadDefaultEndpointsConfig, loadConfigEndpoints } = require('~/server/services/Config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
@@ -10,15 +10,42 @@ async function endpointController(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultEndpointsConfig = await loadDefaultEndpointsConfig();
|
||||
const customConfigEndpoints = await loadConfigEndpoints();
|
||||
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(req);
|
||||
const customConfigEndpoints = await loadConfigEndpoints(req);
|
||||
|
||||
const endpointsConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
|
||||
if (endpointsConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
|
||||
endpointsConfig[EModelEndpoint.assistants].disableBuilder =
|
||||
req.app.locals[EModelEndpoint.assistants].disableBuilder;
|
||||
/** @type {TEndpointsConfig} */
|
||||
const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
|
||||
if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
|
||||
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
|
||||
req.app.locals[EModelEndpoint.assistants];
|
||||
|
||||
mergedConfig[EModelEndpoint.assistants] = {
|
||||
...mergedConfig[EModelEndpoint.assistants],
|
||||
version,
|
||||
retrievalModels,
|
||||
disableBuilder,
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
mergedConfig[EModelEndpoint.azureAssistants] &&
|
||||
req.app.locals?.[EModelEndpoint.azureAssistants]
|
||||
) {
|
||||
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
|
||||
req.app.locals[EModelEndpoint.azureAssistants];
|
||||
|
||||
mergedConfig[EModelEndpoint.azureAssistants] = {
|
||||
...mergedConfig[EModelEndpoint.azureAssistants],
|
||||
version,
|
||||
retrievalModels,
|
||||
disableBuilder,
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
const endpointsConfig = orderEndpointsConfig(mergedConfig);
|
||||
|
||||
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
|
||||
res.send(JSON.stringify(endpointsConfig));
|
||||
}
|
||||
|
||||
@@ -2,12 +2,26 @@ const { CacheKeys } = require('librechat-data-provider');
|
||||
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
async function modelController(req, res) {
|
||||
const getModelsConfig = async (req) => {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
let modelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
|
||||
if (!modelsConfig) {
|
||||
modelsConfig = await loadModels(req);
|
||||
}
|
||||
|
||||
return modelsConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the models from the config.
|
||||
* @param {Express.Request} req - The Express request object.
|
||||
* @returns {Promise<TModelsConfig>} The models config.
|
||||
*/
|
||||
async function loadModels(req) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cachedModelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
|
||||
if (cachedModelsConfig) {
|
||||
res.send(cachedModelsConfig);
|
||||
return;
|
||||
return cachedModelsConfig;
|
||||
}
|
||||
const defaultModelsConfig = await loadDefaultModels(req);
|
||||
const customModelsConfig = await loadConfigModels(req);
|
||||
@@ -15,7 +29,12 @@ async function modelController(req, res) {
|
||||
const modelConfig = { ...defaultModelsConfig, ...customModelsConfig };
|
||||
|
||||
await cache.set(CacheKeys.MODELS_CONFIG, modelConfig);
|
||||
return modelConfig;
|
||||
}
|
||||
|
||||
async function modelController(req, res) {
|
||||
const modelConfig = await loadModels(req);
|
||||
res.send(modelConfig);
|
||||
}
|
||||
|
||||
module.exports = modelController;
|
||||
module.exports = { modelController, loadModels, getModelsConfig };
|
||||
|
||||
@@ -55,19 +55,27 @@ const getAvailablePluginsController = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {{ filteredTools: string[], includedTools: string[] }} */
|
||||
const { filteredTools = [], includedTools = [] } = req.app.locals;
|
||||
const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8');
|
||||
|
||||
const jsonData = JSON.parse(pluginManifest);
|
||||
/** @type {TPlugin[]} */
|
||||
|
||||
const uniquePlugins = filterUniquePlugins(jsonData);
|
||||
const authenticatedPlugins = uniquePlugins.map((plugin) => {
|
||||
if (isPluginAuthenticated(plugin)) {
|
||||
return { ...plugin, authenticated: true };
|
||||
} else {
|
||||
return plugin;
|
||||
}
|
||||
});
|
||||
const plugins = await addOpenAPISpecs(authenticatedPlugins);
|
||||
let authenticatedPlugins = [];
|
||||
for (const plugin of uniquePlugins) {
|
||||
authenticatedPlugins.push(
|
||||
isPluginAuthenticated(plugin) ? { ...plugin, authenticated: true } : plugin,
|
||||
);
|
||||
}
|
||||
|
||||
let plugins = await addOpenAPISpecs(authenticatedPlugins);
|
||||
|
||||
if (includedTools.length > 0) {
|
||||
plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey));
|
||||
} else {
|
||||
plugins = plugins.filter((plugin) => !filteredTools.includes(plugin.pluginKey));
|
||||
}
|
||||
|
||||
await cache.set(CacheKeys.PLUGINS, plugins);
|
||||
res.status(200).json(plugins);
|
||||
} catch (error) {
|
||||
|
||||
650
api/server/controllers/assistants/chatV1.js
Normal file
650
api/server/controllers/assistants/chatV1.js
Normal file
@@ -0,0 +1,650 @@
|
||||
const { v4 } = require('uuid');
|
||||
const {
|
||||
Constants,
|
||||
RunStatus,
|
||||
CacheKeys,
|
||||
ContentTypes,
|
||||
EModelEndpoint,
|
||||
ViolationTypes,
|
||||
ImageVisionTool,
|
||||
checkOpenAIStorage,
|
||||
AssistantStreamEvents,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
initThread,
|
||||
recordUsage,
|
||||
saveUserMessage,
|
||||
checkMessageGaps,
|
||||
addThreadMetadata,
|
||||
saveAssistantMessage,
|
||||
} = require('~/server/services/Threads');
|
||||
const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils');
|
||||
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
|
||||
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { getTransactions } = require('~/models/Transaction');
|
||||
const checkBalance = require('~/models/checkBalance');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { handleAbortError } = require('~/server/middleware');
|
||||
|
||||
const ten_minutes = 1000 * 60 * 10;
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
* @access Public
|
||||
* @param {Express.Request} req - The request object, containing the request data.
|
||||
* @param {Express.Response} res - The response object, used to send back a response.
|
||||
* @returns {void}
|
||||
*/
|
||||
const chatV1 = async (req, res) => {
|
||||
logger.debug('[/assistants/chat/] req.body', req.body);
|
||||
|
||||
const {
|
||||
text,
|
||||
model,
|
||||
endpoint,
|
||||
files = [],
|
||||
promptPrefix,
|
||||
assistant_id,
|
||||
instructions,
|
||||
thread_id: _thread_id,
|
||||
messageId: _messageId,
|
||||
conversationId: convoId,
|
||||
parentMessageId: _parentId = Constants.NO_PARENT,
|
||||
} = req.body;
|
||||
|
||||
/** @type {Partial<TAssistantEndpoint>} */
|
||||
const assistantsConfig = req.app.locals?.[endpoint];
|
||||
|
||||
if (assistantsConfig) {
|
||||
const { supportedIds, excludedIds } = assistantsConfig;
|
||||
const error = { message: 'Assistant not supported' };
|
||||
if (supportedIds?.length && !supportedIds.includes(assistant_id)) {
|
||||
return await handleAbortError(res, req, error, {
|
||||
sender: 'System',
|
||||
conversationId: convoId,
|
||||
messageId: v4(),
|
||||
parentMessageId: _messageId,
|
||||
error,
|
||||
});
|
||||
} else if (excludedIds?.length && excludedIds.includes(assistant_id)) {
|
||||
return await handleAbortError(res, req, error, {
|
||||
sender: 'System',
|
||||
conversationId: convoId,
|
||||
messageId: v4(),
|
||||
parentMessageId: _messageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {OpenAIClient} */
|
||||
let openai;
|
||||
/** @type {string|undefined} - the current thread id */
|
||||
let thread_id = _thread_id;
|
||||
/** @type {string|undefined} - the current run id */
|
||||
let run_id;
|
||||
/** @type {string|undefined} - the parent messageId */
|
||||
let parentMessageId = _parentId;
|
||||
/** @type {TMessage[]} */
|
||||
let previousMessages = [];
|
||||
/** @type {import('librechat-data-provider').TConversation | null} */
|
||||
let conversation = null;
|
||||
/** @type {string[]} */
|
||||
let file_ids = [];
|
||||
/** @type {Set<string>} */
|
||||
let attachedFileIds = new Set();
|
||||
/** @type {TMessage | null} */
|
||||
let requestMessage = null;
|
||||
/** @type {undefined | Promise<ChatCompletion>} */
|
||||
let visionPromise;
|
||||
|
||||
const userMessageId = v4();
|
||||
const responseMessageId = v4();
|
||||
|
||||
/** @type {string} - The conversation UUID - created if undefined */
|
||||
const conversationId = convoId ?? v4();
|
||||
|
||||
const cache = getLogStores(CacheKeys.ABORT_KEYS);
|
||||
const cacheKey = `${req.user.id}:${conversationId}`;
|
||||
|
||||
/** @type {Run | undefined} - The completed run, undefined if incomplete */
|
||||
let completedRun;
|
||||
|
||||
const handleError = async (error) => {
|
||||
const defaultErrorMessage =
|
||||
'The Assistant run failed to initialize. Try sending a message in a new conversation.';
|
||||
const messageData = {
|
||||
thread_id,
|
||||
assistant_id,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender: 'System',
|
||||
user: req.user.id,
|
||||
shouldSaveMessage: false,
|
||||
messageId: responseMessageId,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
if (error.message === 'Run cancelled') {
|
||||
return res.end();
|
||||
} else if (error.message === 'Request closed' && completedRun) {
|
||||
return;
|
||||
} else if (error.message === 'Request closed') {
|
||||
logger.debug('[/assistants/chat/] Request aborted on close');
|
||||
} else if (/Files.*are invalid/.test(error.message)) {
|
||||
const errorMessage = `Files are invalid, or may not have uploaded yet.${
|
||||
endpoint === EModelEndpoint.azureAssistants
|
||||
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
|
||||
: ''
|
||||
}`;
|
||||
return sendResponse(res, messageData, errorMessage);
|
||||
} else if (error?.message?.includes('string too long')) {
|
||||
return sendResponse(
|
||||
res,
|
||||
messageData,
|
||||
'Message too long. The Assistants API has a limit of 32,768 characters per message. Please shorten it and try again.',
|
||||
);
|
||||
} else if (error?.message?.includes(ViolationTypes.TOKEN_BALANCE)) {
|
||||
return sendResponse(res, messageData, error.message);
|
||||
} else {
|
||||
logger.error('[/assistants/chat/]', error);
|
||||
}
|
||||
|
||||
if (!openai || !thread_id || !run_id) {
|
||||
return sendResponse(res, messageData, defaultErrorMessage);
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
try {
|
||||
const status = await cache.get(cacheKey);
|
||||
if (status === 'cancelled') {
|
||||
logger.debug('[/assistants/chat/] Run already cancelled');
|
||||
return res.end();
|
||||
}
|
||||
await cache.delete(cacheKey);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
logger.debug('[/assistants/chat/] Cancelled run:', cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error cancelling run', error);
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
let run;
|
||||
try {
|
||||
run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
user: req.user.id,
|
||||
conversationId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error fetching or processing run', error);
|
||||
}
|
||||
|
||||
let finalEvent;
|
||||
try {
|
||||
const runMessages = await checkMessageGaps({
|
||||
openai,
|
||||
run_id,
|
||||
endpoint,
|
||||
thread_id,
|
||||
conversationId,
|
||||
latestMessageId: responseMessageId,
|
||||
});
|
||||
|
||||
const errorContentPart = {
|
||||
text: {
|
||||
value:
|
||||
error?.message ?? 'There was an error processing your request. Please try again later.',
|
||||
},
|
||||
type: ContentTypes.ERROR,
|
||||
};
|
||||
|
||||
if (!Array.isArray(runMessages[runMessages.length - 1]?.content)) {
|
||||
runMessages[runMessages.length - 1].content = [errorContentPart];
|
||||
} else {
|
||||
const contentParts = runMessages[runMessages.length - 1].content;
|
||||
for (let i = 0; i < contentParts.length; i++) {
|
||||
const currentPart = contentParts[i];
|
||||
/** @type {CodeToolCall | RetrievalToolCall | FunctionToolCall | undefined} */
|
||||
const toolCall = currentPart?.[ContentTypes.TOOL_CALL];
|
||||
if (
|
||||
toolCall &&
|
||||
toolCall?.function &&
|
||||
!(toolCall?.function?.output || toolCall?.function?.output?.length)
|
||||
) {
|
||||
contentParts[i] = {
|
||||
...currentPart,
|
||||
[ContentTypes.TOOL_CALL]: {
|
||||
...toolCall,
|
||||
function: {
|
||||
...toolCall.function,
|
||||
output: 'error processing tool',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
runMessages[runMessages.length - 1].content.push(errorContentPart);
|
||||
}
|
||||
|
||||
finalEvent = {
|
||||
final: true,
|
||||
conversation: await getConvo(req.user.id, conversationId),
|
||||
runMessages,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error finalizing error process', error);
|
||||
return sendResponse(res, messageData, 'The Assistant run failed');
|
||||
}
|
||||
|
||||
return sendResponse(res, finalEvent);
|
||||
};
|
||||
|
||||
try {
|
||||
res.on('close', async () => {
|
||||
if (!completedRun) {
|
||||
await handleError(new Error('Request closed'));
|
||||
}
|
||||
});
|
||||
|
||||
if (convoId && !_thread_id) {
|
||||
completedRun = true;
|
||||
throw new Error('Missing thread_id for existing conversation');
|
||||
}
|
||||
|
||||
if (!assistant_id) {
|
||||
completedRun = true;
|
||||
throw new Error('Missing assistant_id');
|
||||
}
|
||||
|
||||
const checkBalanceBeforeRun = async () => {
|
||||
if (!isEnabled(process.env.CHECK_BALANCE)) {
|
||||
return;
|
||||
}
|
||||
const transactions =
|
||||
(await getTransactions({
|
||||
user: req.user.id,
|
||||
context: 'message',
|
||||
conversationId,
|
||||
})) ?? [];
|
||||
|
||||
const totalPreviousTokens = Math.abs(
|
||||
transactions.reduce((acc, curr) => acc + curr.rawAmount, 0),
|
||||
);
|
||||
|
||||
// TODO: make promptBuffer a config option; buffer for titles, needs buffer for system instructions
|
||||
const promptBuffer = parentMessageId === Constants.NO_PARENT && !_thread_id ? 200 : 0;
|
||||
// 5 is added for labels
|
||||
let promptTokens = (await countTokens(text + (promptPrefix ?? ''))) + 5;
|
||||
promptTokens += totalPreviousTokens + promptBuffer;
|
||||
// Count tokens up to the current context window
|
||||
promptTokens = Math.min(promptTokens, getModelMaxTokens(model));
|
||||
|
||||
await checkBalance({
|
||||
req,
|
||||
res,
|
||||
txData: {
|
||||
model,
|
||||
user: req.user.id,
|
||||
tokenType: 'prompt',
|
||||
amount: promptTokens,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { openai: _openai, client } = await getOpenAIClient({
|
||||
req,
|
||||
res,
|
||||
endpointOption: req.body.endpointOption,
|
||||
initAppClient: true,
|
||||
});
|
||||
|
||||
openai = _openai;
|
||||
|
||||
if (previousMessages.length) {
|
||||
parentMessageId = previousMessages[previousMessages.length - 1].messageId;
|
||||
}
|
||||
|
||||
let userMessage = {
|
||||
role: 'user',
|
||||
content: text,
|
||||
metadata: {
|
||||
messageId: userMessageId,
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {CreateRunBody | undefined} */
|
||||
const body = {
|
||||
assistant_id,
|
||||
model,
|
||||
};
|
||||
|
||||
if (promptPrefix) {
|
||||
body.additional_instructions = promptPrefix;
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
body.instructions = instructions;
|
||||
}
|
||||
|
||||
const getRequestFileIds = async () => {
|
||||
let thread_file_ids = [];
|
||||
if (convoId) {
|
||||
const convo = await getConvo(req.user.id, convoId);
|
||||
if (convo && convo.file_ids) {
|
||||
thread_file_ids = convo.file_ids;
|
||||
}
|
||||
}
|
||||
|
||||
file_ids = files.map(({ file_id }) => file_id);
|
||||
if (file_ids.length || thread_file_ids.length) {
|
||||
userMessage.file_ids = file_ids;
|
||||
attachedFileIds = new Set([...file_ids, ...thread_file_ids]);
|
||||
}
|
||||
};
|
||||
|
||||
const addVisionPrompt = async () => {
|
||||
if (!req.body.endpointOption.attachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {MongoFile[]} */
|
||||
const attachments = await req.body.endpointOption.attachments;
|
||||
if (attachments && attachments.every((attachment) => checkOpenAIStorage(attachment.source))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assistant = await openai.beta.assistants.retrieve(assistant_id);
|
||||
const visionToolIndex = assistant.tools.findIndex(
|
||||
(tool) => tool?.function && tool?.function?.name === ImageVisionTool.function.name,
|
||||
);
|
||||
|
||||
if (visionToolIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let visionMessage = {
|
||||
role: 'user',
|
||||
content: '',
|
||||
};
|
||||
const files = await client.addImageURLs(visionMessage, attachments);
|
||||
if (!visionMessage.image_urls?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageCount = visionMessage.image_urls.length;
|
||||
const plural = imageCount > 1;
|
||||
visionMessage.content = createVisionPrompt(plural);
|
||||
visionMessage = formatMessage({ message: visionMessage, endpoint: EModelEndpoint.openAI });
|
||||
|
||||
visionPromise = openai.chat.completions.create({
|
||||
model: 'gpt-4-vision-preview',
|
||||
messages: [visionMessage],
|
||||
max_tokens: 4000,
|
||||
});
|
||||
|
||||
const pluralized = plural ? 's' : '';
|
||||
body.additional_instructions = `${
|
||||
body.additional_instructions ? `${body.additional_instructions}\n` : ''
|
||||
}The user has uploaded ${imageCount} image${pluralized}.
|
||||
Use the \`${ImageVisionTool.function.name}\` tool to retrieve ${
|
||||
plural ? '' : 'a '
|
||||
}detailed text description${pluralized} for ${plural ? 'each' : 'the'} image${pluralized}.`;
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
const initializeThread = async () => {
|
||||
/** @type {[ undefined | MongoFile[]]}*/
|
||||
const [processedFiles] = await Promise.all([addVisionPrompt(), getRequestFileIds()]);
|
||||
// TODO: may allow multiple messages to be created beforehand in a future update
|
||||
const initThreadBody = {
|
||||
messages: [userMessage],
|
||||
metadata: {
|
||||
user: req.user.id,
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
|
||||
if (processedFiles) {
|
||||
for (const file of processedFiles) {
|
||||
if (!checkOpenAIStorage(file.source)) {
|
||||
attachedFileIds.delete(file.file_id);
|
||||
const index = file_ids.indexOf(file.file_id);
|
||||
if (index > -1) {
|
||||
file_ids.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userMessage.file_ids = file_ids;
|
||||
}
|
||||
|
||||
const result = await initThread({ openai, body: initThreadBody, thread_id });
|
||||
thread_id = result.thread_id;
|
||||
|
||||
createOnTextProgress({
|
||||
openai,
|
||||
conversationId,
|
||||
userMessageId,
|
||||
messageId: responseMessageId,
|
||||
thread_id,
|
||||
});
|
||||
|
||||
requestMessage = {
|
||||
user: req.user.id,
|
||||
text,
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
// TODO: make sure client sends correct format for `files`, use zod
|
||||
files,
|
||||
file_ids,
|
||||
conversationId,
|
||||
isCreatedByUser: true,
|
||||
assistant_id,
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
previousMessages.push(requestMessage);
|
||||
|
||||
/* asynchronous */
|
||||
saveUserMessage({ ...requestMessage, model });
|
||||
|
||||
conversation = {
|
||||
conversationId,
|
||||
endpoint,
|
||||
promptPrefix: promptPrefix,
|
||||
instructions: instructions,
|
||||
assistant_id,
|
||||
// model,
|
||||
};
|
||||
|
||||
if (file_ids.length) {
|
||||
conversation.file_ids = file_ids;
|
||||
}
|
||||
};
|
||||
|
||||
const promises = [initializeThread(), checkBalanceBeforeRun()];
|
||||
await Promise.all(promises);
|
||||
|
||||
const sendInitialResponse = () => {
|
||||
sendMessage(res, {
|
||||
sync: true,
|
||||
conversationId,
|
||||
// messages: previousMessages,
|
||||
requestMessage,
|
||||
responseMessage: {
|
||||
user: req.user.id,
|
||||
messageId: openai.responseMessage.messageId,
|
||||
parentMessageId: userMessageId,
|
||||
conversationId,
|
||||
assistant_id,
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {RunResponse | typeof StreamRunManager | undefined} */
|
||||
let response;
|
||||
|
||||
const processRun = async (retry = false) => {
|
||||
if (endpoint === EModelEndpoint.azureAssistants) {
|
||||
body.model = openai._options.model;
|
||||
openai.attachedFileIds = attachedFileIds;
|
||||
openai.visionPromise = visionPromise;
|
||||
if (retry) {
|
||||
response = await runAssistant({
|
||||
openai,
|
||||
thread_id,
|
||||
run_id,
|
||||
in_progress: openai.in_progress,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/* NOTE:
|
||||
* By default, a Run will use the model and tools configuration specified in Assistant object,
|
||||
* but you can override most of these when creating the Run for added flexibility:
|
||||
*/
|
||||
const run = await createRun({
|
||||
openai,
|
||||
thread_id,
|
||||
body,
|
||||
});
|
||||
|
||||
run_id = run.id;
|
||||
await cache.set(cacheKey, `${thread_id}:${run_id}`, ten_minutes);
|
||||
sendInitialResponse();
|
||||
|
||||
// todo: retry logic
|
||||
response = await runAssistant({ openai, thread_id, run_id });
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {{[AssistantStreamEvents.ThreadRunCreated]: (event: ThreadRunCreated) => Promise<void>}} */
|
||||
const handlers = {
|
||||
[AssistantStreamEvents.ThreadRunCreated]: async (event) => {
|
||||
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, ten_minutes);
|
||||
run_id = event.data.id;
|
||||
sendInitialResponse();
|
||||
},
|
||||
};
|
||||
|
||||
const streamRunManager = new StreamRunManager({
|
||||
req,
|
||||
res,
|
||||
openai,
|
||||
handlers,
|
||||
thread_id,
|
||||
visionPromise,
|
||||
attachedFileIds,
|
||||
responseMessage: openai.responseMessage,
|
||||
// streamOptions: {
|
||||
|
||||
// },
|
||||
});
|
||||
|
||||
await streamRunManager.runAssistant({
|
||||
thread_id,
|
||||
body,
|
||||
});
|
||||
|
||||
response = streamRunManager;
|
||||
};
|
||||
|
||||
await processRun();
|
||||
logger.debug('[/assistants/chat/] response', {
|
||||
run: response.run,
|
||||
steps: response.steps,
|
||||
});
|
||||
|
||||
if (response.run.status === RunStatus.CANCELLED) {
|
||||
logger.debug('[/assistants/chat/] Run cancelled, handled by `abortRun`');
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (response.run.status === RunStatus.IN_PROGRESS) {
|
||||
processRun(true);
|
||||
}
|
||||
|
||||
completedRun = response.run;
|
||||
|
||||
/** @type {ResponseMessage} */
|
||||
const responseMessage = {
|
||||
...(response.responseMessage ?? response.finalMessage),
|
||||
parentMessageId: userMessageId,
|
||||
conversationId,
|
||||
user: req.user.id,
|
||||
assistant_id,
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
sendMessage(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
requestMessage: {
|
||||
parentMessageId,
|
||||
thread_id,
|
||||
},
|
||||
});
|
||||
res.end();
|
||||
|
||||
await saveAssistantMessage({ ...responseMessage, model });
|
||||
|
||||
if (parentMessageId === Constants.NO_PARENT && !_thread_id) {
|
||||
addTitle(req, {
|
||||
text,
|
||||
responseText: response.text,
|
||||
conversationId,
|
||||
client,
|
||||
});
|
||||
}
|
||||
|
||||
await addThreadMetadata({
|
||||
openai,
|
||||
thread_id,
|
||||
messageId: responseMessage.messageId,
|
||||
messages: response.messages,
|
||||
});
|
||||
|
||||
if (!response.run.usage) {
|
||||
await sleep(3000);
|
||||
completedRun = await openai.beta.threads.runs.retrieve(thread_id, response.run.id);
|
||||
if (completedRun.usage) {
|
||||
await recordUsage({
|
||||
...completedRun.usage,
|
||||
user: req.user.id,
|
||||
model: completedRun.model ?? model,
|
||||
conversationId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await recordUsage({
|
||||
...response.run.usage,
|
||||
user: req.user.id,
|
||||
model: response.run.model ?? model,
|
||||
conversationId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = chatV1;
|
||||
618
api/server/controllers/assistants/chatV2.js
Normal file
618
api/server/controllers/assistants/chatV2.js
Normal file
@@ -0,0 +1,618 @@
|
||||
const { v4 } = require('uuid');
|
||||
const {
|
||||
Constants,
|
||||
RunStatus,
|
||||
CacheKeys,
|
||||
ContentTypes,
|
||||
ToolCallTypes,
|
||||
EModelEndpoint,
|
||||
ViolationTypes,
|
||||
retrievalMimeTypes,
|
||||
AssistantStreamEvents,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
initThread,
|
||||
recordUsage,
|
||||
saveUserMessage,
|
||||
checkMessageGaps,
|
||||
addThreadMetadata,
|
||||
saveAssistantMessage,
|
||||
} = require('~/server/services/Threads');
|
||||
const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils');
|
||||
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { getTransactions } = require('~/models/Transaction');
|
||||
const checkBalance = require('~/models/checkBalance');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { handleAbortError } = require('~/server/middleware');
|
||||
|
||||
const ten_minutes = 1000 * 60 * 10;
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
* @access Public
|
||||
* @param {Express.Request} req - The request object, containing the request data.
|
||||
* @param {Express.Response} res - The response object, used to send back a response.
|
||||
* @returns {void}
|
||||
*/
|
||||
const chatV2 = async (req, res) => {
|
||||
logger.debug('[/assistants/chat/] req.body', req.body);
|
||||
|
||||
/** @type {{ files: MongoFile[]}} */
|
||||
const {
|
||||
text,
|
||||
model,
|
||||
endpoint,
|
||||
files = [],
|
||||
promptPrefix,
|
||||
assistant_id,
|
||||
instructions,
|
||||
thread_id: _thread_id,
|
||||
messageId: _messageId,
|
||||
conversationId: convoId,
|
||||
parentMessageId: _parentId = Constants.NO_PARENT,
|
||||
} = req.body;
|
||||
|
||||
/** @type {Partial<TAssistantEndpoint>} */
|
||||
const assistantsConfig = req.app.locals?.[endpoint];
|
||||
|
||||
if (assistantsConfig) {
|
||||
const { supportedIds, excludedIds } = assistantsConfig;
|
||||
const error = { message: 'Assistant not supported' };
|
||||
if (supportedIds?.length && !supportedIds.includes(assistant_id)) {
|
||||
return await handleAbortError(res, req, error, {
|
||||
sender: 'System',
|
||||
conversationId: convoId,
|
||||
messageId: v4(),
|
||||
parentMessageId: _messageId,
|
||||
error,
|
||||
});
|
||||
} else if (excludedIds?.length && excludedIds.includes(assistant_id)) {
|
||||
return await handleAbortError(res, req, error, {
|
||||
sender: 'System',
|
||||
conversationId: convoId,
|
||||
messageId: v4(),
|
||||
parentMessageId: _messageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {OpenAIClient} */
|
||||
let openai;
|
||||
/** @type {string|undefined} - the current thread id */
|
||||
let thread_id = _thread_id;
|
||||
/** @type {string|undefined} - the current run id */
|
||||
let run_id;
|
||||
/** @type {string|undefined} - the parent messageId */
|
||||
let parentMessageId = _parentId;
|
||||
/** @type {TMessage[]} */
|
||||
let previousMessages = [];
|
||||
/** @type {import('librechat-data-provider').TConversation | null} */
|
||||
let conversation = null;
|
||||
/** @type {string[]} */
|
||||
let file_ids = [];
|
||||
/** @type {Set<string>} */
|
||||
let attachedFileIds = new Set();
|
||||
/** @type {TMessage | null} */
|
||||
let requestMessage = null;
|
||||
|
||||
const userMessageId = v4();
|
||||
const responseMessageId = v4();
|
||||
|
||||
/** @type {string} - The conversation UUID - created if undefined */
|
||||
const conversationId = convoId ?? v4();
|
||||
|
||||
const cache = getLogStores(CacheKeys.ABORT_KEYS);
|
||||
const cacheKey = `${req.user.id}:${conversationId}`;
|
||||
|
||||
/** @type {Run | undefined} - The completed run, undefined if incomplete */
|
||||
let completedRun;
|
||||
|
||||
const handleError = async (error) => {
|
||||
const defaultErrorMessage =
|
||||
'The Assistant run failed to initialize. Try sending a message in a new conversation.';
|
||||
const messageData = {
|
||||
thread_id,
|
||||
assistant_id,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender: 'System',
|
||||
user: req.user.id,
|
||||
shouldSaveMessage: false,
|
||||
messageId: responseMessageId,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
if (error.message === 'Run cancelled') {
|
||||
return res.end();
|
||||
} else if (error.message === 'Request closed' && completedRun) {
|
||||
return;
|
||||
} else if (error.message === 'Request closed') {
|
||||
logger.debug('[/assistants/chat/] Request aborted on close');
|
||||
} else if (/Files.*are invalid/.test(error.message)) {
|
||||
const errorMessage = `Files are invalid, or may not have uploaded yet.${
|
||||
endpoint === EModelEndpoint.azureAssistants
|
||||
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
|
||||
: ''
|
||||
}`;
|
||||
return sendResponse(res, messageData, errorMessage);
|
||||
} else if (error?.message?.includes('string too long')) {
|
||||
return sendResponse(
|
||||
res,
|
||||
messageData,
|
||||
'Message too long. The Assistants API has a limit of 32,768 characters per message. Please shorten it and try again.',
|
||||
);
|
||||
} else if (error?.message?.includes(ViolationTypes.TOKEN_BALANCE)) {
|
||||
return sendResponse(res, messageData, error.message);
|
||||
} else {
|
||||
logger.error('[/assistants/chat/]', error);
|
||||
}
|
||||
|
||||
if (!openai || !thread_id || !run_id) {
|
||||
return sendResponse(res, messageData, defaultErrorMessage);
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
try {
|
||||
const status = await cache.get(cacheKey);
|
||||
if (status === 'cancelled') {
|
||||
logger.debug('[/assistants/chat/] Run already cancelled');
|
||||
return res.end();
|
||||
}
|
||||
await cache.delete(cacheKey);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
logger.debug('[/assistants/chat/] Cancelled run:', cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error cancelling run', error);
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
let run;
|
||||
try {
|
||||
run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
user: req.user.id,
|
||||
conversationId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error fetching or processing run', error);
|
||||
}
|
||||
|
||||
let finalEvent;
|
||||
try {
|
||||
const runMessages = await checkMessageGaps({
|
||||
openai,
|
||||
run_id,
|
||||
endpoint,
|
||||
thread_id,
|
||||
conversationId,
|
||||
latestMessageId: responseMessageId,
|
||||
});
|
||||
|
||||
const errorContentPart = {
|
||||
text: {
|
||||
value:
|
||||
error?.message ?? 'There was an error processing your request. Please try again later.',
|
||||
},
|
||||
type: ContentTypes.ERROR,
|
||||
};
|
||||
|
||||
if (!Array.isArray(runMessages[runMessages.length - 1]?.content)) {
|
||||
runMessages[runMessages.length - 1].content = [errorContentPart];
|
||||
} else {
|
||||
const contentParts = runMessages[runMessages.length - 1].content;
|
||||
for (let i = 0; i < contentParts.length; i++) {
|
||||
const currentPart = contentParts[i];
|
||||
/** @type {CodeToolCall | RetrievalToolCall | FunctionToolCall | undefined} */
|
||||
const toolCall = currentPart?.[ContentTypes.TOOL_CALL];
|
||||
if (
|
||||
toolCall &&
|
||||
toolCall?.function &&
|
||||
!(toolCall?.function?.output || toolCall?.function?.output?.length)
|
||||
) {
|
||||
contentParts[i] = {
|
||||
...currentPart,
|
||||
[ContentTypes.TOOL_CALL]: {
|
||||
...toolCall,
|
||||
function: {
|
||||
...toolCall.function,
|
||||
output: 'error processing tool',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
runMessages[runMessages.length - 1].content.push(errorContentPart);
|
||||
}
|
||||
|
||||
finalEvent = {
|
||||
final: true,
|
||||
conversation: await getConvo(req.user.id, conversationId),
|
||||
runMessages,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error finalizing error process', error);
|
||||
return sendResponse(res, messageData, 'The Assistant run failed');
|
||||
}
|
||||
|
||||
return sendResponse(res, finalEvent);
|
||||
};
|
||||
|
||||
try {
|
||||
res.on('close', async () => {
|
||||
if (!completedRun) {
|
||||
await handleError(new Error('Request closed'));
|
||||
}
|
||||
});
|
||||
|
||||
if (convoId && !_thread_id) {
|
||||
completedRun = true;
|
||||
throw new Error('Missing thread_id for existing conversation');
|
||||
}
|
||||
|
||||
if (!assistant_id) {
|
||||
completedRun = true;
|
||||
throw new Error('Missing assistant_id');
|
||||
}
|
||||
|
||||
const checkBalanceBeforeRun = async () => {
|
||||
if (!isEnabled(process.env.CHECK_BALANCE)) {
|
||||
return;
|
||||
}
|
||||
const transactions =
|
||||
(await getTransactions({
|
||||
user: req.user.id,
|
||||
context: 'message',
|
||||
conversationId,
|
||||
})) ?? [];
|
||||
|
||||
const totalPreviousTokens = Math.abs(
|
||||
transactions.reduce((acc, curr) => acc + curr.rawAmount, 0),
|
||||
);
|
||||
|
||||
// TODO: make promptBuffer a config option; buffer for titles, needs buffer for system instructions
|
||||
const promptBuffer = parentMessageId === Constants.NO_PARENT && !_thread_id ? 200 : 0;
|
||||
// 5 is added for labels
|
||||
let promptTokens = (await countTokens(text + (promptPrefix ?? ''))) + 5;
|
||||
promptTokens += totalPreviousTokens + promptBuffer;
|
||||
// Count tokens up to the current context window
|
||||
promptTokens = Math.min(promptTokens, getModelMaxTokens(model));
|
||||
|
||||
await checkBalance({
|
||||
req,
|
||||
res,
|
||||
txData: {
|
||||
model,
|
||||
user: req.user.id,
|
||||
tokenType: 'prompt',
|
||||
amount: promptTokens,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { openai: _openai, client } = await getOpenAIClient({
|
||||
req,
|
||||
res,
|
||||
endpointOption: req.body.endpointOption,
|
||||
initAppClient: true,
|
||||
});
|
||||
|
||||
openai = _openai;
|
||||
|
||||
if (previousMessages.length) {
|
||||
parentMessageId = previousMessages[previousMessages.length - 1].messageId;
|
||||
}
|
||||
|
||||
let userMessage = {
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: ContentTypes.TEXT,
|
||||
text,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
messageId: userMessageId,
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {CreateRunBody | undefined} */
|
||||
const body = {
|
||||
assistant_id,
|
||||
model,
|
||||
};
|
||||
|
||||
if (promptPrefix) {
|
||||
body.additional_instructions = promptPrefix;
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
body.instructions = instructions;
|
||||
}
|
||||
|
||||
const getRequestFileIds = async () => {
|
||||
let thread_file_ids = [];
|
||||
if (convoId) {
|
||||
const convo = await getConvo(req.user.id, convoId);
|
||||
if (convo && convo.file_ids) {
|
||||
thread_file_ids = convo.file_ids;
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length || thread_file_ids.length) {
|
||||
attachedFileIds = new Set([...file_ids, ...thread_file_ids]);
|
||||
|
||||
let attachmentIndex = 0;
|
||||
for (const file of files) {
|
||||
file_ids.push(file.file_id);
|
||||
if (file.type.startsWith('image')) {
|
||||
userMessage.content.push({
|
||||
type: ContentTypes.IMAGE_FILE,
|
||||
[ContentTypes.IMAGE_FILE]: { file_id: file.file_id },
|
||||
});
|
||||
}
|
||||
|
||||
if (!userMessage.attachments) {
|
||||
userMessage.attachments = [];
|
||||
}
|
||||
|
||||
userMessage.attachments.push({
|
||||
file_id: file.file_id,
|
||||
tools: [{ type: ToolCallTypes.CODE_INTERPRETER }],
|
||||
});
|
||||
|
||||
if (file.type.startsWith('image')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mimeType = file.type;
|
||||
const isSupportedByRetrieval = retrievalMimeTypes.some((regex) => regex.test(mimeType));
|
||||
if (isSupportedByRetrieval) {
|
||||
userMessage.attachments[attachmentIndex].tools.push({
|
||||
type: ToolCallTypes.FILE_SEARCH,
|
||||
});
|
||||
}
|
||||
|
||||
attachmentIndex++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initializeThread = async () => {
|
||||
await getRequestFileIds();
|
||||
|
||||
// TODO: may allow multiple messages to be created beforehand in a future update
|
||||
const initThreadBody = {
|
||||
messages: [userMessage],
|
||||
metadata: {
|
||||
user: req.user.id,
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await initThread({ openai, body: initThreadBody, thread_id });
|
||||
thread_id = result.thread_id;
|
||||
|
||||
createOnTextProgress({
|
||||
openai,
|
||||
conversationId,
|
||||
userMessageId,
|
||||
messageId: responseMessageId,
|
||||
thread_id,
|
||||
});
|
||||
|
||||
requestMessage = {
|
||||
user: req.user.id,
|
||||
text,
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
// TODO: make sure client sends correct format for `files`, use zod
|
||||
files,
|
||||
file_ids,
|
||||
conversationId,
|
||||
isCreatedByUser: true,
|
||||
assistant_id,
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
previousMessages.push(requestMessage);
|
||||
|
||||
/* asynchronous */
|
||||
saveUserMessage({ ...requestMessage, model });
|
||||
|
||||
conversation = {
|
||||
conversationId,
|
||||
endpoint,
|
||||
promptPrefix: promptPrefix,
|
||||
instructions: instructions,
|
||||
assistant_id,
|
||||
// model,
|
||||
};
|
||||
|
||||
if (file_ids.length) {
|
||||
conversation.file_ids = file_ids;
|
||||
}
|
||||
};
|
||||
|
||||
const promises = [initializeThread(), checkBalanceBeforeRun()];
|
||||
await Promise.all(promises);
|
||||
|
||||
const sendInitialResponse = () => {
|
||||
sendMessage(res, {
|
||||
sync: true,
|
||||
conversationId,
|
||||
// messages: previousMessages,
|
||||
requestMessage,
|
||||
responseMessage: {
|
||||
user: req.user.id,
|
||||
messageId: openai.responseMessage.messageId,
|
||||
parentMessageId: userMessageId,
|
||||
conversationId,
|
||||
assistant_id,
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {RunResponse | typeof StreamRunManager | undefined} */
|
||||
let response;
|
||||
|
||||
const processRun = async (retry = false) => {
|
||||
if (endpoint === EModelEndpoint.azureAssistants) {
|
||||
body.model = openai._options.model;
|
||||
openai.attachedFileIds = attachedFileIds;
|
||||
if (retry) {
|
||||
response = await runAssistant({
|
||||
openai,
|
||||
thread_id,
|
||||
run_id,
|
||||
in_progress: openai.in_progress,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/* NOTE:
|
||||
* By default, a Run will use the model and tools configuration specified in Assistant object,
|
||||
* but you can override most of these when creating the Run for added flexibility:
|
||||
*/
|
||||
const run = await createRun({
|
||||
openai,
|
||||
thread_id,
|
||||
body,
|
||||
});
|
||||
|
||||
run_id = run.id;
|
||||
await cache.set(cacheKey, `${thread_id}:${run_id}`, ten_minutes);
|
||||
sendInitialResponse();
|
||||
|
||||
// todo: retry logic
|
||||
response = await runAssistant({ openai, thread_id, run_id });
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {{[AssistantStreamEvents.ThreadRunCreated]: (event: ThreadRunCreated) => Promise<void>}} */
|
||||
const handlers = {
|
||||
[AssistantStreamEvents.ThreadRunCreated]: async (event) => {
|
||||
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, ten_minutes);
|
||||
run_id = event.data.id;
|
||||
sendInitialResponse();
|
||||
},
|
||||
};
|
||||
|
||||
const streamRunManager = new StreamRunManager({
|
||||
req,
|
||||
res,
|
||||
openai,
|
||||
handlers,
|
||||
thread_id,
|
||||
attachedFileIds,
|
||||
responseMessage: openai.responseMessage,
|
||||
// streamOptions: {
|
||||
|
||||
// },
|
||||
});
|
||||
|
||||
await streamRunManager.runAssistant({
|
||||
thread_id,
|
||||
body,
|
||||
});
|
||||
|
||||
response = streamRunManager;
|
||||
};
|
||||
|
||||
await processRun();
|
||||
logger.debug('[/assistants/chat/] response', {
|
||||
run: response.run,
|
||||
steps: response.steps,
|
||||
});
|
||||
|
||||
if (response.run.status === RunStatus.CANCELLED) {
|
||||
logger.debug('[/assistants/chat/] Run cancelled, handled by `abortRun`');
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (response.run.status === RunStatus.IN_PROGRESS) {
|
||||
processRun(true);
|
||||
}
|
||||
|
||||
completedRun = response.run;
|
||||
|
||||
/** @type {ResponseMessage} */
|
||||
const responseMessage = {
|
||||
...(response.responseMessage ?? response.finalMessage),
|
||||
parentMessageId: userMessageId,
|
||||
conversationId,
|
||||
user: req.user.id,
|
||||
assistant_id,
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
sendMessage(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
requestMessage: {
|
||||
parentMessageId,
|
||||
thread_id,
|
||||
},
|
||||
});
|
||||
res.end();
|
||||
|
||||
await saveAssistantMessage({ ...responseMessage, model });
|
||||
|
||||
if (parentMessageId === Constants.NO_PARENT && !_thread_id) {
|
||||
addTitle(req, {
|
||||
text,
|
||||
responseText: response.text,
|
||||
conversationId,
|
||||
client,
|
||||
});
|
||||
}
|
||||
|
||||
await addThreadMetadata({
|
||||
openai,
|
||||
thread_id,
|
||||
messageId: responseMessage.messageId,
|
||||
messages: response.messages,
|
||||
});
|
||||
|
||||
if (!response.run.usage) {
|
||||
await sleep(3000);
|
||||
completedRun = await openai.beta.threads.runs.retrieve(thread_id, response.run.id);
|
||||
if (completedRun.usage) {
|
||||
await recordUsage({
|
||||
...completedRun.usage,
|
||||
user: req.user.id,
|
||||
model: completedRun.model ?? model,
|
||||
conversationId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await recordUsage({
|
||||
...response.run.usage,
|
||||
user: req.user.id,
|
||||
model: response.run.model ?? model,
|
||||
conversationId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = chatV2;
|
||||
158
api/server/controllers/assistants/helpers.js
Normal file
158
api/server/controllers/assistants/helpers.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const { EModelEndpoint, CacheKeys, defaultAssistantsVersion } = require('librechat-data-provider');
|
||||
const {
|
||||
initializeClient: initAzureClient,
|
||||
} = require('~/server/services/Endpoints/azureAssistants');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
* @param {Express.Request} req
|
||||
* @param {string} [endpoint]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const getCurrentVersion = async (req, endpoint) => {
|
||||
const index = req.baseUrl.lastIndexOf('/v');
|
||||
let version = index !== -1 ? req.baseUrl.substring(index + 1, index + 3) : null;
|
||||
if (!version && req.body.version) {
|
||||
version = `v${req.body.version}`;
|
||||
}
|
||||
if (!version && endpoint) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cachedEndpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG);
|
||||
version = `v${
|
||||
cachedEndpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]
|
||||
}`;
|
||||
}
|
||||
if (!version?.startsWith('v') && version.length !== 2) {
|
||||
throw new Error(`[${req.baseUrl}] Invalid version: ${version}`);
|
||||
}
|
||||
return version;
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously lists assistants based on provided query parameters.
|
||||
*
|
||||
* Initializes the client with the current request and response objects and lists assistants
|
||||
* according to the query parameters. This function abstracts the logic for non-Azure paths.
|
||||
*
|
||||
* @async
|
||||
* @param {object} params - The parameters object.
|
||||
* @param {object} params.req - The request object, used for initializing the client.
|
||||
* @param {object} params.res - The response object, used for initializing the client.
|
||||
* @param {string} params.version - The API version to use.
|
||||
* @param {object} params.query - The query parameters to list assistants (e.g., limit, order).
|
||||
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
|
||||
*/
|
||||
const listAssistants = async ({ req, res, version, query }) => {
|
||||
const { openai } = await getOpenAIClient({ req, res, version });
|
||||
return openai.beta.assistants.list(query);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously lists assistants for Azure configured groups.
|
||||
*
|
||||
* Iterates through Azure configured assistant groups, initializes the client with the current request and response objects,
|
||||
* lists assistants based on the provided query parameters, and merges their data alongside the model information into a single array.
|
||||
*
|
||||
* @async
|
||||
* @param {object} params - The parameters object.
|
||||
* @param {object} params.req - The request object, used for initializing the client and manipulating the request body.
|
||||
* @param {object} params.res - The response object, used for initializing the client.
|
||||
* @param {string} params.version - The API version to use.
|
||||
* @param {TAzureConfig} params.azureConfig - The Azure configuration object containing assistantGroups and groupMap.
|
||||
* @param {object} params.query - The query parameters to list assistants (e.g., limit, order).
|
||||
* @returns {Promise<AssistantListResponse>} A promise that resolves to an array of assistant data merged with their respective model information.
|
||||
*/
|
||||
const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, query }) => {
|
||||
/** @type {Array<[string, TAzureModelConfig]>} */
|
||||
const groupModelTuples = [];
|
||||
const promises = [];
|
||||
/** @type {Array<TAzureGroup>} */
|
||||
const groups = [];
|
||||
|
||||
const { groupMap, assistantGroups } = azureConfig;
|
||||
|
||||
for (const groupName of assistantGroups) {
|
||||
const group = groupMap[groupName];
|
||||
groups.push(group);
|
||||
|
||||
const currentModelTuples = Object.entries(group?.models);
|
||||
groupModelTuples.push(currentModelTuples);
|
||||
|
||||
/* The specified model is only necessary to
|
||||
fetch assistants for the shared instance */
|
||||
req.body.model = currentModelTuples[0][0];
|
||||
promises.push(listAssistants({ req, res, version, query }));
|
||||
}
|
||||
|
||||
const resolvedQueries = await Promise.all(promises);
|
||||
const data = resolvedQueries.flatMap((res, i) =>
|
||||
res.data.map((assistant) => {
|
||||
const deploymentName = assistant.model;
|
||||
const currentGroup = groups[i];
|
||||
const currentModelTuples = groupModelTuples[i];
|
||||
const firstModel = currentModelTuples[0][0];
|
||||
|
||||
if (currentGroup.deploymentName === deploymentName) {
|
||||
return { ...assistant, model: firstModel };
|
||||
}
|
||||
|
||||
for (const [model, modelConfig] of currentModelTuples) {
|
||||
if (modelConfig.deploymentName === deploymentName) {
|
||||
return { ...assistant, model };
|
||||
}
|
||||
}
|
||||
|
||||
return { ...assistant, model: firstModel };
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
first_id: data[0]?.id,
|
||||
last_id: data[data.length - 1]?.id,
|
||||
object: 'list',
|
||||
has_more: false,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) {
|
||||
let endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint;
|
||||
const version = await getCurrentVersion(req, endpoint);
|
||||
if (!endpoint) {
|
||||
throw new Error(`[${req.baseUrl}] Endpoint is required`);
|
||||
}
|
||||
|
||||
let result;
|
||||
if (endpoint === EModelEndpoint.assistants) {
|
||||
result = await initializeClient({ req, res, version, endpointOption, initAppClient });
|
||||
} else if (endpoint === EModelEndpoint.azureAssistants) {
|
||||
result = await initAzureClient({ req, res, version, endpointOption, initAppClient });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const fetchAssistants = async (req, res) => {
|
||||
const { limit = 100, order = 'desc', after, before, endpoint } = req.query;
|
||||
const version = await getCurrentVersion(req, endpoint);
|
||||
const query = { limit, order, after, before };
|
||||
|
||||
/** @type {AssistantListResponse} */
|
||||
let body;
|
||||
|
||||
if (endpoint === EModelEndpoint.assistants) {
|
||||
({ body } = await listAssistants({ req, res, version, query }));
|
||||
} else if (endpoint === EModelEndpoint.azureAssistants) {
|
||||
const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI];
|
||||
body = await listAssistantsForAzure({ req, res, version, azureConfig, query });
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getOpenAIClient,
|
||||
fetchAssistants,
|
||||
getCurrentVersion,
|
||||
};
|
||||
@@ -1,30 +1,11 @@
|
||||
const multer = require('multer');
|
||||
const express = require('express');
|
||||
const { FileContext, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { updateAssistant, getAssistants } = require('~/models/Assistant');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistant');
|
||||
const { FileContext } = require('librechat-data-provider');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { deleteAssistantActions } = require('~/server/services/ActionService');
|
||||
const { uploadImageBuffer } = require('~/server/services/Files/process');
|
||||
const { updateAssistant, getAssistants } = require('~/models/Assistant');
|
||||
const { getOpenAIClient, fetchAssistants } = require('./helpers');
|
||||
const { deleteFileByFilter } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
const actions = require('./actions');
|
||||
const tools = require('./tools');
|
||||
|
||||
const upload = multer();
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Assistant actions route.
|
||||
* @route GET|POST /assistants/actions
|
||||
*/
|
||||
router.use('/actions', actions);
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
* @route GET /assistants/tools
|
||||
* @returns {TPlugin[]} 200 - application/json
|
||||
*/
|
||||
router.use('/tools', tools);
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
@@ -32,12 +13,11 @@ router.use('/tools', tools);
|
||||
* @param {AssistantCreateParams} req.body - The assistant creation parameters.
|
||||
* @returns {Assistant} 201 - success response - application/json
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
const createAssistant = async (req, res) => {
|
||||
try {
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const { tools = [], ...assistantData } = req.body;
|
||||
const { tools = [], endpoint, ...assistantData } = req.body;
|
||||
assistantData.tools = tools
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
@@ -48,14 +28,28 @@ router.post('/', async (req, res) => {
|
||||
})
|
||||
.filter((tool) => tool);
|
||||
|
||||
let azureModelIdentifier = null;
|
||||
if (openai.locals?.azureOptions) {
|
||||
azureModelIdentifier = assistantData.model;
|
||||
assistantData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
|
||||
}
|
||||
|
||||
assistantData.metadata = {
|
||||
author: req.user.id,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
const assistant = await openai.beta.assistants.create(assistantData);
|
||||
if (azureModelIdentifier) {
|
||||
assistant.model = azureModelIdentifier;
|
||||
}
|
||||
logger.debug('/assistants/', assistant);
|
||||
res.status(201).json(assistant);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants] Error creating assistant', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves an assistant.
|
||||
@@ -63,10 +57,10 @@ router.post('/', async (req, res) => {
|
||||
* @param {string} req.params.id - Assistant identifier.
|
||||
* @returns {Assistant} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/:id', async (req, res) => {
|
||||
const retrieveAssistant = async (req, res) => {
|
||||
try {
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
/* NOTE: not actually being used right now */
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const assistant_id = req.params.id;
|
||||
const assistant = await openai.beta.assistants.retrieve(assistant_id);
|
||||
@@ -75,22 +69,23 @@ router.get('/:id', async (req, res) => {
|
||||
logger.error('[/assistants/:id] Error retrieving assistant', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies an assistant.
|
||||
* @route PATCH /assistants/:id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.id - Assistant identifier.
|
||||
* @param {AssistantUpdateParams} req.body - The assistant update parameters.
|
||||
* @returns {Assistant} 200 - success response - application/json
|
||||
*/
|
||||
router.patch('/:id', async (req, res) => {
|
||||
const patchAssistant = async (req, res) => {
|
||||
try {
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const assistant_id = req.params.id;
|
||||
const updateData = req.body;
|
||||
const { endpoint: _e, ...updateData } = req.body;
|
||||
updateData.tools = (updateData.tools ?? [])
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
@@ -101,59 +96,56 @@ router.patch('/:id', async (req, res) => {
|
||||
})
|
||||
.filter((tool) => tool);
|
||||
|
||||
if (openai.locals?.azureOptions && updateData.model) {
|
||||
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
|
||||
}
|
||||
|
||||
const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData);
|
||||
res.json(updatedAssistant);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/:id] Error updating assistant', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an assistant.
|
||||
* @route DELETE /assistants/:id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.id - Assistant identifier.
|
||||
* @returns {Assistant} 200 - success response - application/json
|
||||
*/
|
||||
router.delete('/:id', async (req, res) => {
|
||||
const deleteAssistant = async (req, res) => {
|
||||
try {
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const assistant_id = req.params.id;
|
||||
const deletionStatus = await openai.beta.assistants.del(assistant_id);
|
||||
if (deletionStatus?.deleted) {
|
||||
await deleteAssistantActions({ req, assistant_id });
|
||||
}
|
||||
res.json(deletionStatus);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/:id] Error deleting assistant', error);
|
||||
res.status(500).json({ error: 'Error deleting assistant' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of assistants.
|
||||
* @route GET /assistants
|
||||
* @param {object} req - Express Request
|
||||
* @param {AssistantListParams} req.query - The assistant list parameters for pagination and sorting.
|
||||
* @returns {AssistantListResponse} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
const listAssistants = async (req, res) => {
|
||||
try {
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
const body = await fetchAssistants(req, res);
|
||||
|
||||
const { limit, order, after, before } = req.query;
|
||||
const response = await openai.beta.assistants.list({
|
||||
limit,
|
||||
order,
|
||||
after,
|
||||
before,
|
||||
});
|
||||
|
||||
/** @type {AssistantListResponse} */
|
||||
let body = response.body;
|
||||
|
||||
if (req.app.locals?.[EModelEndpoint.assistants]) {
|
||||
if (req.app.locals?.[req.query.endpoint]) {
|
||||
/** @type {Partial<TAssistantEndpoint>} */
|
||||
const assistantsConfig = req.app.locals[EModelEndpoint.assistants];
|
||||
const assistantsConfig = req.app.locals[req.query.endpoint];
|
||||
const { supportedIds, excludedIds } = assistantsConfig;
|
||||
if (supportedIds?.length) {
|
||||
body.data = body.data.filter((assistant) => supportedIds.includes(assistant.id));
|
||||
@@ -165,33 +157,36 @@ router.get('/', async (req, res) => {
|
||||
res.json(body);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants] Error listing assistants', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
res.status(500).json({ message: 'Error listing assistants' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of the user's assistant documents (metadata saved to database).
|
||||
* @route GET /assistants/documents
|
||||
* @returns {AssistantDocument[]} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/documents', async (req, res) => {
|
||||
const getAssistantDocuments = async (req, res) => {
|
||||
try {
|
||||
res.json(await getAssistants({ user: req.user.id }));
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/documents] Error listing assistant documents', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads and updates an avatar for a specific assistant.
|
||||
* @route POST /avatar/:assistant_id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.assistant_id - The ID of the assistant.
|
||||
* @param {Express.Multer.File} req.file - The avatar image file.
|
||||
* @param {object} req.body - Request body
|
||||
* @param {string} [req.body.metadata] - Optional metadata for the assistant's avatar.
|
||||
* @returns {Object} 200 - success response - application/json
|
||||
*/
|
||||
router.post('/avatar/:assistant_id', upload.single('file'), async (req, res) => {
|
||||
const uploadAssistantAvatar = async (req, res) => {
|
||||
try {
|
||||
const { assistant_id } = req.params;
|
||||
if (!assistant_id) {
|
||||
@@ -199,10 +194,15 @@ router.post('/avatar/:assistant_id', upload.single('file'), async (req, res) =>
|
||||
}
|
||||
|
||||
let { metadata: _metadata = '{}' } = req.body;
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const image = await uploadImageBuffer({ req, context: FileContext.avatar });
|
||||
const image = await uploadImageBuffer({
|
||||
req,
|
||||
context: FileContext.avatar,
|
||||
metadata: {
|
||||
buffer: req.file.buffer,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
_metadata = JSON.parse(_metadata);
|
||||
@@ -230,12 +230,13 @@ router.post('/avatar/:assistant_id', upload.single('file'), async (req, res) =>
|
||||
const promises = [];
|
||||
promises.push(
|
||||
updateAssistant(
|
||||
{ assistant_id, user: req.user.id },
|
||||
{ assistant_id },
|
||||
{
|
||||
avatar: {
|
||||
filepath: image.filepath,
|
||||
source: req.app.locals.fileStrategy,
|
||||
},
|
||||
user: req.user.id,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -248,6 +249,14 @@ router.post('/avatar/:assistant_id', upload.single('file'), async (req, res) =>
|
||||
logger.error(message, error);
|
||||
res.status(500).json({ message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
module.exports = {
|
||||
createAssistant,
|
||||
retrieveAssistant,
|
||||
patchAssistant,
|
||||
deleteAssistant,
|
||||
listAssistants,
|
||||
getAssistantDocuments,
|
||||
uploadAssistantAvatar,
|
||||
};
|
||||
208
api/server/controllers/assistants/v2.js
Normal file
208
api/server/controllers/assistants/v2.js
Normal file
@@ -0,0 +1,208 @@
|
||||
const { ToolCallTypes } = require('librechat-data-provider');
|
||||
const { validateAndUpdateTool } = require('~/server/services/ActionService');
|
||||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
* @route POST /assistants
|
||||
* @param {AssistantCreateParams} req.body - The assistant creation parameters.
|
||||
* @returns {Assistant} 201 - success response - application/json
|
||||
*/
|
||||
const createAssistant = async (req, res) => {
|
||||
try {
|
||||
/** @type {{ openai: OpenAIClient }} */
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const { tools = [], endpoint, ...assistantData } = req.body;
|
||||
assistantData.tools = tools
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
return tool;
|
||||
}
|
||||
|
||||
return req.app.locals.availableTools[tool];
|
||||
})
|
||||
.filter((tool) => tool);
|
||||
|
||||
let azureModelIdentifier = null;
|
||||
if (openai.locals?.azureOptions) {
|
||||
azureModelIdentifier = assistantData.model;
|
||||
assistantData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
|
||||
}
|
||||
|
||||
assistantData.metadata = {
|
||||
author: req.user.id,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
const assistant = await openai.beta.assistants.create(assistantData);
|
||||
if (azureModelIdentifier) {
|
||||
assistant.model = azureModelIdentifier;
|
||||
}
|
||||
logger.debug('/assistants/', assistant);
|
||||
res.status(201).json(assistant);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants] Error creating assistant', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies an assistant.
|
||||
* @param {object} params
|
||||
* @param {Express.Request} params.req
|
||||
* @param {OpenAIClient} params.openai
|
||||
* @param {string} params.assistant_id
|
||||
* @param {AssistantUpdateParams} params.updateData
|
||||
* @returns {Promise<Assistant>} The updated assistant.
|
||||
*/
|
||||
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
||||
const tools = [];
|
||||
|
||||
let hasFileSearch = false;
|
||||
for (const tool of updateData.tools ?? []) {
|
||||
let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool;
|
||||
|
||||
if (!actualTool) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actualTool.type === ToolCallTypes.FILE_SEARCH) {
|
||||
hasFileSearch = true;
|
||||
}
|
||||
|
||||
if (!actualTool.function) {
|
||||
tools.push(actualTool);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedTool = await validateAndUpdateTool({ req, tool: actualTool, assistant_id });
|
||||
if (updatedTool) {
|
||||
tools.push(updatedTool);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFileSearch && !updateData.tool_resources) {
|
||||
const assistant = await openai.beta.assistants.retrieve(assistant_id);
|
||||
updateData.tool_resources = assistant.tool_resources ?? null;
|
||||
}
|
||||
|
||||
if (hasFileSearch && !updateData.tool_resources?.file_search) {
|
||||
updateData.tool_resources = {
|
||||
...(updateData.tool_resources ?? {}),
|
||||
file_search: {
|
||||
vector_store_ids: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateData.tools = tools;
|
||||
|
||||
if (openai.locals?.azureOptions && updateData.model) {
|
||||
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
|
||||
}
|
||||
|
||||
return await openai.beta.assistants.update(assistant_id, updateData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies an assistant with the resource file id.
|
||||
* @param {object} params
|
||||
* @param {Express.Request} params.req
|
||||
* @param {OpenAIClient} params.openai
|
||||
* @param {string} params.assistant_id
|
||||
* @param {string} params.tool_resource
|
||||
* @param {string} params.file_id
|
||||
* @param {AssistantUpdateParams} params.updateData
|
||||
* @returns {Promise<Assistant>} The updated assistant.
|
||||
*/
|
||||
const addResourceFileId = async ({ req, openai, assistant_id, tool_resource, file_id }) => {
|
||||
const assistant = await openai.beta.assistants.retrieve(assistant_id);
|
||||
const { tool_resources = {} } = assistant;
|
||||
if (tool_resources[tool_resource]) {
|
||||
tool_resources[tool_resource].file_ids.push(file_id);
|
||||
} else {
|
||||
tool_resources[tool_resource] = { file_ids: [file_id] };
|
||||
}
|
||||
|
||||
delete assistant.id;
|
||||
return await updateAssistant({
|
||||
req,
|
||||
openai,
|
||||
assistant_id,
|
||||
updateData: { tools: assistant.tools, tool_resources },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a file ID from an assistant's resource.
|
||||
* @param {object} params
|
||||
* @param {Express.Request} params.req
|
||||
* @param {OpenAIClient} params.openai
|
||||
* @param {string} params.assistant_id
|
||||
* @param {string} [params.tool_resource]
|
||||
* @param {string} params.file_id
|
||||
* @param {AssistantUpdateParams} params.updateData
|
||||
* @returns {Promise<Assistant>} The updated assistant.
|
||||
*/
|
||||
const deleteResourceFileId = async ({ req, openai, assistant_id, tool_resource, file_id }) => {
|
||||
const assistant = await openai.beta.assistants.retrieve(assistant_id);
|
||||
const { tool_resources = {} } = assistant;
|
||||
|
||||
if (tool_resource && tool_resources[tool_resource]) {
|
||||
const resource = tool_resources[tool_resource];
|
||||
const index = resource.file_ids.indexOf(file_id);
|
||||
if (index !== -1) {
|
||||
resource.file_ids.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
for (const resourceKey in tool_resources) {
|
||||
const resource = tool_resources[resourceKey];
|
||||
const index = resource.file_ids.indexOf(file_id);
|
||||
if (index !== -1) {
|
||||
resource.file_ids.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete assistant.id;
|
||||
return await updateAssistant({
|
||||
req,
|
||||
openai,
|
||||
assistant_id,
|
||||
updateData: { tools: assistant.tools, tool_resources },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies an assistant.
|
||||
* @route PATCH /assistants/:id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.id - Assistant identifier.
|
||||
* @param {AssistantUpdateParams} req.body - The assistant update parameters.
|
||||
* @returns {Assistant} 200 - success response - application/json
|
||||
*/
|
||||
const patchAssistant = async (req, res) => {
|
||||
try {
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
const assistant_id = req.params.id;
|
||||
const { endpoint: _e, ...updateData } = req.body;
|
||||
updateData.tools = updateData.tools ?? [];
|
||||
const updatedAssistant = await updateAssistant({ req, openai, assistant_id, updateData });
|
||||
res.json(updatedAssistant);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/:id] Error updating assistant', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
patchAssistant,
|
||||
createAssistant,
|
||||
updateAssistant,
|
||||
addResourceFileId,
|
||||
deleteResourceFileId,
|
||||
};
|
||||
@@ -2,9 +2,11 @@ require('dotenv').config();
|
||||
const path = require('path');
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..') });
|
||||
const cors = require('cors');
|
||||
const axios = require('axios');
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const mongoSanitize = require('express-mongo-sanitize');
|
||||
const validateImageRequest = require('./middleware/validateImageRequest');
|
||||
const errorController = require('./controllers/ErrorController');
|
||||
const { jwtLogin, passportLogin } = require('~/strategies');
|
||||
const configureSocialLogins = require('./socialLogins');
|
||||
@@ -22,6 +24,9 @@ const port = Number(PORT) || 3080;
|
||||
const host = HOST || 'localhost';
|
||||
|
||||
const startServer = async () => {
|
||||
if (typeof Bun !== 'undefined') {
|
||||
axios.defaults.headers.common['Accept-Encoding'] = 'gzip';
|
||||
}
|
||||
await connectDb();
|
||||
logger.info('Connected to MongoDB');
|
||||
await indexSync();
|
||||
@@ -39,7 +44,8 @@ const startServer = async () => {
|
||||
app.use(mongoSanitize());
|
||||
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
|
||||
app.use(express.static(app.locals.paths.dist));
|
||||
app.use(express.static(app.locals.paths.publicPath));
|
||||
app.use(express.static(app.locals.paths.fonts));
|
||||
app.use(express.static(app.locals.paths.assets));
|
||||
app.set('trust proxy', 1); // trust first proxy
|
||||
app.use(cors());
|
||||
|
||||
@@ -78,6 +84,8 @@ const startServer = async () => {
|
||||
app.use('/api/config', routes.config);
|
||||
app.use('/api/assistants', routes.assistants);
|
||||
app.use('/api/files', await routes.files.initialize());
|
||||
app.use('/images/', validateImageRequest, routes.staticRoute);
|
||||
app.use('/api/share', routes.share);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).sendFile(path.join(app.locals.paths.dist, 'index.html'));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { isAssistantsEndpoint } = require('librechat-data-provider');
|
||||
const { sendMessage, sendError, countTokens, isEnabled } = require('~/server/utils');
|
||||
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
|
||||
const { saveMessage, getConvo, getConvoTitle } = require('~/models');
|
||||
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||
const abortControllers = require('./abortControllers');
|
||||
const { redactMessage } = require('~/config/parsers');
|
||||
const spendTokens = require('~/models/spendTokens');
|
||||
const { abortRun } = require('./abortRun');
|
||||
const { logger } = require('~/config');
|
||||
@@ -15,7 +15,7 @@ async function abortMessage(req, res) {
|
||||
abortKey = conversationId;
|
||||
}
|
||||
|
||||
if (endpoint === EModelEndpoint.assistants) {
|
||||
if (isAssistantsEndpoint(endpoint)) {
|
||||
return await abortRun(req, res);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ const createAbortController = (req, res, getAbortData) => {
|
||||
...responseData,
|
||||
conversationId,
|
||||
finish_reason: 'incomplete',
|
||||
endpoint: endpointOption.endpoint,
|
||||
iconURL: endpointOption.iconURL,
|
||||
model: endpointOption.modelOptions.model,
|
||||
unfinished: false,
|
||||
error: false,
|
||||
@@ -100,7 +102,15 @@ const createAbortController = (req, res, getAbortData) => {
|
||||
};
|
||||
|
||||
const handleAbortError = async (res, req, error, data) => {
|
||||
logger.error('[handleAbortError] AI response error; aborting request:', error);
|
||||
if (error?.message?.includes('base64')) {
|
||||
logger.error('[handleAbortError] Error in base64 encoding', {
|
||||
...error,
|
||||
stack: smartTruncateText(error?.stack, 1000),
|
||||
message: truncateText(error.message, 350),
|
||||
});
|
||||
} else {
|
||||
logger.error('[handleAbortError] AI response error; aborting request:', error);
|
||||
}
|
||||
const { sender, conversationId, messageId, parentMessageId, partialText } = data;
|
||||
|
||||
if (error.stack && error.stack.includes('google')) {
|
||||
@@ -109,19 +119,24 @@ const handleAbortError = async (res, req, error, data) => {
|
||||
);
|
||||
}
|
||||
|
||||
const errorText = error?.message?.includes('"type"')
|
||||
? error.message
|
||||
: 'An error occurred while processing your request. Please contact the Admin.';
|
||||
|
||||
const respondWithError = async (partialText) => {
|
||||
const options = {
|
||||
let options = {
|
||||
sender,
|
||||
messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
text: redactMessage(error.message),
|
||||
text: errorText,
|
||||
shouldSaveMessage: true,
|
||||
user: req.user.id,
|
||||
};
|
||||
|
||||
if (partialText) {
|
||||
options.overrideProps = {
|
||||
options = {
|
||||
...options,
|
||||
error: false,
|
||||
unfinished: true,
|
||||
text: partialText,
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistant');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||
const { checkMessageGaps, recordUsage } = require('~/server/services/Threads');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { sendMessage } = require('~/server/utils');
|
||||
// const spendTokens = require('~/models/spendTokens');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const three_minutes = 1000 * 60 * 3;
|
||||
|
||||
async function abortRun(req, res) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
const { abortKey } = req.body;
|
||||
const { abortKey, endpoint } = req.body;
|
||||
const [conversationId, latestMessageId] = abortKey.split(':');
|
||||
const conversation = await getConvo(req.user.id, conversationId);
|
||||
|
||||
if (conversation?.model) {
|
||||
req.body.model = conversation.model;
|
||||
}
|
||||
|
||||
if (!isUUID.safeParse(conversationId).success) {
|
||||
logger.error('[abortRun] Invalid conversationId', { conversationId });
|
||||
@@ -35,9 +41,9 @@ async function abortRun(req, res) {
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
|
||||
try {
|
||||
await cache.set(cacheKey, 'cancelled');
|
||||
await cache.set(cacheKey, 'cancelled', three_minutes);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
logger.debug('Cancelled run:', cancelledRun);
|
||||
logger.debug('[abortRun] Cancelled run:', cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error('[abortRun] Error cancelling run', error);
|
||||
if (
|
||||
@@ -62,16 +68,16 @@ async function abortRun(req, res) {
|
||||
|
||||
runMessages = await checkMessageGaps({
|
||||
openai,
|
||||
latestMessageId,
|
||||
endpoint,
|
||||
thread_id,
|
||||
run_id,
|
||||
latestMessageId,
|
||||
conversationId,
|
||||
});
|
||||
|
||||
const finalEvent = {
|
||||
title: 'New Chat',
|
||||
final: true,
|
||||
conversation: await getConvo(req.user.id, conversationId),
|
||||
conversation,
|
||||
runMessages,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
const { parseConvo, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { processFiles } = require('~/server/services/Files/process');
|
||||
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
||||
const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
|
||||
const assistants = require('~/server/services/Endpoints/assistants');
|
||||
const gptPlugins = require('~/server/services/Endpoints/gptPlugins');
|
||||
const { processFiles } = require('~/server/services/Files/process');
|
||||
const anthropic = require('~/server/services/Endpoints/anthropic');
|
||||
const openAI = require('~/server/services/Endpoints/openAI');
|
||||
const custom = require('~/server/services/Endpoints/custom');
|
||||
const google = require('~/server/services/Endpoints/google');
|
||||
const assistant = require('~/server/services/Endpoints/assistant');
|
||||
const enforceModelSpec = require('./enforceModelSpec');
|
||||
const { handleError } = require('~/server/utils');
|
||||
|
||||
const buildFunction = {
|
||||
[EModelEndpoint.openAI]: openAI.buildOptions,
|
||||
@@ -14,17 +18,56 @@ const buildFunction = {
|
||||
[EModelEndpoint.azureOpenAI]: openAI.buildOptions,
|
||||
[EModelEndpoint.anthropic]: anthropic.buildOptions,
|
||||
[EModelEndpoint.gptPlugins]: gptPlugins.buildOptions,
|
||||
[EModelEndpoint.assistants]: assistant.buildOptions,
|
||||
[EModelEndpoint.assistants]: assistants.buildOptions,
|
||||
[EModelEndpoint.azureAssistants]: azureAssistants.buildOptions,
|
||||
};
|
||||
|
||||
function buildEndpointOption(req, res, next) {
|
||||
async function buildEndpointOption(req, res, next) {
|
||||
const { endpoint, endpointType } = req.body;
|
||||
const parsedBody = parseConvo({ endpoint, endpointType, conversation: req.body });
|
||||
|
||||
if (req.app.locals.modelSpecs?.list && req.app.locals.modelSpecs?.enforce) {
|
||||
/** @type {{ list: TModelSpec[] }}*/
|
||||
const { list } = req.app.locals.modelSpecs;
|
||||
const { spec } = parsedBody;
|
||||
|
||||
if (!spec) {
|
||||
return handleError(res, { text: 'No model spec selected' });
|
||||
}
|
||||
|
||||
const currentModelSpec = list.find((s) => s.name === spec);
|
||||
if (!currentModelSpec) {
|
||||
return handleError(res, { text: 'Invalid model spec' });
|
||||
}
|
||||
|
||||
if (endpoint !== currentModelSpec.preset.endpoint) {
|
||||
return handleError(res, { text: 'Model spec mismatch' });
|
||||
}
|
||||
|
||||
if (
|
||||
currentModelSpec.preset.endpoint !== EModelEndpoint.gptPlugins &&
|
||||
currentModelSpec.preset.tools
|
||||
) {
|
||||
return handleError(res, {
|
||||
text: `Only the "${EModelEndpoint.gptPlugins}" endpoint can have tools defined in the preset`,
|
||||
});
|
||||
}
|
||||
|
||||
const isValidModelSpec = enforceModelSpec(currentModelSpec, parsedBody);
|
||||
if (!isValidModelSpec) {
|
||||
return handleError(res, { text: 'Model spec mismatch' });
|
||||
}
|
||||
}
|
||||
|
||||
req.body.endpointOption = buildFunction[endpointType ?? endpoint](
|
||||
endpoint,
|
||||
parsedBody,
|
||||
endpointType,
|
||||
);
|
||||
|
||||
const modelsConfig = await getModelsConfig(req);
|
||||
req.body.endpointOption.modelsConfig = modelsConfig;
|
||||
|
||||
if (req.body.files) {
|
||||
// hold the promise
|
||||
req.body.endpointOption.attachments = processFiles(req.body.files);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
const Keyv = require('keyv');
|
||||
const uap = require('ua-parser-js');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const { getLogStores } = require('../../cache');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { isEnabled, removePorts } = require('../utils');
|
||||
const keyvRedis = require('../../cache/keyvRedis');
|
||||
const User = require('../../models/User');
|
||||
const keyvRedis = require('~/cache/keyvRedis');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const User = require('~/models/User');
|
||||
|
||||
const banCache = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'bans', ttl: 0 });
|
||||
: new Keyv({ namespace: ViolationTypes.BAN, ttl: 0 });
|
||||
const message = 'Your account has been temporarily banned due to violations of our service.';
|
||||
|
||||
/**
|
||||
@@ -28,7 +29,7 @@ const banResponse = async (req, res) => {
|
||||
if (!ua.browser.name) {
|
||||
return res.status(403).json({ message });
|
||||
} else if (baseUrl === '/api/ask' || baseUrl === '/api/edit') {
|
||||
return await denyRequest(req, res, { type: 'ban' });
|
||||
return await denyRequest(req, res, { type: ViolationTypes.BAN });
|
||||
}
|
||||
|
||||
return res.status(403).json({ message });
|
||||
@@ -87,7 +88,7 @@ const checkBan = async (req, res, next = () => {}) => {
|
||||
return await banResponse(req, res);
|
||||
}
|
||||
|
||||
const banLogs = getLogStores('ban');
|
||||
const banLogs = getLogStores(ViolationTypes.BAN);
|
||||
const duration = banLogs.opts.ttl;
|
||||
|
||||
if (duration <= 0) {
|
||||
|
||||
25
api/server/middleware/checkDomainAllowed.js
Normal file
25
api/server/middleware/checkDomainAllowed.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const { isDomainAllowed } = require('~/server/services/AuthService');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Checks the domain's social login is allowed
|
||||
*
|
||||
* @async
|
||||
* @function
|
||||
* @param {Object} req - Express request object.
|
||||
* @param {Object} res - Express response object.
|
||||
* @param {Function} next - Next middleware function.
|
||||
*
|
||||
* @returns {Promise<function|Object>} - Returns a Promise which when resolved calls next middleware if the domain's email is allowed
|
||||
*/
|
||||
const checkDomainAllowed = async (req, res, next = () => {}) => {
|
||||
const email = req?.user?.email;
|
||||
if (email && !(await isDomainAllowed(email))) {
|
||||
logger.error(`[Social Login] [Social Login not allowed] [Email: ${email}]`);
|
||||
return res.redirect('/login');
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = checkDomainAllowed;
|
||||
58
api/server/middleware/enforceModelSpec.js
Normal file
58
api/server/middleware/enforceModelSpec.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const interchangeableKeys = new Map([
|
||||
['chatGptLabel', ['modelLabel']],
|
||||
['modelLabel', ['chatGptLabel']],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Middleware to enforce the model spec for a conversation
|
||||
* @param {TModelSpec} modelSpec - The model spec to enforce
|
||||
* @param {TConversation} parsedBody - The parsed body of the conversation
|
||||
* @returns {boolean} - Whether the model spec is enforced
|
||||
*/
|
||||
const enforceModelSpec = (modelSpec, parsedBody) => {
|
||||
for (const [key, value] of Object.entries(modelSpec.preset)) {
|
||||
if (key === 'endpoint') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!checkMatch(key, value, parsedBody)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if there is a match for the given key and value in the parsed body
|
||||
* or any of its interchangeable keys, including deep comparison for objects and arrays.
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @param {object} parsedBody
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const checkMatch = (key, value, parsedBody) => {
|
||||
const isEqual = (a, b) => {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return a.length === b.length && a.every((val, index) => isEqual(val, b[index]));
|
||||
} else if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
return keysA.length === keysB.length && keysA.every((k) => isEqual(a[k], b[k]));
|
||||
}
|
||||
return a === b;
|
||||
};
|
||||
|
||||
if (isEqual(parsedBody[key], value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (interchangeableKeys.has(key)) {
|
||||
return interchangeableKeys
|
||||
.get(key)
|
||||
.some((interchangeableKey) => isEqual(parsedBody[interchangeableKey], value));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = enforceModelSpec;
|
||||
47
api/server/middleware/enforceModelSpec.spec.js
Normal file
47
api/server/middleware/enforceModelSpec.spec.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// enforceModelSpec.test.js
|
||||
|
||||
const enforceModelSpec = require('./enforceModelSpec');
|
||||
|
||||
describe('enforceModelSpec function', () => {
|
||||
test('returns true when all model specs match parsed body directly', () => {
|
||||
const modelSpec = { preset: { title: 'Dialog', status: 'Active' } };
|
||||
const parsedBody = { title: 'Dialog', status: 'Active' };
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true when model specs match via interchangeable keys', () => {
|
||||
const modelSpec = { preset: { chatGptLabel: 'GPT-4' } };
|
||||
const parsedBody = { modelLabel: 'GPT-4' };
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false if any key value does not match', () => {
|
||||
const modelSpec = { preset: { language: 'English', level: 'Advanced' } };
|
||||
const parsedBody = { language: 'Spanish', level: 'Advanced' };
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(false);
|
||||
});
|
||||
|
||||
test('ignores the \'endpoint\' key in model spec', () => {
|
||||
const modelSpec = { preset: { endpoint: 'ignored', feature: 'Special' } };
|
||||
const parsedBody = { feature: 'Special' };
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(true);
|
||||
});
|
||||
|
||||
test('handles nested objects correctly', () => {
|
||||
const modelSpec = { preset: { details: { time: 'noon', location: 'park' } } };
|
||||
const parsedBody = { details: { time: 'noon', location: 'park' } };
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(true);
|
||||
});
|
||||
|
||||
test('handles arrays within objects', () => {
|
||||
const modelSpec = { preset: { tags: ['urgent', 'important'] } };
|
||||
const parsedBody = { tags: ['urgent', 'important'] };
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(true);
|
||||
});
|
||||
|
||||
test('fails when arrays in objects do not match', () => {
|
||||
const modelSpec = { preset: { tags: ['urgent', 'important'] } };
|
||||
const parsedBody = { tags: ['important', 'urgent'] }; // Different order
|
||||
expect(enforceModelSpec(modelSpec, parsedBody)).toBe(false);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user