Compare commits

..

26 Commits

Author SHA1 Message Date
Danny Avila
020995514e v0.7.5-rc2 (#3976)
*  v0.7.5-rc2

* docs: update README

* refactor(settings): Update rememberForkOption default value

* a11y: proper screen reader announcements for content blocks

* Update version to 0.7.423 in package-lock.json and packages/data-provider/package.json

* chore: rename rememberForkOption -> rememberDefaultFork to apply new default value

* fix: headlessui menu stealing focus from Settings Dialog when pressing Enter
2024-09-10 19:00:27 -04:00
Marco Beretta
d6c0121b19 ⌨️ a11y(Settings): Improved Keyboard Navigation & Consistent Styling (#3975)
* feat: settings tba accessible

* refactor: cleanup unused code

* refactor: improve accessibility and user experience in ChatDirection component

* style: focus ring primary class

* improve a11y of avatar dialog

* style: a11y improvements for Settings

* style: focus ring primary class in OriginalDialog component

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-09-10 15:11:39 -04:00
Danny Avila
1a1e6850a3 🪨 fix: Minor AWS Bedrock/Misc. Improvements (#3974)
* refactor(EditMessage): avoid manipulation of native paste handling, leverage react-hook-form for textarea changes

* style: apply better theming for MinimalIcon

* fix(useVoicesQuery/useCustomConfigSpeechQuery): make sure to only try request once per render

* feat: edit message content parts

* fix(useCopyToClipboard): handle both assistants and agents content blocks

* refactor: remove save & submit and update text content correctly

* chore(.env.example/config): exclude unsupported bedrock models

* feat: artifacts for aws bedrock

* fix: export options for bedrock conversations
2024-09-10 12:56:19 -04:00
Danny Avila
341e086d70 🛠️ fix: Completion Edge Cases & Improve Error Handling UX (#3968)
* fix: edge cases concerning completion response as an array

* refactor: improve invalid request error UX
2024-09-09 20:58:15 -04:00
Danny Avila
0148b9b097 🔒 refactor: Apply interface settings to all Roles (#3967) 2024-09-09 20:15:08 -04:00
Danny Avila
748b41eda4 🔒 feat: RBAC for Multi-Convo Feature (#3964)
* fix: remove duplicate keys in German language translations

* wip: multi-convo role permissions

* ci: Update loadDefaultInterface tests due to MULTI_CONVO

* ci: update Role.spec.js with tests for MULTI_CONVO permission type

* fix: Update ContentParts component to handle undefined content array

* feat: render Multi-Convo based on UI permissions
2024-09-09 16:29:24 -04:00
Danny Avila
d59b62174f 🪨 feat: AWS Bedrock support (#3935)
* feat: Add BedrockIcon component to SVG library

* feat: EModelEndpoint.bedrock

* feat: first pass, bedrock chat. note: AgentClient is returning `agents` as conversation.endpoint

* fix: declare endpoint in initialization step

* chore: Update @librechat/agents dependency to version 1.4.5

* feat: backend content aggregation for agents/bedrock

* feat: abort agent requests

* feat: AWS Bedrock icons

* WIP: agent provider schema parsing

* chore: Update EditIcon props type

* refactor(useGenerationsByLatest): make agents and bedrock editable

* refactor: non-assistant message content, parts

* fix: Bedrock response `sender`

* fix: use endpointOption.model_parameters not endpointOption.modelOptions

* fix: types for step handler

* refactor: Update Agents.ToolCallDelta type

* refactor: Remove unnecessary assignment of parentMessageId in AskController

* refactor: remove unnecessary assignment of parentMessageId (agent request handler)

* fix(bedrock/agents): message regeneration

* refactor: dynamic form elements using react-hook-form Controllers

* fix: agent icons/labels for messages

* fix: agent actions

* fix: use of new dynamic tags causing application crash

* refactor: dynamic settings touch-ups

* refactor: update Slider component to allow custom track class name

* refactor: update DynamicSlider component styles

* refactor: use Constants value for GLOBAL_PROJECT_NAME (enum)

* feat: agent share global methods/controllers

* fix: agents query

* fix: `getResponseModel`

* fix: share prompt a11y issue

* refactor: update SharePrompt dialog theme styles

* refactor: explicit typing for SharePrompt

* feat: add agent roles/permissions

* chore: update @librechat/agents dependency to version 1.4.7 for tool_call_ids edge case

* fix(Anthropic): messages.X.content.Y.tool_use.input: Input should be a valid dictionary

* fix: handle text parts with tool_call_ids and empty text

* fix: role initialization

* refactor: don't make instructions required

* refactor: improve typing of Text part

* fix: setShowStopButton for agents route

* chore: remove params for now

* fix: add streamBuffer and streamRate to help prevent 'Overloaded' errors from Anthropic API

* refactor: remove console.log statement in ContentRender component

* chore: typing, rename Context to Delete Button

* chore(DeleteButton): logging

* refactor(Action): make accessible

* style(Action): improve a11y again

* refactor: remove use/mention of mongoose sessions

* feat: first pass, sharing agents

* feat: visual indicator for global agent, remove author when serving to non-author

* wip: params

* chore: fix typing issues

* fix(schemas): typing

* refactor: improve accessibility of ListCard component and fix console React warning

* wip: reset templates for non-legacy new convos

* Revert "wip: params"

This reverts commit f8067e91d4.

* Revert "refactor: dynamic form elements using react-hook-form Controllers"

This reverts commit 2150c4815d.

* fix(Parameters): types and parameter effect update to only update local state to parameters

* refactor: optimize useDebouncedInput hook for better performance

* feat: first pass, anthropic bedrock params

* chore: paramEndpoints check for endpointType too

* fix: maxTokens to use coerceNumber.optional(),

* feat: extra chat model params

* chore: reduce code repetition

* refactor: improve preset title handling in SaveAsPresetDialog component

* refactor: improve preset handling in HeaderOptions component

* chore: improve typing, replace legacy dialog for SaveAsPresetDialog

* feat: save as preset from parameters panel

* fix: multi-search in select dropdown when using Option type

* refactor: update default showDefault value to false in Dynamic components

* feat: Bedrock presets settings

* chore: config, fix agents schema, update config version

* refactor: update AWS region variable name in bedrock options endpoint to BEDROCK_AWS_DEFAULT_REGION

* refactor: update baseEndpointSchema in config.ts to include baseURL property

* refactor: update createRun function to include req parameter and set streamRate based on provider

* feat: availableRegions via config

* refactor: remove unused demo agent controller file

* WIP: title

* Update @librechat/agents to version 1.5.0

* chore: addTitle.js to handle empty responseText

* feat: support images and titles

* feat: context token updates

* Refactor BaseClient test to use expect.objectContaining

* refactor: add model select, remove header options params, move side panel params below prompts

* chore: update models list, catch title error

* feat: model service for bedrock models (env)

* chore: Remove verbose debug log in AgentClient class following stream

* feat(bedrock): track token spend; fix: token rates, value key mapping for AWS models

* refactor: handle streamRate in `handleLLMNewToken` callback

* chore: AWS Bedrock example config in `.env.example`

* refactor: Rename bedrockMeta to bedrockGeneral in settings.ts and use for AI21 and Amazon Bedrock providers

* refactor: Update `.env.example` with AWS Bedrock model IDs URL and additional notes

* feat: titleModel support for bedrock

* refactor: Update `.env.example` with additional notes for AWS Bedrock model IDs
2024-09-09 12:06:59 -04:00
Raí Santos
8c14360263 🌍 i18n: Improved Portuguese language translations (#3947)
Co-authored-by: RaiSantos <itzraisu@gmail.com>
2024-09-07 08:18:56 -04:00
Marlon
d0dc858e2d 🌍 i18n: Improved German language translations (#3924) 2024-09-05 21:06:20 -04:00
hide361
9cf390c657 🌍 i18n: Update Japanese translation (#3877) 2024-09-05 21:06:01 -04:00
Hervey
14199d5521 🌍 i18n: Updated Chinese Translation (#3871)
* 🌍 : Updated Chinese Translation

* 🌍 : Updated Chinese Translation
2024-09-05 21:05:40 -04:00
Vesna Tan
b9197f90c6 👐 a11y: Misc. Improvements (#3910)
* fix focus for cancel button in convo delete modal window #3829

* add aria-hidden and aria-label to X and Check svg/button respectively and updated OGDialogClose focus color

* update rename, newchat, newchat icon, ConvoOptions icon
2024-09-05 14:30:17 -04:00
Danny Avila
9ec665dd2c 🪟 fix: Windows Build (npm) (#3889)
* chore: package-lock.json

* chore: remove shadcn files (temp)

* refactor: language comparisons script

* fix: resolve package-lock file for windows compatibility

* chore: Enable Windows unit tests for frontend

* refactor: move shadcn components to data-provider
2024-09-02 10:01:09 -04:00
Danny Avila
136599081c 🧩 fix: plugins build options, prevent undefined tools error (#3876) 2024-09-01 08:35:05 -04:00
Danny Avila
a0291ed155 🚧 chore: merge latest dev build to main repo (#3844)
* agents - phase 1 (#30)

* chore: copy assistant files

* feat: frontend and data-provider

* feat: backend get endpoint test

* fix(MessageEndpointIcon): switched to AgentName and AgentAvatar

* fix: small fixes

* fix: agent endpoint config

* fix: show Agent Builder

* chore: install agentus

* chore: initial scaffolding for agents

* fix: updated Assistant logic to Agent Logic for some Agent components

* WIP first pass, demo of agent package

* WIP: initial backend infra for agents

* fix: agent list error

* wip: agents routing

* chore: Refactor useSSE hook to handle different data events

* wip: correctly emit events

* chore: Update @librechat/agentus npm dependency to version 1.0.9

* remove comment

* first pass: streaming agent text

* chore: Remove @librechat/agentus root-level workspace npm dependency

* feat: Agent Schema and Model

* fix: content handling fixes

* fix: content message save

* WIP: new content data

* fix: run step issue with tool calls

* chore: Update @librechat/agentus npm dependency to version 1.1.5

* feat: update controller and agent routes

* wip: initial backend tool and tool error handling support

* wip: tool chunks

* chore: Update @librechat/agentus npm dependency to version 1.1.7

* chore: update tool_call typing, add test conditions and logs

* fix: create agent

* fix: create agent

* first pass: render completed content parts

* fix: remove logging, fix step handler typing

* chore: Update @librechat/agentus npm dependency to version 1.1.9

* refactor: cleanup maps on unmount

* chore: Update BaseClient.js to safely count tokens for string, number, and boolean values

* fix: support subsequent messages with tool_calls

* chore: export order

* fix: select agent

* fix: tool call types and handling

* chore: switch to anthropic for testing

* fix: AgentSelect

* refactor: experimental: OpenAIClient to use array for intermediateReply

* fix(useSSE): revert old condition for streaming legacy client tokens

* fix: lint

* revert `agent_id` to `id`

* chore: update localization keys for agent-related components

* feat: zod schema handling for actions

* refactor(actions): if no params, no zodSchema

* chore: Update @librechat/agentus npm dependency to version 1.2.1

* feat: first pass, actions

* refactor: empty schema for actions without params

* feat: Update createRun function to accept additional options

* fix: message payload formatting; feat: add more client options

* fix: ToolCall component rendering when action has no args but has output

* refactor(ToolCall): allow non-stringy args

* WIP: first pass, correctly formatted tool_calls between providers

* refactor: Remove duplicate import of 'roles' module

* refactor: Exclude 'vite.config.ts' from TypeScript compilation

* refactor: fix agent related types
> - no need to use endpoint/model fields for identifying agent metadata
> - add `provider` distinction for agent-configured 'endpoint'
- no need for agent-endpoint map
- reduce complexity of tools as functions into tools as string[]
- fix types related to above changes
- reduce unnecessary variables for queries/mutations and corresponding react-query keys

* refactor: Add tools and tool_kwargs fields to agent schema

* refactor: Remove unused code and update dependencies

* refactor: Update updateAgentHandler to use req.body directly

* refactor: Update AgentSelect component to use localized hooks

* refactor: Update agent schema to include tools and provider fields

* refactor(AgentPanel): add scrollbar gutter, add provider field to form, fix agent schema required values

* refactor: Update AgentSwitcher component to use selectedAgentId instead of selectedAgent

* refactor: Update AgentPanel component to include alternateName import and defaultAgentFormValues

* refactor(SelectDropDown): allow setting value as option while still supporting legacy usage (string values only)

* refactor: SelectDropdown changes - Only necessary when the available values are objects with label/value fields and the selected value is expected to be a string.

* refactor: TypeError issues and handle provider as option

* feat: Add placeholder for provider selection in AgentPanel component

* refactor: Update agent schema to include author and provider fields

* fix: show expected 'create agent' placeholder when creating agent

* chore: fix localization strings, hide capabilities form for now

* chore: typing

* refactor: import order and use compact agents schema for now

* chore: typing

* refactor: Update AgentForm type to use AgentCapabilities

* fix agent form agent selection issues

* feat: responsive agent selection

* fix: Handle cancelled fetch in useSelectAgent hook

* fix: reset agent form on accordion close/open

* feat: Add agent_id to default conversation for agents endpoint

* feat: agents endpoint request handling

* refactor: reset conversation model on agent select

* refactor: add `additional_instructions` to conversation schema, organize other fields

* chore: casing

* chore: types

* refactor(loadAgentTools): explicitly pass agent_id, do not pass `model` to loadAgentTools for now, load action sets by agent_id

* WIP: initial draft of real agent client initialization

* WIP: first pass, anthropic agent requests

* feat: remember last selected agent

* feat: openai and azure connected

* fix: prioritize agent model for runs unless an explicit override model is passed from client

* feat: Agent Actions

* fix: save agent id to convo

* feat: model panel (#29)

* feat: model panel

* bring back comments

* fix: method still null

* fix: AgentPanel FormContext

* feat: add more parameters

* fix: style issues; refactor: Agent Controller

* fix: cherry-pick

* fix: Update AgentAvatar component to use AssistantIcon instead of BrainCircuit

* feat: OGDialog for delete agent; feat(assistant): update Agent types, introduced `model_parameters`

* feat: icon and general `model_parameters` update

* feat: use react-hook-form better

* fix: agent builder form reset issue when switching panels

* refactor: modularize agent builder form

---------

Co-authored-by: Danny Avila <danny@librechat.ai>

* fix: AgentPanel and ModelPanel type issues and use `useFormContext` and `watch` instead of `methods` directly and `useWatch`.

* fix: tool call issues due to invalid input (anthropic) of empty string

* fix: handle empty text in Part component

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>

* refactor: remove form ModelPanel and fixed nested ternary expressions in AgentConfig

* fix: Model Parameters not saved correctly

* refactor: remove console log

* feat: avatar upload and get for Agents (#36)

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>

* chore: update to public package

* fix: typing, optional chaining

* fix: cursor not showing for content parts

* chore: conditionally enable agents

* ci: fix azure test

* ci: fix frontend tests, fix eslint api

* refactor: Remove unused errorContentPart variable

* continue of the agent message PR (#40)

* last fixes

* fix: agentMap

* pr merge test  (#41)

* fix: model icon not fetching correctly

* remove console logs

* feat: agent name

* refactor: pass documentsMap as a prop to allow re-render of assistant form

* refactor: pass documentsMap as a prop to allow re-render of assistant form

* chore: Bump version to 0.7.419

* fix: TypeError: Cannot read properties of undefined (reading 'id')

* refactor: update AgentSwitcher component to use ControlCombobox instead of Combobox

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2024-08-31 16:33:51 -04:00
Max Sanna
618be4bf2b ⚖️ feat: Terms and Conditions Dialog (#3712)
* Added UI for Terms and Conditions Modal Dialogue

* Handled the logout on not accepting

* Added logic for terms acceptance

* Add terms and conditions modal

* Fixed bug on terms and conditions modal, clicking out of it won't close it now

* Added acceptance of Terms to Database

* Removed unnecessary api endpoints from index.js

* Added NPM script to reset terms acceptance

* Added translations, markdown terms and samples

* Merged terms and conditions modal feature

* feat/Modal Terms and Conditions Dialog

* Amendments as requested by maintainers

* Reset package-lock (again)
2024-08-31 16:08:04 -04:00
Marco Beretta
79f9cd5a4d 💬 feat: assistant conversation starter (#3699)
* feat: initial UI convoStart

* fix: ConvoStarter UI

* fix: convoStarters bug

* feat: Add input field focus on conversation starters

* style: conversation starter UI update

* feat: apply fixes for starters

* style: update conversationStarters UI and fixed typo

* general UI update

* feat: Add onClick functionality to ConvoStarter component

* fix: quick fix test

* fix(AssistantSelect): remove object check

* fix: updateAssistant `conversation_starters` var

* chore: remove starter autofocus

* fix: no empty conversation starters, always show input, use Constants value for max count

* style: Update defaultTextPropsLabel styles, for a11y placeholder

* refactor: Update ConvoStarter component styles and class names for a11y and theme

* refactor: convostarter, move plus button to within persistent element

* fix: types

* chore: Update landing page assistant description styling with theming

* chore: assistant types

* refactor: documents routes

* refactor: optimize conversation starter mutations/queries

* refactor: Update listAllAssistants return type to Promise<Array<Assistant>>

* feat: edit existing starters

* feat(convo-starters): enhance ConvoStarter component and add animations

    - Update ConvoStarter component styling for better visual appeal
    - Implement fade-in animation for smoother appearance
    - Add hover effect with background color change
    - Improve text overflow handling with line-clamp and text-balance
    - Ensure responsive design for various screen sizes

* feat(assistant): add conversation starters to assistant builder

- Add localization strings for conversation starters
- Update mobile.css with shake animation for max starters reached
- Enhance user experience with tooltips and dynamic input handling

* refactor: select specific fields for assistant documents fetch

* refactor: remove endpoint query key, fetch all assistant docs for now, add conversation_starters to v1 methods

* refactor: add document filters based on endpoint config

* fix: starters not applied during creation

* refactor: update AssistantSelect component to handle undefined lastSelectedModels

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-08-31 13:42:20 -04:00
Max Sanna
63b80c3067 🗣️ fix: Azure OpenAI STT (#3731)
* Fix for Azure OpenAI STT

* chore(STTService): imports order

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2024-08-30 15:11:15 -04:00
Danny Avila
7536e649d4 🚨 feat: Implement INPUT_LENGTH Error Type (#3866)
* feat: CONTEXT_LENGTH error type

* chore: rename error type

* chore: import order
2024-08-30 15:01:29 -04:00
Danny Avila
6936d0059f 🎨 refactor: Prevent Font Asset Hashing in Vite Config (#3865) 2024-08-30 13:56:49 -04:00
Danny Avila
0a359aa705 👐 a11y: Accessible Conversation Menu Options (#3864)
* fix: type issues

* feat: Fix document title setting in Conversation component

* style: new chat theme

* fix: No keyboard access to chat menus in the chat history #3788

* fix: Menu button in the chat history area does not indicate its state #3823

* refactor: use ariakit for DropdownPopup

* style: update sticky z-index in NewChat component

* style: update ConvoOptions menu button styling
2024-08-30 13:39:30 -04:00
Jacob Colyvan
2ce4f66218 🎙️ a11y: update html lang attribute (#3636)
* refactor: remove duplicate localStorage lang call

* refactor: use cookies to handle langcode

* feat: override index.html lang w/ cookie pref

* refactor: only read index on server start

* refactor: rename lang cookie & localstorage as backup

* refactor: use atomWithLocalStorage in language store

* fix: forced reflow warning in language select
2024-08-30 06:57:29 -04:00
Danny Avila
a0042317b2 📝 docs: Update README.md 2024-08-30 06:46:46 -04:00
Danny Avila
dc40e577af 🔊 refactor: Optimize Aria-Live Announcements for macOS VoiceOver (#3851) 2024-08-30 00:14:37 -04:00
Danny Avila
757b6d3275 🔨 refactor: Add Cache Busting to index.html (#3824) 2024-08-28 13:52:14 -04:00
Danny Avila
3b61322459 🚀 feat: Enhance PWA and asset caching strategy (#3822)
* chore: Update VitePWA registerType to 'autoUpdate', add cache busting to static file outputs

* chore: disable windows frontend workflow for now
2024-08-28 12:13:14 -04:00
336 changed files with 22599 additions and 11737 deletions

View File

@@ -111,6 +111,26 @@ ANTHROPIC_API_KEY=user_provided
BINGAI_TOKEN=user_provided
# BINGAI_HOST=https://cn.bing.com
#=================#
# AWS Bedrock #
#=================#
# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided
# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
# Notes on specific models:
# The following models are not support due to not supporting streaming:
# ai21.j2-mid-v1
# The following models are not support due to not supporting conversation history:
# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14
#============#
# Google #
#============#

View File

@@ -53,4 +53,4 @@ jobs:
- name: Run unit tests
run: npm run test:ci --verbose
working-directory: client
working-directory: client

View File

@@ -1,4 +1,4 @@
# v0.7.5-rc1
# v0.7.5-rc2
# Base node image
FROM node:20-alpine AS node

View File

@@ -1,5 +1,5 @@
# Dockerfile.multi
# v0.7.5-rc1
# v0.7.5-rc2
# Base for all builds
FROM node:20-alpine AS base

View File

@@ -42,9 +42,11 @@
- 🖥️ 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)
- Anthropic (Claude), AWS Bedrock, OpenAI, Azure OpenAI, BingAI, ChatGPT, Google Vertex AI, Plugins, Assistants API (including Azure Assistants)
- ✅ Compatible across both **[Remote & Local AI services](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints):**
- groq, Ollama, Cohere, Mistral AI, Apple MLX, koboldcpp, OpenRouter, together.ai, Perplexity, ShuttleAI, and more
- 🪄 Generative UI with **[Code Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3)**
- Create React, HTML code, and Mermaid diagrams right in chat
- 💾 Create, Save, & Share Custom Presets
- 🔀 Switch between AI Endpoints and Presets, mid-chat
- 🔄 Edit, Resubmit, and Continue Messages with Conversation branching

View File

@@ -1,6 +1,14 @@
const crypto = require('crypto');
const fetch = require('node-fetch');
const { supportsBalanceCheck, Constants, CacheKeys, Time } = require('librechat-data-provider');
const {
supportsBalanceCheck,
isAgentsEndpoint,
paramEndpoints,
ErrorTypes,
Constants,
CacheKeys,
Time,
} = require('librechat-data-provider');
const { getMessages, saveMessage, updateMessage, saveConvo } = require('~/models');
const { addSpaceIfNeeded, isEnabled } = require('~/server/utils');
const checkBalance = require('~/models/checkBalance');
@@ -28,6 +36,12 @@ class BaseClient {
this.userMessagePromise;
/** @type {ClientDatabaseSavePromise} */
this.responsePromise;
/** @type {string} */
this.user;
/** @type {string} */
this.conversationId;
/** @type {string} */
this.responseMessageId;
}
setOptions() {
@@ -54,6 +68,17 @@ class BaseClient {
throw new Error('Subclasses attempted to call summarizeMessages without implementing it');
}
/**
* @returns {string}
*/
getResponseModel() {
if (isAgentsEndpoint(this.options.endpoint) && this.options.agent && this.options.agent.id) {
return this.options.agent.id;
}
return this.modelOptions.model;
}
/**
* Abstract method to get the token count for a message. Subclasses must implement this method.
* @param {TMessage} responseMessage
@@ -155,6 +180,8 @@ class BaseClient {
this.currentMessages[this.currentMessages.length - 1].messageId = head;
}
this.responseMessageId = responseMessageId;
return {
...opts,
user,
@@ -203,6 +230,7 @@ class BaseClient {
userMessage,
conversationId,
responseMessageId,
sender: this.sender,
});
}
@@ -341,7 +369,12 @@ class BaseClient {
};
}
async handleContextStrategy({ instructions, orderedMessages, formattedMessages }) {
async handleContextStrategy({
instructions,
orderedMessages,
formattedMessages,
buildTokenMap = true,
}) {
let _instructions;
let tokenCount;
@@ -383,9 +416,10 @@ class BaseClient {
const latestMessage = orderedWithInstructions[orderedWithInstructions.length - 1];
if (payload.length === 0 && !shouldSummarize && latestMessage) {
throw new Error(
`Prompt token count of ${latestMessage.tokenCount} exceeds max token count of ${this.maxContextTokens}.`,
);
const info = `${latestMessage.tokenCount} / ${this.maxContextTokens}`;
const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`;
logger.warn(`Prompt token count exceeds max token count (${info}).`);
throw new Error(errorMessage);
}
if (usePrevSummary) {
@@ -410,19 +444,23 @@ class BaseClient {
maxContextTokens: this.maxContextTokens,
});
let tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
const { messageId } = message;
if (!messageId) {
/** @type {Record<string, number> | undefined} */
let tokenCountMap;
if (buildTokenMap) {
tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
const { messageId } = message;
if (!messageId) {
return map;
}
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
}
map[messageId] = orderedWithInstructions[index].tokenCount;
return map;
}
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
}
map[messageId] = orderedWithInstructions[index].tokenCount;
return map;
}, {});
}, {});
}
const promptTokens = this.maxContextTokens - remainingContextTokens;
@@ -524,6 +562,7 @@ class BaseClient {
});
}
/** @type {string|string[]|undefined} */
const completion = await this.sendCompletion(payload, opts);
this.abortController.requestCompleted = true;
@@ -533,15 +572,23 @@ class BaseClient {
parentMessageId: userMessage.messageId,
isCreatedByUser: false,
isEdited,
model: this.modelOptions.model,
model: this.getResponseModel(),
sender: this.sender,
text: addSpaceIfNeeded(generation) + completion,
promptTokens,
iconURL: this.options.iconURL,
endpoint: this.options.endpoint,
...(this.metadata ?? {}),
};
if (typeof completion === 'string') {
responseMessage.text = addSpaceIfNeeded(generation) + completion;
} else if (Array.isArray(completion) && paramEndpoints.has(this.options.endpoint)) {
responseMessage.text = '';
responseMessage.content = completion;
} else if (Array.isArray(completion)) {
responseMessage.text = addSpaceIfNeeded(generation) + completion.join('');
}
if (
tokenCountMap &&
this.recordTokenUsage &&
@@ -861,8 +908,12 @@ class BaseClient {
processValue(nestedValue);
}
} else {
} else if (typeof value === 'string') {
numTokens += this.getTokenCount(value);
} else if (typeof value === 'number') {
numTokens += this.getTokenCount(value.toString());
} else if (typeof value === 'boolean') {
numTokens += this.getTokenCount(value.toString());
}
};

View File

@@ -1023,7 +1023,7 @@ ${convo}
async chatCompletion({ payload, onProgress, abortController = null }) {
let error = null;
const errorCallback = (err) => (error = err);
let intermediateReply = '';
const intermediateReply = [];
try {
if (!abortController) {
abortController = new AbortController();
@@ -1217,19 +1217,19 @@ ${convo}
}
if (typeof finalMessage.content !== 'string' || finalMessage.content.trim() === '') {
finalChatCompletion.choices[0].message.content = intermediateReply;
finalChatCompletion.choices[0].message.content = intermediateReply.join('');
}
})
.on('finalMessage', (message) => {
if (message?.role !== 'assistant') {
stream.messages.push({ role: 'assistant', content: intermediateReply });
stream.messages.push({ role: 'assistant', content: intermediateReply.join('') });
UnexpectedRoleError = true;
}
});
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content || '';
intermediateReply += token;
intermediateReply.push(token);
onProgress(token);
if (abortController.signal.aborted) {
stream.controller.abort();
@@ -1271,7 +1271,7 @@ ${convo}
const { choices } = chatCompletion;
if (!Array.isArray(choices) || choices.length === 0) {
logger.warn('[OpenAIClient] Chat completion response has no choices');
return intermediateReply;
return intermediateReply.join('');
}
const { message, finish_reason } = choices[0] ?? {};
@@ -1281,15 +1281,16 @@ ${convo}
if (!message) {
logger.warn('[OpenAIClient] Message is undefined in chatCompletion response');
return intermediateReply;
return intermediateReply.join('');
}
if (typeof message.content !== 'string' || message.content.trim() === '') {
const reply = intermediateReply.join('');
logger.debug(
'[OpenAIClient] chatCompletion: using intermediateReply due to empty message.content',
{ intermediateReply },
{ intermediateReply: reply },
);
return intermediateReply;
return reply;
}
return message.content;
@@ -1298,7 +1299,7 @@ ${convo}
err?.message?.includes('abort') ||
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
) {
return intermediateReply;
return intermediateReply.join('');
}
if (
err?.message?.includes(
@@ -1313,10 +1314,10 @@ ${convo}
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
) {
logger.error('[OpenAIClient] Known OpenAI error:', err);
return intermediateReply;
return intermediateReply.join('');
} else if (err instanceof OpenAI.APIError) {
if (intermediateReply) {
return intermediateReply;
if (intermediateReply.length > 0) {
return intermediateReply.join('');
} else {
throw err;
}

View File

@@ -8,7 +8,7 @@ const { isEnabled } = require('~/server/utils');
* @param {Object} options - The options for creating the LLM.
* @param {ModelOptions} options.modelOptions - The options specific to the model, including modelName, temperature, presence_penalty, frequency_penalty, and other model-related settings.
* @param {ConfigOptions} options.configOptions - Configuration options for the API requests, including proxy settings and custom headers.
* @param {Callbacks} options.callbacks - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
* @param {Callbacks} [options.callbacks] - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
* @param {boolean} [options.streaming=false] - Determines if the LLM should operate in streaming mode.
* @param {string} options.openAIApiKey - The API key for OpenAI, used for authentication.
* @param {AzureOptions} [options.azure={}] - Optional Azure-specific configurations. If provided, Azure configurations take precedence over OpenAI configurations.

View File

@@ -1,4 +1,5 @@
const { EModelEndpoint } = require('librechat-data-provider');
const { ToolMessage } = require('@langchain/core/messages');
const { EModelEndpoint, ContentTypes } = require('librechat-data-provider');
const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
/**
@@ -14,11 +15,11 @@ const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
*/
const formatVisionMessage = ({ message, image_urls, endpoint }) => {
if (endpoint === EModelEndpoint.anthropic) {
message.content = [...image_urls, { type: 'text', text: message.content }];
message.content = [...image_urls, { type: ContentTypes.TEXT, text: message.content }];
return message;
}
message.content = [{ type: 'text', text: message.content }, ...image_urls];
message.content = [{ type: ContentTypes.TEXT, text: message.content }, ...image_urls];
return message;
};
@@ -51,7 +52,7 @@ const formatMessage = ({ message, userName, assistantName, endpoint, langChain =
_role = roleMapping[lc_id[2]];
}
const role = _role ?? (sender && sender?.toLowerCase() === 'user' ? 'user' : 'assistant');
const content = text ?? _content ?? '';
const content = _content ?? text ?? '';
const formattedMessage = {
role,
content,
@@ -131,4 +132,79 @@ const formatFromLangChain = (message) => {
};
};
module.exports = { formatMessage, formatLangChainMessages, formatFromLangChain };
/**
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
*
* @param {Array<Partial<TMessage>>} payload - The array of messages to format.
* @returns {Array<(HumanMessage|AIMessage|SystemMessage|ToolMessage)>} - The array of formatted LangChain messages, including ToolMessages for tool calls.
*/
const formatAgentMessages = (payload) => {
const messages = [];
for (const message of payload) {
if (message.role !== 'assistant') {
messages.push(formatMessage({ message, langChain: true }));
continue;
}
let currentContent = [];
let lastAIMessage = null;
for (const part of message.content) {
if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
// If there's pending content, add it as an AIMessage
if (currentContent.length > 0) {
messages.push(new AIMessage({ content: currentContent }));
currentContent = [];
}
// Create a new AIMessage with this text and prepare for tool calls
lastAIMessage = new AIMessage({
content: part.text || '',
});
messages.push(lastAIMessage);
} else if (part.type === ContentTypes.TOOL_CALL) {
if (!lastAIMessage) {
throw new Error('Invalid tool call structure: No preceding AIMessage with tool_call_ids');
}
// Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
const { output, args: _args, ...tool_call } = part.tool_call;
// TODO: investigate; args as dictionary may need to be provider-or-tool-specific
let args = _args;
try {
args = JSON.parse(args);
} catch (e) {
// failed to parse, leave as is
}
tool_call.args = args;
lastAIMessage.tool_calls.push(tool_call);
// Add the corresponding ToolMessage
messages.push(
new ToolMessage({
tool_call_id: tool_call.id,
name: tool_call.name,
content: output,
}),
);
} else {
currentContent.push(part);
}
}
if (currentContent.length > 0) {
messages.push(new AIMessage({ content: currentContent }));
}
}
return messages;
};
module.exports = {
formatMessage,
formatFromLangChain,
formatAgentMessages,
formatLangChainMessages,
};

View File

@@ -565,11 +565,13 @@ describe('BaseClient', () => {
const getReqData = jest.fn();
const opts = { getReqData };
const response = await TestClient.sendMessage('Hello, world!', opts);
expect(getReqData).toHaveBeenCalledWith({
userMessage: expect.objectContaining({ text: 'Hello, world!' }),
conversationId: response.conversationId,
responseMessageId: response.messageId,
});
expect(getReqData).toHaveBeenCalledWith(
expect.objectContaining({
userMessage: expect.objectContaining({ text: 'Hello, world!' }),
conversationId: response.conversationId,
responseMessageId: response.messageId,
}),
);
});
test('onStart is called with the correct arguments', async () => {

View File

@@ -0,0 +1,78 @@
const { z } = require('zod');
const { tool } = require('@langchain/core/tools');
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
function createTavilySearchTool(fields = {}) {
const envVar = 'TAVILY_API_KEY';
const override = fields.override ?? false;
const apiKey = fields.apiKey ?? getApiKey(envVar, override);
const kwargs = fields?.kwargs ?? {};
function getApiKey(envVar, override) {
const key = getEnvironmentVariable(envVar);
if (!key && !override) {
throw new Error(`Missing ${envVar} environment variable.`);
}
return key;
}
return tool(
async (input) => {
const { query, ...rest } = input;
const requestBody = {
api_key: apiKey,
query,
...rest,
...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);
},
{
name: 'tavily_search_results_json',
description:
'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.',
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.'),
}),
},
);
}
module.exports = createTavilySearchTool;

View File

@@ -5,17 +5,16 @@ 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.
* or create a new action if it doesn't exist.
*
* @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.
* @returns {Promise<Action>} 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 };
const updateAction = async (searchParams, updateData) => {
const options = { new: true, upsert: true };
return await Action.findOneAndUpdate(searchParams, updateData, options).lean();
};
@@ -24,7 +23,7 @@ const updateAction = async (searchParams, updateData, session = null) => {
*
* @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.
* @returns {Promise<Array<Action>>} 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();
@@ -49,31 +48,27 @@ const getActions = async (searchParams, includeSensitive = false) => {
};
/**
* Deletes an action by params, within a transaction session if provided.
* Deletes an action by params.
*
* @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.
* @returns {Promise<Action>} 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();
const deleteAction = async (searchParams) => {
return await Action.findOneAndDelete(searchParams).lean();
};
/**
* Deletes actions by params, within a transaction session if provided.
* Deletes actions by params.
*
* @param {Object} searchParams - The search parameters to find the actions to delete.
* @param {string} searchParams.action_id - The ID of the action(s) to delete.
* @param {string} searchParams.user - The user ID of the action's author.
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
* @returns {Promise<Number>} A promise that resolves to the number of deleted action documents.
*/
const deleteActions = async (searchParams, session = null) => {
const options = session ? { session } : {};
const result = await Action.deleteMany(searchParams, options);
const deleteActions = async (searchParams) => {
const result = await Action.deleteMany(searchParams);
return result.deletedCount;
};

142
api/models/Agent.js Normal file
View File

@@ -0,0 +1,142 @@
const mongoose = require('mongoose');
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
const {
getProjectByName,
addAgentIdsToProject,
removeAgentIdsFromProject,
removeAgentFromAllProjects,
} = require('./Project');
const agentSchema = require('./schema/agent');
const Agent = mongoose.model('agent', agentSchema);
/**
* Create an agent with the provided data.
* @param {Object} agentData - The agent data to create.
* @returns {Promise<Agent>} The created agent document as a plain object.
* @throws {Error} If the agent creation fails.
*/
const createAgent = async (agentData) => {
return await Agent.create(agentData);
};
/**
* Get an agent document based on the provided ID.
*
* @param {Object} searchParameter - The search parameters to find the agent to update.
* @param {string} searchParameter.id - The ID of the agent to update.
* @param {string} searchParameter.author - The user ID of the agent's author.
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/
const getAgent = async (searchParameter) => await Agent.findOne(searchParameter).lean();
/**
* Update an agent with new data without overwriting existing
* properties, or create a new agent if it doesn't exist.
*
* @param {Object} searchParameter - The search parameters to find the agent to update.
* @param {string} searchParameter.id - The ID of the agent to update.
* @param {string} [searchParameter.author] - The user ID of the agent's author.
* @param {Object} updateData - An object containing the properties to update.
* @returns {Promise<Agent>} The updated or newly created agent document as a plain object.
*/
const updateAgent = async (searchParameter, updateData) => {
const options = { new: true, upsert: true };
return await Agent.findOneAndUpdate(searchParameter, updateData, options).lean();
};
/**
* Deletes an agent based on the provided ID.
*
* @param {Object} searchParameter - The search parameters to find the agent to delete.
* @param {string} searchParameter.id - The ID of the agent to delete.
* @param {string} [searchParameter.author] - The user ID of the agent's author.
* @returns {Promise<void>} Resolves when the agent has been successfully deleted.
*/
const deleteAgent = async (searchParameter) => {
const agent = await Agent.findOneAndDelete(searchParameter);
if (agent) {
await removeAgentFromAllProjects(agent.id);
}
return agent;
};
/**
* Get all agents.
* @param {Object} searchParameter - The search parameters to find matching agents.
* @param {string} searchParameter.author - The user ID of the agent's author.
* @returns {Promise<Object>} A promise that resolves to an object containing the agents data and pagination info.
*/
const getListAgents = async (searchParameter) => {
const { author, ...otherParams } = searchParameter;
let query = Object.assign({ author }, otherParams);
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
if (globalProject && (globalProject.agentIds?.length ?? 0) > 0) {
const globalQuery = { id: { $in: globalProject.agentIds }, ...otherParams };
delete globalQuery.author;
query = { $or: [globalQuery, query] };
}
const agents = await Agent.find(query, {
id: 1,
name: 1,
avatar: 1,
projectIds: 1,
}).lean();
const hasMore = agents.length > 0;
const firstId = agents.length > 0 ? agents[0].id : null;
const lastId = agents.length > 0 ? agents[agents.length - 1].id : null;
return {
data: agents,
has_more: hasMore,
first_id: firstId,
last_id: lastId,
};
};
/**
* Updates the projects associated with an agent, adding and removing project IDs as specified.
* This function also updates the corresponding projects to include or exclude the agent ID.
*
* @param {string} agentId - The ID of the agent to update.
* @param {string[]} [projectIds] - Array of project IDs to add to the agent.
* @param {string[]} [removeProjectIds] - Array of project IDs to remove from the agent.
* @returns {Promise<MongoAgent>} The updated agent document.
* @throws {Error} If there's an error updating the agent or projects.
*/
const updateAgentProjects = async (agentId, projectIds, removeProjectIds) => {
const updateOps = {};
if (removeProjectIds && removeProjectIds.length > 0) {
for (const projectId of removeProjectIds) {
await removeAgentIdsFromProject(projectId, [agentId]);
}
updateOps.$pull = { projectIds: { $in: removeProjectIds } };
}
if (projectIds && projectIds.length > 0) {
for (const projectId of projectIds) {
await addAgentIdsToProject(projectId, [agentId]);
}
updateOps.$addToSet = { projectIds: { $each: projectIds } };
}
if (Object.keys(updateOps).length === 0) {
return await getAgent({ id: agentId });
}
return await updateAgent({ id: agentId }, updateOps);
};
module.exports = {
createAgent,
getAgent,
updateAgent,
deleteAgent,
getListAgents,
updateAgentProjects,
};

View File

@@ -5,17 +5,16 @@ 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.
* or create a new assistant if it doesn't exist.
*
* @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.
* @returns {Promise<AssistantDocument>} The updated or newly created assistant document as a plain object.
*/
const updateAssistantDoc = async (searchParams, updateData, session = null) => {
const options = { new: true, upsert: true, session };
const updateAssistantDoc = async (searchParams, updateData) => {
const options = { new: true, upsert: true };
return await Assistant.findOneAndUpdate(searchParams, updateData, options).lean();
};
@@ -25,7 +24,7 @@ const updateAssistantDoc = async (searchParams, updateData, session = null) => {
* @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.
* @returns {Promise<AssistantDocument|null>} The assistant document as a plain object, or null if not found.
*/
const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean();
@@ -33,10 +32,17 @@ const getAssistant = async (searchParams) => await Assistant.findOne(searchParam
* 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.
* @param {Object} [select] - Optional. Specifies which document fields to include or exclude.
* @returns {Promise<Array<AssistantDocument>>} A promise that resolves to an array of assistant documents as plain objects.
*/
const getAssistants = async (searchParams) => {
return await Assistant.find(searchParams).lean();
const getAssistants = async (searchParams, select = null) => {
let query = Assistant.find(searchParams);
if (select) {
query = query.select(select);
}
return await query.lean();
};
/**

View File

@@ -35,82 +35,34 @@ const idSchema = z.string().uuid();
* @throws {Error} If there is an error in saving the message.
*/
async function saveMessage(req, params, metadata) {
if (!req?.user?.id) {
throw new Error('User not authenticated');
}
const validConvoId = idSchema.safeParse(params.conversationId);
if (!validConvoId.success) {
logger.warn(`Invalid conversation ID: ${params.conversationId}`);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
logger.info(`---Invalid conversation ID Params: ${JSON.stringify(params, null, 2)}`);
return;
}
try {
if (!req || !req.user || !req.user.id) {
throw new Error('User not authenticated');
}
const {
text,
error,
model,
files,
plugin,
sender,
plugins,
iconURL,
endpoint,
isEdited,
messageId,
unfinished,
tokenCount,
newMessageId,
finish_reason,
conversationId,
parentMessageId,
isCreatedByUser,
} = params;
const validConvoId = idSchema.safeParse(conversationId);
if (!validConvoId.success) {
logger.warn(`Invalid conversation ID: ${conversationId}`);
if (metadata && metadata?.context) {
logger.info(`---\`saveMessage\` context: ${metadata.context}`);
}
logger.info(`---Invalid conversation ID Params:
${JSON.stringify(params, null, 2)}
`);
return;
}
const update = {
...params,
user: req.user.id,
iconURL,
endpoint,
messageId: newMessageId || messageId,
conversationId,
parentMessageId,
sender,
text,
isCreatedByUser,
isEdited,
finish_reason,
error,
unfinished,
tokenCount,
plugin,
plugins,
model,
messageId: params.newMessageId || params.messageId,
};
if (files) {
update.files = files;
}
const message = await Message.findOneAndUpdate({ messageId, user: req.user.id }, update, {
upsert: true,
new: true,
});
const message = await Message.findOneAndUpdate(
{ messageId: params.messageId, user: req.user.id },
update,
{ upsert: true, new: true },
);
return message.toObject();
} catch (err) {
logger.error('Error saving message:', err);
if (metadata && metadata?.context) {
logger.info(`---\`saveMessage\` context: ${metadata.context}`);
}
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
throw err;
}
}

View File

@@ -1,4 +1,5 @@
const { model } = require('mongoose');
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
const projectSchema = require('~/models/schema/projectSchema');
const Project = model('Project', projectSchema);
@@ -33,7 +34,7 @@ const getProjectByName = async function (projectName, fieldsToSelect = null) {
const update = { $setOnInsert: { name: projectName } };
const options = {
new: true,
upsert: projectName === 'instance',
upsert: projectName === GLOBAL_PROJECT_NAME,
lean: true,
select: fieldsToSelect,
};
@@ -81,10 +82,55 @@ const removeGroupFromAllProjects = async (promptGroupId) => {
await Project.updateMany({}, { $pull: { promptGroupIds: promptGroupId } });
};
/**
* Add an array of agent IDs to a project's agentIds array, ensuring uniqueness.
*
* @param {string} projectId - The ID of the project to update.
* @param {string[]} agentIds - The array of agent IDs to add to the project.
* @returns {Promise<MongoProject>} The updated project document.
*/
const addAgentIdsToProject = async function (projectId, agentIds) {
return await Project.findByIdAndUpdate(
projectId,
{ $addToSet: { agentIds: { $each: agentIds } } },
{ new: true },
);
};
/**
* Remove an array of agent IDs from a project's agentIds array.
*
* @param {string} projectId - The ID of the project to update.
* @param {string[]} agentIds - The array of agent IDs to remove from the project.
* @returns {Promise<MongoProject>} The updated project document.
*/
const removeAgentIdsFromProject = async function (projectId, agentIds) {
return await Project.findByIdAndUpdate(
projectId,
{ $pull: { agentIds: { $in: agentIds } } },
{ new: true },
);
};
/**
* Remove an agent ID from all projects.
*
* @param {string} agentId - The ID of the agent to remove from projects.
* @returns {Promise<void>}
*/
const removeAgentFromAllProjects = async (agentId) => {
await Project.updateMany({}, { $pull: { agentIds: agentId } });
};
module.exports = {
getProjectById,
getProjectByName,
/* prompts */
addGroupIdsToProject,
removeGroupIdsFromProject,
removeGroupFromAllProjects,
/* agents */
addAgentIdsToProject,
removeAgentIdsFromProject,
removeAgentFromAllProjects,
};

View File

@@ -1,5 +1,5 @@
const { ObjectId } = require('mongodb');
const { SystemRoles, SystemCategories } = require('librechat-data-provider');
const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider');
const {
getProjectByName,
addGroupIdsToProject,
@@ -123,7 +123,7 @@ const getAllPromptGroups = async (req, filter) => {
let combinedQuery = query;
if (searchShared) {
const project = await getProjectByName('instance', 'promptGroupIds');
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
if (project && project.promptGroupIds.length > 0) {
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
delete projectQuery.author;
@@ -177,7 +177,7 @@ const getPromptGroups = async (req, filter) => {
if (searchShared) {
// const projects = req.user.projects || []; // TODO: handle multiple projects
const project = await getProjectByName('instance', 'promptGroupIds');
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
if (project && project.promptGroupIds.length > 0) {
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
delete projectQuery.author;

View File

@@ -4,8 +4,10 @@ const {
roleDefaults,
PermissionTypes,
removeNullishValues,
agentPermissionsSchema,
promptPermissionsSchema,
bookmarkPermissionsSchema,
multiConvoPermissionsSchema,
} = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const Role = require('~/models/schema/roleSchema');
@@ -71,8 +73,10 @@ const updateRoleByName = async function (roleName, updates) {
};
const permissionSchemas = {
[PermissionTypes.AGENTS]: agentPermissionsSchema,
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
};
/**
@@ -130,6 +134,7 @@ async function updateAccessPermissions(roleName, permissionsUpdate) {
/**
* Initialize default roles in the system.
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
* Updates existing roles with new permission types if they're missing.
*
* @returns {Promise<void>}
*/
@@ -137,14 +142,27 @@ const initializeRoles = async function () {
const defaultRoles = [SystemRoles.ADMIN, SystemRoles.USER];
for (const roleName of defaultRoles) {
let role = await Role.findOne({ name: roleName }).select('name').lean();
let role = await Role.findOne({ name: roleName });
if (!role) {
// Create new role if it doesn't exist
role = new Role(roleDefaults[roleName]);
await role.save();
} else {
// Add missing permission types
let isUpdated = false;
for (const permType of Object.values(PermissionTypes)) {
if (!role[permType]) {
role[permType] = roleDefaults[roleName][permType];
isUpdated = true;
}
}
if (isUpdated) {
await role.save();
}
}
await role.save();
}
};
module.exports = {
getRoleByName,
initializeRoles,

View File

@@ -1,9 +1,14 @@
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { SystemRoles, PermissionTypes } = require('librechat-data-provider');
const Role = require('~/models/schema/roleSchema');
const { updateAccessPermissions } = require('~/models/Role');
const {
SystemRoles,
PermissionTypes,
roleDefaults,
Permissions,
} = require('librechat-data-provider');
const { updateAccessPermissions, initializeRoles } = require('~/models/Role');
const getLogStores = require('~/cache/getLogStores');
const Role = require('~/models/schema/roleSchema');
// Mock the cache
jest.mock('~/cache/getLogStores', () => {
@@ -194,4 +199,222 @@ describe('updateAccessPermissions', () => {
SHARED_GLOBAL: true,
});
});
it('should update MULTI_CONVO permissions', async () => {
await new Role({
name: SystemRoles.USER,
[PermissionTypes.MULTI_CONVO]: {
USE: false,
},
}).save();
await updateAccessPermissions(SystemRoles.USER, {
[PermissionTypes.MULTI_CONVO]: {
USE: true,
},
});
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({
USE: true,
});
});
it('should update MULTI_CONVO permissions along with other permission types', async () => {
await new Role({
name: SystemRoles.USER,
[PermissionTypes.PROMPTS]: {
CREATE: true,
USE: true,
SHARED_GLOBAL: false,
},
[PermissionTypes.MULTI_CONVO]: {
USE: false,
},
}).save();
await updateAccessPermissions(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { SHARED_GLOBAL: true },
[PermissionTypes.MULTI_CONVO]: { USE: true },
});
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
CREATE: true,
USE: true,
SHARED_GLOBAL: true,
});
expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({
USE: true,
});
});
it('should not update MULTI_CONVO permissions when no changes are needed', async () => {
await new Role({
name: SystemRoles.USER,
[PermissionTypes.MULTI_CONVO]: {
USE: true,
},
}).save();
await updateAccessPermissions(SystemRoles.USER, {
[PermissionTypes.MULTI_CONVO]: {
USE: true,
},
});
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({
USE: true,
});
});
});
describe('initializeRoles', () => {
beforeEach(async () => {
await Role.deleteMany({});
});
it('should create default roles if they do not exist', async () => {
await initializeRoles();
const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean();
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(adminRole).toBeTruthy();
expect(userRole).toBeTruthy();
// Check if all permission types exist
Object.values(PermissionTypes).forEach((permType) => {
expect(adminRole[permType]).toBeDefined();
expect(userRole[permType]).toBeDefined();
});
// Check if permissions match defaults (example for ADMIN role)
expect(adminRole[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBe(true);
expect(adminRole[PermissionTypes.BOOKMARKS].USE).toBe(true);
expect(adminRole[PermissionTypes.AGENTS].CREATE).toBe(true);
});
it('should not modify existing permissions for existing roles', async () => {
const customUserRole = {
name: SystemRoles.USER,
[PermissionTypes.PROMPTS]: {
[Permissions.USE]: false,
[Permissions.CREATE]: true,
[Permissions.SHARED_GLOBAL]: true,
},
[PermissionTypes.BOOKMARKS]: {
[Permissions.USE]: false,
},
};
await new Role(customUserRole).save();
await initializeRoles();
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(userRole[PermissionTypes.PROMPTS]).toEqual(customUserRole[PermissionTypes.PROMPTS]);
expect(userRole[PermissionTypes.BOOKMARKS]).toEqual(customUserRole[PermissionTypes.BOOKMARKS]);
expect(userRole[PermissionTypes.AGENTS]).toBeDefined();
});
it('should add new permission types to existing roles', async () => {
const partialUserRole = {
name: SystemRoles.USER,
[PermissionTypes.PROMPTS]: roleDefaults[SystemRoles.USER][PermissionTypes.PROMPTS],
[PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.USER][PermissionTypes.BOOKMARKS],
};
await new Role(partialUserRole).save();
await initializeRoles();
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(userRole[PermissionTypes.AGENTS]).toBeDefined();
expect(userRole[PermissionTypes.AGENTS].CREATE).toBeDefined();
expect(userRole[PermissionTypes.AGENTS].USE).toBeDefined();
expect(userRole[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined();
});
it('should handle multiple runs without duplicating or modifying data', async () => {
await initializeRoles();
await initializeRoles();
const adminRoles = await Role.find({ name: SystemRoles.ADMIN });
const userRoles = await Role.find({ name: SystemRoles.USER });
expect(adminRoles).toHaveLength(1);
expect(userRoles).toHaveLength(1);
const adminRole = adminRoles[0].toObject();
const userRole = userRoles[0].toObject();
// Check if all permission types exist
Object.values(PermissionTypes).forEach((permType) => {
expect(adminRole[permType]).toBeDefined();
expect(userRole[permType]).toBeDefined();
});
});
it('should update roles with missing permission types from roleDefaults', async () => {
const partialAdminRole = {
name: SystemRoles.ADMIN,
[PermissionTypes.PROMPTS]: {
[Permissions.USE]: false,
[Permissions.CREATE]: false,
[Permissions.SHARED_GLOBAL]: false,
},
[PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.ADMIN][PermissionTypes.BOOKMARKS],
};
await new Role(partialAdminRole).save();
await initializeRoles();
const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean();
expect(adminRole[PermissionTypes.PROMPTS]).toEqual(partialAdminRole[PermissionTypes.PROMPTS]);
expect(adminRole[PermissionTypes.AGENTS]).toBeDefined();
expect(adminRole[PermissionTypes.AGENTS].CREATE).toBeDefined();
expect(adminRole[PermissionTypes.AGENTS].USE).toBeDefined();
expect(adminRole[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined();
});
it('should include MULTI_CONVO permissions when creating default roles', async () => {
await initializeRoles();
const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean();
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(adminRole[PermissionTypes.MULTI_CONVO]).toBeDefined();
expect(userRole[PermissionTypes.MULTI_CONVO]).toBeDefined();
// Check if MULTI_CONVO permissions match defaults
expect(adminRole[PermissionTypes.MULTI_CONVO].USE).toBe(
roleDefaults[SystemRoles.ADMIN][PermissionTypes.MULTI_CONVO].USE,
);
expect(userRole[PermissionTypes.MULTI_CONVO].USE).toBe(
roleDefaults[SystemRoles.USER][PermissionTypes.MULTI_CONVO].USE,
);
});
it('should add MULTI_CONVO permissions to existing roles without them', async () => {
const partialUserRole = {
name: SystemRoles.USER,
[PermissionTypes.PROMPTS]: roleDefaults[SystemRoles.USER][PermissionTypes.PROMPTS],
[PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.USER][PermissionTypes.BOOKMARKS],
};
await new Role(partialUserRole).save();
await initializeRoles();
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
expect(userRole[PermissionTypes.MULTI_CONVO]).toBeDefined();
expect(userRole[PermissionTypes.MULTI_CONVO].USE).toBeDefined();
});
});

View File

@@ -39,6 +39,7 @@ const actionSchema = new Schema({
default: 'action_prototype',
},
settings: Schema.Types.Mixed,
agent_id: String,
assistant_id: String,
metadata: {
api_key: String, // private, encrypted

View File

@@ -0,0 +1,71 @@
const mongoose = require('mongoose');
const agentSchema = mongoose.Schema(
{
id: {
type: String,
index: true,
required: true,
},
name: {
type: String,
},
description: {
type: String,
},
instructions: {
type: String,
},
avatar: {
type: {
filepath: String,
source: String,
},
default: undefined,
},
provider: {
type: String,
required: true,
},
model: {
type: String,
required: true,
},
model_parameters: {
type: Object,
},
access_level: {
type: Number,
},
tools: {
type: [String],
default: undefined,
},
tool_kwargs: {
type: [{ type: mongoose.Schema.Types.Mixed }],
},
file_ids: {
type: [String],
default: undefined,
},
actions: {
type: [String],
default: undefined,
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
projectIds: {
type: [mongoose.Schema.Types.ObjectId],
ref: 'Project',
index: true,
},
},
{
timestamps: true,
},
);
module.exports = agentSchema;

View File

@@ -19,6 +19,10 @@ const assistantSchema = mongoose.Schema(
},
default: undefined,
},
conversation_starters: {
type: [String],
default: [],
},
access_level: {
type: Number,
},

View File

@@ -13,6 +13,11 @@ const conversationPreset = {
type: String,
required: false,
},
// for bedrock only
region: {
type: String,
required: false,
},
// for azureOpenAI, openAI only
chatGptLabel: {
type: String,
@@ -78,6 +83,9 @@ const conversationPreset = {
promptCache: {
type: Boolean,
},
system: {
type: String,
},
// files
resendFiles: {
type: Boolean,

View File

@@ -21,6 +21,11 @@ const projectSchema = new Schema(
ref: 'PromptGroup',
default: [],
},
agentIds: {
type: [String],
ref: 'Agent',
default: [],
},
},
{
timestamps: true,

View File

@@ -28,6 +28,26 @@ const roleSchema = new mongoose.Schema({
default: true,
},
},
[PermissionTypes.AGENTS]: {
[Permissions.SHARED_GLOBAL]: {
type: Boolean,
default: false,
},
[Permissions.USE]: {
type: Boolean,
default: true,
},
[Permissions.CREATE]: {
type: Boolean,
default: true,
},
},
[PermissionTypes.MULTI_CONVO]: {
[Permissions.USE]: {
type: Boolean,
default: true,
},
},
});
const Role = mongoose.model('Role', roleSchema);

View File

@@ -122,7 +122,12 @@ const userSchema = mongoose.Schema(
type: Date,
expires: 604800, // 7 days in seconds
},
termsAccepted: {
type: Boolean,
default: false,
},
},
{ timestamps: true },
);

View File

@@ -3,38 +3,28 @@ const defaultRate = 6;
/** AWS Bedrock pricing */
const bedrockValues = {
'anthropic.claude-3-haiku-20240307-v1:0': { prompt: 0.25, completion: 1.25 },
'anthropic.claude-3-sonnet-20240229-v1:0': { prompt: 3.0, completion: 15.0 },
'anthropic.claude-3-opus-20240229-v1:0': { prompt: 15.0, completion: 75.0 },
'anthropic.claude-3-5-sonnet-20240620-v1:0': { prompt: 3.0, completion: 15.0 },
'anthropic.claude-v2:1': { prompt: 8.0, completion: 24.0 },
'anthropic.claude-instant-v1': { prompt: 0.8, completion: 2.4 },
'meta.llama2-13b-chat-v1': { prompt: 0.75, completion: 1.0 },
'meta.llama2-70b-chat-v1': { prompt: 1.95, completion: 2.56 },
'meta.llama3-8b-instruct-v1:0': { prompt: 0.3, completion: 0.6 },
'meta.llama3-70b-instruct-v1:0': { prompt: 2.65, completion: 3.5 },
'meta.llama3-1-8b-instruct-v1:0': { prompt: 0.3, completion: 0.6 },
'meta.llama3-1-70b-instruct-v1:0': { prompt: 2.65, completion: 3.5 },
'meta.llama3-1-405b-instruct-v1:0': { prompt: 5.32, completion: 16.0 },
'mistral.mistral-7b-instruct-v0:2': { prompt: 0.15, completion: 0.2 },
'mistral.mistral-small-2402-v1:0': { prompt: 0.15, completion: 0.2 },
'mistral.mixtral-8x7b-instruct-v0:1': { prompt: 0.45, completion: 0.7 },
'mistral.mistral-large-2402-v1:0': { prompt: 4.0, completion: 12.0 },
'mistral.mistral-large-2407-v1:0': { prompt: 3.0, completion: 9.0 },
'cohere.command-text-v14': { prompt: 1.5, completion: 2.0 },
'cohere.command-light-text-v14': { prompt: 0.3, completion: 0.6 },
'cohere.command-r-v1:0': { prompt: 0.5, completion: 1.5 },
'cohere.command-r-plus-v1:0': { prompt: 3.0, completion: 15.0 },
'llama2-13b': { prompt: 0.75, completion: 1.0 },
'llama2-70b': { prompt: 1.95, completion: 2.56 },
'llama3-8b': { prompt: 0.3, completion: 0.6 },
'llama3-70b': { prompt: 2.65, completion: 3.5 },
'llama3-1-8b': { prompt: 0.3, completion: 0.6 },
'llama3-1-70b': { prompt: 2.65, completion: 3.5 },
'llama3-1-405b': { prompt: 5.32, completion: 16.0 },
'mistral-7b': { prompt: 0.15, completion: 0.2 },
'mistral-small': { prompt: 0.15, completion: 0.2 },
'mixtral-8x7b': { prompt: 0.45, completion: 0.7 },
'mistral-large-2402': { prompt: 4.0, completion: 12.0 },
'mistral-large-2407': { prompt: 3.0, completion: 9.0 },
'command-text': { prompt: 1.5, completion: 2.0 },
'command-light': { prompt: 0.3, completion: 0.6 },
'ai21.j2-mid-v1': { prompt: 12.5, completion: 12.5 },
'ai21.j2-ultra-v1': { prompt: 18.8, completion: 18.8 },
'ai21.jamba-instruct-v1:0': { prompt: 0.5, completion: 0.7 },
'amazon.titan-text-lite-v1': { prompt: 0.15, completion: 0.2 },
'amazon.titan-text-express-v1': { prompt: 0.2, completion: 0.6 },
'amazon.titan-text-premier-v1:0': { prompt: 0.5, completion: 1.5 },
};
for (const [key, value] of Object.entries(bedrockValues)) {
bedrockValues[`bedrock/${key}`] = value;
}
/**
* Mapping of model token sizes to their respective multipliers for prompt and completion.
* The rates are 1 USD per 1M tokens.
@@ -59,6 +49,7 @@ const tokenValues = Object.assign(
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
'claude-2.1': { prompt: 8, completion: 24 },
'claude-2': { prompt: 8, completion: 24 },
'claude-instant': { prompt: 0.8, completion: 2.4 },
'claude-': { prompt: 0.8, completion: 2.4 },
'command-r-plus': { prompt: 3, completion: 15 },
'command-r': { prompt: 0.5, completion: 1.5 },

View File

@@ -1,3 +1,4 @@
const { EModelEndpoint } = require('librechat-data-provider');
const {
defaultRate,
tokenValues,
@@ -224,34 +225,18 @@ describe('AWS Bedrock Model Tests', () => {
it('should return the correct prompt multipliers for all models', () => {
const results = awsModels.map((model) => {
const multiplier = getMultiplier({ valueKey: model, tokenType: 'prompt' });
return multiplier === tokenValues[model].prompt;
const valueKey = getValueKey(model, EModelEndpoint.bedrock);
const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' });
return tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt;
});
expect(results.every(Boolean)).toBe(true);
});
it('should return the correct completion multipliers for all models', () => {
const results = awsModels.map((model) => {
const multiplier = getMultiplier({ valueKey: model, tokenType: 'completion' });
return multiplier === tokenValues[model].completion;
});
expect(results.every(Boolean)).toBe(true);
});
it('should return the correct prompt multipliers for all models with Bedrock prefix', () => {
const results = awsModels.map((model) => {
const modelName = `bedrock/${model}`;
const multiplier = getMultiplier({ valueKey: modelName, tokenType: 'prompt' });
return multiplier === tokenValues[model].prompt;
});
expect(results.every(Boolean)).toBe(true);
});
it('should return the correct completion multipliers for all models with Bedrock prefix', () => {
const results = awsModels.map((model) => {
const modelName = `bedrock/${model}`;
const multiplier = getMultiplier({ valueKey: modelName, tokenType: 'completion' });
return multiplier === tokenValues[model].completion;
const valueKey = getValueKey(model, EModelEndpoint.bedrock);
const multiplier = getMultiplier({ valueKey, tokenType: 'completion' });
return tokenValues[valueKey].completion && multiplier === tokenValues[valueKey].completion;
});
expect(results.every(Boolean)).toBe(true);
});

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.7.5-rc1",
"version": "v0.7.5-rc2",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -40,8 +40,10 @@
"@keyv/mongo": "^2.1.8",
"@keyv/redis": "^2.8.1",
"@langchain/community": "^0.0.46",
"@langchain/core": "^0.2.18",
"@langchain/google-genai": "^0.0.11",
"@langchain/google-vertexai": "^0.0.17",
"@librechat/agents": "^1.5.2",
"axios": "^1.3.4",
"bcryptjs": "^2.4.3",
"cheerio": "^1.0.0-rc.12",
@@ -49,6 +51,7 @@
"compression": "^1.7.4",
"connect-redis": "^7.1.0",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dedent": "^1.5.3",
"dotenv": "^16.0.3",

View File

@@ -123,11 +123,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
};
let response = await client.sendMessage(text, messageOptions);
if (overrideParentMessageId) {
response.parentMessageId = overrideParentMessageId;
}
response.endpoint = endpointOption.endpoint;
const { conversation = {} } = await client.responsePromise;

View File

@@ -44,6 +44,14 @@ async function endpointController(req, res) {
};
}
if (mergedConfig[EModelEndpoint.bedrock] && req.app.locals?.[EModelEndpoint.bedrock]) {
const { availableRegions } = req.app.locals[EModelEndpoint.bedrock];
mergedConfig[EModelEndpoint.bedrock] = {
...mergedConfig[EModelEndpoint.bedrock],
availableRegions,
};
}
const endpointsConfig = orderEndpointsConfig(mergedConfig);
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);

View File

@@ -2,6 +2,9 @@ const { CacheKeys } = require('librechat-data-provider');
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
/**
* @param {ServerRequest} req
*/
const getModelsConfig = async (req) => {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
let modelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
@@ -14,7 +17,7 @@ const getModelsConfig = async (req) => {
/**
* Loads the models from the config.
* @param {Express.Request} req - The Express request object.
* @param {ServerRequest} req - The Express request object.
* @returns {Promise<TModelsConfig>} The models config.
*/
async function loadModels(req) {

View File

@@ -8,6 +8,7 @@ const {
deleteMessages,
deleteUserById,
} = require('~/models');
const User = require('~/models/User');
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
@@ -20,6 +21,32 @@ const getUserController = async (req, res) => {
res.status(200).send(req.user);
};
const getTermsStatusController = async (req, res) => {
try {
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ termsAccepted: !!user.termsAccepted });
} catch (error) {
logger.error('Error fetching terms acceptance status:', error);
res.status(500).json({ message: 'Error fetching terms acceptance status' });
}
};
const acceptTermsController = async (req, res) => {
try {
const user = await User.findByIdAndUpdate(req.user.id, { termsAccepted: true }, { new: true });
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ message: 'Terms accepted successfully' });
} catch (error) {
logger.error('Error accepting terms:', error);
res.status(500).json({ message: 'Error accepting terms' });
}
};
const deleteUserFiles = async (req) => {
try {
const userFiles = await getFiles({ user: req.user.id });
@@ -135,6 +162,8 @@ const resendVerificationController = async (req, res) => {
module.exports = {
getUserController,
getTermsStatusController,
acceptTermsController,
deleteUserController,
verifyEmailController,
updateUserPluginsController,

View File

@@ -0,0 +1,127 @@
const { GraphEvents, ToolEndHandler, ChatModelStreamHandler } = require('@librechat/agents');
/** @typedef {import('@librechat/agents').Graph} Graph */
/** @typedef {import('@librechat/agents').EventHandler} EventHandler */
/** @typedef {import('@librechat/agents').ModelEndData} ModelEndData */
/** @typedef {import('@librechat/agents').ChatModelStreamHandler} ChatModelStreamHandler */
/** @typedef {import('@librechat/agents').ContentAggregatorResult['aggregateContent']} ContentAggregator */
/** @typedef {import('@librechat/agents').GraphEvents} GraphEvents */
/**
* Sends message data in Server Sent Events format.
* @param {ServerResponse} res - The server response.
* @param {{ data: string | Record<string, unknown>, event?: string }} event - The message event.
* @param {string} event.event - The type of event.
* @param {string} event.data - The message to be sent.
*/
const sendEvent = (res, event) => {
if (typeof event.data === 'string' && event.data.length === 0) {
return;
}
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
};
class ModelEndHandler {
/**
* @param {Array<UsageMetadata>} collectedUsage
*/
constructor(collectedUsage) {
if (!Array.isArray(collectedUsage)) {
throw new Error('collectedUsage must be an array');
}
this.collectedUsage = collectedUsage;
}
/**
* @param {string} event
* @param {ModelEndData | undefined} data
* @param {Record<string, unknown> | undefined} metadata
* @param {Graph} graph
* @returns
*/
handle(event, data, metadata, graph) {
if (!graph || !metadata) {
console.warn(`Graph or metadata not found in ${event} event`);
return;
}
const usage = data?.output?.usage_metadata;
if (usage) {
this.collectedUsage.push(usage);
}
}
}
/**
* Get default handlers for stream events.
* @param {Object} options - The options object.
* @param {ServerResponse} options.res - The options object.
* @param {ContentAggregator} options.aggregateContent - The options object.
* @param {Array<UsageMetadata>} options.collectedUsage - The list of collected usage metadata.
* @returns {Record<string, t.EventHandler>} The default handlers.
* @throws {Error} If the request is not found.
*/
function getDefaultHandlers({ res, aggregateContent, collectedUsage }) {
if (!res || !aggregateContent) {
throw new Error(
`[getDefaultHandlers] Missing required options: res: ${!res}, aggregateContent: ${!aggregateContent}`,
);
}
const handlers = {
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
[GraphEvents.TOOL_END]: new ToolEndHandler(),
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
[GraphEvents.ON_RUN_STEP]: {
/**
* Handle ON_RUN_STEP event.
* @param {string} event - The event name.
* @param {StreamEventData} data - The event data.
*/
handle: (event, data) => {
sendEvent(res, { event, data });
aggregateContent({ event, data });
},
},
[GraphEvents.ON_RUN_STEP_DELTA]: {
/**
* Handle ON_RUN_STEP_DELTA event.
* @param {string} event - The event name.
* @param {StreamEventData} data - The event data.
*/
handle: (event, data) => {
sendEvent(res, { event, data });
aggregateContent({ event, data });
},
},
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
/**
* Handle ON_RUN_STEP_COMPLETED event.
* @param {string} event - The event name.
* @param {StreamEventData & { result: ToolEndData }} data - The event data.
*/
handle: (event, data) => {
sendEvent(res, { event, data });
aggregateContent({ event, data });
},
},
[GraphEvents.ON_MESSAGE_DELTA]: {
/**
* Handle ON_MESSAGE_DELTA event.
* @param {string} event - The event name.
* @param {StreamEventData} data - The event data.
*/
handle: (event, data) => {
sendEvent(res, { event, data });
aggregateContent({ event, data });
},
},
};
return handlers;
}
module.exports = {
sendEvent,
getDefaultHandlers,
};

View File

@@ -0,0 +1,608 @@
// const { HttpsProxyAgent } = require('https-proxy-agent');
// const {
// Constants,
// ImageDetail,
// EModelEndpoint,
// resolveHeaders,
// validateVisionModel,
// mapModelToAzureConfig,
// } = require('librechat-data-provider');
const { Callback, createMetadataAggregator } = require('@librechat/agents');
const {
Constants,
EModelEndpoint,
bedrockOutputParser,
providerEndpointMap,
removeNullishValues,
} = require('librechat-data-provider');
const {
extractBaseURL,
// constructAzureURL,
// genAzureChatCompletion,
} = require('~/utils');
const {
formatMessage,
formatAgentMessages,
createContextHandlers,
} = require('~/app/clients/prompts');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const Tokenizer = require('~/server/services/Tokenizer');
const { spendTokens } = require('~/models/spendTokens');
const BaseClient = require('~/app/clients/BaseClient');
// const { sleep } = require('~/server/utils');
const { createRun } = require('./run');
const { logger } = require('~/config');
/** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */
// const providerSchemas = {
// [EModelEndpoint.bedrock]: true,
// };
const providerParsers = {
[EModelEndpoint.bedrock]: bedrockOutputParser,
};
class AgentClient extends BaseClient {
constructor(options = {}) {
super(null, options);
/** @type {'discard' | 'summarize'} */
this.contextStrategy = 'discard';
/** @deprecated @type {true} - Is a Chat Completion Request */
this.isChatCompletion = true;
/** @type {AgentRun} */
this.run;
const {
maxContextTokens,
modelOptions = {},
contentParts,
collectedUsage,
...clientOptions
} = options;
this.modelOptions = modelOptions;
this.maxContextTokens = maxContextTokens;
/** @type {MessageContentComplex[]} */
this.contentParts = contentParts;
/** @type {Array<UsageMetadata>} */
this.collectedUsage = collectedUsage;
this.options = Object.assign({ endpoint: options.endpoint }, clientOptions);
}
/**
* Returns the aggregated content parts for the current run.
* @returns {MessageContentComplex[]} */
getContentParts() {
return this.contentParts;
}
setOptions(options) {
logger.info('[api/server/controllers/agents/client.js] setOptions', options);
}
/**
*
* 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) {
logger.info(
'[api/server/controllers/agents/client.js #checkVisionRequest] not implemented',
attachments,
);
// if (!attachments) {
// return;
// }
// const availableModels = this.options.modelsConfig?.[this.options.endpoint];
// if (!availableModels) {
// return;
// }
// let visionRequestDetected = false;
// for (const file of attachments) {
// if (file?.type?.includes('image')) {
// visionRequestDetected = true;
// break;
// }
// }
// if (!visionRequestDetected) {
// return;
// }
// this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
// if (this.isVisionModel) {
// delete this.modelOptions.stop;
// return;
// }
// for (const model of availableModels) {
// if (!validateVisionModel({ model, availableModels })) {
// continue;
// }
// this.modelOptions.model = model;
// this.isVisionModel = true;
// delete this.modelOptions.stop;
// return;
// }
// if (!availableModels.includes(this.defaultVisionModel)) {
// return;
// }
// if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) {
// return;
// }
// this.modelOptions.model = this.defaultVisionModel;
// this.isVisionModel = true;
// delete this.modelOptions.stop;
}
getSaveOptions() {
const parseOptions = providerParsers[this.options.endpoint];
let runOptions =
this.options.endpoint === EModelEndpoint.agents
? {
model: undefined,
// TODO:
// would need to be override settings; otherwise, model needs to be undefined
// model: this.override.model,
// instructions: this.override.instructions,
// additional_instructions: this.override.additional_instructions,
}
: {};
if (parseOptions) {
runOptions = parseOptions(this.modelOptions);
}
return removeNullishValues(
Object.assign(
{
endpoint: this.options.endpoint,
agent_id: this.options.agent.id,
modelLabel: this.options.modelLabel,
maxContextTokens: this.options.maxContextTokens,
resendFiles: this.options.resendFiles,
imageDetail: this.options.imageDetail,
spec: this.options.spec,
},
// TODO: PARSE OPTIONS BY PROVIDER, MAY CONTAIN SENSITIVE DATA
runOptions,
),
);
}
getBuildMessagesOptions(opts) {
return {
instructions: opts.instructions,
additional_instructions: opts.additional_instructions,
};
}
async addImageURLs(message, attachments) {
const { files, image_urls } = await encodeAndFormat(
this.options.req,
attachments,
this.options.agent.provider,
);
message.image_urls = image_urls.length ? image_urls : undefined;
return files;
}
async buildMessages(
messages,
parentMessageId,
{ instructions = null, additional_instructions = null },
opts,
) {
let orderedMessages = this.constructor.getMessagesForConversation({
messages,
parentMessageId,
summary: this.shouldSummarize,
});
let payload;
/** @type {{ role: string; name: string; content: string } | undefined} */
let systemMessage;
/** @type {number | undefined} */
let promptTokens;
/** @type {string} */
let systemContent = `${instructions ?? ''}${additional_instructions ?? ''}`;
if (this.options.attachments) {
const attachments = await this.options.attachments;
if (this.message_file_map) {
this.message_file_map[orderedMessages[orderedMessages.length - 1].messageId] = attachments;
} else {
this.message_file_map = {
[orderedMessages[orderedMessages.length - 1].messageId]: attachments,
};
}
const files = await this.addImageURLs(
orderedMessages[orderedMessages.length - 1],
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 = formatMessage({
message,
userName: this.options?.name,
assistantName: this.options?.modelLabel,
});
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,
// detail: this.options.imageDetail ?? ImageDetail.auto,
// });
// }
// }
return formattedMessage;
});
if (this.contextHandlers) {
this.augmentedPrompt = await this.contextHandlers.createContext();
systemContent = this.augmentedPrompt + systemContent;
}
if (systemContent) {
systemContent = `${systemContent.trim()}`;
systemMessage = {
role: 'system',
name: 'instructions',
content: systemContent,
};
if (this.contextStrategy) {
const instructionTokens = this.getTokenCountForMessage(systemMessage);
if (instructionTokens >= 0) {
const firstMessageTokens = orderedMessages[0].tokenCount ?? 0;
orderedMessages[0].tokenCount = firstMessageTokens + instructionTokens;
}
}
}
if (this.contextStrategy) {
({ payload, promptTokens, messages } = await this.handleContextStrategy({
orderedMessages,
formattedMessages,
/* prefer usage_metadata from final message */
buildTokenMap: false,
}));
}
const result = {
prompt: payload,
promptTokens,
messages,
};
if (promptTokens >= 0 && typeof opts?.getReqData === 'function') {
opts.getReqData({ promptTokens });
}
return result;
}
/** @type {sendCompletion} */
async sendCompletion(payload, opts = {}) {
this.modelOptions.user = this.user;
await this.chatCompletion({
payload,
onProgress: opts.onProgress,
abortController: opts.abortController,
});
return this.contentParts;
}
/**
* @param {Object} params
* @param {string} [params.model]
* @param {string} [params.context='message']
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
*/
async recordCollectedUsage({ model, context = 'message', collectedUsage = this.collectedUsage }) {
for (const usage of collectedUsage) {
await spendTokens(
{
context,
model: model ?? this.modelOptions.model,
conversationId: this.conversationId,
user: this.user ?? this.options.req.user?.id,
endpointTokenConfig: this.options.endpointTokenConfig,
},
{ promptTokens: usage.input_tokens, completionTokens: usage.output_tokens },
);
}
}
async chatCompletion({ payload, abortController = null }) {
try {
if (!abortController) {
abortController = new AbortController();
}
const baseURL = extractBaseURL(this.completionsUrl);
logger.debug('[api/server/controllers/agents/client.js] chatCompletion', {
baseURL,
payload,
});
// if (this.useOpenRouter) {
// opts.defaultHeaders = {
// 'HTTP-Referer': 'https://librechat.ai',
// 'X-Title': 'LibreChat',
// };
// }
// if (this.options.headers) {
// opts.defaultHeaders = { ...opts.defaultHeaders, ...this.options.headers };
// }
// if (this.options.proxy) {
// opts.httpAgent = new HttpsProxyAgent(this.options.proxy);
// }
// if (this.isVisionModel) {
// 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 Bug, extremely short default `max_tokens` response */
// if (!modelOptions.max_tokens && modelOptions.model === 'gpt-4-vision-preview') {
// modelOptions.max_tokens = 4000;
// }
// /* Azure does not accept `model` in the body, so we need to remove it. */
// delete modelOptions.model;
// opts.baseURL = this.langchainProxy
// ? constructAzureURL({
// baseURL: this.langchainProxy,
// azureOptions: this.azure,
// })
// : this.azureEndpoint.split(/(?<!\/)\/(chat|completion)\//)[0];
// opts.defaultQuery = { 'api-version': this.azure.azureOpenAIApiVersion };
// opts.defaultHeaders = { ...opts.defaultHeaders, 'api-key': this.apiKey };
// }
// if (process.env.OPENAI_ORGANIZATION) {
// opts.organization = process.env.OPENAI_ORGANIZATION;
// }
// if (this.options.addParams && typeof this.options.addParams === 'object') {
// modelOptions = {
// ...modelOptions,
// ...this.options.addParams,
// };
// logger.debug('[api/server/controllers/agents/client.js #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('[api/server/controllers/agents/client.js #chatCompletion] dropped params', {
// dropParams: this.options.dropParams,
// modelOptions,
// });
// }
const run = await createRun({
req: this.options.req,
agent: this.options.agent,
tools: this.options.tools,
toolMap: this.options.toolMap,
runId: this.responseMessageId,
modelOptions: this.modelOptions,
customHandlers: this.options.eventHandlers,
});
const config = {
configurable: {
provider: providerEndpointMap[this.options.agent.provider],
thread_id: this.conversationId,
},
run_id: this.responseMessageId,
signal: abortController.signal,
streamMode: 'values',
version: 'v2',
};
if (!run) {
throw new Error('Failed to create run');
}
this.run = run;
const messages = formatAgentMessages(payload);
await run.processStream({ messages }, config, {
[Callback.TOOL_ERROR]: (graph, error, toolId) => {
logger.error(
'[api/server/controllers/agents/client.js #chatCompletion] Tool Error',
error,
toolId,
);
},
});
this.recordCollectedUsage({ context: 'message' }).catch((err) => {
logger.error(
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
err,
);
});
} catch (err) {
if (!abortController.signal.aborted) {
logger.error(
'[api/server/controllers/agents/client.js #sendCompletion] Unhandled error type',
err,
);
throw err;
}
logger.warn(
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted',
err,
);
}
}
/**
*
* @param {Object} params
* @param {string} params.text
* @param {string} params.conversationId
*/
async titleConvo({ text }) {
if (!this.run) {
throw new Error('Run not initialized');
}
const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator();
const clientOptions = {};
const providerConfig = this.options.req.app.locals[this.options.agent.provider];
if (
providerConfig &&
providerConfig.titleModel &&
providerConfig.titleModel !== Constants.CURRENT_MODEL
) {
clientOptions.model = providerConfig.titleModel;
}
try {
const titleResult = await this.run.generateTitle({
inputText: text,
contentParts: this.contentParts,
clientOptions,
chainOptions: {
callbacks: [
{
handleLLMEnd,
},
],
},
});
const collectedUsage = collectedMetadata.map((item) => {
let input_tokens, output_tokens;
if (item.usage) {
input_tokens = item.usage.input_tokens || item.usage.inputTokens;
output_tokens = item.usage.output_tokens || item.usage.outputTokens;
} else if (item.tokenUsage) {
input_tokens = item.tokenUsage.promptTokens;
output_tokens = item.tokenUsage.completionTokens;
}
return {
input_tokens: input_tokens,
output_tokens: output_tokens,
};
});
this.recordCollectedUsage({
model: clientOptions.model,
context: 'title',
collectedUsage,
}).catch((err) => {
logger.error(
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
err,
);
});
return titleResult.title;
} catch (err) {
logger.error('[api/server/controllers/agents/client.js #titleConvo] Error', err);
return;
}
}
getEncoding() {
return this.modelOptions.model?.includes('gpt-4o') ? 'o200k_base' : 'cl100k_base';
}
/**
* Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
* @param {string} text - The text to get the token count for.
* @returns {number} The token count of the given text.
*/
getTokenCount(text) {
const encoding = this.getEncoding();
return Tokenizer.getTokenCount(text, encoding);
}
}
module.exports = AgentClient;

View File

@@ -0,0 +1,153 @@
// errorHandler.js
const { logger } = require('~/config');
const getLogStores = require('~/cache/getLogStores');
const { CacheKeys, ViolationTypes } = require('librechat-data-provider');
const { recordUsage } = require('~/server/services/Threads');
const { getConvo } = require('~/models/Conversation');
const { sendResponse } = require('~/server/utils');
/**
* @typedef {Object} ErrorHandlerContext
* @property {OpenAIClient} openai - The OpenAI client
* @property {string} run_id - The run ID
* @property {boolean} completedRun - Whether the run has completed
* @property {string} assistant_id - The assistant ID
* @property {string} conversationId - The conversation ID
* @property {string} parentMessageId - The parent message ID
* @property {string} responseMessageId - The response message ID
* @property {string} endpoint - The endpoint being used
* @property {string} cacheKey - The cache key for the current request
*/
/**
* @typedef {Object} ErrorHandlerDependencies
* @property {Express.Request} req - The Express request object
* @property {Express.Response} res - The Express response object
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
* @property {string} [originPath] - The origin path for the error handler
*/
/**
* Creates an error handler function with the given dependencies
* @param {ErrorHandlerDependencies} dependencies - The dependencies for the error handler
* @returns {(error: Error) => Promise<void>} The error handler function
*/
const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/chat/' }) => {
const cache = getLogStores(CacheKeys.ABORT_KEYS);
/**
* Handles errors that occur during the chat process
* @param {Error} error - The error that occurred
* @returns {Promise<void>}
*/
return async (error) => {
const {
openai,
run_id,
endpoint,
cacheKey,
completedRun,
assistant_id,
conversationId,
parentMessageId,
responseMessageId,
} = getContext();
const defaultErrorMessage =
'The Assistant run failed to initialize. Try sending a message in a new conversation.';
const messageData = {
assistant_id,
conversationId,
parentMessageId,
sender: 'System',
user: req.user.id,
shouldSaveMessage: false,
messageId: responseMessageId,
endpoint,
};
if (error.message === 'Run cancelled') {
return res.end();
} else if (error.message === 'Request closed' && completedRun) {
return;
} else if (error.message === 'Request closed') {
logger.debug(`[${originPath}] Request aborted on close`);
} else if (/Files.*are invalid/.test(error.message)) {
const errorMessage = `Files are invalid, or may not have uploaded yet.${
endpoint === 'azureAssistants'
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
: ''
}`;
return sendResponse(req, res, messageData, errorMessage);
} else if (error?.message?.includes('string too long')) {
return sendResponse(
req,
res,
messageData,
'Message too long. The Assistants API has a limit of 32,768 characters per message. Please shorten it and try again.',
);
} else if (error?.message?.includes(ViolationTypes.TOKEN_BALANCE)) {
return sendResponse(req, res, messageData, error.message);
} else {
logger.error(`[${originPath}]`, error);
}
if (!openai || !run_id) {
return sendResponse(req, res, messageData, defaultErrorMessage);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
try {
const status = await cache.get(cacheKey);
if (status === 'cancelled') {
logger.debug(`[${originPath}] Run already cancelled`);
return res.end();
}
await cache.delete(cacheKey);
// const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
// logger.debug(`[${originPath}] Cancelled run:`, cancelledRun);
} catch (error) {
logger.error(`[${originPath}] Error cancelling run`, error);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
let run;
try {
// run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
await recordUsage({
...run.usage,
model: run.model,
user: req.user.id,
conversationId,
});
} catch (error) {
logger.error(`[${originPath}] Error fetching or processing run`, error);
}
let finalEvent;
try {
// const errorContentPart = {
// text: {
// value:
// error?.message ?? 'There was an error processing your request. Please try again later.',
// },
// type: ContentTypes.ERROR,
// };
finalEvent = {
final: true,
conversation: await getConvo(req.user.id, conversationId),
// runMessages,
};
} catch (error) {
logger.error(`[${originPath}] Error finalizing error process`, error);
return sendResponse(req, res, messageData, 'The Assistant run failed');
}
return sendResponse(req, res, finalEvent);
};
};
module.exports = { createErrorHandler };

View File

@@ -0,0 +1,106 @@
const { HttpsProxyAgent } = require('https-proxy-agent');
const { resolveHeaders } = require('librechat-data-provider');
const { createLLM } = require('~/app/clients/llm');
/**
* Initializes and returns a Language Learning Model (LLM) instance.
*
* @param {Object} options - Configuration options for the LLM.
* @param {string} options.model - The model identifier.
* @param {string} options.modelName - The specific name of the model.
* @param {number} options.temperature - The temperature setting for the model.
* @param {number} options.presence_penalty - The presence penalty for the model.
* @param {number} options.frequency_penalty - The frequency penalty for the model.
* @param {number} options.max_tokens - The maximum number of tokens for the model output.
* @param {boolean} options.streaming - Whether to use streaming for the model output.
* @param {Object} options.context - The context for the conversation.
* @param {number} options.tokenBuffer - The token buffer size.
* @param {number} options.initialMessageCount - The initial message count.
* @param {string} options.conversationId - The ID of the conversation.
* @param {string} options.user - The user identifier.
* @param {string} options.langchainProxy - The langchain proxy URL.
* @param {boolean} options.useOpenRouter - Whether to use OpenRouter.
* @param {Object} options.options - Additional options.
* @param {Object} options.options.headers - Custom headers for the request.
* @param {string} options.options.proxy - Proxy URL.
* @param {Object} options.options.req - The request object.
* @param {Object} options.options.res - The response object.
* @param {boolean} options.options.debug - Whether to enable debug mode.
* @param {string} options.apiKey - The API key for authentication.
* @param {Object} options.azure - Azure-specific configuration.
* @param {Object} options.abortController - The AbortController instance.
* @returns {Object} The initialized LLM instance.
*/
function initializeLLM(options) {
const {
model,
modelName,
temperature,
presence_penalty,
frequency_penalty,
max_tokens,
streaming,
user,
langchainProxy,
useOpenRouter,
options: { headers, proxy },
apiKey,
azure,
} = options;
const modelOptions = {
modelName: modelName || model,
temperature,
presence_penalty,
frequency_penalty,
user,
};
if (max_tokens) {
modelOptions.max_tokens = max_tokens;
}
const configOptions = {};
if (langchainProxy) {
configOptions.basePath = langchainProxy;
}
if (useOpenRouter) {
configOptions.basePath = 'https://openrouter.ai/api/v1';
configOptions.baseOptions = {
headers: {
'HTTP-Referer': 'https://librechat.ai',
'X-Title': 'LibreChat',
},
};
}
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
configOptions.baseOptions = {
headers: resolveHeaders({
...headers,
...configOptions?.baseOptions?.headers,
}),
};
}
if (proxy) {
configOptions.httpAgent = new HttpsProxyAgent(proxy);
configOptions.httpsAgent = new HttpsProxyAgent(proxy);
}
const llm = createLLM({
modelOptions,
configOptions,
openAIApiKey: apiKey,
azure,
streaming,
});
return llm;
}
module.exports = {
initializeLLM,
};

View File

@@ -0,0 +1,142 @@
const { Constants } = require('librechat-data-provider');
const { createAbortController, handleAbortError } = require('~/server/middleware');
const { sendMessage } = require('~/server/utils');
const { saveMessage } = require('~/models');
const { logger } = require('~/config');
const AgentController = async (req, res, next, initializeClient, addTitle) => {
let {
text,
endpointOption,
conversationId,
parentMessageId = null,
overrideParentMessageId = null,
} = req.body;
let sender;
let userMessage;
let promptTokens;
let userMessageId;
let responseMessageId;
let userMessagePromise;
const newConvo = !conversationId;
const user = req.user.id;
const getReqData = (data = {}) => {
for (let key in data) {
if (key === 'userMessage') {
userMessage = data[key];
userMessageId = data[key].messageId;
} else if (key === 'userMessagePromise') {
userMessagePromise = data[key];
} else if (key === 'responseMessageId') {
responseMessageId = data[key];
} else if (key === 'promptTokens') {
promptTokens = data[key];
} else if (key === 'sender') {
sender = data[key];
} else if (!conversationId && key === 'conversationId') {
conversationId = data[key];
}
}
};
try {
/** @type {{ client: TAgentClient }} */
const { client } = await initializeClient({ req, res, endpointOption });
const getAbortData = () => ({
sender,
userMessage,
promptTokens,
conversationId,
userMessagePromise,
messageId: responseMessageId,
content: client.getContentParts(),
parentMessageId: overrideParentMessageId ?? userMessageId,
});
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
res.on('close', () => {
logger.debug('[AgentController] Request closed');
if (!abortController) {
return;
} else if (abortController.signal.aborted) {
return;
} else if (abortController.requestCompleted) {
return;
}
abortController.abort();
logger.debug('[AgentController] Request aborted on close');
});
const messageOptions = {
user,
onStart,
getReqData,
conversationId,
parentMessageId,
abortController,
overrideParentMessageId,
progressOptions: {
res,
// parentMessageId: overrideParentMessageId || userMessageId,
},
};
let response = await client.sendMessage(text, messageOptions);
response.endpoint = endpointOption.endpoint;
const { conversation = {} } = await client.responsePromise;
conversation.title =
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
if (client.options.attachments) {
userMessage.files = client.options.attachments;
delete userMessage.image_urls;
}
if (!abortController.signal.aborted) {
sendMessage(res, {
final: true,
conversation,
title: conversation.title,
requestMessage: userMessage,
responseMessage: response,
});
res.end();
await saveMessage(
req,
{ ...response, user },
{ context: 'api/server/controllers/agents/request.js - response end' },
);
}
if (!client.skipSaveUserMessage) {
await saveMessage(req, userMessage, {
context: 'api/server/controllers/agents/request.js - don\'t skip saving user message',
});
}
if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) {
addTitle(req, {
text,
response,
client,
});
}
} catch (error) {
handleAbortError(res, req, error, {
conversationId,
sender,
messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId,
});
}
};
module.exports = AgentController;

View File

@@ -0,0 +1,67 @@
const { Run, Providers } = require('@librechat/agents');
const { providerEndpointMap } = require('librechat-data-provider');
/**
* @typedef {import('@librechat/agents').t} t
* @typedef {import('@librechat/agents').StreamEventData} StreamEventData
* @typedef {import('@librechat/agents').ClientOptions} ClientOptions
* @typedef {import('@librechat/agents').EventHandler} EventHandler
* @typedef {import('@librechat/agents').GraphEvents} GraphEvents
* @typedef {import('@librechat/agents').IState} IState
*/
/**
* Creates a new Run instance with custom handlers and configuration.
*
* @param {Object} options - The options for creating the Run instance.
* @param {ServerRequest} [options.req] - The server request.
* @param {string | undefined} [options.runId] - Optional run ID; otherwise, a new run ID will be generated.
* @param {Agent} options.agent - The agent for this run.
* @param {StructuredTool[] | undefined} [options.tools] - The tools to use in the run.
* @param {Record<string, StructuredTool[]> | undefined} [options.toolMap] - The tool map for the run.
* @param {Record<GraphEvents, EventHandler> | undefined} [options.customHandlers] - Custom event handlers.
* @param {ClientOptions} [options.modelOptions] - Optional model to use; if not provided, it will use the default from modelMap.
* @param {boolean} [options.streaming=true] - Whether to use streaming.
* @param {boolean} [options.streamUsage=true] - Whether to stream usage information.
* @returns {Promise<Run<IState>>} A promise that resolves to a new Run instance.
*/
async function createRun({
runId,
tools,
agent,
toolMap,
modelOptions,
customHandlers,
streaming = true,
streamUsage = true,
}) {
const llmConfig = Object.assign(
{
provider: providerEndpointMap[agent.provider],
streaming,
streamUsage,
},
modelOptions,
);
const graphConfig = {
runId,
llmConfig,
tools,
toolMap,
instructions: agent.instructions,
additional_instructions: agent.additional_instructions,
};
// TEMPORARY FOR TESTING
if (agent.provider === Providers.ANTHROPIC) {
graphConfig.streamBuffer = 2000;
}
return Run.create({
graphConfig,
customHandlers,
});
}
module.exports = { createRun };

View File

@@ -0,0 +1,239 @@
const { nanoid } = require('nanoid');
const { FileContext, Constants } = require('librechat-data-provider');
const {
getAgent,
createAgent,
updateAgent,
deleteAgent,
getListAgents,
} = require('~/models/Agent');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { uploadImageBuffer } = require('~/server/services/Files/process');
const { getProjectByName } = require('~/models/Project');
const { updateAgentProjects } = require('~/models/Agent');
const { deleteFileByFilter } = require('~/models/File');
const { logger } = require('~/config');
/**
* Creates an Agent.
* @route POST /Agents
* @param {ServerRequest} req - The request object.
* @param {AgentCreateParams} req.body - The request body.
* @param {ServerResponse} res - The response object.
* @returns {Agent} 201 - success response - application/json
*/
const createAgentHandler = async (req, res) => {
try {
const { tools = [], provider, name, description, instructions, model, ...agentData } = req.body;
const { id: userId } = req.user;
agentData.tools = tools
.map((tool) => (typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool))
.filter(Boolean);
Object.assign(agentData, {
author: userId,
name,
description,
instructions,
provider,
model,
});
agentData.id = `agent_${nanoid()}`;
const agent = await createAgent(agentData);
res.status(201).json(agent);
} catch (error) {
logger.error('[/Agents] Error creating agent', error);
res.status(500).json({ error: error.message });
}
};
/**
* Retrieves an Agent by ID.
* @route GET /Agents/:id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @param {object} req.user - Authenticated user information
* @param {string} req.user.id - User ID
* @returns {Promise<Agent>} 200 - success response - application/json
* @returns {Error} 404 - Agent not found
*/
const getAgentHandler = async (req, res) => {
try {
const id = req.params.id;
const author = req.user.id;
let query = { id, author };
const globalProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, ['agentIds']);
if (globalProject && (globalProject.agentIds?.length ?? 0) > 0) {
query = {
$or: [{ id, $in: globalProject.agentIds }, query],
};
}
const agent = await getAgent(query);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
if (agent.author !== author) {
delete agent.author;
}
return res.status(200).json(agent);
} catch (error) {
logger.error('[/Agents/:id] Error retrieving agent', error);
res.status(500).json({ error: error.message });
}
};
/**
* Updates an Agent.
* @route PATCH /Agents/:id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @param {AgentUpdateParams} req.body - The Agent update parameters.
* @returns {Agent} 200 - success response - application/json
*/
const updateAgentHandler = async (req, res) => {
try {
const id = req.params.id;
const { projectIds, removeProjectIds, ...updateData } = req.body;
let updatedAgent;
if (Object.keys(updateData).length > 0) {
updatedAgent = await updateAgent({ id, author: req.user.id }, updateData);
}
if (projectIds || removeProjectIds) {
updatedAgent = await updateAgentProjects(id, projectIds, removeProjectIds);
}
return res.json(updatedAgent);
} catch (error) {
logger.error('[/Agents/:id] Error updating Agent', error);
res.status(500).json({ error: error.message });
}
};
/**
* Deletes an Agent based on the provided ID.
* @route DELETE /Agents/:id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 200 - success response - application/json
*/
const deleteAgentHandler = async (req, res) => {
try {
const id = req.params.id;
const agent = await getAgent({ id });
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
await deleteAgent({ id, author: req.user.id });
return res.json({ message: 'Agent deleted' });
} catch (error) {
logger.error('[/Agents/:id] Error deleting Agent', error);
res.status(500).json({ error: error.message });
}
};
/**
*
* @route GET /Agents
* @param {object} req - Express Request
* @param {object} req.query - Request query
* @param {string} [req.query.user] - The user ID of the agent's author.
* @returns {Promise<AgentListResponse>} 200 - success response - application/json
*/
const getListAgentsHandler = async (req, res) => {
try {
const data = await getListAgents({
author: req.user.id,
});
return res.json(data);
} catch (error) {
logger.error('[/Agents] Error listing Agents', error);
res.status(500).json({ error: error.message });
}
};
/**
* Uploads and updates an avatar for a specific agent.
* @route POST /avatar/:agent_id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.agent_id - The ID of the agent.
* @param {Express.Multer.File} req.file - The avatar image file.
* @param {object} req.body - Request body
* @param {string} [req.body.avatar] - Optional avatar for the agent's avatar.
* @returns {Object} 200 - success response - application/json
*/
const uploadAgentAvatarHandler = async (req, res) => {
try {
const { agent_id } = req.params;
if (!agent_id) {
return res.status(400).json({ message: 'Agent ID is required' });
}
let { avatar: _avatar = '{}' } = req.body;
const image = await uploadImageBuffer({
req,
context: FileContext.avatar,
metadata: {
buffer: req.file.buffer,
},
});
try {
_avatar = JSON.parse(_avatar);
} catch (error) {
logger.error('[/avatar/:agent_id] Error parsing avatar', error);
_avatar = {};
}
if (_avatar && _avatar.source) {
const { deleteFile } = getStrategyFunctions(_avatar.source);
try {
await deleteFile(req, { filepath: _avatar.filepath });
await deleteFileByFilter({ filepath: _avatar.filepath });
} catch (error) {
logger.error('[/avatar/:agent_id] Error deleting old avatar', error);
}
}
const promises = [];
const data = {
avatar: {
filepath: image.filepath,
source: req.app.locals.fileStrategy,
},
};
promises.push(await updateAgent({ id: agent_id, author: req.user.id }, data));
const resolved = await Promise.all(promises);
res.status(201).json(resolved[0]);
} catch (error) {
const message = 'An error occurred while updating the Agent Avatar';
logger.error(message, error);
res.status(500).json({ message });
}
};
module.exports = {
createAgent: createAgentHandler,
getAgent: getAgentHandler,
updateAgent: updateAgentHandler,
deleteAgent: deleteAgentHandler,
getListAgents: getListAgentsHandler,
uploadAgentAvatar: uploadAgentAvatarHandler,
};

View File

@@ -64,7 +64,7 @@ const _listAssistants = async ({ req, res, version, query }) => {
* @param {object} params.res - The response object, used for initializing the client.
* @param {string} params.version - The API version to use.
* @param {Omit<AssistantListParams, 'endpoint'>} params.query - The query parameters to list assistants (e.g., limit, order).
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
* @returns {Promise<Array<Assistant>>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
*/
const listAllAssistants = async ({ req, res, version, query }) => {
/** @type {{ openai: OpenAIClient }} */

View File

@@ -18,7 +18,9 @@ const createAssistant = async (req, res) => {
try {
const { openai } = await getOpenAIClient({ req, res });
const { tools = [], endpoint, ...assistantData } = req.body;
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
delete assistantData.conversation_starters;
assistantData.tools = tools
.map((tool) => {
if (typeof tool !== 'string') {
@@ -41,11 +43,22 @@ const createAssistant = async (req, res) => {
};
const assistant = await openai.beta.assistants.create(assistantData);
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
const createData = { user: req.user.id };
if (conversation_starters) {
createData.conversation_starters = conversation_starters;
}
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
if (azureModelIdentifier) {
assistant.model = azureModelIdentifier;
}
await promise;
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@@ -88,7 +101,7 @@ const patchAssistant = async (req, res) => {
await validateAuthor({ req, openai });
const assistant_id = req.params.id;
const { endpoint: _e, ...updateData } = req.body;
const { endpoint: _e, conversation_starters, ...updateData } = req.body;
updateData.tools = (updateData.tools ?? [])
.map((tool) => {
if (typeof tool !== 'string') {
@@ -104,6 +117,15 @@ const patchAssistant = async (req, res) => {
}
const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData);
if (conversation_starters !== undefined) {
const conversationStartersUpdate = await updateAssistantDoc(
{ assistant_id },
{ conversation_starters },
);
updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters;
}
res.json(updatedAssistant);
} catch (error) {
logger.error('[/assistants/:id] Error updating assistant', error);
@@ -153,6 +175,32 @@ const listAssistants = async (req, res) => {
}
};
/**
* Filter assistants based on configuration.
*
* @param {object} params - The parameters object.
* @param {string} params.userId - The user ID to filter private assistants.
* @param {AssistantDocument[]} params.assistants - The list of assistants to filter.
* @param {Partial<TAssistantEndpoint>} [params.assistantsConfig] - The assistant configuration.
* @returns {AssistantDocument[]} - The filtered list of assistants.
*/
function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
const { supportedIds, excludedIds, privateAssistants } = assistantsConfig;
const removeUserId = (doc) => {
const { user: _u, ...document } = doc;
return document;
};
if (privateAssistants) {
return documents.filter((doc) => userId === doc.user.toString()).map(removeUserId);
} else if (supportedIds?.length) {
return documents.filter((doc) => supportedIds.includes(doc.assistant_id)).map(removeUserId);
} else if (excludedIds?.length) {
return documents.filter((doc) => !excludedIds.includes(doc.assistant_id)).map(removeUserId);
}
return documents.map(removeUserId);
}
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
@@ -160,7 +208,25 @@ const listAssistants = async (req, res) => {
*/
const getAssistantDocuments = async (req, res) => {
try {
res.json(await getAssistants({ user: req.user.id }));
const endpoint = req.query;
const assistantsConfig = req.app.locals[endpoint];
const documents = await getAssistants(
{},
{
user: 1,
assistant_id: 1,
conversation_starters: 1,
createdAt: 1,
updatedAt: 1,
},
);
const docs = filterAssistantDocs({
documents,
userId: req.user.id,
assistantsConfig,
});
res.json(docs);
} catch (error) {
logger.error('[/assistants/documents] Error listing assistant documents', error);
res.status(500).json({ error: error.message });

View File

@@ -16,7 +16,9 @@ const createAssistant = async (req, res) => {
/** @type {{ openai: OpenAIClient }} */
const { openai } = await getOpenAIClient({ req, res });
const { tools = [], endpoint, ...assistantData } = req.body;
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
delete assistantData.conversation_starters;
assistantData.tools = tools
.map((tool) => {
if (typeof tool !== 'string') {
@@ -39,11 +41,22 @@ const createAssistant = async (req, res) => {
};
const assistant = await openai.beta.assistants.create(assistantData);
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
const createData = { user: req.user.id };
if (conversation_starters) {
createData.conversation_starters = conversation_starters;
}
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
if (azureModelIdentifier) {
assistant.model = azureModelIdentifier;
}
await promise;
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@@ -64,6 +77,17 @@ const createAssistant = async (req, res) => {
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
await validateAuthor({ req, openai });
const tools = [];
let conversation_starters = null;
if (updateData?.conversation_starters) {
const conversationStartersUpdate = await updateAssistantDoc(
{ assistant_id: assistant_id },
{ conversation_starters: updateData.conversation_starters },
);
conversation_starters = conversationStartersUpdate.conversation_starters;
delete updateData.conversation_starters;
}
let hasFileSearch = false;
for (const tool of updateData.tools ?? []) {
@@ -108,7 +132,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
}
return await openai.beta.assistants.update(assistant_id, updateData);
const assistant = await openai.beta.assistants.update(assistant_id, updateData);
if (conversation_starters) {
assistant.conversation_starters = conversation_starters;
}
return assistant;
};
/**

View File

@@ -7,6 +7,8 @@ const express = require('express');
const compression = require('compression');
const passport = require('passport');
const mongoSanitize = require('express-mongo-sanitize');
const fs = require('fs');
const cookieParser = require('cookie-parser');
const { jwtLogin, passportLogin } = require('~/strategies');
const { connectDb, indexSync } = require('~/lib/db');
const { isEnabled } = require('~/server/utils');
@@ -37,6 +39,9 @@ const startServer = async () => {
app.disable('x-powered-by');
await AppService(app);
const indexPath = path.join(app.locals.paths.dist, 'index.html');
const indexHTML = fs.readFileSync(indexPath, 'utf8');
app.get('/health', (_req, res) => res.status(200).send('OK'));
/* Middleware */
@@ -50,6 +55,7 @@ const startServer = async () => {
app.use(staticCache(app.locals.paths.assets));
app.set('trust proxy', 1); /* trust first proxy */
app.use(cors());
app.use(cookieParser());
if (!isEnabled(DISABLE_COMPRESSION)) {
app.use(compression());
@@ -99,10 +105,16 @@ const startServer = async () => {
app.use('/images/', validateImageRequest, routes.staticRoute);
app.use('/api/share', routes.share);
app.use('/api/roles', routes.roles);
app.use('/api/agents', routes.agents);
app.use('/api/bedrock', routes.bedrock);
app.use('/api/tags', routes.tags);
app.use((req, res) => {
res.sendFile(path.join(app.locals.paths.dist, 'index.html'));
// Replace lang attribute in index.html with lang from cookies or accept-language header
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
const updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${lang}"`);
res.send(updatedIndexHtml);
});
app.listen(port, host, () => {

View File

@@ -1,4 +1,4 @@
const { isAssistantsEndpoint } = require('librechat-data-provider');
const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider');
const { sendMessage, sendError, countTokens, isEnabled } = require('~/server/utils');
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
const clearPendingReq = require('~/cache/clearPendingReq');
@@ -107,7 +107,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
finish_reason: 'incomplete',
endpoint: endpointOption.endpoint,
iconURL: endpointOption.iconURL,
model: endpointOption.modelOptions.model,
model: endpointOption.modelOptions?.model ?? endpointOption.model_parameters?.model,
unfinished: false,
error: false,
isCreatedByUser: false,
@@ -165,10 +165,14 @@ const handleAbortError = async (res, req, error, data) => {
);
}
const errorText = error?.message?.includes('"type"')
let errorText = error?.message?.includes('"type"')
? error.message
: 'An error occurred while processing your request. Please contact the Admin.';
if (error?.type === ErrorTypes.INVALID_REQUEST) {
errorText = `{"type":"${ErrorTypes.INVALID_REQUEST}"}`;
}
const respondWithError = async (partialText) => {
let options = {
sender,

View File

@@ -1,11 +1,13 @@
const { parseCompactConvo, EModelEndpoint } = require('librechat-data-provider');
const { parseCompactConvo, EModelEndpoint, isAgentsEndpoint } = require('librechat-data-provider');
const { getModelsConfig } = require('~/server/controllers/ModelController');
const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
const assistants = require('~/server/services/Endpoints/assistants');
const gptPlugins = require('~/server/services/Endpoints/gptPlugins');
const { processFiles } = require('~/server/services/Files/process');
const anthropic = require('~/server/services/Endpoints/anthropic');
const bedrock = require('~/server/services/Endpoints/bedrock');
const openAI = require('~/server/services/Endpoints/openAI');
const agents = require('~/server/services/Endpoints/agents');
const custom = require('~/server/services/Endpoints/custom');
const google = require('~/server/services/Endpoints/google');
const enforceModelSpec = require('./enforceModelSpec');
@@ -15,6 +17,8 @@ const buildFunction = {
[EModelEndpoint.openAI]: openAI.buildOptions,
[EModelEndpoint.google]: google.buildOptions,
[EModelEndpoint.custom]: custom.buildOptions,
[EModelEndpoint.agents]: agents.buildOptions,
[EModelEndpoint.bedrock]: bedrock.buildOptions,
[EModelEndpoint.azureOpenAI]: openAI.buildOptions,
[EModelEndpoint.anthropic]: anthropic.buildOptions,
[EModelEndpoint.gptPlugins]: gptPlugins.buildOptions,
@@ -59,12 +63,13 @@ async function buildEndpointOption(req, res, next) {
}
}
req.body.endpointOption = buildFunction[endpointType ?? endpoint](
endpoint,
parsedBody,
endpointType,
);
const endpointFn = buildFunction[endpointType ?? endpoint];
const builder = isAgentsEndpoint(endpoint) ? (...args) => endpointFn(req, ...args) : endpointFn;
// TODO: use object params
req.body.endpointOption = builder(endpoint, parsedBody, endpointType);
// TODO: use `getModelsConfig` only when necessary
const modelsConfig = await getModelsConfig(req);
req.body.endpointOption.modelsConfig = modelsConfig;

View File

@@ -1,4 +1,3 @@
const { SystemRoles } = require('librechat-data-provider');
const { getRoleByName } = require('~/models/Role');
/**
@@ -17,10 +16,6 @@ const generateCheckAccess = (permissionType, permissions, bodyProps = {}) => {
return res.status(401).json({ message: 'Authorization required' });
}
if (user.role === SystemRoles.ADMIN) {
return next();
}
const role = await getRoleByName(user.role);
if (role && role[permissionType]) {
const hasAnyPermission = permissions.some((permission) => {

View File

@@ -0,0 +1,166 @@
const express = require('express');
const { nanoid } = require('nanoid');
const { actionDelimiter } = require('librechat-data-provider');
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
const { updateAction, getActions, deleteAction } = require('~/models/Action');
const { getAgent, updateAgent } = require('~/models/Agent');
const { logger } = require('~/config');
const router = express.Router();
/**
* Retrieves all user's actions
* @route GET /actions/
* @param {string} req.params.id - Assistant identifier.
* @returns {Action[]} 200 - success response - application/json
*/
router.get('/', async (req, res) => {
try {
res.json(await getActions({ user: req.user.id }));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* Adds or updates actions for a specific agent.
* @route POST /actions/:agent_id
* @param {string} req.params.agent_id - The ID of the agent.
* @param {FunctionTool[]} req.body.functions - The functions to be added or updated.
* @param {string} [req.body.action_id] - Optional ID for the action.
* @param {ActionMetadata} req.body.metadata - Metadata for the action.
* @returns {Object} 200 - success response - application/json
*/
router.post('/:agent_id', async (req, res) => {
try {
const { agent_id } = req.params;
/** @type {{ functions: FunctionTool[], action_id: string, metadata: ActionMetadata }} */
const { functions, action_id: _action_id, metadata: _metadata } = req.body;
if (!functions.length) {
return res.status(400).json({ message: 'No functions provided' });
}
let metadata = await encryptMetadata(_metadata);
let { domain } = metadata;
domain = await domainParser(req, domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });
}
const action_id = _action_id ?? nanoid();
const initialPromises = [];
// TODO: share agents
initialPromises.push(getAgent({ id: agent_id, author: req.user.id }));
if (_action_id) {
initialPromises.push(getActions({ action_id }, true));
}
/** @type {[Agent, [Action|undefined]]} */
const [agent, actions_result] = await Promise.all(initialPromises);
if (!agent) {
return res.status(404).json({ message: 'Agent not found for adding action' });
}
if (actions_result && actions_result.length) {
const action = actions_result[0];
metadata = { ...action.metadata, ...metadata };
}
const { actions: _actions = [] } = agent ?? {};
const actions = [];
for (const action of _actions) {
const [_action_domain, current_action_id] = action.split(actionDelimiter);
if (current_action_id === action_id) {
continue;
}
actions.push(action);
}
actions.push(`${domain}${actionDelimiter}${action_id}`);
/** @type {string[]}} */
const { tools: _tools = [] } = agent;
const tools = _tools
.filter((tool) => !(tool && (tool.includes(domain) || tool.includes(action_id))))
.concat(functions.map((tool) => `${tool.function.name}${actionDelimiter}${domain}`));
const updatedAgent = await updateAgent(
{ id: agent_id, author: req.user.id },
{ tools, actions },
);
/** @type {[Action]} */
const updatedAction = await updateAction(
{ action_id },
{ metadata, agent_id, user: req.user.id },
);
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
for (let field of sensitiveFields) {
if (updatedAction.metadata[field]) {
delete updatedAction.metadata[field];
}
}
res.json([updatedAgent, updatedAction]);
} catch (error) {
const message = 'Trouble updating the Agent Action';
logger.error(message, error);
res.status(500).json({ message });
}
});
/**
* Deletes an action for a specific agent.
* @route DELETE /actions/:agent_id/:action_id
* @param {string} req.params.agent_id - The ID of the agent.
* @param {string} req.params.action_id - The ID of the action to delete.
* @returns {Object} 200 - success response - application/json
*/
router.delete('/:agent_id/:action_id', async (req, res) => {
try {
const { agent_id, action_id } = req.params;
const agent = await getAgent({ id: agent_id, author: req.user.id });
if (!agent) {
return res.status(404).json({ message: 'Agent not found for deleting action' });
}
const { tools = [], actions = [] } = agent;
let domain = '';
const updatedActions = actions.filter((action) => {
if (action.includes(action_id)) {
[domain] = action.split(actionDelimiter);
return false;
}
return true;
});
domain = await domainParser(req, domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });
}
const updatedTools = tools.filter((tool) => !(tool && tool.includes(domain)));
await updateAgent(
{ id: agent_id, author: req.user.id },
{ tools: updatedTools, actions: updatedActions },
);
await deleteAction({ action_id });
res.status(200).json({ message: 'Action deleted successfully' });
} catch (error) {
const message = 'Trouble deleting the Agent Action';
logger.error(message, error);
res.status(500).json({ message });
}
});
module.exports = router;

View File

@@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
const {
setHeaders,
handleAbort,
// validateModel,
// validateEndpoint,
buildEndpointOption,
} = require('~/server/middleware');
const { initializeClient } = require('~/server/services/Endpoints/agents');
const AgentController = require('~/server/controllers/agents/request');
router.post('/abort', handleAbort());
/**
* @route POST /
* @desc Chat with an assistant
* @access Public
* @param {express.Request} req - The request object, containing the request data.
* @param {express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post(
'/',
// validateModel,
// validateEndpoint,
buildEndpointOption,
setHeaders,
async (req, res, next) => {
await AgentController(req, res, next, initializeClient);
},
);
module.exports = router;

View File

@@ -0,0 +1,21 @@
const express = require('express');
const router = express.Router();
const {
uaParser,
checkBan,
requireJwtAuth,
// concurrentLimiter,
// messageIpLimiter,
// messageUserLimiter,
} = require('~/server/middleware');
const v1 = require('./v1');
const chat = require('./chat');
router.use(requireJwtAuth);
router.use(checkBan);
router.use(uaParser);
router.use('/', v1);
router.use('/chat', chat);
module.exports = router;

View File

@@ -0,0 +1,94 @@
const multer = require('multer');
const express = require('express');
const { PermissionTypes, Permissions } = require('librechat-data-provider');
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
const v1 = require('~/server/controllers/agents/v1');
const actions = require('./actions');
const upload = multer();
const router = express.Router();
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
const checkAgentCreate = generateCheckAccess(PermissionTypes.AGENTS, [
Permissions.USE,
Permissions.CREATE,
]);
const checkGlobalAgentShare = generateCheckAccess(
PermissionTypes.AGENTS,
[Permissions.USE, Permissions.CREATE],
{
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
},
);
router.use(requireJwtAuth);
router.use(checkAgentAccess);
/**
* Agent actions route.
* @route GET|POST /agents/actions
*/
router.use('/actions', actions);
/**
* Get a list of available tools for agents.
* @route GET /agents/tools
* @returns {TPlugin[]} 200 - application/json
*/
router.use('/tools', (req, res) => {
res.json([]);
});
/**
* Creates an agent.
* @route POST /agents
* @param {AgentCreateParams} req.body - The agent creation parameters.
* @returns {Agent} 201 - Success response - application/json
*/
router.post('/', checkAgentCreate, v1.createAgent);
/**
* Retrieves an agent.
* @route GET /agents/:id
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 200 - Success response - application/json
*/
router.get('/:id', checkAgentAccess, v1.getAgent);
/**
* Updates an agent.
* @route PATCH /agents/:id
* @param {string} req.params.id - Agent identifier.
* @param {AgentUpdateParams} req.body - The agent update parameters.
* @returns {Agent} 200 - Success response - application/json
*/
router.patch('/:id', checkGlobalAgentShare, v1.updateAgent);
/**
* Deletes an agent.
* @route DELETE /agents/:id
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 200 - success response - application/json
*/
router.delete('/:id', checkAgentCreate, v1.deleteAgent);
/**
* Returns a list of agents.
* @route GET /agents
* @param {AgentListParams} req.query - The agent list parameters for pagination and sorting.
* @returns {AgentListResponse} 200 - success response - application/json
*/
router.get('/', checkAgentAccess, v1.getListAgents);
/**
* Uploads and updates an avatar for a specific agent.
* @route POST /avatar/:agent_id
* @param {string} req.params.agent_id - The ID of the agent.
* @param {Express.Multer.File} req.file - The avatar image file.
* @param {string} [req.body.metadata] - Optional metadata for the agent's avatar.
* @returns {Object} 200 - success response - application/json
*/
router.post('/avatar/:agent_id', checkAgentAccess, upload.single('file'), v1.uploadAgentAvatar);
module.exports = router;

View File

@@ -1,5 +1,5 @@
const { v4 } = require('uuid');
const express = require('express');
const { nanoid } = require('nanoid');
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
const { actionDelimiter, EModelEndpoint } = require('librechat-data-provider');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
@@ -9,20 +9,6 @@ const { logger } = require('~/config');
const router = express.Router();
/**
* Retrieves all user's actions
* @route GET /actions/
* @param {string} req.params.id - Assistant identifier.
* @returns {Action[]} 200 - success response - application/json
*/
router.get('/', async (req, res) => {
try {
res.json(await getActions());
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* Adds or updates actions for a specific assistant.
* @route POST /actions/:assistant_id
@@ -51,7 +37,7 @@ router.post('/:assistant_id', async (req, res) => {
return res.status(400).json({ message: 'No domain provided' });
}
const action_id = _action_id ?? v4();
const action_id = _action_id ?? nanoid();
const initialPromises = [];
const { openai } = await getOpenAIClient({ req, res });
@@ -178,6 +164,10 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => {
domain = await domainParser(req, domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });
}
const updatedTools = tools.filter(
(tool) => !(tool.function && tool.function.name.includes(domain)),
);

View File

@@ -0,0 +1,13 @@
const express = require('express');
const controllers = require('~/server/controllers/assistants/v1');
const router = express.Router();
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - success response - application/json
*/
router.get('/', controllers.getAssistantDocuments);
module.exports = router;

View File

@@ -1,6 +1,7 @@
const multer = require('multer');
const express = require('express');
const controllers = require('~/server/controllers/assistants/v1');
const documents = require('./documents');
const actions = require('./actions');
const tools = require('./tools');
@@ -20,6 +21,13 @@ router.use('/actions', actions);
*/
router.use('/tools', tools);
/**
* Create an assistant.
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - application/json
*/
router.use('/documents', documents);
/**
* Create an assistant.
* @route POST /assistants
@@ -61,13 +69,6 @@ router.delete('/:id', controllers.deleteAssistant);
*/
router.get('/', controllers.listAssistants);
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - success response - application/json
*/
router.get('/documents', controllers.getAssistantDocuments);
/**
* Uploads and updates an avatar for a specific assistant.
* @route POST /avatar/:assistant_id

View File

@@ -2,6 +2,7 @@ const multer = require('multer');
const express = require('express');
const v1 = require('~/server/controllers/assistants/v1');
const v2 = require('~/server/controllers/assistants/v2');
const documents = require('./documents');
const actions = require('./actions');
const tools = require('./tools');
@@ -21,6 +22,13 @@ router.use('/actions', actions);
*/
router.use('/tools', tools);
/**
* Create an assistant.
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - application/json
*/
router.use('/documents', documents);
/**
* Create an assistant.
* @route POST /assistants
@@ -62,13 +70,6 @@ router.delete('/:id', v1.deleteAssistant);
*/
router.get('/', v1.listAssistants);
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - success response - application/json
*/
router.get('/documents', v1.getAssistantDocuments);
/**
* Uploads and updates an avatar for a specific assistant.
* @route POST /avatar/:assistant_id

View File

@@ -0,0 +1,36 @@
const express = require('express');
const router = express.Router();
const {
setHeaders,
handleAbort,
// validateModel,
// validateEndpoint,
buildEndpointOption,
} = require('~/server/middleware');
const { initializeClient } = require('~/server/services/Endpoints/bedrock');
const AgentController = require('~/server/controllers/agents/request');
const addTitle = require('~/server/services/Endpoints/bedrock/title');
router.post('/abort', handleAbort());
/**
* @route POST /
* @desc Chat with an assistant
* @access Public
* @param {express.Request} req - The request object, containing the request data.
* @param {express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post(
'/',
// validateModel,
// validateEndpoint,
buildEndpointOption,
setHeaders,
async (req, res, next) => {
await AgentController(req, res, next, initializeClient, addTitle);
},
);
module.exports = router;

View File

@@ -0,0 +1,19 @@
const express = require('express');
const router = express.Router();
const {
uaParser,
checkBan,
requireJwtAuth,
// concurrentLimiter,
// messageIpLimiter,
// messageUserLimiter,
} = require('~/server/middleware');
const chat = require('./chat');
router.use(requireJwtAuth);
router.use(checkBan);
router.use(uaParser);
router.use('/chat', chat);
module.exports = router;

View File

@@ -1,5 +1,5 @@
const express = require('express');
const { CacheKeys, defaultSocialLogins } = require('librechat-data-provider');
const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider');
const { getLdapConfig } = require('~/server/services/Config/ldap');
const { getProjectByName } = require('~/models/Project');
const { isEnabled } = require('~/server/utils');
@@ -32,7 +32,7 @@ router.get('/', async function (req, res) {
return today.getMonth() === 1 && today.getDate() === 11;
};
const instanceProject = await getProjectByName('instance', '_id');
const instanceProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, '_id');
const ldap = getLdapConfig();

View File

@@ -1,51 +1,55 @@
const ask = require('./ask');
const edit = require('./edit');
const assistants = require('./assistants');
const categories = require('./categories');
const tokenizer = require('./tokenizer');
const endpoints = require('./endpoints');
const staticRoute = require('./static');
const messages = require('./messages');
const convos = require('./convos');
const presets = require('./presets');
const prompts = require('./prompts');
const search = require('./search');
const tokenizer = require('./tokenizer');
const auth = require('./auth');
const keys = require('./keys');
const oauth = require('./oauth');
const endpoints = require('./endpoints');
const balance = require('./balance');
const models = require('./models');
const plugins = require('./plugins');
const user = require('./user');
const bedrock = require('./bedrock');
const search = require('./search');
const models = require('./models');
const convos = require('./convos');
const config = require('./config');
const assistants = require('./assistants');
const files = require('./files');
const staticRoute = require('./static');
const share = require('./share');
const categories = require('./categories');
const agents = require('./agents');
const roles = require('./roles');
const oauth = require('./oauth');
const files = require('./files');
const share = require('./share');
const tags = require('./tags');
const auth = require('./auth');
const edit = require('./edit');
const keys = require('./keys');
const user = require('./user');
const ask = require('./ask');
module.exports = {
search,
ask,
edit,
messages,
convos,
presets,
prompts,
auth,
keys,
oauth,
user,
tokenizer,
endpoints,
balance,
tags,
roles,
oauth,
files,
share,
agents,
bedrock,
convos,
search,
prompts,
config,
models,
plugins,
config,
presets,
balance,
messages,
endpoints,
tokenizer,
assistants,
files,
staticRoute,
share,
categories,
roles,
tags,
staticRoute,
};

View File

@@ -1,4 +1,5 @@
const express = require('express');
const { ContentTypes } = require('librechat-data-provider');
const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models');
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
const { countTokens } = require('~/server/utils');
@@ -54,11 +55,50 @@ router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) =
router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
try {
const { messageId, model } = req.params;
const { text } = req.body;
const tokenCount = await countTokens(text, model);
const result = await updateMessage(req, { messageId, text, tokenCount });
res.status(200).json(result);
const { conversationId, messageId } = req.params;
const { text, index, model } = req.body;
if (index === undefined) {
const tokenCount = await countTokens(text, model);
const result = await updateMessage(req, { messageId, text, tokenCount });
return res.status(200).json(result);
}
if (typeof index !== 'number' || index < 0) {
return res.status(400).json({ error: 'Invalid index' });
}
const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0];
if (!message) {
return res.status(404).json({ error: 'Message not found' });
}
const existingContent = message.content;
if (!Array.isArray(existingContent) || index >= existingContent.length) {
return res.status(400).json({ error: 'Invalid index' });
}
const updatedContent = [...existingContent];
if (!updatedContent[index]) {
return res.status(400).json({ error: 'Content part not found' });
}
if (updatedContent[index].type !== ContentTypes.TEXT) {
return res.status(400).json({ error: 'Cannot update non-text content' });
}
const oldText = updatedContent[index].text;
updatedContent[index] = { type: ContentTypes.TEXT, text };
let tokenCount = message.tokenCount;
if (tokenCount !== undefined) {
const oldTokenCount = await countTokens(oldText, model);
const newTokenCount = await countTokens(text, model);
tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount;
}
const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount });
return res.status(200).json(result);
} catch (error) {
logger.error('Error updating message:', error);
res.status(500).json({ error: 'Internal server error' });

View File

@@ -24,6 +24,7 @@ const checkPromptCreate = generateCheckAccess(PermissionTypes.PROMPTS, [
Permissions.USE,
Permissions.CREATE,
]);
const checkGlobalPromptShare = generateCheckAccess(
PermissionTypes.PROMPTS,
[Permissions.USE, Permissions.CREATE],

View File

@@ -20,7 +20,10 @@ router.get('/:roleName', async (req, res) => {
// TODO: TEMP, use a better parsing for roleName
const roleName = _r.toUpperCase();
if (req.user.role !== SystemRoles.ADMIN && !roleDefaults[roleName]) {
if (
(req.user.role !== SystemRoles.ADMIN && roleName === SystemRoles.ADMIN) ||
(req.user.role !== SystemRoles.ADMIN && !roleDefaults[roleName])
) {
return res.status(403).send({ message: 'Unauthorized' });
}

View File

@@ -6,11 +6,15 @@ const {
verifyEmailController,
updateUserPluginsController,
resendVerificationController,
getTermsStatusController,
acceptTermsController,
} = require('~/server/controllers/UserController');
const router = express.Router();
router.get('/', requireJwtAuth, getUserController);
router.get('/terms', requireJwtAuth, getTermsStatusController);
router.post('/terms/accept', requireJwtAuth, acceptTermsController);
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
router.post('/verify', verifyEmailController);

View File

@@ -6,6 +6,7 @@ const {
isImageVisionTool,
actionDomainSeparator,
} = require('librechat-data-provider');
const { tool } = require('@langchain/core/tools');
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
const { getActions, deleteActions } = require('~/models/Action');
const { deleteAssistant } = require('~/models/Assistant');
@@ -101,7 +102,8 @@ async function domainParser(req, domain, inverse = false) {
*
* @param {Object} searchParams - The parameters for loading action sets.
* @param {string} searchParams.user - The user identifier.
* @param {string} searchParams.assistant_id - The assistant identifier.
* @param {string} [searchParams.agent_id]- The agent identifier.
* @param {string} [searchParams.assistant_id]- The assistant identifier.
* @returns {Promise<Action[] | null>} A promise that resolves to an array of actions or `null` if no match.
*/
async function loadActionSets(searchParams) {
@@ -114,10 +116,14 @@ async function loadActionSets(searchParams) {
* @param {Object} params - The parameters for loading action sets.
* @param {Action} params.action - The action set. Necessary for decrypting authentication values.
* @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call.
* @returns { { _call: (toolInput: Object) => unknown} } An object with `_call` method to execute the tool input.
* @param {string | undefined} [params.name] - The name of the tool.
* @param {string | undefined} [params.description] - The description for the tool.
* @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition
* @returns { Promsie<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
*/
async function createActionTool({ action, requestBuilder }) {
async function createActionTool({ action, requestBuilder, zodSchema, name, description }) {
action.metadata = await decryptMetadata(action.metadata);
/** @type {(toolInput: Object | string) => Promise<unknown>} */
const _call = async (toolInput) => {
try {
requestBuilder.setParams(toolInput);
@@ -142,6 +148,14 @@ async function createActionTool({ action, requestBuilder }) {
}
};
if (name) {
return tool(_call, {
name,
description: description || '',
schema: zodSchema,
});
}
return {
_call,
};
@@ -151,7 +165,7 @@ async function createActionTool({ action, requestBuilder }) {
* Encrypts sensitive metadata values for an action.
*
* @param {ActionMetadata} metadata - The action metadata to encrypt.
* @returns {ActionMetadata} The updated action metadata with encrypted values.
* @returns {Promise<ActionMetadata>} The updated action metadata with encrypted values.
*/
async function encryptMetadata(metadata) {
const encryptedMetadata = { ...metadata };
@@ -180,7 +194,7 @@ async function encryptMetadata(metadata) {
* Decrypts sensitive metadata values for an action.
*
* @param {ActionMetadata} metadata - The action metadata to decrypt.
* @returns {ActionMetadata} The updated action metadata with decrypted values.
* @returns {Promise<ActionMetadata>} The updated action metadata with decrypted values.
*/
async function decryptMetadata(metadata) {
const decryptedMetadata = { ...metadata };

View File

@@ -94,18 +94,19 @@ const AppService = async (app) => {
);
}
if (endpoints?.[EModelEndpoint.openAI]) {
endpointLocals[EModelEndpoint.openAI] = endpoints[EModelEndpoint.openAI];
}
if (endpoints?.[EModelEndpoint.google]) {
endpointLocals[EModelEndpoint.google] = endpoints[EModelEndpoint.google];
}
if (endpoints?.[EModelEndpoint.anthropic]) {
endpointLocals[EModelEndpoint.anthropic] = endpoints[EModelEndpoint.anthropic];
}
if (endpoints?.[EModelEndpoint.gptPlugins]) {
endpointLocals[EModelEndpoint.gptPlugins] = endpoints[EModelEndpoint.gptPlugins];
}
const endpointKeys = [
EModelEndpoint.openAI,
EModelEndpoint.google,
EModelEndpoint.bedrock,
EModelEndpoint.anthropic,
EModelEndpoint.gptPlugins,
];
endpointKeys.forEach((key) => {
if (endpoints?.[key]) {
endpointLocals[key] = endpoints[key];
}
});
app.locals = {
...defaultLocals,

View File

@@ -45,5 +45,8 @@ module.exports = {
AZURE_ASSISTANTS_BASE_URL,
EModelEndpoint.azureAssistants,
),
[EModelEndpoint.bedrock]: generateConfig(process.env.BEDROCK_AWS_SECRET_ACCESS_KEY),
/* key will be part of separate config */
[EModelEndpoint.agents]: generateConfig(process.env.I_AM_A_TEAPOT),
},
};

View File

@@ -9,13 +9,13 @@ const { config } = require('./EndpointService');
*/
async function loadDefaultEndpointsConfig(req) {
const { google, gptPlugins } = await loadAsyncEndpoints(req);
const { openAI, assistants, azureAssistants, bingAI, anthropic, azureOpenAI, chatGPTBrowser } =
config;
const { assistants, azureAssistants, bingAI, azureOpenAI, chatGPTBrowser } = config;
const enabledEndpoints = getEnabledEndpoints();
const endpointConfig = {
[EModelEndpoint.openAI]: openAI,
[EModelEndpoint.openAI]: config[EModelEndpoint.openAI],
[EModelEndpoint.agents]: config[EModelEndpoint.agents],
[EModelEndpoint.assistants]: assistants,
[EModelEndpoint.azureAssistants]: azureAssistants,
[EModelEndpoint.azureOpenAI]: azureOpenAI,
@@ -23,7 +23,8 @@ async function loadDefaultEndpointsConfig(req) {
[EModelEndpoint.bingAI]: bingAI,
[EModelEndpoint.chatGPTBrowser]: chatGPTBrowser,
[EModelEndpoint.gptPlugins]: gptPlugins,
[EModelEndpoint.anthropic]: anthropic,
[EModelEndpoint.anthropic]: config[EModelEndpoint.anthropic],
[EModelEndpoint.bedrock]: config[EModelEndpoint.bedrock],
};
const orderedAndFilteredEndpoints = enabledEndpoints.reduce((config, key, index) => {

View File

@@ -3,6 +3,7 @@ const { useAzurePlugins } = require('~/server/services/Config/EndpointService').
const {
getOpenAIModels,
getGoogleModels,
getBedrockModels,
getAnthropicModels,
getChatGPTBrowserModels,
} = require('~/server/services/ModelService');
@@ -29,6 +30,7 @@ async function loadDefaultModels(req) {
return {
[EModelEndpoint.openAI]: openAI,
[EModelEndpoint.agents]: openAI,
[EModelEndpoint.google]: google,
[EModelEndpoint.anthropic]: anthropic,
[EModelEndpoint.gptPlugins]: gptPlugins,
@@ -37,6 +39,7 @@ async function loadDefaultModels(req) {
[EModelEndpoint.chatGPTBrowser]: chatGPTBrowser,
[EModelEndpoint.assistants]: assistants,
[EModelEndpoint.azureAssistants]: azureAssistants,
[EModelEndpoint.bedrock]: getBedrockModels(),
};
}

View File

@@ -0,0 +1,28 @@
const { getAgent } = require('~/models/Agent');
const { logger } = require('~/config');
const buildOptions = (req, endpoint, parsedBody) => {
const { agent_id, instructions, spec, ...model_parameters } = parsedBody;
const agentPromise = getAgent({
id: agent_id,
// TODO: better author handling
author: req.user.id,
}).catch((error) => {
logger.error(`[/agents/:${agent_id}] Error retrieving agent during build options step`, error);
return undefined;
});
const endpointOption = {
agent: agentPromise,
endpoint,
agent_id,
instructions,
spec,
model_parameters,
};
return endpointOption;
};
module.exports = { buildOptions };

View File

@@ -0,0 +1,7 @@
const build = require('./build');
const initialize = require('./initialize');
module.exports = {
...build,
...initialize,
};

View File

@@ -0,0 +1,133 @@
// const {
// ErrorTypes,
// EModelEndpoint,
// resolveHeaders,
// mapModelToAzureConfig,
// } = require('librechat-data-provider');
// const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
// const { isEnabled, isUserProvided } = require('~/server/utils');
// const { getAzureCredentials } = require('~/utils');
// const { OpenAIClient } = require('~/app');
const { z } = require('zod');
const { tool } = require('@langchain/core/tools');
const { createContentAggregator } = require('@librechat/agents');
const {
EModelEndpoint,
providerEndpointMap,
getResponseSender,
} = require('librechat-data-provider');
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
// for testing purposes
// const createTavilySearchTool = require('~/app/clients/tools/structured/TavilySearch');
const initAnthropic = require('~/server/services/Endpoints/anthropic/initializeClient');
const initOpenAI = require('~/server/services/Endpoints/openAI/initializeClient');
const { loadAgentTools } = require('~/server/services/ToolService');
const AgentClient = require('~/server/controllers/agents/client');
const { getModelMaxTokens } = require('~/utils');
/* For testing errors */
const _getWeather = tool(
async ({ location }) => {
if (location === 'SAN FRANCISCO') {
return 'It\'s 60 degrees and foggy';
} else if (location.toLowerCase() === 'san francisco') {
throw new Error('Input queries must be all capitals');
} else {
throw new Error('Invalid input.');
}
},
{
name: 'get_weather',
description: 'Call to get the current weather',
schema: z.object({
location: z.string(),
}),
},
);
const providerConfigMap = {
[EModelEndpoint.openAI]: initOpenAI,
[EModelEndpoint.azureOpenAI]: initOpenAI,
[EModelEndpoint.anthropic]: initAnthropic,
};
const initializeClient = async ({ req, res, endpointOption }) => {
if (!endpointOption) {
throw new Error('Endpoint option not provided');
}
// TODO: use endpointOption to determine options/modelOptions
const { contentParts, aggregateContent } = createContentAggregator();
const eventHandlers = getDefaultHandlers({ res, aggregateContent });
// const tools = [createTavilySearchTool()];
// const tools = [_getWeather];
// const tool_calls = [{ name: 'getPeople_action_swapi---dev' }];
// const tool_calls = [{ name: 'dalle' }];
// const tool_calls = [{ name: 'getItmOptions_action_YWlhcGkzLn' }];
// const tool_calls = [{ name: 'tavily_search_results_json' }];
// const tool_calls = [
// { name: 'searchListings_action_emlsbG93NT' },
// { name: 'searchAddress_action_emlsbG93NT' },
// { name: 'searchMLS_action_emlsbG93NT' },
// { name: 'searchCoordinates_action_emlsbG93NT' },
// { name: 'searchUrl_action_emlsbG93NT' },
// { name: 'getPropertyDetails_action_emlsbG93NT' },
// ];
if (!endpointOption.agent) {
throw new Error('No agent promise provided');
}
/** @type {Agent} */
const agent = await endpointOption.agent;
const { tools, toolMap } = await loadAgentTools({
req,
tools: agent.tools,
agent_id: agent.id,
// openAIApiKey: process.env.OPENAI_API_KEY,
});
let modelOptions = { model: agent.model };
const getOptions = providerConfigMap[agent.provider];
if (!getOptions) {
throw new Error(`Provider ${agent.provider} not supported`);
}
// TODO: pass-in override settings that are specific to current run
endpointOption.model_parameters.model = agent.model;
const options = await getOptions({
req,
res,
endpointOption,
optionsOnly: true,
overrideEndpoint: agent.provider,
overrideModel: agent.model,
});
modelOptions = Object.assign(modelOptions, options.llmConfig);
const sender = getResponseSender({
...endpointOption,
model: endpointOption.model_parameters.model,
});
const client = new AgentClient({
req,
agent,
tools,
sender,
toolMap,
contentParts,
modelOptions,
eventHandlers,
endpoint: EModelEndpoint.agents,
configOptions: options.configOptions,
maxContextTokens:
agent.max_context_tokens ??
getModelMaxTokens(modelOptions.model, providerEndpointMap[agent.provider]),
});
return { client };
};
module.exports = { initializeClient };

View File

@@ -23,7 +23,7 @@ const addTitle = async (req, { text, response, client }) => {
const title = await client.titleConvo({
text,
responseText: response?.text,
responseText: response?.text ?? '',
conversationId: response.conversationId,
});
await titleCache.set(key, title, 120000);

View File

@@ -1,8 +1,9 @@
const { EModelEndpoint } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
const { AnthropicClient } = require('~/app');
const initializeClient = async ({ req, res, endpointOption }) => {
const initializeClient = async ({ req, res, endpointOption, optionsOnly }) => {
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
const expiresAt = req.body.key;
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
@@ -34,6 +35,18 @@ const initializeClient = async ({ req, res, endpointOption }) => {
clientOptions.streamRate = allConfig.streamRate;
}
if (optionsOnly) {
const requestOptions = Object.assign(
{
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
proxy: PROXY ?? null,
modelOptions: endpointOption.modelOptions,
},
clientOptions,
);
return getLLMConfig(anthropicApiKey, requestOptions);
}
const client = new AnthropicClient(anthropicApiKey, {
req,
res,

View File

@@ -0,0 +1,55 @@
const { HttpsProxyAgent } = require('https-proxy-agent');
const { anthropicSettings, removeNullishValues } = require('librechat-data-provider');
/**
* Generates configuration options for creating an Anthropic language model (LLM) instance.
*
* @param {string} apiKey - The API key for authentication with Anthropic.
* @param {Object} [options={}] - Additional options for configuring the LLM.
* @param {Object} [options.modelOptions] - Model-specific options.
* @param {string} [options.modelOptions.model] - The name of the model to use.
* @param {number} [options.modelOptions.maxOutputTokens] - The maximum number of tokens to generate.
* @param {number} [options.modelOptions.temperature] - Controls randomness in output generation.
* @param {number} [options.modelOptions.topP] - Controls diversity of output generation.
* @param {number} [options.modelOptions.topK] - Controls the number of top tokens to consider.
* @param {string[]} [options.modelOptions.stop] - Sequences where the API will stop generating further tokens.
* @param {boolean} [options.modelOptions.stream] - Whether to stream the response.
* @param {string} [options.proxy] - Proxy server URL.
* @param {string} [options.reverseProxyUrl] - URL for a reverse proxy, if used.
*
* @returns {Object} Configuration options for creating an Anthropic LLM instance, with null and undefined values removed.
*/
function getLLMConfig(apiKey, options = {}) {
const defaultOptions = {
model: anthropicSettings.model.default,
maxOutputTokens: anthropicSettings.maxOutputTokens.default,
stream: true,
};
const mergedOptions = Object.assign(defaultOptions, options.modelOptions);
const requestOptions = {
apiKey,
model: mergedOptions.model,
stream: mergedOptions.stream,
temperature: mergedOptions.temperature,
top_p: mergedOptions.topP,
top_k: mergedOptions.topK,
stop_sequences: mergedOptions.stop,
max_tokens:
mergedOptions.maxOutputTokens || anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
};
const configOptions = {};
if (options.proxy) {
configOptions.httpAgent = new HttpsProxyAgent(options.proxy);
}
if (options.reverseProxyUrl) {
configOptions.baseURL = options.reverseProxyUrl;
}
return { llmConfig: removeNullishValues(requestOptions), configOptions };
}
module.exports = { getLLMConfig };

View File

@@ -0,0 +1,44 @@
const { removeNullishValues, bedrockInputParser } = require('librechat-data-provider');
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { logger } = require('~/config');
const buildOptions = (endpoint, parsedBody) => {
const {
modelLabel: name,
promptPrefix,
maxContextTokens,
resendFiles = true,
imageDetail,
iconURL,
greeting,
spec,
artifacts,
...model_parameters
} = parsedBody;
let parsedParams = model_parameters;
try {
parsedParams = bedrockInputParser.parse(model_parameters);
} catch (error) {
logger.warn('Failed to parse bedrock input', error);
}
const endpointOption = removeNullishValues({
endpoint,
name,
resendFiles,
imageDetail,
iconURL,
greeting,
spec,
promptPrefix,
maxContextTokens,
model_parameters: parsedParams,
});
if (typeof artifacts === 'string') {
endpointOption.artifactsPrompt = generateArtifactsPrompt({ endpoint, artifacts });
}
return endpointOption;
};
module.exports = { buildOptions };

View File

@@ -0,0 +1,7 @@
const build = require('./build');
const initialize = require('./initialize');
module.exports = {
...build,
...initialize,
};

View File

@@ -0,0 +1,76 @@
const { createContentAggregator } = require('@librechat/agents');
const {
EModelEndpoint,
providerEndpointMap,
getResponseSender,
} = require('librechat-data-provider');
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
// const { loadAgentTools } = require('~/server/services/ToolService');
const getOptions = require('~/server/services/Endpoints/bedrock/options');
const AgentClient = require('~/server/controllers/agents/client');
const { getModelMaxTokens } = require('~/utils');
const initializeClient = async ({ req, res, endpointOption }) => {
if (!endpointOption) {
throw new Error('Endpoint option not provided');
}
/** @type {Array<UsageMetadata>} */
const collectedUsage = [];
const { contentParts, aggregateContent } = createContentAggregator();
const eventHandlers = getDefaultHandlers({ res, aggregateContent, collectedUsage });
// const tools = [createTavilySearchTool()];
/** @type {Agent} */
const agent = {
id: EModelEndpoint.bedrock,
name: endpointOption.name,
instructions: endpointOption.promptPrefix,
provider: EModelEndpoint.bedrock,
model: endpointOption.model_parameters.model,
model_parameters: endpointOption.model_parameters,
};
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
agent.instructions = `${agent.instructions ?? ''}\n${endpointOption.artifactsPrompt}`.trim();
}
let modelOptions = { model: agent.model };
// TODO: pass-in override settings that are specific to current run
const options = await getOptions({
req,
res,
endpointOption,
});
modelOptions = Object.assign(modelOptions, options.llmConfig);
const maxContextTokens =
agent.max_context_tokens ??
getModelMaxTokens(modelOptions.model, providerEndpointMap[agent.provider]);
const sender = getResponseSender({
...endpointOption,
model: endpointOption.model_parameters.model,
});
const client = new AgentClient({
req,
agent,
sender,
// tools,
// toolMap,
modelOptions,
contentParts,
eventHandlers,
collectedUsage,
maxContextTokens,
endpoint: EModelEndpoint.bedrock,
configOptions: options.configOptions,
attachments: endpointOption.attachments,
});
return { client };
};
module.exports = { initializeClient };

View File

@@ -0,0 +1,90 @@
const { HttpsProxyAgent } = require('https-proxy-agent');
const {
EModelEndpoint,
Constants,
AuthType,
removeNullishValues,
} = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { sleep } = require('~/server/utils');
const getOptions = async ({ req, endpointOption }) => {
const {
BEDROCK_AWS_SECRET_ACCESS_KEY,
BEDROCK_AWS_ACCESS_KEY_ID,
BEDROCK_REVERSE_PROXY,
BEDROCK_AWS_DEFAULT_REGION,
PROXY,
} = process.env;
const expiresAt = req.body.key;
const isUserProvided = BEDROCK_AWS_SECRET_ACCESS_KEY === AuthType.USER_PROVIDED;
const credentials = isUserProvided
? await getUserKey({ userId: req.user.id, name: EModelEndpoint.bedrock })
: {
accessKeyId: BEDROCK_AWS_ACCESS_KEY_ID,
secretAccessKey: BEDROCK_AWS_SECRET_ACCESS_KEY,
};
if (!credentials) {
throw new Error('Bedrock credentials not provided. Please provide them again.');
}
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(expiresAt, EModelEndpoint.bedrock);
}
/** @type {number} */
let streamRate = Constants.DEFAULT_STREAM_RATE;
/** @type {undefined | TBaseEndpoint} */
const bedrockConfig = req.app.locals[EModelEndpoint.bedrock];
if (bedrockConfig && bedrockConfig.streamRate) {
streamRate = bedrockConfig.streamRate;
}
/** @type {undefined | TBaseEndpoint} */
const allConfig = req.app.locals.all;
if (allConfig && allConfig.streamRate) {
streamRate = allConfig.streamRate;
}
/** @type {import('@librechat/agents').BedrockConverseClientOptions} */
const requestOptions = Object.assign(
{
credentials,
model: endpointOption.model,
region: BEDROCK_AWS_DEFAULT_REGION,
streaming: true,
streamUsage: true,
callbacks: [
{
handleLLMNewToken: async () => {
if (!streamRate) {
return;
}
await sleep(streamRate);
},
},
],
},
endpointOption.model_parameters,
);
const configOptions = {};
if (PROXY) {
configOptions.httpAgent = new HttpsProxyAgent(PROXY);
}
if (BEDROCK_REVERSE_PROXY) {
configOptions.endpointHost = BEDROCK_REVERSE_PROXY;
}
return {
llmConfig: removeNullishValues(requestOptions),
configOptions,
};
};
module.exports = getOptions;

View File

@@ -0,0 +1,40 @@
const { CacheKeys } = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const { isEnabled } = require('~/server/utils');
const { saveConvo } = require('~/models');
const addTitle = async (req, { text, response, client }) => {
const { TITLE_CONVO = true } = process.env ?? {};
if (!isEnabled(TITLE_CONVO)) {
return;
}
if (client.options.titleConvo === false) {
return;
}
// If the request was aborted, don't generate the title.
if (client.abortController.signal.aborted) {
return;
}
const titleCache = getLogStores(CacheKeys.GEN_TITLE);
const key = `${req.user.id}-${response.conversationId}`;
const title = await client.titleConvo({
text,
responseText: response?.text ?? '',
conversationId: response.conversationId,
});
await titleCache.set(key, title, 120000);
await saveConvo(
req,
{
conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/bedrock/title.js' },
);
};
module.exports = addTitle;

View File

@@ -49,7 +49,7 @@ const addTitle = async (req, { text, response, client }) => {
const title = await titleClient.titleConvo({
text,
responseText: response?.text,
responseText: response?.text ?? '',
conversationId: response.conversationId,
});
await titleCache.set(key, title, 120000);

View File

@@ -6,7 +6,7 @@ const buildOptions = (endpoint, parsedBody) => {
chatGptLabel,
promptPrefix,
agentOptions,
tools,
tools = [],
iconURL,
greeting,
spec,
@@ -19,7 +19,7 @@ const buildOptions = (endpoint, parsedBody) => {
tools:
tools
.map((tool) => tool?.pluginKey ?? tool)
.filter((toolName) => typeof toolName === 'string') ?? [],
.filter((toolName) => typeof toolName === 'string'),
chatGptLabel,
promptPrefix,
agentOptions,

View File

@@ -23,7 +23,7 @@ const addTitle = async (req, { text, response, client }) => {
const title = await client.titleConvo({
text,
responseText: response?.text,
responseText: response?.text ?? '',
conversationId: response.conversationId,
});
await titleCache.set(key, title, 120000);

View File

@@ -5,11 +5,19 @@ const {
mapModelToAzureConfig,
} = require('librechat-data-provider');
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getLLMConfig } = require('~/server/services/Endpoints/openAI/llm');
const { isEnabled, isUserProvided } = require('~/server/utils');
const { getAzureCredentials } = require('~/utils');
const { OpenAIClient } = require('~/app');
const initializeClient = async ({ req, res, endpointOption }) => {
const initializeClient = async ({
req,
res,
endpointOption,
optionsOnly,
overrideEndpoint,
overrideModel,
}) => {
const {
PROXY,
OPENAI_API_KEY,
@@ -19,7 +27,9 @@ const initializeClient = async ({ req, res, endpointOption }) => {
OPENAI_SUMMARIZE,
DEBUG_OPENAI,
} = process.env;
const { key: expiresAt, endpoint, model: modelName } = req.body;
const { key: expiresAt } = req.body;
const modelName = overrideModel ?? req.body.model;
const endpoint = overrideEndpoint ?? req.body.endpoint;
const contextStrategy = isEnabled(OPENAI_SUMMARIZE) ? 'summarize' : null;
const credentials = {
@@ -45,12 +55,10 @@ const initializeClient = async ({ req, res, endpointOption }) => {
let baseURL = userProvidesURL ? userValues?.baseURL : baseURLOptions[endpoint];
const clientOptions = {
debug: isEnabled(DEBUG_OPENAI),
contextStrategy,
reverseProxyUrl: baseURL ? baseURL : null,
proxy: PROXY ?? null,
req,
res,
debug: isEnabled(DEBUG_OPENAI),
reverseProxyUrl: baseURL ? baseURL : null,
...endpointOption,
};
@@ -119,7 +127,17 @@ const initializeClient = async ({ req, res, endpointOption }) => {
throw new Error(`${endpoint} API Key not provided.`);
}
const client = new OpenAIClient(apiKey, clientOptions);
if (optionsOnly) {
const requestOptions = Object.assign(
{
modelOptions: endpointOption.modelOptions,
},
clientOptions,
);
return getLLMConfig(apiKey, requestOptions);
}
const client = new OpenAIClient(apiKey, Object.assign({ req, res }, clientOptions));
return {
client,
openAIApiKey: apiKey,

View File

@@ -0,0 +1,120 @@
const { HttpsProxyAgent } = require('https-proxy-agent');
const { sanitizeModelName, constructAzureURL } = require('~/utils');
const { isEnabled } = require('~/server/utils');
/**
* Generates configuration options for creating a language model (LLM) instance.
* @param {string} apiKey - The API key for authentication.
* @param {Object} options - Additional options for configuring the LLM.
* @param {Object} [options.modelOptions] - Model-specific options.
* @param {string} [options.modelOptions.model] - The name of the model to use.
* @param {number} [options.modelOptions.temperature] - Controls randomness in output generation (0-2).
* @param {number} [options.modelOptions.top_p] - Controls diversity via nucleus sampling (0-1).
* @param {number} [options.modelOptions.frequency_penalty] - Reduces repetition of token sequences (-2 to 2).
* @param {number} [options.modelOptions.presence_penalty] - Encourages discussing new topics (-2 to 2).
* @param {number} [options.modelOptions.max_tokens] - The maximum number of tokens to generate.
* @param {string[]} [options.modelOptions.stop] - Sequences where the API will stop generating further tokens.
* @param {string} [options.reverseProxyUrl] - URL for a reverse proxy, if used.
* @param {boolean} [options.useOpenRouter] - Flag to use OpenRouter API.
* @param {Object} [options.headers] - Additional headers for API requests.
* @param {string} [options.proxy] - Proxy server URL.
* @param {Object} [options.azure] - Azure-specific configurations.
* @param {boolean} [options.streaming] - Whether to use streaming mode.
* @param {Object} [options.addParams] - Additional parameters to add to the model options.
* @param {string[]} [options.dropParams] - Parameters to remove from the model options.
* @returns {Object} Configuration options for creating an LLM instance.
*/
function getLLMConfig(apiKey, options = {}) {
const {
modelOptions = {},
reverseProxyUrl,
useOpenRouter,
headers,
proxy,
azure,
streaming = true,
addParams,
dropParams,
} = options;
let llmConfig = {
model: 'gpt-4o-mini',
streaming,
};
Object.assign(llmConfig, modelOptions);
if (addParams && typeof addParams === 'object') {
Object.assign(llmConfig, addParams);
}
if (dropParams && Array.isArray(dropParams)) {
dropParams.forEach((param) => {
delete llmConfig[param];
});
}
const configOptions = {};
// Handle OpenRouter or custom reverse proxy
if (useOpenRouter || reverseProxyUrl === 'https://openrouter.ai/api/v1') {
configOptions.basePath = 'https://openrouter.ai/api/v1';
configOptions.baseOptions = {
headers: Object.assign(
{
'HTTP-Referer': 'https://librechat.ai',
'X-Title': 'LibreChat',
},
headers,
),
};
} else if (reverseProxyUrl) {
configOptions.basePath = reverseProxyUrl;
if (headers) {
configOptions.baseOptions = { headers };
}
}
if (proxy) {
const proxyAgent = new HttpsProxyAgent(proxy);
Object.assign(configOptions, {
httpAgent: proxyAgent,
httpsAgent: proxyAgent,
});
}
if (azure) {
const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME);
azure.azureOpenAIApiDeploymentName = useModelName
? sanitizeModelName(llmConfig.model)
: azure.azureOpenAIApiDeploymentName;
if (process.env.AZURE_OPENAI_DEFAULT_MODEL) {
llmConfig.model = process.env.AZURE_OPENAI_DEFAULT_MODEL;
}
if (configOptions.basePath) {
const azureURL = constructAzureURL({
baseURL: configOptions.basePath,
azureOptions: azure,
});
azure.azureOpenAIBasePath = azureURL.split(`/${azure.azureOpenAIApiDeploymentName}`)[0];
}
Object.assign(llmConfig, azure);
llmConfig.model = llmConfig.azureOpenAIApiDeploymentName;
} else {
llmConfig.openAIApiKey = apiKey;
// Object.assign(llmConfig, {
// configuration: { apiKey },
// });
}
if (process.env.OPENAI_ORGANIZATION && this.azure) {
llmConfig.organization = process.env.OPENAI_ORGANIZATION;
}
return { llmConfig, configOptions };
}
module.exports = { getLLMConfig };

View File

@@ -1,5 +1,6 @@
const { Readable } = require('stream');
const axios = require('axios');
const FormData = require('form-data');
const { Readable } = require('stream');
const { extractEnvVariable, STTProviders } = require('librechat-data-provider');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { genAzureEndpoint } = require('~/utils');
@@ -136,8 +137,10 @@ class STTService {
}
const formData = new FormData();
const audioBlob = new Blob([audioBuffer], { type: audioFile.mimetype });
formData.append('file', audioBlob, audioFile.originalname);
formData.append('file', audioBuffer, {
filename: audioFile.originalname,
contentType: audioFile.mimetype,
});
const headers = {
'Content-Type': 'multipart/form-data',
@@ -146,7 +149,7 @@ class STTService {
[headers].forEach(this.removeUndefined);
return [url, formData, headers];
return [url, formData, { ...headers, ...formData.getHeaders() }];
}
/**
@@ -171,11 +174,6 @@ class STTService {
const [url, data, headers] = strategy.call(this, sttSchema, audioReadStream, audioFile);
if (!Readable.from && data instanceof FormData) {
const audioBlob = new Blob([audioBuffer], { type: audioFile.mimetype });
data.set('file', audioBlob, audioFile.originalname);
}
try {
const response = await axios.post(url, data, { headers });

View File

@@ -23,7 +23,13 @@ async function fetchImageToBase64(url) {
}
}
const base64Only = new Set([EModelEndpoint.google, EModelEndpoint.anthropic, 'Ollama', 'ollama']);
const base64Only = new Set([
EModelEndpoint.google,
EModelEndpoint.anthropic,
'Ollama',
'ollama',
EModelEndpoint.bedrock,
]);
/**
* Encodes and formats the given files.

View File

@@ -5,6 +5,21 @@ const { extractBaseURL, inputSchema, processModelData, logAxiosError } = require
const { OllamaClient } = require('~/app/clients/OllamaClient');
const getLogStores = require('~/cache/getLogStores');
/**
* Splits a string by commas and trims each resulting value.
* @param {string} input - The input string to split.
* @returns {string[]} An array of trimmed values.
*/
const splitAndTrim = (input) => {
if (!input || typeof input !== 'string') {
return [];
}
return input
.split(',')
.map((item) => item.trim())
.filter(Boolean);
};
const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService').config;
/**
@@ -194,7 +209,7 @@ const getOpenAIModels = async (opts) => {
}
if (process.env[key]) {
models = String(process.env[key]).split(',');
models = splitAndTrim(process.env[key]);
return models;
}
@@ -208,7 +223,7 @@ const getOpenAIModels = async (opts) => {
const getChatGPTBrowserModels = () => {
let models = ['text-davinci-002-render-sha', 'gpt-4'];
if (process.env.CHATGPT_MODELS) {
models = String(process.env.CHATGPT_MODELS).split(',');
models = splitAndTrim(process.env.CHATGPT_MODELS);
}
return models;
@@ -217,7 +232,7 @@ const getChatGPTBrowserModels = () => {
const getAnthropicModels = () => {
let models = defaultModels[EModelEndpoint.anthropic];
if (process.env.ANTHROPIC_MODELS) {
models = String(process.env.ANTHROPIC_MODELS).split(',');
models = splitAndTrim(process.env.ANTHROPIC_MODELS);
}
return models;
@@ -226,7 +241,16 @@ const getAnthropicModels = () => {
const getGoogleModels = () => {
let models = defaultModels[EModelEndpoint.google];
if (process.env.GOOGLE_MODELS) {
models = String(process.env.GOOGLE_MODELS).split(',');
models = splitAndTrim(process.env.GOOGLE_MODELS);
}
return models;
};
const getBedrockModels = () => {
let models = defaultModels[EModelEndpoint.bedrock];
if (process.env.BEDROCK_AWS_MODELS) {
models = splitAndTrim(process.env.BEDROCK_AWS_MODELS);
}
return models;
@@ -234,7 +258,9 @@ const getGoogleModels = () => {
module.exports = {
fetchModels,
splitAndTrim,
getOpenAIModels,
getBedrockModels,
getChatGPTBrowserModels,
getAnthropicModels,
getGoogleModels,

View File

@@ -1,7 +1,16 @@
const axios = require('axios');
const { EModelEndpoint, defaultModels } = require('librechat-data-provider');
const { logger } = require('~/config');
const { fetchModels, getOpenAIModels } = require('./ModelService');
const {
fetchModels,
splitAndTrim,
getOpenAIModels,
getGoogleModels,
getBedrockModels,
getAnthropicModels,
} = require('./ModelService');
jest.mock('~/utils', () => {
const originalUtils = jest.requireActual('~/utils');
return {
@@ -329,3 +338,71 @@ describe('fetchModels with Ollama specific logic', () => {
);
});
});
describe('splitAndTrim', () => {
it('should split a string by commas and trim each value', () => {
const input = ' model1, model2 , model3,model4 ';
const expected = ['model1', 'model2', 'model3', 'model4'];
expect(splitAndTrim(input)).toEqual(expected);
});
it('should return an empty array for empty input', () => {
expect(splitAndTrim('')).toEqual([]);
});
it('should return an empty array for null input', () => {
expect(splitAndTrim(null)).toEqual([]);
});
it('should return an empty array for undefined input', () => {
expect(splitAndTrim(undefined)).toEqual([]);
});
it('should filter out empty values after trimming', () => {
const input = 'model1,, ,model2,';
const expected = ['model1', 'model2'];
expect(splitAndTrim(input)).toEqual(expected);
});
});
describe('getAnthropicModels', () => {
it('returns default models when ANTHROPIC_MODELS is not set', () => {
delete process.env.ANTHROPIC_MODELS;
const models = getAnthropicModels();
expect(models).toEqual(defaultModels[EModelEndpoint.anthropic]);
});
it('returns models from ANTHROPIC_MODELS when set', () => {
process.env.ANTHROPIC_MODELS = 'claude-1, claude-2 ';
const models = getAnthropicModels();
expect(models).toEqual(['claude-1', 'claude-2']);
});
});
describe('getGoogleModels', () => {
it('returns default models when GOOGLE_MODELS is not set', () => {
delete process.env.GOOGLE_MODELS;
const models = getGoogleModels();
expect(models).toEqual(defaultModels[EModelEndpoint.google]);
});
it('returns models from GOOGLE_MODELS when set', () => {
process.env.GOOGLE_MODELS = 'gemini-pro, bard ';
const models = getGoogleModels();
expect(models).toEqual(['gemini-pro', 'bard']);
});
});
describe('getBedrockModels', () => {
it('returns default models when BEDROCK_AWS_MODELS is not set', () => {
delete process.env.BEDROCK_AWS_MODELS;
const models = getBedrockModels();
expect(models).toEqual(defaultModels[EModelEndpoint.bedrock]);
});
it('returns models from BEDROCK_AWS_MODELS when set', () => {
process.env.BEDROCK_AWS_MODELS = 'anthropic.claude-v2, ai21.j2-ultra ';
const models = getBedrockModels();
expect(models).toEqual(['anthropic.claude-v2', 'ai21.j2-ultra']);
});
});

View File

@@ -0,0 +1,64 @@
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
const { logger } = require('~/config');
class Tokenizer {
constructor() {
this.tokenizersCache = {};
this.tokenizerCallsCount = 0;
}
getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
let tokenizer;
if (this.tokenizersCache[encoding]) {
tokenizer = this.tokenizersCache[encoding];
} else {
if (isModelName) {
tokenizer = encodingForModel(encoding, extendSpecialTokens);
} else {
tokenizer = getEncoding(encoding, extendSpecialTokens);
}
this.tokenizersCache[encoding] = tokenizer;
}
return tokenizer;
}
freeAndResetAllEncoders() {
try {
Object.keys(this.tokenizersCache).forEach((key) => {
if (this.tokenizersCache[key]) {
this.tokenizersCache[key].free();
delete this.tokenizersCache[key];
}
});
this.tokenizerCallsCount = 1;
} catch (error) {
logger.error('[Tokenizer] Free and reset encoders error', error);
}
}
resetTokenizersIfNecessary() {
if (this.tokenizerCallsCount >= 25) {
if (this.options?.debug) {
logger.debug('[Tokenizer] freeAndResetAllEncoders: reached 25 encodings, resetting...');
}
this.freeAndResetAllEncoders();
}
this.tokenizerCallsCount++;
}
getTokenCount(text, encoding = 'cl100k_base') {
this.resetTokenizersIfNecessary();
try {
const tokenizer = this.getTokenizer(encoding);
return tokenizer.encode(text, 'all').length;
} catch (error) {
this.freeAndResetAllEncoders();
const tokenizer = this.getTokenizer(encoding);
return tokenizer.encode(text, 'all').length;
}
}
}
const tokenizerService = new Tokenizer();
module.exports = tokenizerService;

View File

@@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
const { StructuredTool } = require('langchain/tools');
const { tool: toolFn } = require('@langchain/core/tools');
const { zodToJsonSchema } = require('zod-to-json-schema');
const { Calculator } = require('langchain/tools/calculator');
const {
@@ -180,7 +181,7 @@ async function processRequiredActions(client, requiredActions) {
const tools = requiredActions.map((action) => action.tool);
const loadedTools = await loadTools({
user: client.req.user.id,
model: client.req.body.model ?? 'gpt-3.5-turbo-1106',
model: client.req.body.model ?? 'gpt-4o-mini',
tools,
functions: true,
options: {
@@ -372,8 +373,120 @@ async function processRequiredActions(client, requiredActions) {
};
}
/**
* Processes the runtime tool calls and returns a combined toolMap.
* @param {Object} params - Run params containing user and request information.
* @param {ServerRequest} params.req - The request object.
* @param {string} params.agent_id - The agent ID.
* @param {string[]} params.tools - The agent's available tools.
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
* @returns {Promise<{ tools?: StructuredTool[]; toolMap?: Record<string, StructuredTool>}>} The combined toolMap.
*/
async function loadAgentTools({ req, agent_id, tools, openAIApiKey }) {
if (!tools || tools.length === 0) {
return {};
}
const loadedTools = await loadTools({
user: req.user.id,
// model: req.body.model ?? 'gpt-4o-mini',
tools,
functions: true,
options: {
req,
openAIApiKey,
returnMetadata: true,
processFileURL,
uploadImageBuffer,
fileStrategy: req.app.locals.fileStrategy,
},
skipSpecs: true,
});
const agentTools = [];
for (let i = 0; i < loadedTools.length; i++) {
const tool = loadedTools[i];
const toolInstance = toolFn(
async (...args) => {
return tool['_call'](...args);
},
{
name: tool.name,
description: tool.description,
schema: tool.schema,
},
);
agentTools.push(toolInstance);
}
const ToolMap = loadedTools.reduce((map, tool) => {
map[tool.name] = tool;
return map;
}, {});
let actionSets = [];
const ActionToolMap = {};
for (const toolName of tools) {
if (!ToolMap[toolName]) {
if (!actionSets.length) {
actionSets = (await loadActionSets({ agent_id })) ?? [];
}
let actionSet = null;
let currentDomain = '';
for (let action of actionSets) {
const domain = await domainParser(req, action.metadata.domain, true);
if (toolName.includes(domain)) {
currentDomain = domain;
actionSet = action;
break;
}
}
if (actionSet) {
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
if (validationResult.spec) {
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
validationResult.spec,
true,
);
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
const requestBuilder = requestBuilders[functionName];
const zodSchema = zodSchemas[functionName];
if (requestBuilder) {
const tool = await createActionTool({
action: actionSet,
requestBuilder,
zodSchema,
name: toolName,
description: functionSig.description,
});
agentTools.push(tool);
ActionToolMap[toolName] = tool;
}
}
}
}
}
if (tools.length > 0 && agentTools.length === 0) {
throw new Error('No tools found for the specified tool calls.');
}
const toolMap = { ...ToolMap, ...ActionToolMap };
return {
tools: agentTools,
toolMap,
};
}
module.exports = {
formatToOpenAIAssistantTool,
loadAgentTools,
loadAndFormatTools,
processRequiredActions,
formatToOpenAIAssistantTool,
};

View File

@@ -31,11 +31,18 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
prompts: interfaceConfig?.prompts ?? defaults.prompts,
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
});
await updateAccessPermissions(roleName, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
});
await updateAccessPermissions(SystemRoles.ADMIN, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
});
let i = 0;

View File

@@ -16,6 +16,7 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
@@ -28,6 +29,7 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: false },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
@@ -40,6 +42,7 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
@@ -52,6 +55,7 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
@@ -64,6 +68,7 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
@@ -76,6 +81,72 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
it('should call updateAccessPermissions with the correct parameters when multiConvo is true', async () => {
const config = { interface: { multiConvo: true } };
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
});
});
it('should call updateAccessPermissions with false when multiConvo is false', async () => {
const config = { interface: { multiConvo: false } };
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
});
});
it('should call updateAccessPermissions with undefined when multiConvo is not specified in config', async () => {
const config = {};
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
});
});
it('should call updateAccessPermissions with all interface options including multiConvo', async () => {
const config = { interface: { prompts: true, bookmarks: false, multiConvo: true } };
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
});
});
it('should use default values for multiConvo when config is undefined', async () => {
const config = undefined;
const configDefaults = { interface: { prompts: true, bookmarks: true, multiConvo: false } };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
});
});
});

View File

@@ -8,6 +8,60 @@
* @memberof typedefs
*/
/**
* @exports ServerRequest
* @typedef {import('express').Request} ServerRequest
* @memberof typedefs
*/
/**
* @exports ServerResponse
* @typedef {import('express').Response} ServerResponse
* @memberof typedefs
*/
/**
* @exports AgentRun
* @typedef {import('@librechat/agents').Run} AgentRun
* @memberof typedefs
*/
/**
* @exports IState
* @typedef {import('@librechat/agents').IState} IState
* @memberof typedefs
*/
/**
* @exports ClientCallbacks
* @typedef {import('@librechat/agents').ClientCallbacks} ClientCallbacks
* @memberof typedefs
*/
/**
* @exports BedrockClientOptions
* @typedef {import('@librechat/agents').BedrockConverseClientOptions} BedrockClientOptions
* @memberof typedefs
*/
/**
* @exports StreamEventData
* @typedef {import('@librechat/agents').StreamEventData} StreamEventData
* @memberof typedefs
*/
/**
* @exports ToolEndData
* @typedef {import('@librechat/agents').ToolEndData} ToolEndData
* @memberof typedefs
*/
/**
* @exports UsageMetadata
* @typedef {import('@langchain/core/messages').UsageMetadata} UsageMetadata
* @memberof typedefs
*/
/**
* @exports Ollama
* @typedef {import('ollama').Ollama} Ollama
@@ -724,6 +778,36 @@
* @memberof typedefs
*/
/**
* @exports Agent
* @typedef {import('librechat-data-provider').Agent} Agent
* @memberof typedefs
*/
/**
* @exports AgentCreateParams
* @typedef {import('librechat-data-provider').AgentCreateParams} AgentCreateParams
* @memberof typedefs
*/
/**
* @exports AgentUpdateParams
* @typedef {import('librechat-data-provider').AgentUpdateParams} AgentUpdateParams
* @memberof typedefs
*/
/**
* @exports AgentListParams
* @typedef {import('librechat-data-provider').AgentListParams} AgentListParams
* @memberof typedefs
*/
/**
* @exports AgentListResponse
* @typedef {import('librechat-data-provider').AgentListResponse} AgentListResponse
* @memberof typedefs
*/
/**
* Represents details of the message creation by the run step, including the ID of the created message.
*
@@ -833,6 +917,12 @@
* @memberof typedefs
*/
/**
* @exports TAgentClient
* @typedef {import('./server/controllers/agents/client')} TAgentClient
* @memberof typedefs
*/
/**
* @exports ImportBatchBuilder
* @typedef {import('./server/utils/import/importBatchBuilder.js').ImportBatchBuilder} ImportBatchBuilder

View File

@@ -7,13 +7,13 @@ const openAIModels = {
'gpt-4-32k': 32758, // -10 from max
'gpt-4-32k-0314': 32758, // -10 from max
'gpt-4-32k-0613': 32758, // -10 from max
'gpt-4-1106': 127990, // -10 from max
'gpt-4-0125': 127990, // -10 from max
'gpt-4o': 127990, // -10 from max
'gpt-4o-mini': 127990, // -10 from max
'gpt-4o-2024-08-06': 127990, // -10 from max
'gpt-4-turbo': 127990, // -10 from max
'gpt-4-vision': 127990, // -10 from max
'gpt-4-1106': 127500, // -500 from max
'gpt-4-0125': 127500, // -500 from max
'gpt-4o': 127500, // -500 from max
'gpt-4o-mini': 127500, // -500 from max
'gpt-4o-2024-08-06': 127500, // -500 from max
'gpt-4-turbo': 127500, // -500 from max
'gpt-4-vision': 127500, // -500 from max
'gpt-3.5-turbo': 16375, // -10 from max
'gpt-3.5-turbo-0613': 4092, // -5 from max
'gpt-3.5-turbo-0301': 4092, // -5 from max
@@ -21,9 +21,15 @@ const openAIModels = {
'gpt-3.5-turbo-16k-0613': 16375, // -10 from max
'gpt-3.5-turbo-1106': 16375, // -10 from max
'gpt-3.5-turbo-0125': 16375, // -10 from max
};
const mistralModels = {
'mistral-': 31990, // -10 from max
llama3: 8187, // -5 from max
'llama-3': 8187, // -5 from max
'mistral-7b': 31990, // -10 from max
'mistral-small': 31990, // -10 from max
'mixtral-8x7b': 31990, // -10 from max
'mistral-large-2402': 127500,
'mistral-large-2407': 127500,
};
const cohereModels = {
@@ -54,6 +60,7 @@ const googleModels = {
const anthropicModels = {
'claude-': 100000,
'claude-instant': 100000,
'claude-2': 100000,
'claude-2.1': 200000,
'claude-3-haiku': 200000,
@@ -63,14 +70,47 @@ const anthropicModels = {
'claude-3.5-sonnet': 200000,
};
const aggregateModels = { ...openAIModels, ...googleModels, ...anthropicModels, ...cohereModels };
const metaModels = {
'llama2-13b': 4000,
'llama2-70b': 4000,
'llama3-8b': 8000,
'llama3-70b': 8000,
'llama3-1-8b': 127500,
'llama3-1-70b': 127500,
'llama3-1-405b': 127500,
};
const ai21Models = {
'ai21.j2-mid-v1': 8182, // -10 from max
'ai21.j2-ultra-v1': 8182, // -10 from max
'ai21.jamba-instruct-v1:0': 255500, // -500 from max
};
const amazonModels = {
'amazon.titan-text-lite-v1': 4000,
'amazon.titan-text-express-v1': 8000,
'amazon.titan-text-premier-v1:0': 31500, // -500 from max
};
const bedrockModels = {
...anthropicModels,
...mistralModels,
...cohereModels,
...metaModels,
...ai21Models,
...amazonModels,
};
const aggregateModels = { ...openAIModels, ...googleModels, ...bedrockModels };
const maxTokensMap = {
[EModelEndpoint.azureOpenAI]: openAIModels,
[EModelEndpoint.openAI]: aggregateModels,
[EModelEndpoint.agents]: aggregateModels,
[EModelEndpoint.custom]: aggregateModels,
[EModelEndpoint.google]: googleModels,
[EModelEndpoint.anthropic]: anthropicModels,
[EModelEndpoint.bedrock]: bedrockModels,
};
/**

View File

@@ -20,18 +20,6 @@ describe('getModelMaxTokens', () => {
);
});
test('should return correct tokens for LLama 3 models', () => {
expect(getModelMaxTokens('meta-llama/llama-3-8b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['llama-3'],
);
expect(getModelMaxTokens('meta-llama/llama-3-8b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['llama3'],
);
expect(getModelMaxTokens('llama-3-500b')).toBe(maxTokensMap[EModelEndpoint.openAI]['llama-3']);
expect(getModelMaxTokens('llama3-70b')).toBe(maxTokensMap[EModelEndpoint.openAI]['llama3']);
expect(getModelMaxTokens('llama3:latest')).toBe(maxTokensMap[EModelEndpoint.openAI]['llama3']);
});
test('should return undefined for no match', () => {
expect(getModelMaxTokens('unknown-model')).toBeUndefined();
});

View File

@@ -22,7 +22,7 @@
type="image/png"
sizes="16x16"
href="/assets/favicon-16x16.png"
/>
/>
<link
rel="apple-touch-icon"
href="/assets/apple-touch-icon-180x180.png"

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
"version": "v0.7.5-rc1",
"version": "v0.7.5-rc2",
"description": "",
"type": "module",
"scripts": {
@@ -65,6 +65,7 @@
"filenamify": "^6.0.0",
"html-to-image": "^1.11.11",
"image-blob-reduce": "^4.1.0",
"js-cookie": "^3.0.5",
"librechat-data-provider": "*",
"lodash": "^4.17.21",
"lucide-react": "^0.394.0",
@@ -111,6 +112,7 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.3.0",
"@types/react": "^18.2.11",
"@types/react-dom": "^18.2.4",
@@ -130,11 +132,10 @@
"postcss": "^8.4.31",
"postcss-loader": "^7.1.0",
"postcss-preset-env": "^8.2.0",
"rollup": "^4.21.1",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.1.0",
"typescript": "^5.0.4",
"vite": "^5.4.2",
"vite": "^5.1.1",
"vite-plugin-node-polyfills": "^0.17.0",
"vite-plugin-pwa": "^0.19.8"
}

View File

@@ -0,0 +1,27 @@
import { useForm, FormProvider } from 'react-hook-form';
import { createContext, useContext } from 'react';
import { defaultAgentFormValues } from 'librechat-data-provider';
import type { UseFormReturn } from 'react-hook-form';
import type { AgentForm } from '~/common';
type AgentsContextType = UseFormReturn<AgentForm>;
export const AgentsContext = createContext<AgentsContextType>({} as AgentsContextType);
export function useAgentsContext() {
const context = useContext(AgentsContext);
if (context === undefined) {
throw new Error('useAgentsContext must be used within an AgentsProvider');
}
return context;
}
export default function AgentsProvider({ children }) {
const methods = useForm<AgentForm>({
defaultValues: defaultAgentFormValues,
});
return <FormProvider {...methods}>{children}</FormProvider>;
}

View File

@@ -0,0 +1,6 @@
import { createContext, useContext } from 'react';
import useAgentsMap from '~/hooks/Agents/useAgentsMap';
type AgentsMapContextType = ReturnType<typeof useAgentsMap>;
export const AgentsMapContext = createContext<AgentsMapContextType>({} as AgentsMapContextType);
export const useAgentsMapContext = () => useContext(AgentsMapContext);

View File

@@ -1,12 +1,6 @@
// AnnouncerContext.tsx
import React from 'react';
export interface AnnounceOptions {
message: string;
id?: string;
isStream?: boolean;
isComplete?: boolean;
}
import type { AnnounceOptions } from '~/common';
interface AnnouncerContextType {
announceAssertive: (options: AnnounceOptions) => void;

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