Compare commits

...

268 Commits

Author SHA1 Message Date
Danny Avila
093d5bb05f feat: Add sleep to OpenAIClient.js for better performance 2024-05-07 22:17:03 -04:00
Danny Avila
b6d6343f54 📧 feat: Mention "@" Command Popover (#2635)
* feat: initial mockup

* wip: activesetting, may use or not use

* wip: mention with useCombobox usage

* feat: connect textarea to new mention popover

* refactor: consolidate icon logic for Landing/convos

* refactor: cleanup URL logic

* refactor(useTextarea): key up handler

* wip: render desired mention options

* refactor: improve mention detection

* feat: modular chat the default option

* WIP: first pass mention selection

* feat: scroll mention items with keypad

* chore(showMentionPopoverFamily): add typing to atomFamily

* feat: removeAtSymbol

* refactor(useListAssistantsQuery): use defaultOrderQuery as default param

* feat: assistants mentioning

* fix conversation switch errors

* filter mention selections based on startup settings and available endpoints

* fix: mentions model spec icon URL

* style: archive icon

* fix: convo renaming behavior on click

* fix(Convo): toggle hover state

* style: EditMenu refactor

* fix: archive chats table

* fix: errorsToString import

* chore: remove comments

* chore: remove comment

* feat: mention descriptions

* refactor: make sure continue hover button is always last, add correct fork button alt text
2024-05-07 13:13:55 -04:00
Yuichi Ohneda
89b1e33be0 🚀feat: Archive conversations (#2590)
* 🔧chore: add internationalization labels for archive feature

*  feat: Add function to useArchiveConversationMutation()

This commit adds a new mutation function `useArchiveConversationMutation()` for archiving conversations. This function takes the ID string of the conversation to be archived and returns a mutation result object. Upon successful archiving, it removes and refreshes the conversation from the query data cache.
While ChatGPT PATCHes the archived status by sending `{is_archived: true}` to the URL `/backend-api/conversation/$conversation_id`, this implementation uses the `dataService.updateConversation(payload)` with a POST method, aligning with the existing code conventions.

*  feat(api): add is_archived field to Conversation schema and update getConvosByPage method

This commit adds a new field `is_archived` with a default value of false to the Conversation schema. It also modifies the `getConvosByPage` method within the Conversation API to adjust the query to only target conversations where `is_archived` is set to false or where the `is_archived` field does not exist. The function `getConvosQueried`, which returns conversations for a specified Conversation ID, was determined not to require consideration of whether `is_archived` is true or false, and thus was not modified.

* ♻️ refactor: add className prop to DotsIcon component

To enhance the versatility of the DotsIcon component, this commit introduces the ability to specify a className prop, allowing for greater customization.

*  feat(ui): add Edit Button to group Title change and Conversation delete buttons

Added a new Edit Button to the conversations, similar to the ChatGPT UI, which groups options for editing the conversation title and deleting conversations. This grouping is accessible through a dialogue that appears when the three-dot icon is clicked.

* ♻️ refactor(ui): enhance Delete Button to accept className and label options

Enhanced the Delete Button component to accept a `className` for customization and an optional `appendLabel`. The DeleteButton component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.

* ♻️ refactor(ui): enhance RenameButton to accept label options

Added the ability to optionally display labels; the Rename Button component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.

* 🔧 chors: additional localization labels

* ♻️  refactor: change is_archived property of conversation to camelCase

* Refactor the is_archived property of conversation to camelCase (isArchived) to adhere to the existing code conventions
* Modify the function that retrieves conversations to accept the isArchived parameter

* ♻️ refactor: add archiveConversation mutation

I thought I could divert dataService.updateConversation, but added a new archiveConversation because the request types are different. It might be better to make them common, but to avoid side effects, I added a new function this time.
Added process to deleteConversationMutation to delete archived conversations

*  feat: Add the function to hide a cancel button in DialogTemplate component

The Cancel button is not needed when displaying the archive list, so I made the Cancel button optional.

* ♻️ refactor: Add support for filtering archived conversations in Nav component

This commit modifies the Nav component to add the ability to filter out archived conversations when fetching data. This is done by adding `isArchived: false` to the query parameters for both the `useConversationsInfiniteQuery()` and `useSearchInfiniteQuery()` hooks, effectively excluding any archived conversations from the results returned.

* ♻️ refactor: add Tooltip to DeleteButton

* Add Tooltip to DeleteButton component
* Display Tooltip when DeleteButton only shows an Icon without text

*  feat(ui): add ArchiveButton component for archiving conversations

To be compatible with the ChatGPT UI, no confirmation dialog is displayed when ArchiveButton is clicked. The basic behavior conforms to DeleteButton and RenameButton.

*  feat(ui): add Archive button to list of conversations

Modify the Nav of the conversation list to include a dropdown that contains the Rename and Delete options, similar to the ChatGPT UI. Additionally, an Archive button has been added adjacent to the dropdown menu.

*  feat: Add ArchivedChatsTable component

Adds the `ArchivedChatsTable` component, which displays a table of archived chats. It has been implemented to be as compatible with the ChatGPT UI as possible.

* 🚑 fix(tooltip): increase z-index to ensure visibility over Dialog

Resolve an issue where tooltips were not visible when displayed over a Dialog. The z-index of `DialogPrimitive.Portal` in `Dialog.tsx` is set to 999. Since the rationale for this value is unclear, the z-index of the tooltip has been increased to 1000 to guarantee its visibility above the Dialog component.

* 🔧 chors: add internationalization labels
2024-05-06 23:07:00 -04:00
Marco Beretta
436f7195b5 🌍: Update Italian translation (#2622) 2024-05-06 07:51:01 -04:00
Marco Beretta
2aec4a6250 🔄 refactor: improved RAG animations/messages (#2616)
* fix: warning slow process rag  message

* refactor: improved useProgress hook for Files
2024-05-05 15:35:51 -04:00
Marco Beretta
b77bd19092 🎨 style(Fork): update light/dark theme (#2621) 2024-05-05 15:35:16 -04:00
Danny Avila
446ffe0417 📝 docs: update README.md 2024-05-05 12:51:38 -04:00
Danny Avila
b9bcaee656 📝 docs: update README.md 2024-05-05 12:48:26 -04:00
Danny Avila
110c0535fb Update README.md 2024-05-05 12:38:17 -04:00
Danny Avila
25fceb78b7 🌿 feat: Fork Messages/Conversations (#2617)
* typedef for ImportBatchBuilder

* feat: first pass, fork conversations

* feat: fork - getMessagesUpToTargetLevel

* fix: additional tests and fix getAllMessagesUpToParent

* chore: arrow function return

* refactor: fork 3 options

* chore: remove unused genbuttons

* chore: remove unused hover buttons code

* feat: fork first pass

* wip: fork remember setting

* style: user icon

* chore: move clear chats to data tab

* WIP: fork UI options

* feat: data-provider fork types/services/vars and use generic MutationOptions

* refactor: use single param for fork option, use enum, fix mongo errors, use Date.now(), add records flag for testing, use endpoint from original convo and messages, pass originalConvo to finishConversation

* feat: add fork mutation hook and consolidate type imports

* refactor: use enum

* feat: first pass, fork mutation

* chore: add enum for target level fork option

* chore: add enum for target level fork option

* show toast when checking remember selection

* feat: splitAtTarget

* feat: split at target option

* feat: navigate to new fork, show toasts, set result query data

* feat: hover info for all fork options

* refactor: add Messages settings tab

* fix(Fork): remember text info

* ci: test for single message and is target edge case

* feat: additional tests for getAllMessagesUpToParent

* ci: additional tests and cycle detection for getMessagesUpToTargetLevel

* feat: circular dependency checks for getAllMessagesUpToParent

* fix: getMessagesUpToTargetLevel circular dep. check

* ci: more tests for getMessagesForConversation

* style: hover text for checkbox fork items

* refactor: add statefulness to conversation import
2024-05-05 11:48:20 -04:00
Danny Avila
c8baceac76 🐛 fix: Prevent Empty File Uploads & Assistants Fixes (#2611)
* chore: update default models for openai/assistants

* fix: allows assistants models fetching

* change default models order, ensure assistant_id is defined if intended

* fix: prevent empty files from being uploaded
2024-05-03 12:49:26 -04:00
Kai Kreuzer
a0288f1c5c 🧾 docs: Fix Typo in librechat.example.yaml (#2606) 2024-05-02 16:27:11 -04:00
Danny Avila
5d3c90be26 📦 chore: update package.json/package-lock.json (#2600)
* 📦 chore: update package.json/package-lock.json

* 📦 chore: remove unused dependencies
2024-05-02 03:23:38 -04:00
Denis Palnitsky
ab6fbe48f1 📥 feat: Import Conversations from LibreChat, ChatGPT, Chatbot UI (#2355)
* Basic implementation of ChatGPT conversation import

* remove debug code

* Handle citations

* Fix updatedAt in import

* update default model

* Use job scheduler to handle import requests

* import job status endpoint

* Add wrapper around Agenda

* Rate limits for import endpoint

* rename import api path

* Batch save import to mongo

* Improve naming

* Add documenting comments

* Test for importers

* Change button for importing conversations

* Frontend changes

* Import job status endpoint

* Import endpoint response

* Add translations to new phrases

* Fix conversations refreshing

* cleanup unused functions

* set timeout for import job status polling

* Add documentation

* get extra spaces back

* Improve error message

* Fix translation files after merge

* fix translation files 2

* Add zh translation for import functionality

* Sync mailisearch index after import

* chore: add dummy uri for jest tests, as MONGO_URI should only be real for E2E tests

* docs: fix links

* docs: fix conversationsImport section

* fix: user role issue for librechat imports

* refactor: import conversations from json
- organize imports
- add additional jsdocs
- use multer with diskStorage to avoid loading file into memory outside of job
- use filepath instead of loading data string for imports
- replace console logs and some logger.info() with logger.debug
- only use multer for import route

* fix: undefined metadata edge case and replace ChatGtp -> ChatGpt

* Refactor importChatGptConvo function to handle undefined metadata edge case and replace ChatGtp with ChatGpt

* fix: chatgpt importer

* feat: maintain tree relationship for librechat messages

* chore: use enum

* refactor: saveMessage to use single object arg, replace console logs, add userId to log message

* chore: additional comment

* chore: multer edge case

* feat: first pass, maintain tree relationship

* chore: organize

* chore: remove log

* ci: add heirarchy test for chatgpt

* ci: test maintaining of heirarchy for librechat

* wip: allow non-text content type messages

* refactor: import content part object json string

* refactor: more content types to format

* chore: consolidate messageText formatting

* docs: update on changes, bump data-provider/config versions, update readme

* refactor(indexSync): singleton pattern for MeiliSearchClient

* refactor: debug log after batch is done

* chore: add back indexSync error handling

---------

Co-authored-by: jakubmieszczak <jakub.mieszczak@zendesk.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2024-05-02 02:48:26 -04:00
Yuichi Ohneda
3b44741cf9 🚑 fix(dialog): showCloseButton Prop Warning in DialogContent Component (#2597)
Fix DialogPrimitive.Content to not pass a showCloseButton property that does not exist in it to avoid outputting a warning.
2024-05-02 02:08:37 -04:00
Extremys
d21a05606e 🍎 feat: Apple MLX as Known Endpoint (#2580)
* add integration with Apple MLX

* fix: apple icon + image mkd link

---------

Co-authored-by: “Extremys” <“Extremys@email.com”>
Co-authored-by: Danny Avila <danny@librechat.ai>
2024-05-01 03:27:02 -04:00
Danny Avila
0e50c07e3f 🤖 feat: Model Specs & Save Tools per Convo/Preset (#2578)
* WIP: first pass ModelSpecs

* refactor(onSelectEndpoint): use `getConvoSwitchLogic`

* feat: introduce iconURL, greeting, frontend fields for conversations/presets/messages

* feat: conversation.iconURL & greeting in Landing

* feat: conversation.iconURL & greeting in New Chat button

* feat: message.iconURL

* refactor: ConversationIcon -> ConvoIconURL

* WIP: add spec as a conversation field

* refactor: useAppStartup, set spec on initial load for new chat, allow undefined spec, add localStorage keys enum, additional type fields for spec

* feat: handle `showIconInMenu`, `showIconInHeader`, undefined `iconURL` and no specs on initial load

* chore: handle undefined or empty modelSpecs

* WIP: first pass, modelSpec schema for custom config

* refactor: move default filtered tools definition to ToolService

* feat: pass modelSpecs from backend via startupConfig

* refactor: modelSpecs config, return and define list

* fix: react error and include iconURL in responseMessage

* refactor: add iconURL to responseMessage only

* refactor: getIconEndpoint

* refactor: pass TSpecsConfig

* fix(assistants): differentiate compactAssistantSchema, correctly resets shared conversation state with other endpoints

* refactor: assistant id prefix localStorage key

* refactor: add more LocalStorageKeys and replace hardcoded values

* feat: prioritize spec on new chat behavior: last selected modelSpec behavior (localStorage)

* feat: first pass, interface config

* chore: WIP, todo: add warnings based on config.modelSpecs settings.

* feat: enforce modelSpecs if configured

* feat: show config file yaml errors

* chore: delete unused legacy Plugins component

* refactor: set tools to localStorage from recoil store

* chore: add stable recoil setter to useEffect deps

* refactor: save tools to conversation documents

* style(MultiSelectPop): dynamic height, remove unused import

* refactor(react-query): use localstorage keys and pass config to useAvailablePluginsQuery

* feat(utils): add mapPlugins

* refactor(Convo): use conversation.tools if defined, lastSelectedTools if not

* refactor: remove unused legacy code using `useSetOptions`, remove conditional flag `isMultiChat` for using legacy settings

* refactor(PluginStoreDialog): add exhaustive-deps which are stable react state setters

* fix(HeaderOptions): pass `popover` as true

* refactor(useSetStorage): use project enums

* refactor: use LocalStorageKeys enum

* fix: prevent setConversation from setting falsy values in lastSelectedTools

* refactor: use map for availableTools state and available Plugins query

* refactor(updateLastSelectedModel): organize logic better and add note on purpose

* fix(setAgentOption): prevent reseting last model to secondary model for gptPlugins

* refactor(buildDefaultConvo): use enum

* refactor: remove `useSetStorage` and consolidate areas where conversation state is saved to localStorage

* fix: conversations retain tools on refresh

* fix(gptPlugins): prevent nullish tools from being saved

* chore: delete useServerStream

* refactor: move initial plugins logic to useAppStartup

* refactor(MultiSelectDropDown): add more pass-in className props

* feat: use tools in presets

* chore: delete unused usePresetOptions

* refactor: new agentOptions default handling

* chore: note

* feat: add label and custom instructions to agents

* chore: remove 'disabled with tools' message

* style: move plugins to 2nd column in parameters

* fix: TPreset type for agentOptions

* fix: interface controls

* refactor: add interfaceConfig, use Separator within Switcher

* refactor: hide Assistants panel if interface.parameters are disabled

* fix(Header): only modelSpecs if list is greater than 0

* refactor: separate MessageIcon logic from useMessageHelpers for better react rule-following

* fix(AppService): don't use reserved keyword 'interface'

* feat: set existing Icon for custom endpoints through iconURL

* fix(ci): tests passing for App Service

* docs: refactor custom_config.md for readability and better organization, also include missing values

* docs: interface section and re-organize docs

* docs: update modelSpecs info

* chore: remove unused files

* chore: remove unused files

* chore: move useSetIndexOptions

* chore: remove unused file

* chore: move useConversation(s)

* chore: move useDefaultConvo

* chore: move useNavigateToConvo

* refactor: use plugin install hook so it can be used elsewhere

* chore: import order

* update docs

* refactor(OpenAI/Plugins): allow modelLabel as an initial value for chatGptLabel

* chore: remove unused EndpointOptionsPopover and hide 'Save as Preset' button if preset UI visibility disabled

* feat(loadDefaultInterface): issue warnings based on values

* feat: changelog for custom config file

* docs: add additional changelog note

* fix: prevent unavailable tool selection from preset and update availableTools on Plugin installations

* feat: add `filteredTools` option in custom config

* chore: changelog

* fix(MessageIcon): always overwrite conversation.iconURL in messageSettings

* fix(ModelSpecsMenu): icon edge cases

* fix(NewChat): dynamic icon

* fix(PluginsClient): always include endpoint in responseMessage

* fix: always include endpoint and iconURL in responseMessage across different response methods

* feat: interchangeable keys for modelSpec enforcing
2024-04-30 22:11:48 -04:00
Ventz Petkov
a5cac03fa4 🚅 docs: load LiteLLM into LibreChat (#2573)
Documentation was missing one significant component, on loading librechat.yaml
2024-04-29 19:56:19 -04:00
Fuegovic
ba4fa6150e 🦙 docs: fix litellm.md (#2566) 2024-04-28 08:33:51 -04:00
Neelesh Kumar
463ca5d613 📄 docs: Update apipie fetch.py in ai_endpoints.md (#2547)
* Update apipie fetch.py in ai_endpoints.md

Made the python code more pythonic

* fix bug that caused duplicate model_ids
2024-04-27 18:37:33 -04:00
Neelesh Kumar
039c7ae880 📄 docs: update GOOGLE_MODELS in .env.example (#2506)
* update GOOGLE_MODELS in .env.example

* Update .env.example

* Update .env.example

* Update .env.example

* Update .env.example

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2024-04-27 18:36:54 -04:00
Danny Avila
63ef15ab63 🦙 feat: Fetch list of Ollama Models (#2565)
* 🦙 feat: Fetch list of Ollama Models

* style: better Tag text styling for light mode
2024-04-27 18:27:04 -04:00
Ventz Petkov
8a78500fe2 🚅 docs: update LiteLLM config with more models (#2553)
Added all OpenAI, Azure OpenAI, Amazon Bedrock, and Google GCP models available.

Specifically latest: Llama3; Google Gemini 1.5 Pro Preview; Claude Opus.

Here are all of the models:
  - model_name: claude-3-haiku
  - model_name: claude-3-sonnet
  - model_name: claude-3-opus
  - model_name: claude-v2
  - model_name: claude-instant
  - model_name: llama2-13b
  - model_name: llama2-70b
  - model_name: llama3-8b
  - model_name: llama3-70b
  - model_name: mistral-7b-instruct
  - model_name: mixtral-8x7b-instruct
  - model_name: mixtral-large
  - model_name: cohere-command-v14
  - model_name: cohere-command-light-v14
  - model_name: ai21-j2-mid
  - model_name: ai21-j2-ultra
  - model_name: amazon-titan-lite
  - model_name: amazon-titan-express
  - model_name: azure-gpt-4-turbo-preview
  - model_name: azure-gpt-3.5-turbo
  - model_name: azure-gpt-4
  - model_name: azure-gpt-3.5-turbo-16k
  - model_name: azure-gpt-4-32k
  - model_name: gpt-4-turbo
  - model_name: old-gpt-4-turbo-preview
  - model_name: gpt-3.5-turbo
  - model_name: gpt-4
  - model_name: gpt-3.5-turbo-16k
  - model_name: gpt-4-32k
  - model_name: gpt-4-vision-preview
  - model_name: google-chat-bison
  - model_name: google-chat-bison-32k
  - model_name: google-gemini-pro-1.0
  - model_name: google-gemini-pro-1.5-preview
2024-04-27 06:46:20 -04:00
Fuegovic
144fd5f6aa ⚙️ docs: update dotenv.md (#2551)
* ⚙️ docs: update dotenv.md

* Update dotenv.md - formatting
2024-04-26 14:34:50 -04:00
Danny Avila
2720327aa1 👩‍💻 fix: Minor UI fixes (#2548)
* fix(useMessageHelpers): define iconEndpoint

* fix: rely on Assistant Switcher effect for defining `assistant_id`, ensure ChatRoute `newConversation` only fires once
2024-04-26 10:27:49 -04:00
Martin Dahlö
4d0806d3e8 📋 refactor: allow paste in confirm field when resetting passwords (#2542)
* Disabled paste prevention in the confirm password field when resetting passwords.

* chore(ResetPassword): remove comments

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2024-04-25 20:26:33 -04:00
Neelesh Kumar
5b5f9b950b 🐋 refactor: Update docker-compose.yml (#2507) 2024-04-25 20:25:51 -04:00
いしかず
3ccff19821 🌍: Update Japanese translation (#2519)
* Update japanese translation

* translate japanese

* Added 5 Japanese translations

---------

Co-authored-by: k-ishii1020 <40662914+8492GitKaz@users.noreply.github.com>
2024-04-25 13:55:59 -04:00
Marco Beretta
11d5e232b3 🧪 refactor(isDomainAllowed): change directory, add tests (#2539) 2024-04-25 13:14:07 -04:00
Danny Avila
099aa9dead feat: Stop Sequences for Conversations & Presets (#2536)
* feat: `stop` conversation parameter

* feat: Tag primitive

* feat: dynamic tags

* refactor: update tag styling

* feat: add stop sequences to OpenAI settings

* fix(Presentation): prevent `SidePanel` re-renders that flicker side panel

* refactor: use stop placeholder

* feat: type and schema update for `stop` and `TPreset` in generation param related types

* refactor: pass conversation to dynamic settings

* refactor(OpenAIClient): remove default handling for `modelOptions.stop`

* docs: fix Google AI Setup formatting

* feat: current_model

* docs: WIP update

* fix(ChatRoute): prevent default preset override before `hasSetConversation.current` becomes true by including latest conversation state as template

* docs: update docs with more info on `stop`

* chore: bump config_version

* refactor: CURRENT_MODEL handling
2024-04-25 11:40:17 -04:00
Danny Avila
4121818124 ✍️ fix(useTextarea): Rich Text Format paste from MS Word (#2530) 2024-04-25 00:10:41 -04:00
Fuegovic
ca9a0fe629 🥧 feat: APIpie support (#2524) 2024-04-24 20:32:18 -04:00
Danny Avila
bde6bb0152 🔀 fix: Remove use of Mongo Transactions (#2525)
* fix: remove use of transactions

* fix: remove use of transactions

* refactor(actions): perform OpenAI API operation first, before attempting database updates
2024-04-24 16:04:46 -04:00
ilsubyeega
667f5f91fe 👤 fix: Use user?.username if user?.name is undefined (#2511)
* Use `user?.username` if `user?.name` is undefined

* Add useLocalize hook to Icon component
2024-04-24 12:34:01 -04:00
Mathieu Breton
75da75be08 🛂 feat(oauth): add domain restriction on social login (#2512) 2024-04-24 12:14:27 -04:00
Danny Avila
cdab1e9cda 🔒 fix: package-lock file for cross-compatibility (#2515) 2024-04-24 10:42:18 -04:00
Danny Avila
3df4fac118 v0.7.1 (#2502)
* chore: make openai package definition explicit

*  v0.7.1

* chore: gpt-4-vision correct context length

* add `llava` to vision models list
2024-04-23 08:57:20 -04:00
Danny Avila
0ae98ff011 🧪 ci: Add .env.test for backend-review.yml (#2501)
* 🧪 ci: Add `.env.test` for `backend-review.yml`

* chore: touch example file
2024-04-23 08:21:12 -04:00
Walber Cardoso
4d05e5b79a 👟 style: WrenchIcon and ImageGen SVG Animations (#2382)
* refactor(WrenchIcon, ImageGen SVG)

* refactor(WrenchIcon SVG)

* refactor(ImageGen SVG)

* refactor(ImageGen SVG) - add CSS
2024-04-23 08:14:28 -04:00
Danny Avila
199f9f32e6 🐋 fix(Dockerfile.multi): Resolve OpenAI SDK @ Latest for Assistants v1 2024-04-22 20:23:28 -04:00
Danny Avila
f94a782b4f 💻 fix(client): Allow Code Filetypes and Suppress Known Vite Warnings (#2492)
* refactor(vite): suppress known warnings

* fix: allow known code filetypes if there is a mismatch of expected type, or originalFile.type is empty

* refactor(useFileHandling): naming, use variable
2024-04-22 20:08:34 -04:00
Fuegovic
738207de50 ✏️docs: remove "copilot-gpt4-service" (#2491)
removed "copilot-gpt4-service", the repository has been disabled
2024-04-22 16:35:05 -04:00
Danny Avila
c96f067689 🔧 fix: Resolve Proper Dependencies to fix Application Error (#2488)
* chore: bump data-provider

* feat: script to check recent dependency updates

* fix: override vite/rollup version for vite build fix
- also remove unused vite-plugin-html
- add vite build to file output command

* chore: bump rollup override to last known working version (v4.16.0 is breaking)

* chore(vite): increase file size cache for workbox

* fix: resolve openai to last known version using assistants v1 latest features and default header

* chore: update openrouter examples
2024-04-22 12:52:30 -04:00
Danny Avila
3bfd185cab feat: Added PWA Setup & Manual Chunks via Vite (#2477)
* added pwa setup via vite config

Added apple status bar meta data

added maskable 512 icon for chrome and android devices

added vite-plugin-pwa

updated vite config to setup the pwa service worker and manifest upon build

* fix(vite): avoid pre-caching generated images

* chore: add manual chunking of larger vendor package

* chore: remove comments

---------

Co-authored-by: davecrab <65996799+davecrab@users.noreply.github.com>
2024-04-21 10:39:15 -04:00
Danny Avila
c937b8cd07 🧑‍💻 refactor: Display Client-facing Errors (#2476)
* fix(Google): allow presets to configure expected maxOutputTokens

* refactor: standardize client-facing errors

* refactor(checkUserKeyExpiry): pass endpoint instead of custom message

* feat(UserService): JSDocs and getUserKeyValues

* refactor: add NO_BASE_URL error type, make use of getUserKeyValues, throw user-specific errors

* ci: update tests with recent changes
2024-04-21 08:31:54 -04:00
Passerby1011
6db91978ca 🎨 style: update CodeSherpa icon (#2417)
The icon address error leads to the icon not displaying.
2024-04-20 15:09:59 -04:00
Danny Avila
8c22bb1d3d 🛠️ fix(Azure/Assistants): Handle Long Domain Names & Other Minor chores (#2475)
* chore: replace violation cache accessors with enum

* chore: fix test

* chore(fileSchema): index timestamps

* fix(ActionService): use encoding/caching strategy for handling assistant function character length limit

* refactor(actions): async `domainParser` also resolve retrieved model (which is deployment name) to user-defined model

* style(AssistantAction): add `whitespace-nowrap` for ellipsis

* refactor(ActionService): if domain is less than or equal to encoded domain fixed length, return domain with replacement of separator

* refactor(actions): use sessions/transactions for updating Assistant Action database records

* chore: remove TTL from ENCODED_DOMAINS cache

* refactor(domainParser): minor optimization and add tests

* fix(spendTokens): use txData.user for token usage logging

* refactor(actions): add helper function `withSession` for database operations with sessions/transactions

* fix(PluginsClient): logger debug `message` field edge case
2024-04-20 15:02:56 -04:00
Noah Ispas
5d642d0187 📙 docs: Remove duplicate information (#2451) 2024-04-19 22:57:40 -04:00
Fuegovic
4196a86fa9 🦙 doc update: llama3 (#2470)
* docs: update breaking_changes.md

* docs: update ai_endpoints.md -> llama3 for Ollama and groq

* librechat.yaml: update groq models

* Update breaking_changes.md

logs location

* Update breaking_changes.md

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-04-19 21:40:12 -04:00
Danny Avila
e6310c806a 🤖 fix: Minor Assistants Endpoint Fixes (#2472)
* fix(useCreateAssistantMutation): force re-render of assistants map by avoiding use shallow reference of listRes.data

* fix(AppService): regression by not including azure assistant defaults when no assistant endpoint values are set
2024-04-19 19:05:25 -04:00
Danny Avila
3d1dec62a4 🧼 refactor(AppService): Consolidate Logic & Issue more Warnings (#2468)
* chore: bump example config version

* refactor(AppService): issue warnings from separate modules where possible

* refactor(AppService): consolidate AppService logic to separate modules as much as possible

* chore: bump data-provider

* chore: remove unn. variable definition

* chore: warning wording
2024-04-19 12:05:39 -04:00
Danny Avila
de3987cbaf 🛂 refactor(openidStrategy): Use Strategy Functions for Avatars (#2467) 2024-04-19 09:12:55 -04:00
Fuegovic
f406a85633 🖥️ docs: env changes v0.6.10→v0.7.0+ (#2442)
* docs: env changes v0.6.10→v0.7.0+ | update railway referral

* docs: env changes v0.6.10→v0.7.0+
2024-04-18 08:27:52 -04:00
Danny Avila
692ce3b346 🛠️ fix: Merge Textarea Ref with Form for Simplified Handling (#2456)
* share ref correctly

* chore: remove extraneous textarea handlers and add excel text data
2024-04-18 08:19:52 -04:00
Fuegovic
26ea990045 📘 docs: update docker_compose_install.md (#2447)
* Update docker_compose_install.md

* Update docker_compose_install.md
2024-04-18 07:31:44 -04:00
Fuegovic
265abbc1c8 ⚙️ docs: Update .env.example (#2449) 2024-04-18 07:30:46 -04:00
marlonka
0b7da72be6 🌍 : Update German Translations (#2409)
* Improved and additional German translations

* Improved and added german translations and removed language translations here

* Spelling error fixed

* Use single quotes instead of double

* Use single quotes instead of double
2024-04-18 07:26:44 -04:00
Danny Avila
3c184e9410 🛠️ fix: Ensure imageOutputType is Always Defined (#2438)
* avatar fix

* chore: ensure `imageOutputType` is always defined

* ci(AppService): extra test for default value

* chore: replace default value for `desiredFormat` with `EImageOutputType` enum
2024-04-16 16:34:19 -04:00
Danny Avila
bf4e64ce63 🪙 fix(Google): Update maxOutputTokens Condition (#2434)
- generalize condition to fix max output tokens for gemini-1.5
2024-04-16 10:23:13 -04:00
Danny Avila
9d854dac07 🤖 feat: Gemini 1.5 Support (+Vertex AI) (#2383)
* WIP: gemini-1.5 support

* feat: extended vertex ai support

* fix: handle possibly undefined modelName

* fix: gpt-4-turbo-preview invalid vision model

* feat: specify `fileConfig.imageOutputType` and make PNG default image conversion type

* feat: better truncation for errors including base64 strings

* fix: gemini inlineData formatting

* feat: RAG augmented prompt for gemini-1.5

* feat: gemini-1.5 rates and token window

* chore: adjust tokens, update docs, update vision Models

* chore: add back `ChatGoogleVertexAI` for chat models via vertex ai

* refactor: ask/edit controllers to not use `unfinished` field for google endpoint

* chore: remove comment

* chore(ci): fix AppService test

* chore: remove comment

* refactor(GoogleSearch): use `GOOGLE_SEARCH_API_KEY` instead, issue warning for old variable

* chore: bump data-provider to 0.5.4

* chore: update docs

* fix: condition for gemini-1.5 using generative ai lib

* chore: update docs

* ci: add additional AppService test for `imageOutputType`

* refactor: optimize new config value `imageOutputType`

* chore: bump CONFIG_VERSION

* fix(assistants): avatar upload
2024-04-16 08:32:40 -04:00
Danny Avila
fce7246ac1 🔓 refactor: Make Image URL Security Optional (#2415) 2024-04-14 19:34:13 -04:00
Danny Avila
2cc580ba52 *️⃣ refactor(DeleteButton): Conversation List Behavior after Deletion (#2414)
* Refactor DeleteButton component in client/src/components/Conversations/DeleteButton.tsx

* chore: bump data-provider package

* chore: remove console.log
2024-04-14 19:11:55 -04:00
Danny Avila
d2d9ac0280 feat: Add 'EnterToSend' Option & Update Br. Translation 🇧🇷 (#2413)
* chore: Add EnterToSend, Translation Portuguese Brazilian Update

* Inverted selection and corrected translation

* fix: removed Trailing spaces not allowed

* feat: Refactor key event handler & updated translations

* fix: removed return; & updated files translations

* fix: duplicate switchs on General.tsx

* fix: added again switch

* refactor(useTextarea): limit refactoring of handleKeyDown

* refactor: correct keyDown handler and add English localization

---------

Co-authored-by: Raí Santos <140329135+itzraiss@users.noreply.github.com>
Co-authored-by: Raí Santos <raimorningstarchristus@gmail.com>
2024-04-14 19:06:20 -04:00
Ventz Petkov
f380f261a5 🛂 fix: OIDC Username Array Edge Case (#2394)
* Patch for OpenID username

`username` is generally based on email, rather than `given_name`. The challenge with `given_name` is that it can be a multi-value array (ex: "Nick, Fullname"), which completely breaks the system with: 

```
LibreChat      | ValidationError: User validation failed: username: Cast to string failed for value "[ 'Nickname', 'Firstname' ]" (type Array) at path "username"
LibreChat      |     at Document.invalidate (/app/node_modules/mongoose/lib/document.js:3200:32)
LibreChat      |     at model.$set (/app/node_modules/mongoose/lib/document.js:1459:12)
LibreChat      |     at model.set [as username] (/app/node_modules/mongoose/lib/helpers/document/compile.js:205:19)
LibreChat      |     at OpenIDConnectStrategy._verify (/app/api/strategies/openidStrategy.js:127:27)
LibreChat      |     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
```

* Update openidStrategy.js

* refactor(openidStrategy): add helper function for stringy username

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-04-12 12:39:11 -04:00
jp789
9d137ce42f 📑 docs: Add claude haiku to example anthropic models (#2391)
Co-authored-by: juliaanp <juliaan.posaratnanathan@zafin.com>
2024-04-11 15:31:04 -04:00
Melaton
25f92dd1c3 🌎 : Update chinese localization (#2384)
* 🌍 Update Chinese Translation

Update Simplified Chinese Translation

* Update Chinese Translation

Formatting file, adding new translations, delete unused translations
2024-04-11 11:23:23 -04:00
Danny Avila
9277e2a0c5 🔒 feat: Authenticated Image Requests (#2389)
* 🔒 feat: Authenticated Image Requests

* fix: reserved keyword `static`
2024-04-11 02:50:57 -04:00
Danny Avila
c19dfddd0f 👷 fix: Minor Fixes and Refactors (#2388)
* refactor(useTextarea): set Textarea disabled message due to key higher in priority

* fix(SidePanel): intended behavior for non-user provided keys

* fix: generate specs

* style: update combobox styling as before, with better dynamic height

* chore: remove unused import
2024-04-11 02:12:48 -04:00
Danny Avila
0fe47cf1f8 🤖 feat: Update Context Limit for gpt-3.5-turbo (#2381) 2024-04-10 15:10:21 -04:00
Danny Avila
8e5f1ad575 📦 feat: Model & Assistants Combobox for Side Panel (#2380)
* WIP: dynamic settings

* WIP: update tests and validations

* refactor(SidePanel): use hook for Links

* WIP: dynamic settings, slider implemented

* feat(useDebouncedInput): dynamic typing with generic

* refactor(generate): add `custom` optionType to be non-conforming to conversation schema

* feat: DynamicDropdown

* refactor(DynamicSlider): custom optionType handling and useEffect for conversation updates elsewhere

* refactor(Panel): add more test cases

* chore(DynamicSlider): note

* refactor(useDebouncedInput): import defaultDebouncedDelay from ~/common`

* WIP: implement remaining ComponentTypes

* chore: add com_sidepanel_parameters

* refactor: add langCode handling for dynamic settings

* chore(useOriginNavigate): change path to '/c/'

* refactor: explicit textarea focus on new convo, share textarea idea via ~/common

* refactor: useParameterEffects: reset if convo or preset Ids change, share and maintain statefulness in side panel

* wip: combobox

* chore: minor styling for Select components

* wip: combobox select styling for side panel

* feat: complete combobox

* refactor: model select for side panel switcher

* refactor(Combobox): add portal

* chore: comment out dynamic parameters panel for future PR and delete prompt files

* refactor(Combobox): add icon field for options, change hover bg-color, add displayValue

* fix(useNewConvo): proper textarea focus with setTimeout

* refactor(AssistantSwitcher): use Combobox

* refactor(ModelSwitcher): add textarea focus on model switch
2024-04-10 14:27:22 -04:00
matt burnett
f64a2cb0b0 🧑‍🎨 style: Remove Plugins Icon Background (#2368) 2024-04-10 07:30:56 -04:00
Walber Cardoso
e4c07eb895 👟 style: CodeAnalyze Animation (#2348)
* refactor(CodeAnaluzer SVG)

* refactor(CodeAnalyzer SVG)

* style: center terminal animation, reduce scaling

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2024-04-09 20:34:22 -04:00
Melaton
2240fee44a 🌍: Update Chinese Translation (#2351)
Update Simplified Chinese Translation
2024-04-09 19:43:24 -04:00
Danny Avila
cb64b84846 ⬇️ refactor: Assistant File Downloads (#2364)
* refactor(getFiledownload): explicit accept of `application/octet-stream`

* chore: test compose file

* chore: test compose file fix

* chore(files/download): add more logs

* Fix proxy_pass URLs in nginx.conf

* fix: proxy_pass URLs in nginx.conf to fix file downloads from URL

* chore: move test compose file to utils dir

* refactor(useFileDownload): simplify API request by passing `file_id` instead of `filepath`
2024-04-09 14:26:46 -04:00
Danny Avila
cc71125fa1 🎨 feat: Title Improvements (#2363)
* fix(assistants): keep generated title upon continued messages in active conversation

* feat: update document.title on successful gentitle mutation
2024-04-09 14:01:27 -04:00
Danny Avila
6f0eb35365 🐞 fix: Balance and Token Usage Improvements (#2350)
* fix(processModelData): handle `openrouter/auto` edge case

* fix(Tx.create): prevent negative multiplier edge case and prevent balance from becoming negative

* fix(NavLinks): render 0 balance properly

* refactor(NavLinks): show only up to 2 decimal places for balance

* fix(OpenAIClient/titleConvo): fix cohere condition and record token usage for `this.options.titleMethod === 'completion'`
2024-04-07 23:28:40 -04:00
Danny Avila
3411d7a543 🚀 feat: Enhance Message Editing with File Resubmission (#2347)
* chore: fix type issue with File Table fakeData

* refactor: new lazy loading image strategy and load images/files as part of Message Container

* feat: resubmit files when editing messages with attached files
2024-04-07 13:25:24 -04:00
Danny Avila
caabab4489 ⚠️ docs: Default Value Warnings & Docker Docs Update (#2343)
* feat(AppService): default secret value warnings

* docs: update docker/ubuntu related guides
2024-04-06 18:20:48 -04:00
chrislbrown84
0b165260f7 📘 docs: Add Note to nginx.md (#2341)
added reference for the need to do 'sudo apt update'
2024-04-06 17:05:55 -04:00
Danny Avila
334b603247 🚧 refactor: Attempt Default Preset Fix & Other Changes (#2342)
* fix(useTextarea): trigger SendButton re-render on undo and clearing text

* refactor(PresetItems): show pin icon for default preset

* fix(ChatRoute): do not use conversation.model for useEffect, do not set default Preset if real model list is not yet fetched
2024-04-06 16:09:16 -04:00
David LaPorte
476767355b 🚅 docs(ai_endpoints): Reflect correct LiteLLM baseURL when using docker-compose (#2324)
Added note to LiteLLM baseURL to reflect docker-compose usage
2024-04-05 19:35:34 -04:00
Ventz Petkov
e80debb704 🚅 docs: Working Examples for LiteLLM, Docker, LibreChat and LiteLLM models for AWS, Azure, GCP (#2323)
Updated documentation with working config examples and clarifying many details.

Added working examples for:
* LiteLLM (litellm/litellm-config.yaml)
* Docker (docker-compose.override.yml)
* LibreChat (librechat.yaml)

Added LiteLLM "ready to use" model for:
* AWS Bedrock
* Azure OpenAI
* OpenAI
* GCP
2024-04-05 19:34:11 -04:00
Ventz Petkov
549026f677 🚦 docs: Update traefik.md - Documentation Fix for edge case race condition (#2322)
Sometimes Traefik created a race condition where LibreChat was up on tcp/3080, and while Traefik was up on tcp/443, it could not route to the LibreChat container due to the multiple interfaces -- depending on how they came up. This is easily solved by simply using one interface.
2024-04-05 19:32:51 -04:00
Danny Avila
f6a84887e1 💽 refactor(client): Optimize ModelsConfig Query Cache (#2330)
* refactor(client): remove double caching of models via recoil to rely exclusively on react-query

* chore(useConversation): add modelsQuery.data dep to callback
2024-04-05 17:08:37 -04:00
Danny Avila
fb80af05be 🧠 fix(Cohere): map to expected SDK params (#2329) 2024-04-05 16:45:18 -04:00
Danny Avila
cd7f3a51e1 🧠 feat: Cohere support as Custom Endpoint (#2328)
* chore: bump cohere-ai, fix firebase vulnerabilities by going down versions

* feat: cohere rates and context windows

* feat(createCoherePayload): transform openai payload for cohere compatibility

* feat: cohere backend support

* refactor(UnknownIcon): optimize icon render and add cohere

* docs: add cohere to Compatible AI Endpoints

* Update ai_endpoints.md
2024-04-05 15:19:41 -04:00
Paul
daa5f43ac6 📝 docs: Correct Google OAuth Callback URL Example (#2311) 2024-04-04 13:36:06 -04:00
ochen1
d0d8e47ec8 🐋 refactor(Dockerfile.multi): Optimize client build by caching npm install step (#2275)
* 🐋 fix(Dockerfile): Optimize client build by caching npm install step

* 🐋 fix(Dockerfile): Possible interference from librechat-data-provider in client build
2024-04-04 08:58:34 -04:00
Marius
09cd1a7e74 🦙 docs: Update Ollama + LiteLLM Instructions (#2302)
* Update litellm.md

* set OPENAI_API_KEY of litellm service (needs to be set if ollama's openai api compatibility is used)
2024-04-04 08:32:36 -04:00
Ventz Petkov
94950b6e8b 🚥 docs: fixed Traefik web layout (#2305)
Fixed Traefik config for broken web rending
2024-04-04 08:08:31 -04:00
Danny Avila
e418edd3dc 🔧 fix: Catch deleteVectors Errors & Update RAG API docs (#2299)
* fix(deleteVectors): handle errors gracefully

* chore: update docs based on new alternate env vars prefixed with RAG to avoid conflicts with LibreChat keys
2024-04-03 14:24:46 -04:00
Marco Beretta
e3c236ba3b 🔄 chore: converted translation files to .ts (#2288)
* chore: converted translation files to

* chore(Sv.ts): removed  and the comment

* chore: add  comment
2024-04-02 14:35:35 -04:00
Danny Avila
7bd03a6e70 🛠️ fix: Correct Unwanted Newlines after Undo in Textarea (#2289)
* docs: edit docker_override note for deploy-compose

* 🛠️  fix: Correct Unwanted Newlines after Undo in Textarea
2024-04-02 12:14:42 -04:00
happy_ryo
f146db5c59 🌍: Add new Japanese Localization entries (#2282) 2024-04-02 10:00:40 -04:00
Andi
9922baf7d1 🔗 docs: Fix Link to Docker Compose Override File (#2287) 2024-04-02 09:19:10 -04:00
Danny Avila
09da05afa1 🔨 fix(ToolService): remove userId filter from loadActionSets & Docs Update (#2286)
* fix(ToolService): remove userId filter from `loadActionSets`

* docs: updates to rag_api and docker_override explaining key variable conflicts
2024-04-02 09:11:30 -04:00
illgitthat
e66aa280c0 📝 docs: Remove Google Domains Reference (#2267) 2024-04-02 03:26:35 -04:00
Till Zoppke
ed17e17a73 📖 docs: Note on 'host.docker.internal' for Ollama Config (#2274)
* docs: update URL to access ollama and comment on 'host.docker.internal'

* Update ai_endpoints.md

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2024-04-02 03:25:15 -04:00
Christoph Reiter
30d084e696 🐋 fix(Dockerfile): Create Necessary Directories at Build time (#2277)
When creating volumes for /app/client/public/images and /app/api/logs
docker will inherit the permissions from the existing directores in the
image. Since they are missing it defaults to root, and since
librechat now uses the "node" user instead of "root" storing images,
files and logs will fail.

Fix by creating those directories in the docker image with the node
user, so that if docker creates the volumes the permissions are inherited
and the directories are owned by "node" and not "root".
2024-04-02 03:20:41 -04:00
Zentix
93af814596 📗 docs: Update NagaAI (#2278) 2024-04-02 03:09:37 -04:00
Danny Avila
1bafe80e78 🛂 feat: Required OpenID Role (#2279)
* feat: add possibility to filter by roles for OpenID provider

---------

Co-authored-by: Sirius <siriusfrk@gmail.com>
2024-04-02 03:08:17 -04:00
Danny Avila
49753a35e5 v0.7.0 (#2273) 2024-04-01 19:24:01 -04:00
Danny Avila
1605ef3793 🐳 hotfix: Tag Images Workflow Update (#2272) 2024-04-01 19:04:37 -04:00
Danny Avila
8b3f80fe24 🐳 hotfix: Necessary Dockerfile Update (#2271)
* chore: remove version comment from pre-commit shell script

* chore: Dockerfile update
2024-04-01 18:46:12 -04:00
bsu3338
038063d4d1 🐞Fix: Stable Diffusion User Directory (#2270) 2024-04-01 15:55:44 -04:00
Danny Avila
5c8b16fbaf v0.7.0 (#2266)
*  v0.7.0

* chore: gitignore

* 🐳 ci: update release image workflows
2024-04-01 15:48:57 -04:00
Danny Avila
aff219c655 📋 fix: Ensure Textarea Resizes in Clipboard Edge Case (#2268)
* chore: ts-ignore fake conversation data used for testing

* chore(useTextarea): import helper functions to declutter hook

* fix(Textarea): reset textarea value explicitly by resetting `textAreaRef.current.value`
2024-04-01 13:40:21 -04:00
Danny Avila
d07396d308 🐞 fix: Handle Empty Model Error in Assistants Form (#2265) 2024-04-01 09:20:11 -04:00
pxz2016
cc92597f14 🐞 fix: Handle Garbled Chinese Characters in File Upload (#2261)
Co-authored-by: 彭修照 <pengxiuzhao.uh@haier.com>
2024-04-01 08:25:36 -04:00
Danny Avila
4854b39f41 🚀 feat: Add CLI Helper Scripts to API Container Image (#2257) 2024-03-31 18:59:07 -04:00
Danny Avila
bb8a40dd98 🎨 fix: Optimize StableDiffusion API Tool and Fix for Assistants Usage (#2253)
* chore: update docs

* fix(StableDiffusion): optimize API responses and file handling, return expected metadata for Assistants endpoint
2024-03-30 20:09:59 -04:00
Danny Avila
56ea0f9ae7 🐳 feat: RAG for Default Docker Compose Files + Docs Update (#2246)
* refactor(deploy-compose.yml): use long-syntax to avoid implicit folder creation of librechat.yaml

* refactor(docker-compose.override.yml.example): use long-syntax to avoid implicit folder creation of librechat.yaml

* chore: add simple health check for RAG_API_URL

* chore: improve axios error handling, adding `logAxiosError`

* chore: more informative message detailing RAG_API_URL path

* feat: add rag_api and vectordb to default compose file

* chore(rag.yml): update standalone rag compose file to use RAG_PORT

* chore: documentation updates

* docs: Update rag_api.md with images

* Update rag_api.md

* Update rag_api.md, assistants clarification

* add RAG API note to breaking changes
2024-03-29 21:15:36 -04:00
Danny Avila
6a6b2e79b0 🔧 fix: Improve Assistants File Citation & Download Handling (#2248)
* fix(processMessages): properly handle assistant file citations and add sources list

* feat: improve file download UX by making any downloaded files accessible within the app post-download

* refactor(processOpenAIImageOutput): correctly handle two different outputs for images since OpenAI generates a file in their storage, shares filepath for image rendering

* refactor: create `addFileToCache` helper to use across frontend

* refactor: add ImageFile parts to cache on processing content stream
2024-03-29 19:09:16 -04:00
Danny Avila
bc2a628902 🌍 fix(Translations): Map Partial langCode and Add Unit Tests (#2240) 2024-03-29 12:17:07 -04:00
Danny Avila
dec7879cc1 refactor(loadConfigModels): Stricter Default Model Fallback (#2239)
* chore: add TEndpoint type/typedef

* refactor(loadConfigModels.spec): stricter default model matching (fails with current impl.)

* refactor(loadConfigModels): return default models on endpoint basis and not fetch basis

* refactor: rename `uniqueKeyToNameMap` to `uniqueKeyToEndpointsMap` for clarity
2024-03-29 11:49:38 -04:00
Danny Avila
0a8118deed 🗨️ fix(useSSE): Prevent 'New Chat' Title after Regenerating Initial Message (#2238) 2024-03-29 10:56:51 -04:00
Raí Santos
59a8165379 🌍 : Updated & Added new Portuguese and Spanish Translations (#2228)
* 🌍 : Updated & Added news Portuguese and Spanish Translations

* fix: \' to "

* fix(Br.tsx): revert Snyk placeholders

* fix(Es.tsx): revert Snyk placeholders

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2024-03-29 10:50:09 -04:00
Danny Avila
3a1d07136c refactor(loadConfigModels): Fallback to Default Models if Fetch Fails (#2236) 2024-03-29 10:43:36 -04:00
Danny Avila
a00756c469 ⬇️ feat: Assistant File Downloads (#2234)
* WIP: basic route for file downloads and file strategy for generating readablestream to pipe as res

* chore(DALLE3): add typing for OpenAI client

* chore: add `CONSOLE_JSON` notes to dotenv.md

* WIP: first pass OpenAI Assistants File Output handling

* feat: first pass assistants output file download from openai

* chore: yml vs. yaml variation to .gitignore for `librechat.yml`

* refactor(retrieveAndProcessFile): remove redundancies

* fix(syncMessages): explicit sort of apiMessages to fix message order on abort

* chore: add logs for warnings and errors, show toast on frontend

* chore: add logger where console was still being used
2024-03-29 08:23:38 -04:00
Fuegovic
7945fea0f9 ✏️ doc update: dotenv.md (#2226) 2024-03-27 16:29:40 -04:00
Ivan Dachev
84656b9812 💽 feat: Add Script for User Stats (#2224) 2024-03-27 14:41:29 -04:00
Fuegovic
b5d25f5e4f 🔎 chore: bump meilisearch v1.7 / v0.38.0 (#2175)
* 🔎 chore: bump meilisearch v1.7 / v0.38.0

* ✏️ breaking_changes.md
2024-03-27 10:08:20 -04:00
Ivan Dachev
d4b0af3dba 💽 feat: Add CONSOLE_JSON for deploying to GCP K8S env (#2146)
* Add CONSOLE_JSON

* Update example env

* Moved to utils
2024-03-27 10:07:04 -04:00
suzuki.sh
57d1f12574 🔗 docs: Fix Link to Code of Conduct (#2206)
Fix link to Code of Conduct
2024-03-26 19:29:22 -04:00
Danny Avila
182c9f7080 🔍 chore: Clean Up Documentation Pt. 4 (#2220) 2024-03-26 14:02:22 -04:00
Danny Avila
5df0ec06ea 🔍 chore: Clean Up Documentation Part 3 (#2219) 2024-03-26 13:57:25 -04:00
Danny Avila
ea54cf03e9 🔍 chore: Clean Up Documentation Part 2 (#2218) 2024-03-26 13:48:20 -04:00
Danny Avila
7f83a060a0 🔍 chore: Clean Up Documentation (#2217)
* fix(initializeClient.spec.js): remove condition failing test on local installations

* docs: remove comments and invalid html as is required by embeddings generator and add new documentation guidelines
2024-03-26 13:40:00 -04:00
Danny Avila
2259bf8b03 🚀 feat: Add GitHub Actions Workflow for Generating Docs Embeddings (#2216) 2024-03-26 11:57:04 -04:00
Danny Avila
5c3c28009f 🧹 chore: Update Docker Docs & Make cache field Optional for Custom Config (#2211)
* docs: updating docker

* fix(customConfig): make `cache` field optional as intended (though not recommended for local setups)
2024-03-26 05:45:20 -04:00
Danny Avila
f55bd3d0e9 🎨 style: Ensure Side Panel state Remains on Refresh (#2210) 2024-03-26 05:21:40 -04:00
Danny Avila
718572b7c8 🎨 style: Refine SidePanel and Textarea Styling (#2209)
* experimental: use TextareaAutosize wrapper with useLayoutEffect to hopefully fix random textarea jankiness

* fix(Textarea): force a resize when placeholder text changes

* style(ScrollToBottom): update styling for scroll button

* style: memoize values and improve side panel toggle states

* refactor(SidePanel): more control for toggle states, new hide panel button, and improve toggle state logic

* chore: hide resizable panel handle on smaller screens
2024-03-26 04:19:51 -04:00
Florian Kohrt
cb62847838 📖 docs: Add details for Azure OpenAI Assistants (#2173)
The default `.env` contains the line `ASSISTANTS_API_KEY=user_provided`. When pre-configuring Azure OpenAI models, this setting makes it impossible to use assistants due to a missing user provided key. Only by commenting the line out the Azure setup works.
2024-03-25 18:27:36 -04:00
Danny Avila
3ef46132eb 🐞 fix(client): Prevent Async Reset of Latest Message (#2203)
* refactor: use debug statement runStepCompleted message

* fix(ChatRoute): prevent use of `newConversation` from reseting `latestMessage`, which would fire asynchronously and finalize after `latestMessage` was already correctly set
2024-03-25 11:16:18 -04:00
Danny Avila
8fc52348e8 🌟 fix: Handle Assistants Edge Cases, Improve Filter Styling (#2201)
* fix(assistants): default query to limit of 100 and `desc` order

* refactor(useMultiSearch): use object as params and fix styling for assistants

* feat: informative message for thread initialization failing due to long message
2024-03-25 08:55:33 -04:00
Fuegovic
a4f4ec85f8 🧑‍💻docs: Update General Docs and Contribution Guidelines (#2194)
* doc upddate: documentation_guidelines.md

* doc upddate: how_to_contribute.md

* doc upddate: testing.md / how_to_contribute.md

* doc upddate: translation_contribution.md/testing.md/how_to_contribute.md

* doc upddate: coding_conventions.md

* fix formatting: how_to_contribute.md

* fix formatting (again) : how_to_contribute.md
2024-03-25 07:26:43 -04:00
Danny Avila
f86d80de59 🔧 fix(assistants): Vision minor fix & Add Docs (#2196)
* 👓 fix(assistants): Only Retrieve Assistant Data for Vision Requests if attachments exist in Host Storage

* docs: add  capability
2024-03-25 00:02:54 -04:00
Danny Avila
798e8763d0 👓 feat: Vision Support for Assistants (#2195)
* refactor(assistants/chat): use promises to speed up initialization, initialize shared variables, include `attachedFileIds` to streamRunManager

* chore: additional typedefs

* fix(OpenAIClient): handle edge case where attachments promise is resolved

* feat: createVisionPrompt

* feat: Vision Support for Assistants
2024-03-24 23:43:00 -04:00
Danny Avila
1f0fb497f8 🎉 feat: Optimizations and Anthropic Title Generation (#2184)
* feat: add claude-3-haiku-20240307 to default anthropic list

* refactor: optimize `saveMessage` calls mid-stream via throttling

* chore: remove addMetadata operations and consolidate in BaseClient

* fix(listAssistantsForAzure): attempt to specify correct model mapping as accurately as possible (#2177)

* refactor(client): update last conversation setup with current assistant model, call newConvo again when assistants load to allow fast initial load and ensure assistant model is always the default, not the last selected model

* refactor(cache): explicitly add TTL of 2 minutes when setting titleCache and add default TTL of 10 minutes to abortKeys cache

* feat(AnthropicClient): conversation titling using Anthropic Function Calling

* chore: remove extraneous token usage logging

* fix(convos): unhandled edge case for conversation grouping (undefined conversation)

* style: Improved style of Search Bar after recent UI update

* chore: remove unused code, content part helpers

* feat: always show code option
2024-03-23 20:21:40 -04:00
Florian Kohrt
8e7816468d 📚 docs: Fix Broken Links (#2171)
Fix broken links to the custom config file on `timeoutMs` and `supportedIds`.
2024-03-23 11:05:52 -04:00
Danny Avila
45a95acec2 📂 feat: RAG Improvements (#2169)
* feat: new vector file processing strategy

* chore: remove unused client files

* chore: remove more unused client files

* chore: remove more unused client files and move used to new dir

* chore(DataIcon): add className

* WIP: Model Endpoint Settings Update, draft additional context settings

* feat: improve parsing for augmented prompt, add full context option

* chore: remove volume mounting from rag.yml as no longer necessary
2024-03-22 19:07:08 -04:00
Danny Avila
f427ad792a 🚀 feat: Assistants Streaming (#2159)
* chore: bump openai to 4.29.0 and npm audit fix

* chore: remove unnecessary stream field from ContentData

* feat: new enum and types for AssistantStreamEvent

* refactor(AssistantService): remove stream field and add conversationId to text ContentData
> - return `finalMessage` and `text` on run completion
> - move `processMessages` to services/Threads to avoid circular dependencies with new stream handling
> - refactor(processMessages/retrieveAndProcessFile): add new `client` field to differentiate new RunClient type

* WIP: new assistants stream handling

* chore: stores messages to StreamRunManager

* chore: add additional typedefs

* fix: pass req and openai to StreamRunManager

* fix(AssistantService): pass openai as client to `retrieveAndProcessFile`

* WIP: streaming tool i/o, handle in_progress and completed run steps

* feat(assistants): process required actions with streaming enabled

* chore: condense early return check for useSSE useEffect

* chore: remove unnecessary comments and only handle completed tool calls when not function

* feat: add TTL for assistants run abort cacheKey

* feat: abort stream runs

* fix(assistants): render streaming cursor

* fix(assistants): hide edit icon as functionality is not supported

* fix(textArea): handle pasting edge cases; first, when onChange events wouldn't fire; second, when textarea wouldn't resize

* chore: memoize Conversations

* chore(useTextarea): reverse args order

* fix: load default capabilities when an azure is configured to support assistants, but `assistants` endpoint is not configured

* fix(AssistantSelect): update form assistant model on assistant form select

* fix(actions): handle azure strict validation for function names to fix crud for actions

* chore: remove content data debug log as it fires in rapid succession

* feat: improve UX for assistant errors mid-request

* feat: add tool call localizations and replace any domain separators from azure action names

* refactor(chat): error out tool calls without outputs during handleError

* fix(ToolService): handle domain separators allowing Azure use of actions

* refactor(StreamRunManager): types and throw Error if tool submission fails
2024-03-21 22:42:25 -04:00
Hermes Trismegistus
ed64c76053 📖 docs: Update ShuttleAI Fibonacci Image (#2160) 2024-03-21 22:41:58 -04:00
Danny Avila
25a0487ce5 chore: Revise of PR #2157, move global steps earlier, execute as root 2024-03-21 12:33:30 -04:00
Danny Avila
3f77fe18b7 🐋 chore: Revise of PR #2157, move step earlier 2024-03-21 12:28:40 -04:00
Danny Avila
09de9a2b42 🐋 fix(Dockerfile): add back additional deps., handle permissions, use --no-audit flag on install (#2157) 2024-03-21 12:24:40 -04:00
Danny Avila
a673f62831 🐋 chore: Cleanup Dockerfile (#2156) 2024-03-21 11:18:53 -04:00
Danny Avila
e0dd0381b2 🌑 style(File Manager): Localize and Update Dark Mode Stylings (#2155)
* 🌑 style: Update Dark Mode Stylings for File Manager

* 🌐 feat: localize file manager text

* 🌐 feat: file panel table localization
2024-03-21 10:52:45 -04:00
Hermes Trismegistus
1ee2c32a67 🚀 feat: Add ShuttleAI as Known Endpoint (#2152)
Added new Official Known Endpoint (ShuttleAI)
2024-03-21 09:17:57 -04:00
Flynn
f521040784 🔧 fix(menu): Menu Item Filter Improvements (#2153)
* small-fix: Ensure that fake seperators in model lists do not show in search

* Ensure Plugin search uses correct placeholder and key filtering in search
2024-03-21 09:15:25 -04:00
Marco Beretta
30f6d90cfe 🖌️ style: Improve Dark Theme Accessibility (#2125)
* style: all landing page components

* chore: converted all slate to gray, since slate doesnt work

* style: assistant panel

* style: basic UI components, userprovided, preset

* style: update in multiple components

* fix(PluginStoreDialog): justify-center

* fixed some minor Ui styles

* style(MultiSearch): update dark bg

* style: update Convo styling

* style: lower textarea max height slightly

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-03-21 09:02:00 -04:00
Walber Cardoso
e95c0aaaed 🔧 style(fix): Convo Fade Effect (#2147)
* 🔧 (fix) Convo Fade Effect

* 🔧style(fix): Convo Fade Effect (#2117)

* 🔧 style(fix): Convo Fade Effect (#2117)
2024-03-21 08:39:43 -04:00
Danny Avila
9bab595204 🔬 chore: Add Circular Dependency Check to backend-review (#2149)
* 🔬 chore: Add Circular Dependency check to `backend-review`

* chore: touch random file for workflow trigger

* chore: workflow step order

* chore: update workflow to create empty auth.json file

* fix: attempt empty auth.json creation

* chore: add test_bundle ESLint ignore pattern
2024-03-20 12:15:42 -04:00
Danny Avila
4f17d97eb2 fix(sendEmail): circular dependency 2024-03-20 11:52:05 -04:00
Danny Avila
e4ac58012f 📧 fix: Correct Handling of Self-Signed Certificates in sendEmail (#2148)
- note: To put it in a different way, if you put rejectUnauthorized: true, it means that self-signed certificates should not be allowed. This means, that EMAIL_ALLOW_SELFSIGNED is set to false
2024-03-20 11:48:54 -04:00
Danny Avila
f7761df52c 🗃️ feat: General File Support for OpenAI, Azure, Custom, Anthropic and Google (RAG) (#2143)
* refactor: re-purpose `resendImages` as `resendFiles`

* refactor: re-purpose `resendImages` as `resendFiles`

* feat: upload general files

* feat: embed file during upload

* feat: delete file embeddings on file deletion

* chore(fileConfig): add epub+zip type

* feat(encodeAndFormat): handle non-image files

* feat(createContextHandlers): build context prompt from file attachments and successful RAG

* fix: prevent non-temp files as well as embedded files to be deleted on new conversation

* fix: remove temp_file_id on usage, prevent non-temp files as well as embedded files to be deleted on new conversation

* fix: prevent non-temp files as well as embedded files to be deleted on new conversation

* feat(OpenAI/Anthropic/Google): basic RAG support

* fix: delete `resendFiles` only when true (Default)

* refactor(RAG): update endpoints and pass JWT

* fix(resendFiles): default values

* fix(context/processFile): query unique ids only

* feat: rag-api.yaml

* feat: file upload improved ux for longer uploads

* chore: await embed call and catch embedding errors

* refactor: store augmentedPrompt in Client

* refactor(processFileUpload): throw error if not assistant file upload

* fix(useFileHandling): handle markdown empty mimetype issue

* chore: necessary compose file changes
2024-03-19 20:54:30 -04:00
SailFlorve
af347cccde 🎨 style: HoverButton UI adjustment, change code font (#2017)
* style: HoverButton UI adjustment

* style: make Consolas as default code font

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-03-19 13:55:41 -04:00
Danny Avila
86db0a1043 Revert "🔧 style(fix): Convo Title Fade Effect (#2117)" (#2139)
This reverts commit 1796821888.
2024-03-19 13:54:35 -04:00
Walber Cardoso
1796821888 🔧 style(fix): Convo Title Fade Effect (#2117)
* feat: Improve Google search plugin to assistants

* 🔧 fix(Nav SidePanel): Center buttons when collapsed

* 🔧(fix) Convo title fade effect

* 🔧(fix) Convo title fade effect / remove deletion

* 🔧(fix) Convo title fade effect / remove deletion .env.example

* 🔧(fix) Convo title fade effect

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-03-19 13:43:24 -04:00
Ido Ophir
d8304ec1bb 📋 chore: add requirements.txt to documentation (#2122)
* chore: add requirements.txt to documentation, to ease maintenance

* docs: Update documentation_guidelines.md
2024-03-19 13:38:18 -04:00
Danny Avila
382b303963 🔍 feat: Filter MultiSelect and SelectDropDown (+variants) + CSS fixes for Scrollbar (#2138)
* Initial implementation of MultiSearch. Added implementation to MultiSelect and SelectDropDown and variants

* Update scrollbar styles to prevent breakages on Chrome

* Revert changes to vite.config.ts (redundant for now)

* chore(New Chat): organize imports

* style(scrollbar-transparent): use webkit as standard, expected behavior

* chore: useCallback for mouse enter/leave

* fix(Footer): resolve map key error

* chore: memoize Conversations

* style(MultiSearch): improve multisearch styling

* style: dark mode search input

* fix: react warnings due to unrecognize html props

* chore: debounce OpenAI settings inputs

* fix(useDebouncedInput): only use event value as newValue if not object

---------

Co-authored-by: Flynn <gpg@flyn.ca>
2024-03-19 13:35:10 -04:00
Danny Avila
f51ac74e12 🪰 fix: Azure Parsing and Assistants Payload (#2133)
* fix(azure): fix regex to prevent edge cases

* fix(assistants): pass relevant endpoint options to avoid sending them to API
2024-03-18 19:48:42 -04:00
Danny Avila
7cddd943d0 🔧 feat(actions): Allow Multiple Actions from Same Domain per Assistant (#2120) 2024-03-16 19:40:51 -04:00
Danny Avila
89f6b35e6c 🔧 fix: Remove Unique Index from Actions Model and Initialize Empty Actions for Deletion (#2118) 2024-03-16 18:53:43 -04:00
Danny Avila
a8cdd3460c 🔧 feat: Share Assistant Actions between Users (#2116)
* fix: remove unique field from assistant_id, which can be shared between different users

* refactor: remove unique user fields from actions/assistant queries

* feat: only allow user who saved action to delete it

* refactor: allow deletions for anyone with builder access

* refactor: update user.id when updating assistants/actions records, instead of searching with it

* fix: stringify response data in case it's an object

* fix: correctly handle path input

* fix(decryptV2): handle edge case where value is already decrypted
2024-03-16 16:49:11 -04:00
Marco Beretta
2f90c8764a 🖊️ fix(MessageContent): Error Message typo (#2112) 2024-03-16 13:05:56 -04:00
Fuegovic
39042f8761 🎨 style: Privacy Policy & Terms of Service (#2111) 2024-03-16 13:05:18 -04:00
Danny Avila
a9d2d3fe40 🪙 feat: Assistants Token Balance & other improvements (#2114)
* chore: add assistants to supportsBalanceCheck

* feat(Transaction): getTransactions and refactor export of model

* refactor: use enum: ViolationTypes.TOKEN_BALANCE

* feat(assistants): check balance

* refactor(assistants): only add promptBuffer if new convo (for title), and remove endpoint definition

* refactor(assistants): Count tokens up to the current context window

* fix(Switcher): make Select list explicitly controlled

* feat(assistants): use assistant's default model when no model is specified instead of the last selected assistant, prevent assistant_id from being recorded in non-assistant endpoints

* chore(assistants/chat): import order

* chore: bump librechat-data-provider due to changes
2024-03-15 19:48:42 -04:00
SailFlorve
f848d752e0 🌍 : Update Chinese Translations (#2098) 2024-03-15 16:12:02 -04:00
Fuegovic
8881346889 📑 docs: update .env.example (#2109) 2024-03-15 16:11:31 -04:00
Danny Avila
f769077ab4 🤖 fix(assistants): Default Capabilities and Retrieval Models (#2102) 2024-03-14 20:42:56 -04:00
Danny Avila
5cd5c3bef8 🅰️ feat: Azure OpenAI Assistants API Support (#1992)
* chore: rename dir from `assistant` to plural

* feat: `assistants` field for azure config, spread options in AppService

* refactor: rename constructAzureURL param for azure as `azureOptions`

* chore: bump openai and bun

* chore(loadDefaultModels): change naming of assistant -> assistants

* feat: load azure settings with currect baseURL for assistants' initializeClient

* refactor: add `assistants` flags to groups and model configs, add mapGroupToAzureConfig

* feat(loadConfigEndpoints): initialize assistants endpoint if azure flag `assistants` is enabled

* feat(AppService): determine assistant models on startup, throw Error if none

* refactor(useDeleteAssistantMutation): send model along with assistant id for delete mutations

* feat: support listing and deleting assistants with azure

* feat: add model query to assistant avatar upload

* feat: add azure support for retrieveRun method

* refactor: update OpenAIClient initialization

* chore: update README

* fix(ci): tests passing

* refactor(uploadOpenAIFile): improve logging and use more efficient REST API method

* refactor(useFileHandling): add model to metadata to target Azure region compatible with current model

* chore(files): add azure naming pattern for valid file id recognition

* fix(assistants): initialize openai with first available assistant model if none provided

* refactor(uploadOpenAIFile): add content type for azure, initialize formdata before azure options

* refactor(sleep): move sleep function out of Runs and into `~/server/utils`

* fix(azureOpenAI/assistants): make sure to only overwrite models with assistant models if `assistants` flag is enabled

* refactor(uploadOpenAIFile): revert to old method

* chore(uploadOpenAIFile): use enum for file purpose

* docs: azureOpenAI update guide with more info, examples

* feat: enable/disable assistant capabilities and specify retrieval models

* refactor: optional chain conditional statement in loadConfigModels.js

* docs: add assistants examples

* chore: update librechat.example.yaml

* docs(azure): update note of file upload behavior in Azure OpenAI Assistants

* chore: update docs and add descriptive message about assistant errors

* fix: prevent message submission with invalid assistant or if files loading

* style: update Landing icon & text when assistant is not selected

* chore: bump librechat-data-provider to 0.4.8

* fix(assistants/azure): assign req.body.model for proper azure init to abort runs
2024-03-14 17:21:42 -04:00
Flynn
1b243c6f8c 📜 feat: Customize Privacy Policy & Terms of Service (#2091) 2024-03-14 16:43:18 -04:00
Alexei Smirnov
d4190c9320 🌍 : Update Russian Translation (#2061)
* feat(chore): add missing translations in Ru.tsx

* feat(chore): add missing translation for My Files menu and headers

* change com_ui_my_files to com_ui_nav_files

* move useLocalize above utils

* feat(chore): add missing translation for My Files menu and headers
2024-03-14 11:26:44 -04:00
MACHINSOFT
cba135d456 style: Auth Error and Preset Items Styling (#2069)
* Change the style of the error message.

* ui preset items

* fix style

* Change the color of the border and adjust the background of the selected input
2024-03-14 09:07:55 -04:00
Raí Santos
f27e7c720f 🔧 fix: Convo Corners & Updated Colors (#2046)
* 🔧 fix: Convo Corners & Updated Colors

* refactored code

* chore: JSON.parse with a try/catch block, removed useless useEffect & and restored Focus

* restored typescript

* import all back
2024-03-14 09:04:09 -04:00
Danny Avila
1b8c0f0bfd chore: Update AnthropicIcon.tsx 2024-03-13 19:00:22 -04:00
Vilmondes Queiroz
0f417aaec0 🧹 chore: remove unused import (#2072) 2024-03-11 18:27:29 -04:00
Danny Avila
d1c37e8bde 🧊 style: Adjust Endpoint Icons (#2070)
* 🧊 style: Adjust Endpoint Icons

* Update MessageParts.tsx
2024-03-11 13:40:31 -04:00
Danny Avila
0bd8c2ba00 🌑 style(AnthropicIcon): adjust for Dark Mode 2024-03-11 11:36:54 -04:00
Danny Avila
ebcca16b94 🌐 feat: librechat.yaml from URL (#2064)
* feat: librechat.yaml from URL

* doc update: librechat.yaml from URL

* update dotenv.md - typo

* Update loadCustomConfig.js

* ci: specs for loadCustomConfig

* fix(processFileURL): safe destructuring of saveURL result

---------

Co-authored-by: fuegovic <fueg@live.ca>
Co-authored-by: Fuegovic <32828263+fuegovic@users.noreply.github.com>
2024-03-11 10:52:54 -04:00
MACHINSOFT
f5a754c8be 🖌️ style: Minor UI Updates (#2011)
* UI Design update

* Add an error icon next to the avatar.

* fix

* Change the style of buttons

* fix: avatar
2024-03-11 10:31:32 -04:00
Walber Cardoso
2e77813952 🔧 style(SidePanel): Center buttons when collapsed (#2045)
* feat: Improve Google search plugin to assistants

* 🔧 fix(Nav SidePanel): Center buttons when collapsed
2024-03-11 09:24:38 -04:00
Danny Avila
f307488dd4 ✍️ refactor(Textarea): Optimize Text Input & Enhance UX (#2058)
* refactor(useDebouncedInput): make object as input arg and accept setter

* refactor(ChatForm/Textarea): consolidate textarea/form logic to one component, use react-hook-form, programmatically click send button instead of passing submitMessage, forwardRef and memoize SendButton

* refactor(Textarea): use Controller field value to avoid manual update of ref

* chore: remove forms provider

* chore: memoize AttachFile

* refactor(ChatForm/SendButton): only re-render SendButton when there is text input

* chore: make iconURL bigger

* chore: optimize Root/Nav

* refactor(SendButton): memoize disabled prop based on text

* chore: memoize Nav and ChatForm

* chore: remove textarea ref text on submission

* feat(EditMessage): Make Esc exit the edit mode and dismiss changes when editing a message

* style(MenuItem): Display the ☑️  icon only on the selected model
2024-03-11 09:18:10 -04:00
Fuegovic
f489aee518 📧 update email templates (#2057)
* 📧 chore: update email templates

* 📧 update password reset confirmation
2024-03-11 09:07:09 -04:00
Fuegovic
2f88c5cb8a ✏️ docs: Railway, Traefik, and Improvements (#2060)
* docs: documentation guidelines

* docs: deploy documentation update
2024-03-11 09:06:27 -04:00
Marco Beretta
6fcaeaafe2 🔧 fix(ThemeContext): Listen for Theme Changes (#2037)
* fix(ThemeContext): listen for changes

* fix(Dropdown): theme auto-update not working
2024-03-09 11:36:04 -05:00
Fuegovic
db870e55c3 🔖 chore: update groq models (#2031) 2024-03-09 08:32:08 -05:00
Fuegovic
5d0d02f5f7 🖊️chore: fix deployment guides (#2021) 2024-03-08 08:52:26 -05:00
Danny Avila
40e884b3ec 🖼️ fix: Clipboard Files & File Name Issues (#2015)
* fix: ensure image handling fetchs image to base64 for multiple images

* fix: append file_id's when writing uploaded files

* feat: timestamp files uploaded from clipboard

* chore: add a different fileid+name separator
2024-03-07 12:27:42 -05:00
Danny Avila
18edd2660b 👥 fix(assistants): Improve Error handling (#2012)
* feat: make assistants endpoint appendable since message state is not managed by LibreChat

* fix(ask): search currentMessages for thread_id if it's not defined

* refactor(abortMiddleware): remove use of `overrideProps` and spread unknown fields instead

* chore: remove console.log in `abortConversation`

* refactor(assistants): improve error handling/cancellation flow
2024-03-07 10:50:01 -05:00
Walber Cardoso
d4fe8fc82d 🔍 feat: Add Google Search Tool for Assistants (#1994) 2024-03-07 10:49:48 -05:00
Ido Ophir
a5f4292d2d 🌊 docs: refactor DigitalOcean guide (#2006) 2024-03-07 08:12:39 -05:00
Fuegovic
fbdf1d17ea 💾 chore: Update .env.example (#2004)
* Update .env.example

Make assistants show in the UI by default

* Update dotenv.md
2024-03-07 08:11:32 -05:00
Ido Ophir
11bca134e7 📝 docs: additions to deployment guide (#2001)
* docs: add intro to deployment guide

* doc: update intro

* doc: Add NGINX deployment guide and update reverse proxy link

* doc:: add  reverse proxy pages and weight for the pages

* doc: Update NGINX configuration file

* doc: imporve new doc

* Doc: fix file names

* doc: fix references names + improve the introduction with chatgpt :-)

* doc: update introduction  guide headings
2024-03-07 08:10:44 -05:00
Danny Avila
ab66747e97 🔧 style: Improve UI and UX with Style Fixes and Code Refactors (#2002)
* refactor(useSSE): add useCallback to all event handlers

* chore: remove modelName in defaultAssistantFormValues

* fix(SidePanel): fix layout shift on chrome my removing sidenav scrollbar

* style(ChatForm): match ChatGPT textarea effect styling

* style: fix flickering of old background color on refresh
2024-03-06 17:49:53 -05:00
Marco Beretta
b2ab6fd19d 🖌️ style: update dialog position (#1999)
* style(ChatForm): update styling and fixed style bug

* style:(Dialog): reduced max height  style(Settings): fixed dialog position height

* style(Settings): fixed large screen  position
2024-03-06 17:03:23 -05:00
Fuegovic
ab263c7a50 📝 docs update: Anthropic models + Traversaal (#1995)
* 📝 docs update: Anthropic models + Traversaal

* 📝 docs update: Anthropic models
2024-03-06 16:52:42 -05:00
Marco Beretta
911babd3e0 🖌️ style: Update Light/Dark UI Themes (#1754)
* BIG UI UPDATE

* fix: search bar, dialog template, new chat icon, convo icon and delete/rename button

* moved some color config and a lot of files

* small text fixes and tailwind config refactor

* Update localization and UI styles

* Update styles and add user-select:none to Tooltip component

* Update mobile.css styles for navigation mask and background color

* Update component imports and styles

* Update DeleteButton imports and references

* Update UI components

* Update tooltip delay duration

* Fix styling and update text in various components

* fixed assistant style

* minor style fixes

* revert: removed CreationHeader & CreationPanel

* style: match new styling for SidePanel

* style: match bg-gray-800 to ChatGPT (#212121)

* style: remove slate for gray where applicable to match new light theme

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-03-06 12:05:43 -05:00
Danny Avila
2733c5ebe7 🔎 fix(Traversaal): Recognize authField during Tool Initialization 2024-03-06 10:59:00 -05:00
Danny Avila
959d6153f6 🔎 feat: Traversaal Search Tool (#1991)
* wip: Traversaal Search Tool

* fix(traversaal): properly handle tool error, show error to LLM, log

* feat(traversaal): finish implementation of structured tool

* chore: change traversaal order
2024-03-06 10:25:38 -05:00
Danny Avila
14dd3dd240 🖋️ fix(OpenAIClient): remove typo 2024-03-06 09:19:52 -05:00
Danny Avila
8263ddda3f 🤖 feat(Anthropic): Claude 3 & Vision Support (#1984)
* chore: bump anthropic SDK

* chore: update anthropic config settings (fileSupport, default models)

* feat: anthropic multi modal formatting

* refactor: update vision models and use endpoint specific max long side resizing

* feat(anthropic): multimodal messages, retry logic, and messages payload

* chore: add more safety to trimming content due to whitespace error for assistant messages

* feat(anthropic): token accounting and resending multiple images in progress

* chore: bump data-provider

* feat(anthropic): resendImages feature

* chore: optimize Edit/Ask controllers, switch model back to req model

* fix: false positive of invalid model

* refactor(validateVisionModel): use object as arg, pass in additional/available models

* refactor(validateModel): use helper function, `getModelsConfig`

* feat: add modelsConfig to endpointOption so it gets passed to all clients, use for properly validating vision models

* refactor: initialize default vision model and make sure it's available before assigning it

* refactor(useSSE): avoid resetting model if user selected a new model between request and response

* feat: show rate in transaction logging

* fix: return tokenCountMap regardless of payload shape
2024-03-06 00:04:52 -05:00
Danny Avila
b023c5683d 🛠️ refactor(loadConfigModels): make apiKey and baseURL pairings more versatile (#1985) 2024-03-05 15:42:19 -05:00
Fuegovic
a33db54b81 🔎 update meilisearch to v1.6 / 0.37.0 (#1981)
* 🔎 update meilisearch to v1.6 / 0.37.0

* 🔎 update meilisearch to v1.6 / 0.37.0
2024-03-05 14:36:01 -05:00
Danny Avila
7a6a41a72e 🧪 fix(ci): update failing initializeClient tests with new expected values (#1982)
* fix(ci): update failing tests with new expected values from `getUserKey`

* refactor: safer optional chaining, and ensure apiKey is defined
2024-03-05 14:33:45 -05:00
Fuegovic
2ea6e8c18a 🥷🪦 docs: remove ninja and chatgptBrowser (#1973) 2024-03-04 19:49:34 -05:00
Ido Ophir
7c85b35af0 🌍 : Add Hebrew Translation (#1953)
* feat: add hebrew

* fix: review issues

* fix language options
2024-03-04 17:16:49 -05:00
Fuegovic
eccf7bbbde 🦙 doc: add Ollama to index and update icon (#1967) 2024-03-04 17:16:33 -05:00
Danny Avila
8bef084bfc 🧩 fix(Plugins): Keep User agentModel and Model Validation (#1972)
* fix: do not override model

* temp fix for secondary model validation
2024-03-04 17:07:30 -05:00
Danny Avila
62834e18fb 🪙 fix(config): use new field for balance 2024-03-04 16:37:06 -05:00
Marco Beretta
2da0a7661d 🔧 fix(EditMessage): duplicate text when pasting (#1970)
* fix(EditMessage): duplicate text when pasting on chromium

* add back paste data handling, prevent default behavior

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-03-04 16:27:34 -05:00
Marco Beretta
7d633f4018 🔧 fix(useTextarea): duplicate text when pasting on chromium (#1951) 2024-03-02 15:53:13 -05:00
bsu3338
78f52859c4 📚 docs: Separate LiteLLM and Ollama Documentation (#1948)
* Separate LiteLLM and Ollama Documentation

* Clarify Ollama Setup

* Fix litellm config
2024-03-02 12:42:02 -05:00
Danny Avila
b2ef75e009 🖥️ feat: Match STDOUT Logs with Debug File Logs (#1944)
* chore: improve token balance logging post-request

* feat: match stdout logging with file debug logging when using DEBUG_CONSOLE
2024-03-01 13:42:04 -05:00
Danny Avila
ef86b25dae 👤 feat: Show Default Icon if No Avatar or Username provided (#1943) 2024-03-01 13:08:27 -05:00
Danny Avila
c52ea9490b 📝 feat: Improved Textarea Functionality (#1942)
* feat: paste plain text from apps with rich paste data, improved edit message textarea, improved height resizing for long text

* feat(EditMessage): autofocus

* chore: retain user text color when entering edit mode
2024-03-01 12:46:15 -05:00
Fuegovic
de0cee3f56 🔎docs: update meilisearch instruction (#1930)
* 🔎docs: update meilisearch in mac_install.md

Update the Meilisearch .env variables in `mac_install.md`

* 🔎🐧
2024-03-01 12:32:32 -05:00
Danny Avila
1caa31b035 🐳chore(Dockerfile): add additional steps to prevent arm64 build failure 2024-02-29 10:04:36 -05:00
Danny Avila
ed7d7c2fda 🐳 chore(Dockerfile): replace npm ci with npm install for OS specific builds 2024-02-29 09:46:33 -05:00
Danny Avila
93803323cf 🐳 experimental: Dev Image Workflow & Remove Unused Code (#1928)
* chore: remove unused code in progressCallback, as well as handle reply.trim(), post `getCompletion`

* chore(Dockerfile): remove curl installation

* experimental: dev image parallelized with matrix strategy and building for amd64/arm64 support

* make platforms explicit
2024-02-29 09:24:55 -05:00
Danny Avila
388dc1789b 🛠️ fix: RunManager, AssistantService and useContentHandler Issues (#1920)
* fix(useContentHandler): retain undefined parts and handle them within `ContentParts` rendering

* fix(AssistantService/in_progress): skip empty messages

* refactor(RunManager): create highly specific `seenSteps` Set keys for RunSteps with use of `getDetailsSignature` and `getToolCallSignature`,to ensure changes from polling are always captured
2024-02-28 15:15:45 -05:00
Fuegovic
057fcf6274 🌍 feat: Extend regex to support international usernames (#1918)
* 🌍 Extend regex to support international usernames

* update validators.spec.js
2024-02-28 14:27:57 -05:00
Danny Avila
2f92b54787 🔗 feat: User Provided Base URL for OpenAI endpoints (#1919)
* chore: bump browserslist-db@latest

* refactor(EndpointService): simplify with `generateConfig`, utilize optional baseURL for OpenAI-based endpoints, use `isUserProvided` helper fn wherever needed

* refactor(custom/initializeClient): use standardized naming for common variables

* feat: user provided baseURL for openAI-based endpoints

* refactor(custom/initializeClient): re-order operations

* fix: knownendpoints enum definition and add FetchTokenConfig, bump data-provider

* refactor(custom): use tokenKey dependent on userProvided conditions for caching and fetching endpointTokenConfig, anticipate token rates from custom config

* refactor(custom): assure endpointTokenConfig is only accessed from cache if qualifies for fetching

* fix(ci): update tests for initializeClient based on userProvideURL changes

* fix(EndpointService): correct baseURL env var for assistants: `ASSISTANTS_BASE_URL`

* fix: unnecessary run cancellation on res.close() when response.run is completed

* feat(assistants): user provided URL option

* ci: update tests and add test for `assistants` endpoint

* chore: leaner condition for request closing

* chore: more descriptive error message to provide keys again
2024-02-28 14:27:19 -05:00
Fuegovic
53ae2d7bfb 🤖feat: add multiple known endpoints (#1917)
* feat: add known endpoints

* docs: add known endpoints

* update ai_endpoints.md

remove the groq icon from the example

* Update ai_endpoints.md

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-02-28 08:46:21 -05:00
Marco Beretta
156abe2fca 🔗 feat: NavLinks customization for Help & Faq URL (#1872)
* help and faq

* fix: using only one var

* revert(types.ts): showHelpAndFaq

* Update dotenv.md

* Update dotenv.md
2024-02-27 17:59:56 -05:00
Danny Avila
c37d5568bf 🍞 fix: Minor fixes and improved Bun support (#1916)
* fix(bun): fix bun compatibility to allow gzip header: https://github.com/oven-sh/bun/issues/267#issuecomment-1854460357

* chore: update custom config examples

* fix(OpenAIClient.chatCompletion): remove redundant call of stream.controller.abort() as `break` aborts the request and prevents abort errors when not called redundantly

* chore: bump bun.lockb

* fix: remove result-thinking class when message is no longer streaming

* fix(bun): improve Bun support by forcing use of old method in bun env, also update old methods with new customizable params

* fix(ci): pass tests
2024-02-27 17:51:16 -05:00
Danny Avila
5d887492ea 🤖 docs: Add Groq and other Compatible AI Endpoints (#1915)
* chore: bump bun dependencies

* feat: make `groq` a known endpoint

* docs: compatible ai endpoints

* Update ai_endpoints.md

* Update ai_endpoints.md
2024-02-27 13:42:10 -05:00
Danny Avila
04eeb59d47 🛠️ chore: Abort AI Requests on Close & Remove Verbose Logs for Plugins (#1914)
* chore: remove verbose logging of ChatOpenAI

* feat: abort AI requests on request close
2024-02-27 10:21:06 -05:00
Danny Avila
08d4b3cc8a 🅰️ feat: Azure AI Studio, Models as a Service Support (#1902)
* feat(data-provider): add Azure serverless inference handling through librechat.yaml

* feat(azureOpenAI): serverless inference handling in api

* docs: update docs with new azureOpenAI endpoint config fields and serverless inference endpoint setup

* chore: remove unnecessary checks for apiKey as schema would not allow apiKey to be undefined

* ci(azureOpenAI): update tests for serverless configurations
2024-02-26 19:10:29 -05:00
Raí Santos
6d6b3c9c1d 🌍 : Update Portuguese Translations (#1867)
* 🌍 : Update Portuguese Translations

* 🌍 : Fix Portuguese Translations

* fix(Br): lint errors

---------

Co-authored-by: Berry-13 <81851188+Berry-13@users.noreply.github.com>
2024-02-26 14:37:08 -05:00
Danny Avila
49744d1af9 🔥chore: bump firebase dependency (#1900) 2024-02-26 14:36:48 -05:00
Marco Beretta
b4dc8cc2ad 🖌️ style: auth dark theme (#1862)
* Remove minLength validation and update login link style

* Add theme selector component and update login form styles

* Update styling in Login and LoginForm components

* Update ResetPassword component styles and text color

* Refactor login component and add theme selector

* Add ThemeSelector component to Registration, RequestPasswordReset, and ResetPassword pages

* chore(Login.tsx): remove unused `useCallback`

* chore(Login.tsx) import order

* Update ResetPassword.tsx import order

* Update RequestPasswordReset.tsx import order

* Update Registration.tsx import order

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-02-26 14:21:17 -05:00
Danny Avila
097a978e5b 🅰️ feat: Azure Config to Allow Different Deployments per Model (#1863)
* wip: first pass for azure endpoint schema

* refactor: azure config to return groupMap and modelConfigMap

* wip: naming and schema changes

* refactor(errorsToString): move to data-provider

* feat: rename to azureGroups, add additional tests, tests all expected outcomes, return errors

* feat(AppService): load Azure groups

* refactor(azure): use imported types, write `mapModelToAzureConfig`

* refactor: move `extractEnvVariable` to data-provider

* refactor(validateAzureGroups): throw on duplicate groups or models; feat(mapModelToAzureConfig): throw if env vars not present, add tests

* refactor(AppService): ensure each model is properly configured on startup

* refactor: deprecate azureOpenAI environment variables in favor of librechat.yaml config

* feat: use helper functions to handle and order enabled/default endpoints; initialize azureOpenAI from config file

* refactor: redefine types as well as load azureOpenAI models from config file

* chore(ci): fix test description naming

* feat(azureOpenAI): use validated model grouping for request authentication

* chore: bump data-provider following rebase

* chore: bump config file version noting significant changes

* feat: add title options and switch azure configs for titling and vision requests

* feat: enable azure plugins from config file

* fix(ci): pass tests

* chore(.env.example): mark `PLUGINS_USE_AZURE` as deprecated

* fix(fetchModels): early return if apiKey not passed

* chore: fix azure config typing

* refactor(mapModelToAzureConfig): return baseURL and headers as well as azureOptions

* feat(createLLM): use `azureOpenAIBasePath`

* feat(parsers): resolveHeaders

* refactor(extractBaseURL): handle invalid input

* feat(OpenAIClient): handle headers and baseURL for azureConfig

* fix(ci): pass `OpenAIClient` tests

* chore: extract env var for azureOpenAI group config, baseURL

* docs: azureOpenAI config setup docs

* feat: safe check of potential conflicting env vars that map to unique placeholders

* fix: reset apiKey when model switches from originally requested model (vision or title)

* chore: linting

* docs: CONFIG_PATH notes in custom_config.md
2024-02-26 14:12:25 -05:00
Andreas
7a55132e42 🔧 feat: optional librechat.yaml path via environment variable (#1858)
Co-authored-by: afel <andreas.feldl@netlight.com>
2024-02-26 13:59:19 -05:00
Arno Angerer
c1a4733d50 📒 docs: Add newline for list to be correctly rendered in UI (#1873)
Currently in the documentation page the bullet list is not rendered correctly. (See first paragraph on this docs page: https://docs.librechat.ai/install/configuration/litellm.html)
2024-02-23 15:29:36 -05:00
Danny Avila
f431c8fb00 🔀 fix: Correct Expected Behavior for Modular Chat Feature (#1871) 2024-02-23 12:14:58 -05:00
Fuegovic
5445d55af2 🐋 docs: update breaking_changes.md (#1864)
add note about the use of the pre-built image in docker-compose.yml
2024-02-23 11:51:17 -05:00
Danny Avila
6a25dd38a4 🗨️ fix: Prevent Resetting Title to 'New Chat' on Follow-Up Message (#1870)
* fix: prevent reseting title to 'New Chat' on follow up message

* chore(useSSE): remove empty line
2024-02-23 10:20:46 -05:00
Fuegovic
ece5d9f588 ✏️docs: add tavily to env.example and dotenv.md (#1866)
* update .env.example

add "TAVILY_API_KEY=" to .env.example

* update dotenv.md

add Tavily to dotenv.md
2024-02-23 10:08:49 -05:00
Danny Avila
5f6d1f3db0 🎨 feat: Create Avatars of Initials Locally (#1869) 2024-02-23 09:23:29 -05:00
Fuegovic
4012dea4ab 🐋 Feat: docker pre-built image by default (#1860)
* 🐋 Feat: docker pre-built image by default

* 🐋 Feat: docker LibreChat ports from .env
2024-02-22 13:20:27 -05:00
Danny Avila
128446601a 🐛 fix: Preserve Default Model in Message Requests (#1857)
* fix: do not remove default model from message request

* chore: bump data-provider
2024-02-21 13:29:21 -05:00
Danny Avila
dd8038b375 🛠️ refactor: Model Loading and Custom Endpoint Error Handling (#1849)
* fix: handle non-assistant role ChatCompletionMessage error

* refactor(ModelController): decouple res.send from loading/caching models

* fix(custom/initializeClient): only fetch custom endpoint models if models.fetch is true

* refactor(validateModel): load models if modelsConfig is not yet cached

* docs: update on file upload rate limiting
2024-02-20 12:57:58 -05:00
Danny Avila
542494fad6 📋 feat: Accumulate Text Parts to Clipboard for Assistant Outputs (#1847) 2024-02-20 09:33:31 -05:00
Danny Avila
64e81392f2 ⬤ style: Uniform Display of Result-Streaming Cursor (#1842) 2024-02-19 22:55:58 -05:00
Danny Avila
a8a19c6caa 🛡️ feat: Model Validation Middleware (#1841)
* refactor: add ViolationTypes enum and add new violation for illegal model requests

* feat: validateModel middleware to protect the backend against illicit requests for unlisted models
2024-02-19 22:47:39 -05:00
Danny Avila
d8038e3b19 📤 refactor: Utilize intermediateReply when message.content is Empty 2024-02-19 11:03:42 -05:00
Danny Avila
ee97179edb 📝 chore: Update README.md 2024-02-19 09:45:59 -05:00
Danny Avila
63a5039fae 🔗 chore: Add Stable Discord and Homepage Links (#1835) 2024-02-19 09:42:57 -05:00
Fuegovic
7442955a1d 📝 docs: add env changes to breaking_changes.md and minor fixes (#1812)
* 📝 docs: add env changes to breacking_changes.md

* 📝 docs: replace example in docker_override.md

* 📝 docs: fix images in zeabur.md
2024-02-19 09:41:07 -05:00
Danny Avila
5291d18f38 🔀 fix: Endpoint Type Mismatch when Switching Conversations (#1834)
* refactor(useUpdateUserKeysMutation): only invalidate the endpoint whose key is being updated by user

* fix(assistants): await `getUserKeyExpiry` call

* chore: fix spinner loading color

* refactor(initializeClient): make known which endpoint api Key is missing

* fix: prevent an `endpointType` mismatch by making it impossible to assign when the `endpointsConfig` doesn't have a `type` defined, also prefer `getQueryData` call to useQuery in useChatHelpers
2024-02-19 01:31:38 -05:00
Danny Avila
d1eb7fcfc7 Update main-image-workflow.yml 2024-02-16 16:05:18 -05:00
Danny Avila
ce1cdea3de Update main-image-workflow.yml 2024-02-16 15:55:12 -05:00
Danny Avila
0da30b9481 Update main-image-workflow.yml 2024-02-16 15:46:09 -05:00
Danny Avila
b7aebf6c51 Update main-image-workflow.yml 2024-02-16 15:43:31 -05:00
Danny Avila
29ee4423a6 🐋 chore: Add Docker Compose Build Latest Main Image workflow (#1819) 2024-02-16 15:37:32 -05:00
Danny Avila
98064244bf fix: necessary font changes (#1818)
* fix: necessary font changes

* chore: minor bump
2024-02-16 14:15:05 -05:00
Danny Avila
fe0ef2ce61 📝 docs: Update docker-compose.override.yml.example 2024-02-15 12:56:28 -05:00
Danny Avila
637a1a41c2 👥 fix: Reinstate Default Social Login Values (#1811)
* fix: social logins accidentally removed default in AppService, reinstated and added test

* chore: move birthday to OTHER section and make disabled by default
2024-02-15 08:20:06 -05:00
Danny Avila
60b1d1332c 🤖 chore: Improve Assistants Run Logging (#1801) 2024-02-14 13:33:33 -05:00
Walber Cardoso
9d3215dcaa ✏️ fix(Convo): Rename Title UX (#1793) 2024-02-14 09:43:37 -05:00
CXwudi
c7020e8651 📝 docs: Enhance LibreChat Docker deployment guide (#1796)
- Revise the description for clarity and conciseness.
- Provide a direct comparison between the advanced Docker Compose deployment and the simpler `docker-compose.override.yml` setup.
- Update screenshot link for visual reference.
2024-02-14 09:42:12 -05:00
Danny Avila
04af1cad52 🤖 docs(assistants): Additional Setup & Tool Selection Info (#1798) 2024-02-13 22:24:24 -05:00
Danny Avila
d947244348 🔧 fix(data-provider): add openapi-types dependency (#1797)
* fix(data-provider): add openapi-types dependency

* chore: Bump version to 0.4.1 in package.json
2024-02-13 21:08:02 -05:00
Danny Avila
ecd63eb9f1 feat: Assistants API, General File Support, Side Panel, File Explorer (#1696)
* feat: assistant name/icon in Landing & Header

* feat: assistname in textarea placeholder, and use `Assistant` as default name

* feat: display non-image files in user messages

* fix: only render files if files.length is > 0

* refactor(config -> file-config): move file related configuration values to separate module, add excel types

* chore: spreadsheet file rendering

* fix(Landing): dark mode style for Assistant Name

* refactor: move progress incrementing to own hook, start smaller, cap near limit \(1\)

* refactor(useContentHandler): add empty Text part if last part was completed tool or image

* chore: add accordion trigger border styling for dark mode

* feat: Assistant Builder model selection

* chore: use Spinner when Assistant is mutating

* fix(get/assistants): return correct response object `AssistantListResponse`

* refactor(Spinner): pass size as prop

* refactor: make assistant crud mutations optimistic, add types for options

* chore: remove assistants route and view

* chore: move assistant builder components to separate directory

* feat(ContextButton): delete Assistant via context button/dialog, add localization

* refactor: conditionally show use and context menu buttons, add localization for create assistant

* feat: save side panel states to localStorage

* style(SidePanel): improve avatar menu and assistant select styling for dark mode

* refactor: make NavToggle reusable for either side (left or right), add SidePanel Toggle with ability to close it completely

* fix: resize handle and navToggle behavior

* fix(/avatar/:assistant_id): await `deleteFile` and assign unique name to uploaded image

* WIP: file UI components from PR #576

* refactor(OpenAIMinimalIcon): pass className

* feat: formatDate helper fn

* feat: DataTableColumnHeader

* feat: add row selection, formatted row values, number of rows selected

* WIP: add files to Side panel temporarily

* feat: `LB_QueueAsyncCall`: Leaky Bucket queue for external APIs, use in `processDeleteRequest`

* fix(TFile): correct `source` type with `FileSources`

* fix(useFileHandling): use `continue` instead of return when iterating multiple files, add file type to extendedFile

* chore: add generic setter type

* refactor(processDeleteRequest): settle promises to prevent rejections from processing deletions, log errors

* feat: `useFileDeletion` to reuse file deletion logic

* refactor(useFileDeletion): make `setFiles` an optional param and use object as param

* feat: useDeleteFilesFromTable

* feat: use real `files` data and add deletion action to data table

* fix(Table): make headers sticky

* feat: add dynamic filtering for columns; only show to user Host or OpenAI storage type

* style(DropdownMenu): replace `slate` with `gray`

* style(DataTable): apply dark mode themes and other misc styling

* style(Columns): add color to OpenAI Storage option

* refactor(FileContainer): make file preview reusable

* refactor(Images): make image preview reusable

* refactor(FilePreview): make file prop optional for FileIcon and FilePreview, fix relative style

* feat(Columns): add file/image previews, set a minimum size to show for file size in bytes

* WIP: File Panel with real files and formatted

* feat: open files dialog from panel

* style: file data table mobile and general column styling fixes

* refactor(api/files): return files sorted by the most recently updated

* refactor: provide fileMap through context to prevent re-selecting files to map in different areas; remove unused imports commented out in PanelColumns

* refactor(ExtendFile): make File type optional, add `attached` to prevent attached files from being deleted on remove, make Message.files a partial TFile type

* feat: attach files through file panel

* refactor(useFileHandling): move files to the start of cache list when uploaded

* refactor(useDeleteFilesMutation): delete files from cache when successfully deleted from server

* fix(FileRow): handle possible edge case of duplication due to attaching recently uploaded file

* style(SidePanel): make resize grip border transparent, remove unnecessary styling on close sidepanel button

* feat: action utilities and tests

* refactor(actions): add `ValidationResult` type and change wording for no server URL found

* refactor(actions): check for empty server URL

* fix(data-provider): revert tsconfig to fix type issue resolution

* feat(client): first pass of actions input for assistants

* refactor(FunctionSignature): change method to output object instead of string

* refactor(models/Assistant): add actions field to schema, use searchParams object for methods, and add `getAssistant`

* feat: post actions input first pass
- create new Action document
- add actions to Assistant DB document
- create /action/:assistant_id POST route
- pass more props down from PanelSwitcher, derive assistant_id from switcher
- move privacy policy to ActionInput
- reset data on input change/validation
- add `useUpdateAction`
- conform FunctionSignature type to FunctionTool
- add action, assistant doc, update hook related types

* refactor: optimize assistant/actions relationship
- past domain in metadata as hostname and not a URL
- include domain in tool name
- add `getActions` for actions retrieval by user
- add `getAssistants` for assistant docs retrieval by user
- add `assistant_id` to Action schema
- move actions to own module as a subroute to `api/assistants`
- add `useGetActionsQuery` and `useGetAssistantDocsQuery` hooks
- fix Action type def

* feat: show assistant actions in assistant builder

* feat: switch to actions on action click, editing action styling

* fix: add Assistant state for builder panel to allow immediate selection of newly created assistants as well as retaining the current assistant when switching to a different panel within the builder

* refactor(SidePanel/NavToggle): offset less from right when SidePanel is completely collapsed

* chore: rename `processActions` -> `processRequiredActions`

* chore: rename Assistant API Action to RequiredAction

* refactor(actions): avoid nesting actual API params under generic `requestBody` to optimize LLM token usage

* fix(handleTools): avoid calling `validTool` if not defined, add optional param to skip the loading of specs, which throws an error in the context of assistants

* WIP: working first pass of toolCalls generated from openapi specs

* WIP: first pass ToolCall styling

* feat: programmatic iv encryption/decryption helpers

* fix: correct ActionAuth types/enums, and define type for AuthForm

* feat: encryption/decryption helpers for Action AuthMetadata

* refactor(getActions): remove sensitive fields from query response

* refactor(POST/actions): encrypt and remove sensitive fields from mutation response

* fix(ActionService): change ESM import to CJS

* feat: frontend auth handling for actions + optimistic update on action update/creation

* refactor(actions): use the correct variables and types for setAuth method

* refactor: POST /:assistant_id action can now handle updating an existing action, add `saved_auth_fields` to determine when user explicitly saves new auth creds. only send auth metadata if user explicitly saved fields

* refactor(createActionTool): catch errors and send back meaningful error message, add flag to `getActions` to determine whether to retrieve sensitive values or not

* refactor(ToolService): add `action` property to ToolCall PartMetadata to determine if the tool call was an action, fix parsing function name issue with actionDelimiter

* fix(ActionRequest): use URL class to correctly join endpoint parts for `execute` call

* feat: delete assistant actions

* refactor: conditionally show Available actions

* refactor: show `retrieval` and `code_interpreter` as Capabilities, swap `Switch` for `Checkbox`

* chore: remove shadow-stroke from messages

* WIP: first pass of Assistants Knowledge attachments

* refactor: remove AssistantsProvider in favor of FormProvider, fix selectedAssistant re-render bug, map Assistant file_ids to files via fileMap, initialize Knowledge component with mapped files if any exist

* fix: prevent deleting files on assistant file upload

* chore: remove console.log

* refactor(useUploadFileMutation): update files and assistants cache on upload

* chore: disable oauth option as not supported yet

* feat: cancel assistant runs

* refactor: initialize OpenAI client with helper function, resolve all related circular dependencies

* fix(DALL-E): initialization

* fix(process): openai client initialization

* fix: select an existing Assistant when the active one is deleted

* chore: allow attaching files for assistant endpoint, send back relevant OpenAI error message when uploading, deconstruct openAI initialization correctly, add `message_file` to formData when a file is attached to the message but not the assistant

* fix: add assistant_id on newConvo

* fix(initializeClient): import fix

* chore: swap setAssistant for setOption in useEffect

* fix(DALL-E): add processFileURL to loadTools call

* chore: add customConfig to debug logs

* feat: delete threads on convo delete

* chore: replace Assistants icon

* chore: remove console.dir() in `abortRun`

* feat(AssistantService): accumulate text values from run in openai.responseText

* feat: titling for assistants endpoint

* chore: move panel file components to appropriate directory, add file checks for attaching files, change icon for Attach Files

* refactor: add localizations to tools, plugins, add condition for adding/remove user plugins so tool selections don't affect this value

* chore: disable `import from url` action for now

* chore: remove textMimeTypes from default fileConfig for now

* fix: catch tool errors and send as outputs with error messages

* fix: React warning about button as descendant of button

* style: retrieval and cancelled icon

* WIP: pass isSubmitting to Parts, use InProgressCall to display cancelled tool calls correctly, show domain/function name

* fix(meilisearch): fix `postSaveHook` issue where indexing expects a mongo document, and join all text content parts for meili indexing

* ci: fix dall-e tests

* ci: fix client tests

* fix: button types in actions panel

* fix: plugin auth form persisting across tool selections

* fix(ci): update AppService spec with `loadAndFormatTools`

* fix(clearConvos): add id check earlier on

* refactor(AssistantAvatar): set previewURL dynamically when emtadata.avatar changes

* feat(assistants): addTitle cache setting

* fix(useSSE): resolve rebase conflicts

* fix: delete mutation

* style(SidePanel): make grip visible on active and hover, invisible otherwise

* ci: add data-provider tests to workflow, also update eslint/tsconfig to recognize specs, and add `text/csv` to fileConfig

* fix: handle edge case where auth object is undefined, and log errors

* refactor(actions): resolve  schemas, add tests for resolving refs, import specs from separate file for tests

* chore: remove comment

* fix(ActionsInput): re-render bug when initializing states with action fields

* fix(patch/assistant): filter undefined tools

* chore: add logging for errors in assistants routes

* fix(updateAssistant): map actions to functions to avoid overwriting

* fix(actions): properly handle GET paths

* fix(convos): unhandled delete thread exception

* refactor(AssistantService): pass both thread_id and conversationId when sending intermediate assistant messages, remove `mapMessagesToSteps` from AssistantService

* refactor(useSSE): replace all messages with runMessages and pass latestMessageId to abortRun; fix(checkMessageGaps): include tool calls when  syncing messages

* refactor(assistants/chat): invoke `createOnTextProgress` after thread creation

* chore: add typing

* style: sidepanel styling

* style: action tool call domain styling

* feat(assistants): default models, limit retrieval to certain models, add env variables to to env.example

* feat: assistants api key in EndpointService

* refactor: set assistant model to conversation on assistant switch

* refactor: set assistant model to conversation on assistant select from panel

* fix(retrieveAndProcessFile): catch attempt to download file with `assistant` purpose which is not allowed; add logging

* feat: retrieval styling, handling, and logging

* chore: rename ASSISTANTS_REVERSE_PROXY to ASSISTANTS_BASE_URL

* feat: FileContext for file metadata

* feat: context file mgmt and filtering

* style(Select): hover/rounded changes

* refactor: explicit conversation switch, endpoint dependent, through `useSelectAssistant`, which does not create new chat if current endpoint is assistant endpoint

* fix(AssistantAvatar): make empty previewURL if no avatar present

* refactor: side panel mobile styling

* style: merge tool and action section, optimize mobile styling for action/tool buttons

* fix: localStorage issues

* fix(useSelectAssistant): invoke react query hook directly in select hook as Map was not being updated in time

* style: light mode fixes

* fix: prevent sidepanel nav styling from shifting layout up

* refactor: change default layout (collapsed by default)

* style: mobile optimization of DataTable

* style: datatable

* feat: client-side hide right-side panel

* chore(useNewConvo): add partial typing for preset

* fix(useSelectAssistant): pass correct model name by using template as preset

* WIP: assistant presets

* refactor(ToolService): add native solution for `TavilySearchResults` and log tool output errors

* refactor: organize imports and use native TavilySearchResults

* fix(TavilySearchResults): stringify result

* fix(ToolCall): show tool call outputs when not an action

* chore: rename Prompt Prefix to custom instructions (in user facing text only)

* refactor(EditPresetDialog): Optimize setting title by debouncing, reset preset on dialog close to avoid state mixture

* feat: add `presetOverride` to overwrite active conversation settings when saving a Preset (relevant for client side updates only)

* feat: Assistant preset settings (client-side)

* fix(Switcher): only set assistant_id and model if current endpoint is Assistants

* feat: use `useDebouncedInput` for updating conversation settings, starting with EditPresetDialog title setting and Assistant instructions setting

* feat(Assistants): add instructions field to settings

* feat(chat/assistants): pass conversation settings to run body

* wip: begin localization and only allow actions if the assistant is created

* refactor(AssistantsPanel): knowledge localization, allow tools on creation

* feat: experimental: allow 'priming' values before assistant is created, that would normally require an assistant_id to be defined

* chore: trim console logs and make more meaningful

* chore: toast messages

* fix(ci): date test

* feat: create file when uploading Assistant Avatar

* feat: file upload rate limiting from custom config with dynamic file route initialization

* refactor: use file upload limiters on post routes only

* refactor(fileConfig): add endpoints field for endpoint specific fileconfigs, add mergeConfig function, add tests

* refactor: fileConfig route, dynamic multer instances used on all '/' and '/images' POST routes, data service and query hook

* feat: supportedMimeTypesSchema, test for array of regex

* feat: configurable file config limits

* chore: clarify assistants file knowledge prereq.

* chore(useTextarea): default to localized 'Assistant' if assistant name is empty

* feat: configurable file limits and toggle file upload per endpoint

* fix(useUploadFileMutation): prevent updating assistant.files cache if file upload is a message_file attachment

* fix(AssistantSelect): set last selected assistant only when timeout successfully runs

* refactor(queries): disable assistant queries if assistants endpoint is not enabled

* chore(Switcher): add localization

* chore: pluralize `assistant` for `EModelEndpoint key and value

* feat: show/hide assistant UI components based on endpoint availability; librechat.yaml config for disabling builder section and setting polling/timeout intervals

* fix(compactEndpointSchemas): use EModelEndpoint for schema access

* feat(runAssistant): use configured values from `librechat.yaml` for `pollIntervalMs` and `timeout`

* fix: naming issue

* wip: revert landing

* 🎉 happy birthday LibreChat (#1768)

* happy birthday LibreChat

* Refactor endpoint condition in Landing component

* Update birthday message in Eng.tsx

* fix(/config): avoid nesting ternaries

* refactor(/config): check birthday

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>

* fix: landing

* fix: landing

* fix(useMessageHelpers): hardcoded check to use EModelEndpoint instead

* fix(ci): convo test revert to main

* fix(assistants/chat): fix issue where assistant_id was being saved as model for convo

* chore: added logging, promises racing to prevent longer timeouts, explicit setting of maxRetries and timeouts, robust catching of invalid abortRun params

* refactor: use recoil state for `showStopButton` and only show for assistants endpoint after syncing conversation data

* refactor: optimize abortRun strategy using localStorage, refactor `abortConversation` to use async/await and await the result, refactor how the abortKey cache is set for runs

* fix(checkMessageGaps): assign `assistant_id` to synced messages if defined; prevents UI from showing blank assistant for cancelled messages

* refactor: re-order sequence of chat route, only allow aborting messages after run is created, cancel abortRun if there was a cancelling error (likely due already cancelled in chat route), and add extra logging

* chore(typedefs): add httpAgent type to OpenAIClient

* refactor: use custom implementation of retrieving run with axios to allow for timing out run query

* fix(waitForRun): handle timed out run retrieval query

* refactor: update preset conditions:
- presets will retain settings when a different endpoint is selected; for existing convos, either when modular or is assistant switch
- no longer use `navigateToConvo` on preset select

* fix: temporary calculator hack as expects string input when invoked

* fix: cancel abortRun only when cancelling error is a result of the run already being cancelled

* chore: remove use of `fileMaxSizeMB` and total counterpart (redundant)

* docs: custom config documentation update

* docs: assistants api setup and dotenv, new custom config fields

* refactor(Switcher): make Assistant switcher sticky in SidePanel

* chore(useSSE): remove console log of data and message index

* refactor(AssistantPanel): button styling and add secondary select button to bottom of panel

* refactor(OpenAIClient): allow passing conversationId to RunManager through titleConvo and initializeLLM to properly record title context tokens used in cases where conversationId was not defined by the client

* feat(assistants): token tracking for assistant runs

* chore(spendTokens): improve logging

* feat: support/exclude specific assistant Ids

* chore: add update `librechat.example.yaml`, optimize `AppService` handling, new tests for `AppService`, optimize missing/outdate config logging

* chore: mount docker logs to root of project

* chore: condense axios errors

* chore: bump vite

* chore: vite hot reload fix using latest version

* chore(getOpenAIModels): sort instruct models to the end of models list

* fix(assistants): user provided key

* fix(assistants): user provided key, invalidate more queries on revoke

---------

Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
2024-02-13 20:42:27 -05:00
Danny Avila
cd2786441a 🐋 chore: add tag for latest for ci/cd docker builds (#1791) 2024-02-13 11:31:48 -05:00
Danny Avila
050eeb1211 chore: Update Docker build and push actions (#1790) 2024-02-13 10:59:46 -05:00
810 changed files with 59621 additions and 18357 deletions

View File

@@ -13,9 +13,6 @@
# Server Configuration #
#==================================================#
APP_TITLE=LibreChat
# CUSTOM_FOOTER="My custom footer"
HOST=localhost
PORT=3080
@@ -26,7 +23,12 @@ DOMAIN_SERVER=http://localhost:3080
NO_INDEX=true
SHOW_BIRTHDAY_ICON=true
#===============#
# JSON Logging #
#===============#
# Use when process console logs in cloud deployment like GCP/AWS
CONSOLE_JSON=false
#===============#
# Debug Logging #
@@ -42,38 +44,63 @@ 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,azureOpenAI,bingAI,chatGPTBrowser,google,gptPlugins,anthropic
# ENDPOINTS=openAI,assistants,azureOpenAI,bingAI,google,gptPlugins,anthropic
PROXY=
#===================================#
# Known Endpoints - librechat.yaml #
#===================================#
# https://docs.librechat.ai/install/configuration/ai_endpoints.html
# ANYSCALE_API_KEY=
# APIPIE_API_KEY=
# FIREWORKS_API_KEY=
# GROQ_API_KEY=
# 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 #
@@ -82,22 +109,19 @@ 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.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-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
#============#
# OpenAI #
#============#
@@ -117,12 +141,20 @@ DEBUG_OPENAI=false
# OPENAI_REVERSE_PROXY=
# OPENAI_ORGANIZATION=
# OPENAI_ORGANIZATION=
#====================#
# Assistants API #
#====================#
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
#============#
# OpenRouter #
#============#
# !!!Warning: Use the variable above instead of this one. Using this one will override the OpenAI endpoint
# OPENROUTER_API_KEY=
#============#
@@ -166,7 +198,7 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
# Google
#-----------------
GOOGLE_API_KEY=
GOOGLE_SEARCH_API_KEY=
GOOGLE_CSE_ID=
# SerpAPI
@@ -177,6 +209,14 @@ SERPAPI_API_KEY=
#-----------------
SD_WEBUI_URL=http://host.docker.internal:7860
# Tavily
#-----------------
TAVILY_API_KEY=
# Traversaal
#-----------------
TRAVERSAAL_API_KEY=
# WolframAlpha
#-----------------
WOLFRAM_APP_ID=
@@ -232,6 +272,8 @@ LIMIT_MESSAGE_USER=false
MESSAGE_USER_MAX=40
MESSAGE_USER_WINDOW=1
ILLEGAL_MODEL_REQ_SCORE=5
#========================#
# Balance #
#========================#
@@ -280,6 +322,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=
@@ -288,15 +333,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
#========================#
@@ -310,6 +355,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 #
#==================================================#

View File

@@ -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,12 @@ module.exports = {
},
],
},
{
files: ['./packages/data-provider/specs/**/*.ts'],
parserOptions: {
project: './packages/data-provider/tsconfig.spec.json',
},
},
],
settings: {
react: {

View File

@@ -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

View File

@@ -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

View File

@@ -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

6
.github/SECURITY.md vendored
View File

@@ -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

View File

@@ -15,8 +15,9 @@ 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
- [ ] Documentation update
## Testing
@@ -26,6 +27,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 +37,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.
- [ ] New documents have been locally validated with mkdocs

View File

@@ -35,11 +35,32 @@ 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
- name: Run librechat-data-provider unit tests
run: cd packages/data-provider && npm run test:ci
- name: Run linters
uses: wearerequired/lint-action@v2
with:
eslint: true
eslint: true

View File

@@ -1,92 +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
# Login to Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 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
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/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
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/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

View File

@@ -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 }}

View 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'

View File

@@ -1,89 +0,0 @@
name: Docker Compose Build on Main Branch
on:
workflow_dispatch: # This line allows manual triggering
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
# Login to Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 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
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/librechat-api
tags: |
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
- 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
# 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
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/librechat
tags: |
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
- 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

View 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 }}

67
.github/workflows/tag-images.yml vendored Normal file
View 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 }}

11
.gitignore vendored
View File

@@ -50,6 +50,7 @@ bower_components/
#config file
librechat.yaml
librechat.yml
# Environment
.npmrc
@@ -74,6 +75,8 @@ src/style - official.css
config.local.ts
**/storageState.json
junit.xml
**/.venv/
**/venv/
# docker override file
docker-compose.override.yaml
@@ -88,4 +91,10 @@ auth.json
/packages/ux-shared/
/images
!client/src/components/Nav/SettingsTabs/Data/
!client/src/components/Nav/SettingsTabs/Data/
# User uploads
uploads/
# owner
release/

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/usr/bin/env sh
set -e
. "$(dirname -- "$0")/_/husky.sh"
[ -n "$CI" ] && exit 0

View File

@@ -1,20 +1,35 @@
# Base node image
FROM node:18-alpine AS node
# v0.7.1
COPY . /app
# Base node image
FROM node:18-alpine3.18 AS node
RUN apk add g++ make py3-pip
RUN npm install -g node-gyp
RUN apk --no-cache add curl
RUN mkdir -p /app && chown node:node /app
WORKDIR /app
USER node
COPY --chown=node:node . .
# 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
RUN npm config set fetch-retry-maxtimeout 600000
RUN npm config set fetch-retries 5
RUN npm config set fetch-retry-mintimeout 15000
RUN npm install --no-audit
# React client build
ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm run frontend
# Create directories for the volumes to inherit
# the correct permissions
RUN mkdir -p /app/client/public/images /app/api/logs
# Node API setup
EXPOSE 3080
ENV HOST=0.0.0.0

View File

@@ -1,3 +1,5 @@
# v0.7.1
# Build API, Client and Data Provider
FROM node:20-alpine AS base
@@ -11,11 +13,12 @@ RUN npm run build
# React client build
FROM data-provider-build 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 ./client/ ./
ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm run build
@@ -24,6 +27,8 @@ FROM data-provider-build 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/

View File

@@ -1,10 +1,10 @@
<p align="center">
<a href="https://docs.librechat.ai">
<a href="https://librechat.ai">
<img src="docs/assets/LibreChat.svg" height="256">
</a>
<a href="https://docs.librechat.ai">
<h1 align="center">LibreChat</h1>
</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,30 +39,44 @@
</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
- 🖥️ 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://docs.librechat.ai/install/configuration/ai_endpoints.html#intro):**
- groq, Ollama, Cohere, Mistral AI, Apple MLX, koboldcpp, OpenRouter, together.ai, Perplexity, ShuttleAI, and more
- 💾 Create, Save, & Share Custom Presets
- 🔀 Switch between AI Endpoints and Presets, mid-chat
- 🔄 Edit, Resubmit, and Continue Messages with Conversation branching
- 🌿 Fork Messages & Conversations for Advanced Context control
- 💬 Multimodal Chat:
- Upload and analyze images with Claude 3, GPT-4, and Gemini Vision 📸
- 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/features/plugins/introduction.html) 📚
## 🪶 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 -->
[![Watch the video](https://img.youtube.com/vi/pNIOs1ovsXw/maxresdefault.jpg)](https://youtu.be/pNIOs1ovsXw)
@@ -71,11 +85,13 @@ 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)
---
## 📝 Changelog
## 📝 Changelog
Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
**⚠️ [Breaking Changes](docs/general_info/breaking_changes.md)**
@@ -96,14 +112,15 @@ Please consult the breaking changes before updating.
---
## ✨ 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>

View File

@@ -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' });
}

View File

@@ -1,5 +1,6 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const { Constants, EModelEndpoint } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('../server/services/UserService');
const browserClient = async ({
@@ -17,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' });
}
@@ -48,7 +46,7 @@ const browserClient = async ({
options = { ...options, parentMessageId, conversationId };
}
if (parentMessageId === '00000000-0000-0000-0000-000000000000') {
if (parentMessageId === Constants.NO_PARENT) {
delete options.conversationId;
}

View File

@@ -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 {
titleFunctionPrompt,
parseTitleFromPrompt,
truncateText,
formatMessage,
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,6 +68,12 @@ 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.maxResponseTokens = this.modelOptions.maxOutputTokens || 1500;
@@ -87,7 +114,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 +131,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 +208,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 +372,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 +443,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 +515,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,36 +564,88 @@ 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();
}
@@ -317,6 +654,10 @@ class AnthropicClient extends BaseClient {
return {
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 +683,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 = parseTitleFromPrompt(text);
} 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;

View File

@@ -1,8 +1,9 @@
const crypto = require('crypto');
const { supportsBalanceCheck } = require('librechat-data-provider');
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,
@@ -77,7 +74,7 @@ class BaseClient {
const saveOptions = this.getSaveOptions();
this.abortController = opts.abortController ?? new AbortController();
const conversationId = opts.conversationId ?? crypto.randomUUID();
const parentMessageId = opts.parentMessageId ?? '00000000-0000-0000-0000-000000000000';
const parentMessageId = opts.parentMessageId ?? Constants.NO_PARENT;
const userMessageId = opts.overrideParentMessageId ?? crypto.randomUUID();
let responseMessageId = opts.responseMessageId ?? crypto.randomUUID();
let head = isEdited ? responseMessageId : parentMessageId;
@@ -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,
@@ -552,15 +565,15 @@ class BaseClient {
*
* Each message object should have an 'id' or 'messageId' property and may have a 'parentMessageId' property.
* The 'parentMessageId' is the ID of the message that the current message is a reply to.
* If 'parentMessageId' is not present, null, or is '00000000-0000-0000-0000-000000000000',
* If 'parentMessageId' is not present, null, or is Constants.NO_PARENT,
* 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,
@@ -607,9 +620,7 @@ class BaseClient {
}
currentMessageId =
message.parentMessageId === '00000000-0000-0000-0000-000000000000'
? null
: message.parentMessageId;
message.parentMessageId === Constants.NO_PARENT ? null : message.parentMessageId;
}
orderedMessages.reverse();
@@ -683,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;

View File

@@ -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',

View File

@@ -1,19 +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, validateVisionModel } = require('~/server/services/Files/images');
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
const {
validateVisionModel,
getResponseSender,
EModelEndpoint,
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');
@@ -123,18 +127,11 @@ 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 =
@@ -219,6 +216,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),
@@ -226,18 +250,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;
@@ -253,18 +350,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);
}
@@ -478,13 +586,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 = {}) {
@@ -496,7 +615,7 @@ class GoogleClient extends BaseClient {
let clientOptions = { ...parameters, maxRetries: 2 };
if (!this.isGenerativeModel) {
if (this.project_id) {
clientOptions['authOptions'] = {
credentials: {
...this.serviceKey,
@@ -509,7 +628,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;
}
@@ -540,16 +659,46 @@ 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 result = await client.generateContentStream(requestOptions);
for await (const chunk of result.stream) {
const chunkText = chunk.text();
this.generateTextStream(chunkText, onProgress, {
delay: 12,
});
reply += chunkText;
}
return reply;
}
const stream = await model.stream(messages, {
signal: abortController.signal,
timeout: 7000,
});
for await (const chunk of stream) {
await this.generateTextStream(chunk?.content ?? chunk, onProgress, {
const chunkText = chunk?.content ?? chunk;
this.generateTextStream(chunkText, onProgress, {
delay: this.isGenerativeModel ? 12 : 8,
});
reply += chunk?.content ?? chunk;
reply += chunkText;
}
return reply;
@@ -559,6 +708,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,
};
}

View File

@@ -1,21 +1,36 @@
const OpenAI = require('openai');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { getResponseSender, ImageDetailCost, ImageDetail } = require('librechat-data-provider');
const {
Constants,
ImageDetail,
EModelEndpoint,
resolveHeaders,
ImageDetailCost,
CohereConstants,
getResponseSender,
validateVisionModel,
mapModelToAzureConfig,
} = require('librechat-data-provider');
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
const {
getModelMaxTokens,
genAzureChatCompletion,
extractBaseURL,
constructAzureURL,
getModelMaxTokens,
genAzureChatCompletion,
} = require('~/utils');
const { encodeAndFormat, validateVisionModel } = require('~/server/services/Files/images');
const { truncateText, formatMessage, CUT_OFF_PROMPT } = require('./prompts');
const {
truncateText,
formatMessage,
CUT_OFF_PROMPT,
titleInstruction,
createContextHandlers,
} = require('./prompts');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const { handleOpenAIErrors } = require('./tools/util');
const { isEnabled, sleep } = require('~/server/utils');
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');
@@ -32,7 +47,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';
@@ -40,6 +58,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
@@ -83,7 +105,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) {
@@ -174,16 +201,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);
@@ -214,13 +231,20 @@ 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);
const availableModels = this.options.modelsConfig?.[this.options.endpoint];
this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
if (attachments && !this.isVisionModel) {
this.modelOptions.model = 'gpt-4-vision-preview';
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;
}
@@ -355,8 +379,11 @@ class OpenAIClient extends BaseClient {
return {
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,
};
}
@@ -369,54 +396,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
@@ -427,8 +406,7 @@ class OpenAIClient extends BaseClient {
*/
async addImageURLs(message, attachments) {
const { files, image_urls } = await encodeAndFormat(this.options.req, attachments);
message.image_urls = image_urls;
message.image_urls = image_urls.length ? image_urls : undefined;
return files;
}
@@ -456,23 +434,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;
@@ -490,6 +454,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,
@@ -508,6 +479,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,
@@ -519,6 +495,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({
@@ -546,15 +540,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]') {
@@ -587,12 +582,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,
});
@@ -600,9 +599,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) {
@@ -612,11 +616,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({
@@ -630,6 +634,7 @@ class OpenAIClient extends BaseClient {
context,
tokenBuffer,
initialMessageCount,
conversationId,
}) {
const modelOptions = {
modelName: modelName ?? model,
@@ -659,6 +664,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);
@@ -677,7 +692,7 @@ class OpenAIClient extends BaseClient {
callbacks: runManager.createCallbacks({
context,
tokenBuffer,
conversationId: this.conversationId,
conversationId: this.conversationId ?? conversationId,
initialMessageCount,
}),
});
@@ -693,12 +708,13 @@ class OpenAIClient extends BaseClient {
*
* @param {Object} params - The parameters for the conversation title generation.
* @param {string} params.text - The user's input.
* @param {string} [params.conversationId] - The current conversationId, if not already defined on client initialization.
* @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 = '' }) {
async titleConvo({ text, conversationId, responseText = '' }) {
let title = 'New Chat';
const convo = `||>User:
"${truncateText(text)}"
@@ -707,7 +723,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
@@ -718,6 +737,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;
@@ -729,8 +781,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}
@@ -738,10 +789,18 @@ ${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',
@@ -758,7 +817,12 @@ ${convo}
try {
this.abortController = new AbortController();
const llm = this.initializeLLM({ ...modelOptions, context: 'title', tokenBuffer: 150 });
const llm = this.initializeLLM({
...modelOptions,
conversationId,
context: 'title',
tokenBuffer: 150,
});
title = await runTitleChain({ llm, text, convo, signal: this.abortController.signal });
} catch (e) {
if (e?.message?.toLowerCase()?.includes('abort')) {
@@ -784,7 +848,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,
@@ -888,13 +956,12 @@ ${convo}
}
}
async recordTokenUsage({ promptTokens, completionTokens }) {
logger.debug('[OpenAIClient] recordTokenUsage:', { promptTokens, completionTokens });
async recordTokenUsage({ promptTokens, completionTokens, context = 'message' }) {
await spendTokens(
{
context,
user: this.user,
model: this.modelOptions.model,
context: 'message',
conversationId: this.conversationId,
endpointTokenConfig: this.options.endpointTokenConfig,
},
@@ -909,7 +976,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 = '';
@@ -930,15 +997,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,
@@ -963,6 +1021,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;
@@ -970,9 +1060,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 };
}
@@ -982,6 +1073,7 @@ ${convo}
}
let chatCompletion;
/** @type {OpenAI} */
const openai = new OpenAI({
apiKey: this.apiKey,
...opts,
@@ -1013,12 +1105,20 @@ ${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,
});
}
let UnexpectedRoleError = false;
@@ -1034,6 +1134,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 });
@@ -1049,6 +1159,8 @@ ${convo}
stream.controller.abort();
break;
}
await sleep(25);
}
if (!UnexpectedRoleError) {
@@ -1079,12 +1191,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 (
@@ -1097,6 +1217,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') ||

View File

@@ -3,6 +3,7 @@ const { CallbackManager } = require('langchain/callbacks');
const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents');
const { addImages, buildErrorInput, buildPromptPrefix } = require('./output_parsers');
const { processFileURL } = require('~/server/services/Files/process');
const { EModelEndpoint } = require('librechat-data-provider');
const { formatLangChainMessages } = require('./prompts');
const checkBalance = require('~/models/checkBalance');
@@ -30,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) {
@@ -45,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,
};
}
@@ -113,6 +114,7 @@ class PluginsClient extends OpenAIClient {
openAIApiKey: this.openAIApiKey,
conversationId: this.conversationId,
fileStrategy: this.options.req.app.locals.fileStrategy,
processFileURL,
message,
},
});
@@ -146,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);
@@ -246,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,
@@ -306,6 +310,8 @@ class PluginsClient extends OpenAIClient {
}
const responseMessage = {
endpoint: EModelEndpoint.gptPlugins,
iconURL: this.options.iconURL,
messageId: responseMessageId,
conversationId,
parentMessageId: userMessage.messageId,

View File

@@ -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),

View File

@@ -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',

View 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;

View File

@@ -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,

View File

@@ -1,7 +1,9 @@
const createLLM = require('./createLLM');
const RunManager = require('./RunManager');
const createCoherePayload = require('./createCoherePayload');
module.exports = {
createLLM,
RunManager,
createCoherePayload,
};

View 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;

View 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;

View File

@@ -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) {

View File

@@ -1,5 +1,6 @@
const { formatMessage, formatLangChainMessages, formatFromLangChain } = require('./formatMessages');
const { Constants } = require('librechat-data-provider');
const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
const { formatMessage, formatLangChainMessages, formatFromLangChain } = require('./formatMessages');
describe('formatMessage', () => {
it('formats user message', () => {
@@ -61,7 +62,7 @@ describe('formatMessage', () => {
isCreatedByUser: true,
isEdited: false,
model: null,
parentMessageId: '00000000-0000-0000-0000-000000000000',
parentMessageId: Constants.NO_PARENT,
sender: 'User',
text: 'hi',
tokenCount: 5,

View File

@@ -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,
};

View File

@@ -27,7 +27,63 @@ ${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>`;
/**
* Parses titles from title functions based on the provided prompt.
* @param {string} prompt - The prompt containing the title function.
* @returns {string} The parsed title. "New Chat" if no title is found.
*/
function parseTitleFromPrompt(prompt) {
const titleRegex = /<title>(.+?)<\/title>/;
const titleMatch = prompt.match(titleRegex);
if (titleMatch && titleMatch[1]) {
const title = titleMatch[1].trim();
// // Capitalize the first letter of each word; Note: unnecessary due to title case prompting
// const capitalizedTitle = title.replace(/\b\w/g, (char) => char.toUpperCase());
return title;
}
return 'New Chat';
}
module.exports = {
langPrompt,
titleInstruction,
createTitlePrompt,
titleFunctionPrompt,
parseTitleFromPrompt,
};

View File

@@ -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 };

View File

@@ -1,3 +1,4 @@
const { Constants } = require('librechat-data-provider');
const { initializeFakeClient } = require('./FakeClient');
jest.mock('../../../lib/db/connectDb');
@@ -307,7 +308,7 @@ describe('BaseClient', () => {
const unorderedMessages = [
{ id: '3', parentMessageId: '2', text: 'Message 3' },
{ id: '2', parentMessageId: '1', text: 'Message 2' },
{ id: '1', parentMessageId: '00000000-0000-0000-0000-000000000000', text: 'Message 1' },
{ id: '1', parentMessageId: Constants.NO_PARENT, text: 'Message 1' },
];
it('should return ordered messages based on parentMessageId', () => {
@@ -316,7 +317,7 @@ describe('BaseClient', () => {
parentMessageId: '3',
});
expect(result).toEqual([
{ id: '1', parentMessageId: '00000000-0000-0000-0000-000000000000', text: 'Message 1' },
{ id: '1', parentMessageId: Constants.NO_PARENT, text: 'Message 1' },
{ id: '2', parentMessageId: '1', text: 'Message 2' },
{ id: '3', parentMessageId: '2', text: 'Message 3' },
]);

View File

@@ -1,6 +1,7 @@
const crypto = require('crypto');
const { Constants } = require('librechat-data-provider');
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
const PluginsClient = require('../PluginsClient');
const crypto = require('crypto');
jest.mock('~/lib/db/connectDb');
jest.mock('~/models/Conversation', () => {
@@ -66,7 +67,7 @@ describe('PluginsClient', () => {
TestAgent.setOptions(opts);
}
const conversationId = opts.conversationId || crypto.randomUUID();
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
const parentMessageId = opts.parentMessageId || Constants.NO_PARENT;
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
this.pastMessages = await TestAgent.loadHistory(
conversationId,

View File

@@ -3,8 +3,8 @@ const OpenAI = require('openai');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('langchain/tools');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { FileContext } = require('librechat-data-provider');
const { getImageBasename } = require('~/server/services/Files/images');
const { processFileURL } = require('~/server/services/Files/process');
const extractBaseURL = require('~/utils/extractBaseURL');
const { logger } = require('~/config');
@@ -14,6 +14,9 @@ class OpenAICreateImage extends Tool {
this.userId = fields.userId;
this.fileStrategy = fields.fileStrategy;
if (fields.processFileURL) {
this.processFileURL = fields.processFileURL.bind(this);
}
let apiKey = fields.DALLE2_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
const config = { apiKey };
@@ -80,13 +83,21 @@ Guidelines:
}
async _call(input) {
const resp = await this.openai.images.generate({
prompt: this.replaceUnwantedChars(input),
// TODO: Future idea -- could we ask an LLM to extract these arguments from an input that might contain them?
n: 1,
// size: '1024x1024'
size: '512x512',
});
let resp;
try {
resp = await this.openai.images.generate({
prompt: this.replaceUnwantedChars(input),
// TODO: Future idea -- could we ask an LLM to extract these arguments from an input that might contain them?
n: 1,
// size: '1024x1024'
size: '512x512',
});
} catch (error) {
logger.error('[DALL-E] Problem generating the image:', error);
return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable:
Error Message: ${error.message}`;
}
const theImageUrl = resp.data[0].url;
@@ -110,15 +121,16 @@ Guidelines:
});
try {
const result = await processFileURL({
const result = await this.processFileURL({
fileStrategy: this.fileStrategy,
userId: this.userId,
URL: theImageUrl,
fileName: imageName,
basePath: 'images',
context: FileContext.image_generation,
});
this.result = this.wrapInMarkdown(result);
this.result = this.wrapInMarkdown(result.filepath);
} catch (error) {
logger.error('Error while saving the image:', error);
this.result = `Failed to save the image locally. ${error.message}`;

View File

@@ -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;

View File

@@ -1,35 +1,44 @@
const GoogleSearchAPI = require('./GoogleSearch');
const OpenAICreateImage = require('./DALL-E');
const DALLE3 = require('./structured/DALLE3');
const StructuredSD = require('./structured/StableDiffusion');
const StableDiffusionAPI = require('./StableDiffusion');
const availableTools = require('./manifest.json');
// Basic Tools
const CodeBrew = require('./CodeBrew');
const WolframAlphaAPI = require('./Wolfram');
const StructuredWolfram = require('./structured/Wolfram');
const SelfReflectionTool = require('./SelfReflection');
const AzureAiSearch = require('./AzureAiSearch');
const StructuredACS = require('./structured/AzureAISearch');
const OpenAICreateImage = require('./DALL-E');
const StableDiffusionAPI = require('./StableDiffusion');
const SelfReflectionTool = require('./SelfReflection');
// Structured Tools
const DALLE3 = require('./structured/DALLE3');
const ChatTool = require('./structured/ChatTool');
const E2BTools = require('./structured/E2BTools');
const CodeSherpa = require('./structured/CodeSherpa');
const StructuredSD = require('./structured/StableDiffusion');
const StructuredACS = require('./structured/AzureAISearch');
const CodeSherpaTools = require('./structured/CodeSherpaTools');
const availableTools = require('./manifest.json');
const CodeBrew = require('./CodeBrew');
const GoogleSearchAPI = require('./structured/GoogleSearch');
const StructuredWolfram = require('./structured/Wolfram');
const TavilySearchResults = require('./structured/TavilySearchResults');
const TraversaalSearch = require('./structured/TraversaalSearch');
module.exports = {
availableTools,
GoogleSearchAPI,
OpenAICreateImage,
DALLE3,
StableDiffusionAPI,
StructuredSD,
WolframAlphaAPI,
StructuredWolfram,
SelfReflectionTool,
AzureAiSearch,
StructuredACS,
E2BTools,
ChatTool,
CodeSherpa,
CodeSherpaTools,
// Basic Tools
CodeBrew,
AzureAiSearch,
GoogleSearchAPI,
WolframAlphaAPI,
OpenAICreateImage,
StableDiffusionAPI,
SelfReflectionTool,
// Structured Tools
DALLE3,
ChatTool,
E2BTools,
CodeSherpa,
StructuredSD,
StructuredACS,
CodeSherpaTools,
StructuredWolfram,
TavilySearchResults,
TraversaalSearch,
};

View File

@@ -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",
@@ -108,6 +121,19 @@
}
]
},
{
"name": "Tavily Search",
"pluginKey": "tavily_search_results_json",
"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": [
{
"authField": "TAVILY_API_KEY",
"label": "Tavily API Key",
"description": "Get your API key here: https://app.tavily.com/"
}
]
},
{
"name": "Calculator",
"pluginKey": "calculator",

View File

@@ -19,6 +19,13 @@ class AzureAISearch extends StructuredTool {
this.name = 'azure-ai-search';
this.description =
'Use the \'azure-ai-search\' tool to retrieve search results relevant to your input';
/* Used to initialize the Tool without necessary variables. */
this.override = fields.override ?? false;
// Define schema
this.schema = z.object({
query: z.string().describe('Search word or phrase to Azure AI Search'),
});
// Initialize properties using helper function
this.serviceEndpoint = this._initializeField(
@@ -51,12 +58,16 @@ class AzureAISearch extends StructuredTool {
);
// Check for required fields
if (!this.serviceEndpoint || !this.indexName || !this.apiKey) {
if (!this.override && (!this.serviceEndpoint || !this.indexName || !this.apiKey)) {
throw new Error(
'Missing AZURE_AI_SEARCH_SERVICE_ENDPOINT, AZURE_AI_SEARCH_INDEX_NAME, or AZURE_AI_SEARCH_API_KEY environment variable.',
);
}
if (this.override) {
return;
}
// Create SearchClient
this.client = new SearchClient(
this.serviceEndpoint,
@@ -64,11 +75,6 @@ class AzureAISearch extends StructuredTool {
new AzureKeyCredential(this.apiKey),
{ apiVersion: this.apiVersion },
);
// Define schema
this.schema = z.object({
query: z.string().describe('Search word or phrase to Azure AI Search'),
});
}
// Improved error handling and logging

View File

@@ -4,17 +4,26 @@ const OpenAI = require('openai');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('langchain/tools');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { FileContext } = require('librechat-data-provider');
const { getImageBasename } = require('~/server/services/Files/images');
const { processFileURL } = require('~/server/services/Files/process');
const extractBaseURL = require('~/utils/extractBaseURL');
const { logger } = require('~/config');
class DALLE3 extends Tool {
constructor(fields = {}) {
super();
/** @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;
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);
}
let apiKey = fields.DALLE3_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
const config = { apiKey };
if (process.env.DALLE_REVERSE_PROXY) {
@@ -35,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.
@@ -81,7 +91,7 @@ class DALLE3 extends Tool {
getApiKey() {
const apiKey = process.env.DALLE3_API_KEY ?? process.env.DALLE_API_KEY ?? '';
if (!apiKey) {
if (!apiKey && !this.override) {
throw new Error('Missing DALLE_API_KEY environment variable.');
}
return apiKey;
@@ -115,6 +125,7 @@ class DALLE3 extends Tool {
n: 1,
});
} catch (error) {
logger.error('[DALL-E-3] Problem generating the image:', error);
return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable:
Error Message: ${error.message}`;
}
@@ -145,15 +156,20 @@ Error Message: ${error.message}`;
});
try {
const result = await processFileURL({
const result = await this.processFileURL({
fileStrategy: this.fileStrategy,
userId: this.userId,
URL: theImageUrl,
fileName: imageName,
basePath: 'images',
context: FileContext.image_generation,
});
this.result = this.wrapInMarkdown(result);
if (this.returnMetadata) {
this.result = result;
} else {
this.result = this.wrapInMarkdown(result.filepath);
}
} catch (error) {
logger.error('Error while saving the image:', error);
this.result = `Failed to save the image locally. ${error.message}`;

View 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;

View File

@@ -4,12 +4,28 @@ 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();
/** @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();
this.description_for_model = `// Generate images and visuals using text.
@@ -44,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 `![generated image](/${imageUrl})`;
@@ -52,7 +68,7 @@ class StableDiffusionAPI extends StructuredTool {
getServerURL() {
const url = process.env.SD_WEBUI_URL || '';
if (!url) {
if (!url && !this.override) {
throw new Error('Missing SD_WEBUI_URL environment variable.');
}
return url;
@@ -70,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;

View File

@@ -0,0 +1,92 @@
const { z } = require('zod');
const { Tool } = require('@langchain/core/tools');
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
class TavilySearchResults extends Tool {
static lc_name() {
return 'TavilySearchResults';
}
constructor(fields = {}) {
super(fields);
this.envVar = 'TAVILY_API_KEY';
/* Used to initialize the Tool without necessary variables. */
this.override = fields.override ?? false;
this.apiKey = fields.apiKey ?? this.getApiKey();
this.kwargs = fields?.kwargs ?? {};
this.name = 'tavily_search_results_json';
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 5.'),
search_depth: z
.enum(['basic', 'advanced'])
.optional()
.describe(
'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.',
),
include_images: z
.boolean()
.optional()
.describe(
'Whether to include a list of query-related images in the response. Default is False.',
),
include_answer: z
.boolean()
.optional()
.describe('Whether to include answers in the search results. Default is False.'),
// include_raw_content: z.boolean().optional().describe('Whether to include raw content in the search results. Default is False.'),
// include_domains: z.array(z.string()).optional().describe('A list of domains to specifically include in the search results.'),
// exclude_domains: z.array(z.string()).optional().describe('A list of domains to specifically exclude from the search results.'),
});
}
getApiKey() {
const apiKey = getEnvironmentVariable(this.envVar);
if (!apiKey && !this.override) {
throw new Error(`Missing ${this.envVar} environment variable.`);
}
return apiKey;
}
async _call(input) {
const validationResult = this.schema.safeParse(input);
if (!validationResult.success) {
throw new Error(`Validation failed: ${JSON.stringify(validationResult.error.issues)}`);
}
const { query, ...rest } = validationResult.data;
const requestBody = {
api_key: this.apiKey,
query,
...rest,
...this.kwargs,
};
const response = await fetch('https://api.tavily.com/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
const json = await response.json();
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}: ${json.error}`);
}
return JSON.stringify(json);
}
}
module.exports = TavilySearchResults;

View 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;

View File

@@ -7,6 +7,9 @@ const { logger } = require('~/config');
class WolframAlphaAPI extends StructuredTool {
constructor(fields) {
super();
/* Used to initialize the Tool without necessary variables. */
this.override = fields.override ?? false;
this.name = 'wolfram';
this.apiKey = fields.WOLFRAM_APP_ID || this.getAppId();
this.description_for_model = `// Access dynamic computation and curated data from WolframAlpha and Wolfram Cloud.
@@ -55,7 +58,7 @@ class WolframAlphaAPI extends StructuredTool {
getAppId() {
const appId = process.env.WOLFRAM_APP_ID || '';
if (!appId) {
if (!appId && !this.override) {
throw new Error('Missing WOLFRAM_APP_ID environment variable.');
}
return appId;

View File

@@ -1,14 +1,11 @@
const OpenAI = require('openai');
const DALLE3 = require('../DALLE3');
const { processFileURL } = require('~/server/services/Files/process');
const { logger } = require('~/config');
jest.mock('openai');
jest.mock('~/server/services/Files/process', () => ({
processFileURL: jest.fn(),
}));
const processFileURL = jest.fn();
jest.mock('~/server/services/Files/images', () => ({
getImageBasename: jest.fn().mockImplementation((url) => {
@@ -69,7 +66,7 @@ describe('DALLE3', () => {
jest.resetModules();
process.env = { ...originalEnv, DALLE_API_KEY: mockApiKey };
// Instantiate DALLE3 for tests that do not depend on DALLE3_SYSTEM_PROMPT
dalle = new DALLE3();
dalle = new DALLE3({ processFileURL });
});
afterEach(() => {
@@ -78,7 +75,8 @@ describe('DALLE3', () => {
process.env = originalEnv;
});
it('should throw an error if DALLE_API_KEY is missing', () => {
it('should throw an error if all potential API keys are missing', () => {
delete process.env.DALLE3_API_KEY;
delete process.env.DALLE_API_KEY;
expect(() => new DALLE3()).toThrow('Missing DALLE_API_KEY environment variable.');
});
@@ -112,7 +110,9 @@ describe('DALLE3', () => {
};
generate.mockResolvedValue(mockResponse);
processFileURL.mockResolvedValue('http://example.com/img-test.png');
processFileURL.mockResolvedValue({
filepath: 'http://example.com/img-test.png',
});
const result = await dalle._call(mockData);

View File

@@ -6,19 +6,23 @@ const { OpenAIEmbeddings } = require('langchain/embeddings/openai');
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
const {
availableTools,
// Basic Tools
CodeBrew,
AzureAISearch,
GoogleSearchAPI,
WolframAlphaAPI,
StructuredWolfram,
OpenAICreateImage,
StableDiffusionAPI,
// Structured Tools
DALLE3,
StructuredSD,
AzureAISearch,
StructuredACS,
E2BTools,
CodeSherpa,
StructuredSD,
StructuredACS,
CodeSherpaTools,
CodeBrew,
TraversaalSearch,
StructuredWolfram,
TavilySearchResults,
} = require('../');
const { loadToolSuite } = require('./loadToolSuite');
const { loadSpecs } = require('./loadSpecs');
@@ -151,8 +155,10 @@ const loadTools = async ({
returnMap = false,
tools = [],
options = {},
skipSpecs = false,
}) => {
const toolConstructors = {
tavily_search_results_json: TavilySearchResults,
calculator: Calculator,
google: GoogleSearchAPI,
wolfram: functions ? StructuredWolfram : WolframAlphaAPI,
@@ -160,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);
@@ -229,10 +236,19 @@ const loadTools = async ({
toolConstructors.codesherpa = CodeSherpa;
}
const imageGenOptions = {
req: options.req,
fileStrategy: options.fileStrategy,
processFileURL: options.processFileURL,
returnMetadata: options.returnMetadata,
uploadImageBuffer: options.uploadImageBuffer,
};
const toolOptions = {
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' },
dalle: { fileStrategy: options.fileStrategy },
'dall-e': { fileStrategy: options.fileStrategy },
dalle: imageGenOptions,
'dall-e': imageGenOptions,
'stable-diffusion': imageGenOptions,
};
const toolAuthFields = {};
@@ -271,7 +287,7 @@ const loadTools = async ({
}
let specs = null;
if (functions && remainingTools.length > 0) {
if (functions && remainingTools.length > 0 && skipSpecs !== true) {
specs = await loadSpecs({
llm: model,
user,
@@ -298,6 +314,9 @@ const loadTools = async ({
let result = [];
for (const tool of tools) {
const validTool = requestedTools[tool];
if (!validTool) {
continue;
}
const plugin = await validTool();
if (Array.isArray(plugin)) {

View File

@@ -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}`);
}
}

View File

@@ -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) {

View File

@@ -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),
});
});

View File

@@ -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 })
@@ -33,19 +34,33 @@ const genTitle = isEnabled(USE_REDIS) // ttl: 2 minutes
const modelQueries = isEnabled(process.env.USE_REDIS)
? new Keyv({ store: keyvRedis })
: new Keyv({ namespace: 'models' });
: new Keyv({ namespace: CacheKeys.MODEL_QUERIES });
const abortKeys = isEnabled(USE_REDIS)
? new Keyv({ store: keyvRedis })
: 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'),
[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,
[CacheKeys.GEN_TITLE]: genTitle,
[CacheKeys.MODEL_QUERIES]: modelQueries,

View File

@@ -33,6 +33,10 @@ function getMatchingSensitivePatterns(valueStr) {
* @returns {string} - The redacted console message.
*/
function redactMessage(str) {
if (!str) {
return '';
}
const patterns = getMatchingSensitivePatterns(str);
if (patterns.length === 0) {

View File

@@ -1,7 +1,14 @@
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'),
};

View File

@@ -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 {

View File

@@ -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 () => {

69
api/models/Action.js Normal file
View File

@@ -0,0 +1,69 @@
const mongoose = require('mongoose');
const actionSchema = require('./schema/action');
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, 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, session = null) => {
const options = { new: true, upsert: true, session };
return await Action.findOneAndUpdate(searchParams, updateData, options).lean();
};
/**
* Retrieves all actions that match the given search parameters.
*
* @param {Object} searchParams - The search parameters to find matching actions.
* @param {boolean} includeSensitive - Flag to include sensitive data in the metadata.
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
*/
const getActions = async (searchParams, includeSensitive = false) => {
const actions = await Action.find(searchParams).lean();
if (!includeSensitive) {
for (let i = 0; i < actions.length; i++) {
const metadata = actions[i].metadata;
if (!metadata) {
continue;
}
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
for (let field of sensitiveFields) {
if (metadata[field]) {
delete metadata[field];
}
}
}
}
return actions;
};
/**
* Deletes an action by params, within a transaction session if provided.
*
* @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, session = null) => {
const options = session ? { session } : {};
return await Action.findOneAndDelete(searchParams, options).lean();
};
module.exports = {
updateAction,
getActions,
deleteAction,
};

46
api/models/Assistant.js Normal file
View File

@@ -0,0 +1,46 @@
const mongoose = require('mongoose');
const assistantSchema = require('./schema/assistant');
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, 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, session = null) => {
const options = { new: true, upsert: true, session };
return await Assistant.findOneAndUpdate(searchParams, updateData, options).lean();
};
/**
* Retrieves an assistant document based on the provided ID.
*
* @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.
* @returns {Promise<Object|null>} The assistant document as a plain object, or null if not found.
*/
const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean();
/**
* Retrieves all assistants that match the given search parameters.
*
* @param {Object} searchParams - The search parameters to find matching assistants.
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
*/
const getAssistants = async (searchParams) => {
return await Assistant.find(searchParams).lean();
};
module.exports = {
updateAssistant,
getAssistants,
getAssistant,
};

View File

@@ -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)

View File

@@ -14,24 +14,32 @@ const findFileById = async (file_id, options = {}) => {
};
/**
* Retrieves files matching a given filter.
* Retrieves files matching a given filter, sorted by the most recently updated.
* @param {Object} filter - The filter criteria to apply.
* @param {Object} [_sortOptions] - Optional sort parameters.
* @returns {Promise<Array<MongoFile>>} A promise that resolves to an array of file documents.
*/
const getFiles = async (filter) => {
return await File.find(filter).lean();
const getFiles = async (filter, _sortOptions) => {
const sortOptions = { updatedAt: -1, ..._sortOptions };
return await File.find(filter).sort(sortOptions).lean();
};
/**
* Creates a new file with a TTL of 1 hour.
* @param {MongoFile} data - The file data to be created, must contain file_id.
* @param {boolean} disableTTL - Whether to disable the TTL.
* @returns {Promise<MongoFile>} A promise that resolves to the created file document.
*/
const createFile = async (data) => {
const createFile = async (data, disableTTL) => {
const fileData = {
...data,
expiresAt: new Date(Date.now() + 3600 * 1000),
};
if (disableTTL) {
delete fileData.expiresAt;
}
return await File.findOneAndUpdate({ file_id: data.file_id }, fileData, {
new: true,
upsert: true,
@@ -61,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();
};
@@ -75,6 +83,15 @@ const deleteFile = async (file_id) => {
return await File.findOneAndDelete({ file_id }).lean();
};
/**
* Deletes a file identified by a filter.
* @param {object} filter - The filter criteria to apply.
* @returns {Promise<MongoFile>} A promise that resolves to the deleted file document or null.
*/
const deleteFileByFilter = async (filter) => {
return await File.findOneAndDelete(filter).lean();
};
/**
* Deletes multiple files identified by an array of file_ids.
* @param {Array<string>} file_ids - The unique identifiers of the files to delete.
@@ -93,4 +110,5 @@ module.exports = {
updateFileUsage,
deleteFile,
deleteFiles,
deleteFileByFilter,
};

View File

@@ -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,11 +74,68 @@ 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.
*
* @async
* @function recordMessage
* @param {Object} params - The message data object.
* @param {string} params.user - The identifier of the user.
* @param {string} params.endpoint - The endpoint where the message originated.
* @param {string} params.messageId - The unique identifier for the message.
* @param {string} params.conversationId - The identifier of the conversation.
* @param {string} [params.parentMessageId] - The identifier of the parent message, if any.
* @param {Partial<TMessage>} rest - Any additional properties from the TMessage typedef not explicitly listed.
* @returns {Promise<Object>} The updated or newly inserted message document.
* @throws {Error} If there is an error in saving the message.
*/
async recordMessage({ user, endpoint, messageId, conversationId, parentMessageId, ...rest }) {
try {
// No parsing of convoId as may use threadId
const message = {
user,
endpoint,
messageId,
conversationId,
parentMessageId,
...rest,
};
return await Message.findOneAndUpdate({ user, messageId }, message, {
upsert: true,
new: true,
});
} catch (err) {
logger.error('Error saving message:', err);
throw new Error('Failed to save message.');
}
},
async updateMessage(message) {
try {
const { messageId, ...update } = message;
update.isEdited = true;
const updatedMessage = await Message.findOneAndUpdate({ messageId }, update, { new: true });
const updatedMessage = await Message.findOneAndUpdate({ messageId }, update, {
new: true,
});
if (!updatedMessage) {
throw new Error('Message not found.');

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -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,

View File

@@ -1,12 +1,14 @@
const {
getMessages,
saveMessage,
recordMessage,
updateMessage,
deleteMessagesSince,
deleteMessages,
} = require('./Message');
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
const { hashPassword, getUser, updateUser } = require('./userMethods');
const {
findFileById,
createFile,
@@ -20,17 +22,20 @@ 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,
getUser,
getMessages,
saveMessage,
recordMessage,
updateMessage,
deleteMessagesSince,
deleteMessages,

View File

@@ -183,6 +183,15 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
if (object.conversationId && object.conversationId.includes('|')) {
object.conversationId = object.conversationId.replace(/\|/g, '--');
}
if (object.content && Array.isArray(object.content)) {
object.text = object.content
.filter((item) => item.type === 'text' && item.text && item.text.value)
.map((item) => item.text.value)
.join(' ');
delete object.content;
}
return object;
}

View File

@@ -0,0 +1,59 @@
const mongoose = require('mongoose');
const { Schema } = mongoose;
const AuthSchema = new Schema(
{
authorization_type: String,
custom_auth_header: String,
type: {
type: String,
enum: ['service_http', 'oauth', 'none'],
},
authorization_content_type: String,
authorization_url: String,
client_url: String,
scope: String,
token_exchange_method: {
type: String,
enum: ['default_post', 'basic_auth_header', null],
},
},
{ _id: false },
);
const actionSchema = new Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
index: true,
required: true,
},
action_id: {
type: String,
index: true,
required: true,
},
type: {
type: String,
default: 'action_prototype',
},
settings: Schema.Types.Mixed,
assistant_id: String,
metadata: {
api_key: String, // private, encrypted
auth: AuthSchema,
domain: {
type: String,
required: true,
},
// json_schema: Schema.Types.Mixed,
privacy_policy_url: String,
raw_spec: String,
oauth_client_id: String, // private, encrypted
oauth_client_secret: String, // private, encrypted
},
});
// }, { minimize: false }); // Prevent removal of empty objects
module.exports = actionSchema;

View File

@@ -0,0 +1,33 @@
const mongoose = require('mongoose');
const assistantSchema = mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
assistant_id: {
type: String,
index: true,
required: true,
},
avatar: {
type: {
filepath: String,
source: String,
},
default: undefined,
},
access_level: {
type: Number,
},
file_ids: { type: [String], default: undefined },
actions: { type: [String], default: undefined },
},
{
timestamps: true,
},
);
module.exports = assistantSchema;

View File

@@ -11,152 +11,153 @@ const conversationPreset = {
// for azureOpenAI, openAI, chatGPTBrowser only
model: {
type: String,
// default: null,
required: false,
},
// for azureOpenAI, openAI only
chatGptLabel: {
type: String,
// default: null,
required: false,
},
// for google only
modelLabel: {
type: String,
// default: null,
required: false,
},
promptPrefix: {
type: String,
// default: null,
required: false,
},
temperature: {
type: Number,
// default: 1,
required: false,
},
top_p: {
type: Number,
// default: 1,
required: false,
},
// for google only
topP: {
type: Number,
// default: 0.95,
required: false,
},
topK: {
type: Number,
// default: 40,
required: false,
},
maxOutputTokens: {
type: Number,
// default: 1024,
required: false,
},
presence_penalty: {
type: Number,
// default: 0,
required: false,
},
frequency_penalty: {
type: Number,
// default: 0,
required: false,
},
// for bingai only
jailbreak: {
type: Boolean,
// default: false,
},
context: {
type: String,
// default: null,
},
systemMessage: {
type: String,
// default: null,
},
toneStyle: {
type: String,
// default: null,
},
file_ids: { type: [{ type: String }], default: undefined },
// deprecated
resendImages: {
type: Boolean,
},
// files
resendFiles: {
type: Boolean,
},
imageDetail: {
type: String,
},
/* assistants */
assistant_id: {
type: String,
},
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 },
};
const agentOptions = {
model: {
type: String,
// default: null,
required: false,
},
// for azureOpenAI, openAI only
chatGptLabel: {
type: String,
// default: null,
required: false,
},
modelLabel: {
type: String,
// default: null,
required: false,
},
promptPrefix: {
type: String,
// default: null,
required: false,
},
temperature: {
type: Number,
// default: 1,
required: false,
},
top_p: {
type: Number,
// default: 1,
required: false,
},
// for google only
topP: {
type: Number,
// default: 0.95,
required: false,
},
topK: {
type: Number,
// default: 40,
required: false,
},
maxOutputTokens: {
type: Number,
// default: 1024,
required: false,
},
presence_penalty: {
type: Number,
// default: 0,
required: false,
},
frequency_penalty: {
type: Number,
// default: 0,
required: false,
},
context: {
type: String,
// default: null,
},
systemMessage: {
type: String,
// default: null,
},
};

View File

@@ -3,6 +3,8 @@ const mongoose = require('mongoose');
/**
* @typedef {Object} MongoFile
* @property {mongoose.Schema.Types.ObjectId} [_id] - MongoDB Document ID
* @property {number} [__v] - MongoDB Version Key
* @property {mongoose.Schema.Types.ObjectId} user - User ID
* @property {string} [conversationId] - Optional conversation ID
* @property {string} file_id - File identifier
@@ -13,10 +15,15 @@ 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
* @property {Date} [expiresAt] - Optional height of the file
* @property {Date} [createdAt] - Date when the file was created
* @property {Date} [updatedAt] - Date when the file was updated
*/
const fileSchema = mongoose.Schema(
{
@@ -57,10 +64,17 @@ const fileSchema = mongoose.Schema(
required: true,
default: 'file',
},
embedded: {
type: Boolean,
},
type: {
type: String,
required: true,
},
context: {
type: String,
// required: true,
},
usage: {
type: Number,
required: true,
@@ -70,6 +84,9 @@ const fileSchema = mongoose.Schema(
type: String,
default: FileSources.local,
},
model: {
type: String,
},
width: Number,
height: Number,
expiresAt: {
@@ -82,4 +99,6 @@ const fileSchema = mongoose.Schema(
},
);
fileSchema.index({ createdAt: 1, updatedAt: 1 });
module.exports = fileSchema;

View File

@@ -17,6 +17,7 @@ const messageSchema = mongoose.Schema(
user: {
type: String,
index: true,
required: true,
default: null,
},
model: {
@@ -46,12 +47,10 @@ const messageSchema = mongoose.Schema(
},
sender: {
type: String,
required: true,
meiliIndex: true,
},
text: {
type: String,
required: true,
meiliIndex: true,
},
summary: {
@@ -103,6 +102,18 @@ const messageSchema = mongoose.Schema(
default: undefined,
},
plugins: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined },
content: {
type: [{ type: mongoose.Schema.Types.Mixed }],
default: undefined,
meiliIndex: true,
},
thread_id: {
type: String,
},
/* frontend components */
iconURL: {
type: String,
},
},
{ timestamps: true },
);

View File

@@ -1,4 +1,4 @@
const Transaction = require('./Transaction');
const { Transaction } = require('./Transaction');
const { logger } = require('~/config');
/**
@@ -21,6 +21,15 @@ const { logger } = require('~/config');
*/
const spendTokens = async (txData, tokenUsage) => {
const { promptTokens, completionTokens } = tokenUsage;
logger.debug(
`[spendTokens] conversationId: ${txData.conversationId}${
txData?.context ? ` | Context: ${txData?.context}` : ''
} | Token usage: `,
{
promptTokens,
completionTokens,
},
);
let prompt, completion;
try {
if (promptTokens >= 0) {
@@ -42,7 +51,16 @@ const spendTokens = async (txData, tokenUsage) => {
rawAmount: -completionTokens,
});
logger.debug('[spendTokens] post-transaction', { prompt, completion });
prompt &&
completion &&
logger.debug('[spendTokens] Transaction data record against balance:', {
user: txData.user,
prompt: prompt.prompt,
promptRate: prompt.rate,
completion: completion.completion,
completionRate: completion.rate,
balance: completion.balance,
});
} catch (err) {
logger.error('[spendTokens]', err);
}

View File

@@ -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 = {
@@ -13,6 +14,21 @@ const tokenValues = {
'gpt-3.5-turbo-1106': { prompt: 1, completion: 2 },
'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 +52,8 @@ const getValueKey = (model, endpoint) => {
return 'gpt-3.5-turbo-1106';
} else if (modelName.includes('gpt-3.5')) {
return '4k';
} 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 +64,8 @@ const getValueKey = (model, endpoint) => {
return '32k';
} else if (modelName.includes('gpt-4')) {
return '8k';
} else if (tokenValues[modelName]) {
return modelName;
}
return undefined;

View File

@@ -34,6 +34,13 @@ 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');
});
});
describe('getMultiplier', () => {

46
api/models/userMethods.js Normal file
View File

@@ -0,0 +1,46 @@
const bcrypt = require('bcryptjs');
const User = require('./User');
const hashPassword = async (password) => {
const hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, 10, function (err, hash) {
if (err) {
reject(err);
} else {
resolve(hash);
}
});
});
return hashedPassword;
};
/**
* Retrieve a user by ID and convert the found user document to a plain object.
*
* @param {string} userId - The ID of the user to find and return as a plain object.
* @returns {Promise<Object>} A plain object representing the user document, or `null` if no user is found.
*/
const getUser = async function (userId) {
return await User.findById(userId).lean();
};
/**
* Update a user with new data without overwriting existing properties.
*
* @param {string} userId - The ID of the user to update.
* @param {Object} updateData - An object containing the properties to update.
* @returns {Promise<Object>} The updated user document as a plain object, or `null` if no user is found.
*/
const updateUser = async function (userId, updateData) {
return await User.findByIdAndUpdate(userId, updateData, {
new: true,
runValidators: true,
}).lean();
};
module.exports = {
hashPassword,
updateUser,
getUser,
};

View File

@@ -1,13 +1,19 @@
{
"name": "@librechat/backend",
"version": "0.6.9",
"version": "0.7.1",
"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,17 +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/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",
@@ -44,6 +54,7 @@
"express-mongo-sanitize": "^2.2.0",
"express-rate-limit": "^6.9.0",
"express-session": "^1.17.3",
"file-type": "^18.7.0",
"firebase": "^10.6.0",
"googleapis": "^126.0.1",
"handlebars": "^4.7.7",
@@ -57,13 +68,14 @@
"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",
"openai": "4.36.0",
"openai-chat-tokens": "^0.2.8",
"openid-client": "^5.4.2",
"passport": "^0.6.0",

View File

@@ -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, 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,
});
@@ -140,7 +145,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
await saveMessage(userMessage);
if (addTitle && parentMessageId === '00000000-0000-0000-0000-000000000000' && newConvo) {
if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) {
addTitle(req, {
text,
response,

View File

@@ -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,
};

View File

@@ -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,
});

View File

@@ -1,4 +1,4 @@
const { CacheKeys } = require('librechat-data-provider');
const { CacheKeys, EModelEndpoint, orderEndpointsConfig } = require('librechat-data-provider');
const { loadDefaultEndpointsConfig, loadConfigEndpoints } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
@@ -10,10 +10,23 @@ 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 };
/** @type {TEndpointsConfig} */
const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
const { disableBuilder, retrievalModels, capabilities, ..._rest } =
req.app.locals[EModelEndpoint.assistants];
mergedConfig[EModelEndpoint.assistants] = {
...mergedConfig[EModelEndpoint.assistants],
retrievalModels,
disableBuilder,
capabilities,
};
}
const endpointsConfig = orderEndpointsConfig(mergedConfig);
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
res.send(JSON.stringify(endpointsConfig));

View File

@@ -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 };

View File

@@ -1,4 +1,3 @@
const path = require('path');
const { promises: fs } = require('fs');
const { CacheKeys } = require('librechat-data-provider');
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
@@ -56,12 +55,13 @@ const getAvailablePluginsController = async (req, res) => {
return;
}
const manifestFile = await fs.readFile(
path.join(__dirname, '..', '..', 'app', 'clients', 'tools', 'manifest.json'),
'utf8',
);
/** @type {{ filteredTools: string[] }} */
const { filteredTools = [] } = req.app.locals;
const jsonData = JSON.parse(manifestFile);
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)) {
@@ -70,7 +70,10 @@ const getAvailablePluginsController = async (req, res) => {
return plugin;
}
});
const plugins = await addOpenAPISpecs(authenticatedPlugins);
let plugins = await addOpenAPISpecs(authenticatedPlugins);
plugins = plugins.filter((plugin) => !filteredTools.includes(plugin.pluginKey));
await cache.set(CacheKeys.PLUGINS, plugins);
res.status(200).json(plugins);
} catch (error) {
@@ -78,6 +81,53 @@ const getAvailablePluginsController = async (req, res) => {
}
};
/**
* Retrieves and returns a list of available tools, either from a cache or by reading a plugin manifest file.
*
* This function first attempts to retrieve the list of tools from a cache. If the tools are not found in the cache,
* it reads a plugin manifest file, filters for unique plugins, and determines if each plugin is authenticated.
* Only plugins that are marked as available in the application's local state are included in the final list.
* The resulting list of tools is then cached and sent to the client.
*
* @param {object} req - The request object, containing information about the HTTP request.
* @param {object} res - The response object, used to send back the desired HTTP response.
* @returns {Promise<void>} A promise that resolves when the function has completed.
*/
const getAvailableTools = async (req, res) => {
try {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedTools = await cache.get(CacheKeys.TOOLS);
if (cachedTools) {
res.status(200).json(cachedTools);
return;
}
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 tools = authenticatedPlugins.filter(
(plugin) => req.app.locals.availableTools[plugin.pluginKey] !== undefined,
);
await cache.set(CacheKeys.TOOLS, tools);
res.status(200).json(tools);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
module.exports = {
getAvailableTools,
getAvailablePluginsController,
};

View File

@@ -8,16 +8,19 @@ const getUserController = async (req, res) => {
const updateUserPluginsController = async (req, res) => {
const { user } = req;
const { pluginKey, action, auth } = req.body;
const { pluginKey, action, auth, isAssistantTool } = req.body;
let authService;
try {
const userPluginsService = await updateUserPluginsService(user, pluginKey, action);
if (!isAssistantTool) {
const userPluginsService = await updateUserPluginsService(user, pluginKey, action);
if (userPluginsService instanceof Error) {
logger.error('[userPluginsService]', userPluginsService);
const { status, message } = userPluginsService;
res.status(status).send({ message });
if (userPluginsService instanceof Error) {
logger.error('[userPluginsService]', userPluginsService);
const { status, message } = userPluginsService;
res.status(status).send({ message });
}
}
if (auth) {
const keys = Object.keys(auth);
const values = Object.values(auth);

View File

@@ -2,15 +2,18 @@ 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');
const { connectDb, indexSync } = require('~/lib/db');
const AppService = require('./services/AppService');
const noIndex = require('./middleware/noIndex');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const routes = require('./routes');
@@ -21,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();
@@ -38,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());
@@ -53,7 +60,7 @@ const startServer = async () => {
passport.use(await jwtLogin());
passport.use(passportLogin());
if (ALLOW_SOCIAL_LOGIN?.toLowerCase() === 'true') {
if (isEnabled(ALLOW_SOCIAL_LOGIN)) {
configureSocialLogins(app);
}
@@ -76,7 +83,8 @@ const startServer = async () => {
app.use('/api/plugins', routes.plugins);
app.use('/api/config', routes.config);
app.use('/api/assistants', routes.assistants);
app.use('/api/files', routes.files);
app.use('/api/files', await routes.files.initialize());
app.use('/images/', validateImageRequest, routes.staticRoute);
app.use((req, res) => {
res.status(404).sendFile(path.join(app.locals.paths.dist, 'index.html'));

View File

@@ -1,18 +1,24 @@
const { EModelEndpoint } = 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');
async function abortMessage(req, res) {
let { abortKey, conversationId } = req.body;
let { abortKey, conversationId, endpoint } = req.body;
if (!abortKey && conversationId) {
abortKey = conversationId;
}
if (endpoint === EModelEndpoint.assistants) {
return await abortRun(req, res);
}
if (!abortControllers.has(abortKey) && !res.headersSent) {
return res.status(204).send({ message: 'Request not found' });
}
@@ -67,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,
@@ -94,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')) {
@@ -103,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,

View File

@@ -0,0 +1,92 @@
const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider');
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 { 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 [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 });
return res.status(400).send({ message: 'Invalid conversationId' });
}
const cacheKey = `${req.user.id}:${conversationId}`;
const cache = getLogStores(CacheKeys.ABORT_KEYS);
const runValues = await cache.get(cacheKey);
const [thread_id, run_id] = runValues.split(':');
if (!run_id) {
logger.warn('[abortRun] Couldn\'t find run for cancel request', { thread_id });
return res.status(204).send({ message: 'Run not found' });
} else if (run_id === 'cancelled') {
logger.warn('[abortRun] Run already cancelled', { thread_id });
return res.status(204).send({ message: 'Run already cancelled' });
}
let runMessages = [];
/** @type {{ openai: OpenAI }} */
const { openai } = await initializeClient({ req, res });
try {
await cache.set(cacheKey, 'cancelled', three_minutes);
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
logger.debug('[abortRun] Cancelled run:', cancelledRun);
} catch (error) {
logger.error('[abortRun] Error cancelling run', error);
if (
error?.message?.includes(RunStatus.CANCELLED) ||
error?.message?.includes(RunStatus.CANCELLING)
) {
return res.end();
}
}
try {
const 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('[abortRun] Error fetching or processing run', error);
}
runMessages = await checkMessageGaps({
openai,
latestMessageId,
thread_id,
run_id,
conversationId,
});
const finalEvent = {
final: true,
conversation,
runMessages,
};
if (res.headersSent && finalEvent) {
return sendMessage(res, finalEvent);
}
res.json(finalEvent);
}
module.exports = {
abortRun,
};

View File

@@ -1,10 +1,14 @@
const { parseConvo, EModelEndpoint } = require('librechat-data-provider');
const { processFiles } = require('~/server/services/Files/process');
const { getModelsConfig } = require('~/server/controllers/ModelController');
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 enforceModelSpec = require('./enforceModelSpec');
const { handleError } = require('~/server/utils');
const buildFunction = {
[EModelEndpoint.openAI]: openAI.buildOptions,
@@ -13,16 +17,46 @@ const buildFunction = {
[EModelEndpoint.azureOpenAI]: openAI.buildOptions,
[EModelEndpoint.anthropic]: anthropic.buildOptions,
[EModelEndpoint.gptPlugins]: gptPlugins.buildOptions,
[EModelEndpoint.assistants]: assistants.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' });
}
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);

View File

@@ -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) {

View 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;

View File

@@ -1,7 +1,7 @@
const crypto = require('crypto');
const { saveMessage } = require('~/models');
const { getResponseSender, Constants } = require('librechat-data-provider');
const { sendMessage, sendError } = require('~/server/utils');
const { getResponseSender } = require('librechat-data-provider');
const { saveMessage } = require('~/models');
/**
* Denies a request by sending an error message and optionally saves the user's message.
@@ -38,8 +38,7 @@ const denyRequest = async (req, res, errorMessage) => {
};
sendMessage(res, { message: userMessage, created: true });
const shouldSaveMessage =
_convoId && parentMessageId && parentMessageId !== '00000000-0000-0000-0000-000000000000';
const shouldSaveMessage = _convoId && parentMessageId && parentMessageId !== Constants.NO_PARENT;
if (shouldSaveMessage) {
await saveMessage({ ...userMessage, user: req.user.id });

View File

@@ -0,0 +1,47 @@
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.
* @param {string} key
* @param {any} value
* @param {TConversation} parsedBody
* @returns {boolean}
*/
const checkMatch = (key, value, parsedBody) => {
if (parsedBody[key] === value) {
return true;
}
if (interchangeableKeys.has(key)) {
return interchangeableKeys
.get(key)
.some((interchangeableKey) => parsedBody[interchangeableKey] === value);
}
return false;
};
module.exports = enforceModelSpec;

View File

@@ -0,0 +1,69 @@
const rateLimit = require('express-rate-limit');
const { ViolationTypes } = require('librechat-data-provider');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
const IMPORT_IP_MAX = parseInt(process.env.IMPORT_IP_MAX) || 100;
const IMPORT_IP_WINDOW = parseInt(process.env.IMPORT_IP_WINDOW) || 15;
const IMPORT_USER_MAX = parseInt(process.env.IMPORT_USER_MAX) || 50;
const IMPORT_USER_WINDOW = parseInt(process.env.IMPORT_USER_WINDOW) || 15;
const importIpWindowMs = IMPORT_IP_WINDOW * 60 * 1000;
const importIpMax = IMPORT_IP_MAX;
const importIpWindowInMinutes = importIpWindowMs / 60000;
const importUserWindowMs = IMPORT_USER_WINDOW * 60 * 1000;
const importUserMax = IMPORT_USER_MAX;
const importUserWindowInMinutes = importUserWindowMs / 60000;
return {
importIpWindowMs,
importIpMax,
importIpWindowInMinutes,
importUserWindowMs,
importUserMax,
importUserWindowInMinutes,
};
};
const createImportHandler = (ip = true) => {
const { importIpMax, importIpWindowInMinutes, importUserMax, importUserWindowInMinutes } =
getEnvironmentVariables();
return async (req, res) => {
const type = ViolationTypes.FILE_UPLOAD_LIMIT;
const errorMessage = {
type,
max: ip ? importIpMax : importUserMax,
limiter: ip ? 'ip' : 'user',
windowInMinutes: ip ? importIpWindowInMinutes : importUserWindowInMinutes,
};
await logViolation(req, res, type, errorMessage);
res.status(429).json({ message: 'Too many conversation import requests. Try again later' });
};
};
const createImportLimiters = () => {
const { importIpWindowMs, importIpMax, importUserWindowMs, importUserMax } =
getEnvironmentVariables();
const importIpLimiter = rateLimit({
windowMs: importIpWindowMs,
max: importIpMax,
handler: createImportHandler(),
});
const importUserLimiter = rateLimit({
windowMs: importUserWindowMs,
max: importUserMax,
handler: createImportHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
},
});
return { importIpLimiter, importUserLimiter };
};
module.exports = { createImportLimiters };

View File

@@ -1,9 +1,12 @@
const abortMiddleware = require('./abortMiddleware');
const checkBan = require('./checkBan');
const checkDomainAllowed = require('./checkDomainAllowed');
const uaParser = require('./uaParser');
const setHeaders = require('./setHeaders');
const loginLimiter = require('./loginLimiter');
const validateModel = require('./validateModel');
const requireJwtAuth = require('./requireJwtAuth');
const uploadLimiters = require('./uploadLimiters');
const registerLimiter = require('./registerLimiter');
const messageLimiters = require('./messageLimiters');
const requireLocalAuth = require('./requireLocalAuth');
@@ -12,10 +15,13 @@ const concurrentLimiter = require('./concurrentLimiter');
const validateMessageReq = require('./validateMessageReq');
const buildEndpointOption = require('./buildEndpointOption');
const validateRegistration = require('./validateRegistration');
const validateImageRequest = require('./validateImageRequest');
const moderateText = require('./moderateText');
const noIndex = require('./noIndex');
const importLimiters = require('./importLimiters');
module.exports = {
...uploadLimiters,
...abortMiddleware,
...messageLimiters,
checkBan,
@@ -30,6 +36,10 @@ module.exports = {
validateMessageReq,
buildEndpointOption,
validateRegistration,
validateImageRequest,
validateModel,
moderateText,
noIndex,
...importLimiters,
checkDomainAllowed,
};

View File

@@ -1,5 +1,7 @@
const axios = require('axios');
const { ErrorTypes } = require('librechat-data-provider');
const denyRequest = require('./denyRequest');
const { logger } = require('~/config');
async function moderateText(req, res, next) {
if (process.env.OPENAI_MODERATION === 'true') {
@@ -23,12 +25,12 @@ async function moderateText(req, res, next) {
const flagged = results.some((result) => result.flagged);
if (flagged) {
const type = 'moderation';
const type = ErrorTypes.MODERATION;
const errorMessage = { type };
return await denyRequest(req, res, errorMessage);
}
} catch (error) {
console.error('Error in moderateText:', error);
logger.error('Error in moderateText:', error);
const errorMessage = 'error in moderation check';
return await denyRequest(req, res, errorMessage);
}

Some files were not shown because too many files have changed in this diff Show More