Compare commits

..

8 Commits

Author SHA1 Message Date
Marco Beretta
f75010369c feat: add back recoil 2025-07-12 22:19:45 +02:00
Marco Beretta
dc9d219f3e fix: package; refactor: tsconfig 2025-07-12 22:19:05 +02:00
Marco Beretta
b0c5db6756 fix: cleanup 2025-07-12 22:16:48 +02:00
Marco Beretta
ff722366e9 feat: cleanup unused types from common/index.ts
- Remove 104 unused type exports from packages/client/src/common/index.ts
- Keep only 7 actually used exports (93% reduction)
- Add cleanup script with enhanced import pattern detection
- Support both named imports and namespace imports (* as t)
- Create automatic backups and comprehensive documentation
- Maintain type safety with build verification
- No breaking changes to existing code

Kept exports:
- TShowToast, Option, OptionWithIcon, DropdownValueSetter
- MentionOption, NotificationSeverity, MenuItemProps

Scripts: cleanup-common-types-safe.js, README-CLEANUP.md
2025-07-12 22:13:51 +02:00
Marco Beretta
4f06c159be fix build client package 2025-07-10 23:47:05 +02:00
Marco Beretta
e042e1500f feat: Add jotai as a peer dependency 2025-07-06 15:32:47 +02:00
Marco Beretta
0503f0f903 feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools 2025-07-06 12:32:21 +02:00
Marco Beretta
e4adfe771b feat: init @librechat/client 2025-07-05 22:49:28 +02:00
284 changed files with 22767 additions and 1620 deletions

View File

@@ -142,10 +142,10 @@ GOOGLE_KEY=user_provided
# GOOGLE_AUTH_HEADER=true
# Gemini API (AI Studio)
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash,gemini-2.0-flash-lite
# GOOGLE_MODELS=gemini-2.5-pro-preview-05-06,gemini-2.5-flash-preview-04-17,gemini-2.0-flash-001,gemini-2.0-flash-exp,gemini-2.0-flash-lite-001,gemini-1.5-pro-002,gemini-1.5-flash-002
# Vertex AI
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
# GOOGLE_MODELS=gemini-2.5-pro-preview-05-06,gemini-2.5-flash-preview-04-17,gemini-2.0-flash-001,gemini-2.0-flash-exp,gemini-2.0-flash-lite-001,gemini-1.5-pro-002,gemini-1.5-flash-002
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
@@ -657,4 +657,4 @@ OPENWEATHER_API_KEY=
# Reranker (Required)
# JINA_API_KEY=your_jina_api_key
# or
# COHERE_API_KEY=your_cohere_api_key
# COHERE_API_KEY=your_cohere_api_key

View File

@@ -1,7 +1,7 @@
const { google } = require('googleapis');
const { Tokenizer } = require('@librechat/api');
const { concat } = require('@langchain/core/utils/stream');
const { ChatVertexAI } = require('@langchain/google-vertexai');
const { Tokenizer, getSafetySettings } = require('@librechat/api');
const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
const { GoogleGenerativeAI: GenAI } = require('@google/generative-ai');
const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
@@ -12,13 +12,13 @@ const {
endpointSettings,
parseTextParts,
EModelEndpoint,
googleSettings,
ContentTypes,
VisionModes,
ErrorTypes,
Constants,
AuthKeys,
} = require('librechat-data-provider');
const { getSafetySettings } = require('~/server/services/Endpoints/google/llm');
const { encodeAndFormat } = require('~/server/services/Files/images');
const { spendTokens } = require('~/models/spendTokens');
const { getModelMaxTokens } = require('~/utils');
@@ -166,16 +166,6 @@ class GoogleClient extends BaseClient {
);
}
// Add thinking configuration
this.modelOptions.thinkingConfig = {
thinkingBudget:
(this.modelOptions.thinking ?? googleSettings.thinking.default)
? this.modelOptions.thinkingBudget
: 0,
};
delete this.modelOptions.thinking;
delete this.modelOptions.thinkingBudget;
this.sender =
this.options.sender ??
getResponseSender({

View File

@@ -1,8 +1,7 @@
const { logger } = require('@librechat/data-schemas');
const { isEnabled, math } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { isEnabled, math, removePorts } = require('~/server/utils');
const { deleteAllUserSessions } = require('~/models');
const { removePorts } = require('~/server/utils');
const getLogStores = require('./getLogStores');
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};

View File

@@ -1,7 +1,7 @@
const { Keyv } = require('keyv');
const { isEnabled, math } = require('@librechat/api');
const { CacheKeys, ViolationTypes, Time } = require('librechat-data-provider');
const { logFile, violationFile } = require('./keyvFiles');
const { isEnabled, math } = require('~/server/utils');
const keyvRedis = require('./keyvRedis');
const keyvMongo = require('./keyvMongo');

View File

@@ -1,6 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { createTempChatExpirationDate } = require('@librechat/api');
const getCustomConfig = require('~/server/services/Config/loadCustomConfig');
const { getMessages, deleteMessages } = require('./Message');
const { Conversation } = require('~/db/models');
@@ -100,15 +98,10 @@ module.exports = {
update.conversationId = newConversationId;
}
if (req?.body?.isTemporary) {
try {
const customConfig = await getCustomConfig();
update.expiredAt = createTempChatExpirationDate(customConfig);
} catch (err) {
logger.error('Error creating temporary chat expiration date:', err);
logger.info(`---\`saveConvo\` context: ${metadata?.context}`);
update.expiredAt = null;
}
if (req.body.isTemporary) {
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + 30);
update.expiredAt = expiredAt;
} else {
update.expiredAt = null;
}

View File

@@ -1,7 +1,5 @@
const { z } = require('zod');
const { logger } = require('@librechat/data-schemas');
const { createTempChatExpirationDate } = require('@librechat/api');
const getCustomConfig = require('~/server/services/Config/loadCustomConfig');
const { Message } = require('~/db/models');
const idSchema = z.string().uuid();
@@ -56,14 +54,9 @@ async function saveMessage(req, params, metadata) {
};
if (req?.body?.isTemporary) {
try {
const customConfig = await getCustomConfig();
update.expiredAt = createTempChatExpirationDate(customConfig);
} catch (err) {
logger.error('Error creating temporary chat expiration date:', err);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
update.expiredAt = null;
}
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + 30);
update.expiredAt = expiredAt;
} else {
update.expiredAt = null;
}

View File

@@ -48,7 +48,7 @@
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.46",
"@librechat/agents": "^2.4.42",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@node-saml/passport-saml": "^5.0.0",

View File

@@ -1,17 +1,17 @@
const cookies = require('cookie');
const jwt = require('jsonwebtoken');
const openIdClient = require('openid-client');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
requestPasswordReset,
setOpenIDAuthTokens,
registerUser,
resetPassword,
setAuthTokens,
registerUser,
requestPasswordReset,
setOpenIDAuthTokens,
} = require('~/server/services/AuthService');
const { findUser, getUserById, deleteAllUserSessions, findSession } = require('~/models');
const { getOpenIdConfig } = require('~/strategies');
const { isEnabled } = require('~/server/utils');
const registrationController = async (req, res) => {
try {

View File

@@ -1,5 +1,3 @@
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { getResponseSender } = require('librechat-data-provider');
const {
handleAbortError,
@@ -12,8 +10,9 @@ const {
clientRegistry,
requestDataMap,
} = require('~/server/cleanup');
const { createOnProgress } = require('~/server/utils');
const { sendMessage, createOnProgress } = require('~/server/utils');
const { saveMessage } = require('~/models');
const { logger } = require('~/config');
const EditController = async (req, res, next, initializeClient) => {
let {
@@ -199,7 +198,7 @@ const EditController = async (req, res, next, initializeClient) => {
const finalUserMessage = reqDataContext.userMessage;
const finalResponseMessage = { ...response };
sendEvent(res, {
sendMessage(res, {
final: true,
conversation,
title: conversation.title,

View File

@@ -44,17 +44,6 @@ const BaseClient = require('~/app/clients/BaseClient');
const { loadAgent } = require('~/models/Agent');
const { getMCPManager } = require('~/config');
const omitTitleOptions = new Set([
'stream',
'thinking',
'streaming',
'clientOptions',
'thinkingConfig',
'thinkingBudget',
'includeThoughts',
'maxOutputTokens',
]);
/**
* @param {ServerRequest} req
* @param {Agent} agent
@@ -1049,16 +1038,6 @@ class AgentClient extends BaseClient {
delete clientOptions.maxTokens;
}
clientOptions = Object.assign(
Object.fromEntries(
Object.entries(clientOptions).filter(([key]) => !omitTitleOptions.has(key)),
),
);
if (provider === Providers.GOOGLE) {
clientOptions.json = true;
}
try {
const titleResult = await this.run.generateTitle({
provider,

View File

@@ -1,10 +1,10 @@
// errorHandler.js
const { logger } = require('@librechat/data-schemas');
const { logger } = require('~/config');
const getLogStores = require('~/cache/getLogStores');
const { CacheKeys, ViolationTypes } = require('librechat-data-provider');
const { sendResponse } = require('~/server/middleware/error');
const { recordUsage } = require('~/server/services/Threads');
const { getConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { sendResponse } = require('~/server/utils');
/**
* @typedef {Object} ErrorHandlerContext
@@ -75,7 +75,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
} 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."
? ' 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);

View File

@@ -1,5 +1,3 @@
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { Constants } = require('librechat-data-provider');
const {
handleAbortError,
@@ -7,7 +5,9 @@ const {
cleanupAbortController,
} = require('~/server/middleware');
const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup');
const { sendMessage } = require('~/server/utils');
const { saveMessage } = require('~/models');
const { logger } = require('~/config');
const AgentController = async (req, res, next, initializeClient, addTitle) => {
let {
@@ -206,7 +206,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
// Create a new response object with minimal copies
const finalResponse = { ...response };
sendEvent(res, {
sendMessage(res, {
final: true,
conversation,
title: conversation.title,

View File

@@ -1,7 +1,4 @@
const { v4 } = require('uuid');
const { sleep } = require('@librechat/agents');
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
Time,
Constants,
@@ -22,20 +19,20 @@ const {
addThreadMetadata,
saveAssistantMessage,
} = require('~/server/services/Threads');
const { sendResponse, sendMessage, sleep, countTokens } = require('~/server/utils');
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { createRunBody } = require('~/server/services/createRunBody');
const { sendResponse } = require('~/server/middleware/error');
const { getTransactions } = require('~/models/Transaction');
const { checkBalance } = require('~/models/balanceMethods');
const { getConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { countTokens } = require('~/server/utils');
const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
/**
* @route POST /
@@ -474,7 +471,7 @@ const chatV1 = async (req, res) => {
await Promise.all(promises);
const sendInitialResponse = () => {
sendEvent(res, {
sendMessage(res, {
sync: true,
conversationId,
// messages: previousMessages,
@@ -590,7 +587,7 @@ const chatV1 = async (req, res) => {
iconURL: endpointOption.iconURL,
};
sendEvent(res, {
sendMessage(res, {
final: true,
conversation,
requestMessage: {

View File

@@ -1,7 +1,4 @@
const { v4 } = require('uuid');
const { sleep } = require('@librechat/agents');
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
Time,
Constants,
@@ -25,14 +22,15 @@ const { createErrorHandler } = require('~/server/controllers/assistants/errors')
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { sendMessage, sleep, countTokens } = require('~/server/utils');
const { createRunBody } = require('~/server/services/createRunBody');
const { getTransactions } = require('~/models/Transaction');
const { checkBalance } = require('~/models/balanceMethods');
const { getConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { countTokens } = require('~/server/utils');
const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
/**
* @route POST /
@@ -311,7 +309,7 @@ const chatV2 = async (req, res) => {
await Promise.all(promises);
const sendInitialResponse = () => {
sendEvent(res, {
sendMessage(res, {
sync: true,
conversationId,
// messages: previousMessages,
@@ -434,7 +432,7 @@ const chatV2 = async (req, res) => {
iconURL: endpointOption.iconURL,
};
sendEvent(res, {
sendMessage(res, {
final: true,
conversation,
requestMessage: {

View File

@@ -1,10 +1,10 @@
// errorHandler.js
const { logger } = require('@librechat/data-schemas');
const { CacheKeys, ViolationTypes, ContentTypes } = require('librechat-data-provider');
const { recordUsage, checkMessageGaps } = require('~/server/services/Threads');
const { sendResponse } = require('~/server/middleware/error');
const { getConvo } = require('~/models/Conversation');
const { sendResponse } = require('~/server/utils');
const { logger } = require('~/config');
const getLogStores = require('~/cache/getLogStores');
const { CacheKeys, ViolationTypes, ContentTypes } = require('librechat-data-provider');
const { getConvo } = require('~/models/Conversation');
const { recordUsage, checkMessageGaps } = require('~/server/services/Threads');
/**
* @typedef {Object} ErrorHandlerContext
@@ -78,7 +78,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
} 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."
? ' 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);

View File

@@ -1,13 +1,13 @@
const { logger } = require('@librechat/data-schemas');
const { countTokens, isEnabled, sendEvent } = require('@librechat/api');
// abortMiddleware.js
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');
const { sendError } = require('~/server/middleware/error');
const { spendTokens } = require('~/models/spendTokens');
const abortControllers = require('./abortControllers');
const { saveMessage, getConvo } = require('~/models');
const { abortRun } = require('./abortRun');
const { logger } = require('~/config');
const abortDataMap = new WeakMap();
@@ -101,7 +101,7 @@ async function abortMessage(req, res) {
cleanupAbortController(abortKey);
if (res.headersSent && finalEvent) {
return sendEvent(res, finalEvent);
return sendMessage(res, finalEvent);
}
res.setHeader('Content-Type', 'application/json');
@@ -174,7 +174,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
* @param {string} responseMessageId
*/
const onStart = (userMessage, responseMessageId) => {
sendEvent(res, { message: userMessage, created: true });
sendMessage(res, { message: userMessage, created: true });
const abortKey = userMessage?.conversationId ?? req.user.id;
getReqData({ abortKey });

View File

@@ -1,11 +1,11 @@
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider');
const { initializeClient } = require('~/server/services/Endpoints/assistants');
const { checkMessageGaps, recordUsage } = require('~/server/services/Threads');
const { deleteMessages } = require('~/models/Message');
const { getConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { sendMessage } = require('~/server/utils');
const { logger } = require('~/config');
const three_minutes = 1000 * 60 * 3;
@@ -34,7 +34,7 @@ async function abortRun(req, res) {
const [thread_id, run_id] = runValues.split(':');
if (!run_id) {
logger.warn("[abortRun] Couldn't find run for cancel request", { thread_id });
logger.warn('[abortRun] Couldn\'t find run for cancel request', { thread_id });
return res.status(204).send({ message: 'Run not found' });
} else if (run_id === 'cancelled') {
logger.warn('[abortRun] Run already cancelled', { thread_id });
@@ -93,7 +93,7 @@ async function abortRun(req, res) {
};
if (res.headersSent && finalEvent) {
return sendEvent(res, finalEvent);
return sendMessage(res, finalEvent);
}
res.json(finalEvent);

View File

@@ -1,7 +1,6 @@
const crypto = require('crypto');
const { sendEvent } = require('@librechat/api');
const { getResponseSender, Constants } = require('librechat-data-provider');
const { sendError } = require('~/server/middleware/error');
const { sendMessage, sendError } = require('~/server/utils');
const { saveMessage } = require('~/models');
/**
@@ -37,7 +36,7 @@ const denyRequest = async (req, res, errorMessage) => {
isCreatedByUser: true,
text,
};
sendEvent(res, { message: userMessage, created: true });
sendMessage(res, { message: userMessage, created: true });
const shouldSaveMessage = _convoId && parentMessageId && parentMessageId !== Constants.NO_PARENT;

View File

@@ -1,5 +1,4 @@
const express = require('express');
const { addTool } = require('@librechat/api');
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
const { getAvailableTools } = require('~/server/controllers/PluginController');
const { toolCallLimiter } = require('~/server/middleware/limiters');
@@ -37,12 +36,4 @@ router.get('/:toolId/auth', verifyToolAuth);
*/
router.post('/:toolId/call', toolCallLimiter, callTool);
/**
* Add a new tool to the system
* @route POST /agents/tools/add
* @param {object} req.body - Request body containing tool data
* @returns {object} Created tool object
*/
router.post('/add', addTool);
module.exports = router;

View File

@@ -1,7 +1,4 @@
const { klona } = require('klona');
const { sleep } = require('@librechat/agents');
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
StepTypes,
RunStatus,
@@ -14,10 +11,11 @@ const {
} = require('librechat-data-provider');
const { retrieveAndProcessFile } = require('~/server/services/Files/process');
const { processRequiredActions } = require('~/server/services/ToolService');
const { createOnProgress, sendMessage, sleep } = require('~/server/utils');
const { RunManager, waitForRun } = require('~/server/services/Runs');
const { processMessages } = require('~/server/services/Threads');
const { createOnProgress } = require('~/server/utils');
const { TextStream } = require('~/app/clients');
const { logger } = require('~/config');
/**
* Sorts, processes, and flattens messages to a single string.
@@ -66,7 +64,7 @@ async function createOnTextProgress({
};
logger.debug('Content data:', contentData);
sendEvent(openai.res, contentData);
sendMessage(openai.res, contentData);
};
}

View File

@@ -1,6 +1,5 @@
const { isUserProvided } = require('@librechat/api');
const { EModelEndpoint } = require('librechat-data-provider');
const { generateConfig } = require('~/server/utils/handleText');
const { isUserProvided, generateConfig } = require('~/server/utils');
const {
OPENAI_API_KEY: openAIApiKey,

View File

@@ -1,18 +1,18 @@
const path = require('path');
const axios = require('axios');
const yaml = require('js-yaml');
const keyBy = require('lodash/keyBy');
const { loadYaml } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
CacheKeys,
configSchema,
paramSettings,
EImageOutputType,
agentParamSettings,
validateSettingDefinitions,
agentParamSettings,
paramSettings,
} = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const loadYaml = require('~/utils/loadYaml');
const { logger } = require('~/config');
const axios = require('axios');
const yaml = require('js-yaml');
const keyBy = require('lodash/keyBy');
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');

View File

@@ -1,9 +1,6 @@
jest.mock('axios');
jest.mock('~/cache/getLogStores');
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
loadYaml: jest.fn(),
}));
jest.mock('~/utils/loadYaml');
jest.mock('librechat-data-provider', () => {
const actual = jest.requireActual('librechat-data-provider');
return {
@@ -33,22 +30,11 @@ jest.mock('librechat-data-provider', () => {
};
});
jest.mock('@librechat/data-schemas', () => {
return {
logger: {
info: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
};
});
const axios = require('axios');
const { loadYaml } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const loadCustomConfig = require('./loadCustomConfig');
const getLogStores = require('~/cache/getLogStores');
const loadYaml = require('~/utils/loadYaml');
const { logger } = require('~/config');
describe('loadCustomConfig', () => {
const mockSet = jest.fn();

View File

@@ -1,93 +0,0 @@
const initializeClient = require('./initialize');
jest.mock('@librechat/api', () => ({
resolveHeaders: jest.fn(),
getOpenAIConfig: jest.fn(),
createHandleLLMNewToken: jest.fn(),
}));
jest.mock('librechat-data-provider', () => ({
CacheKeys: { TOKEN_CONFIG: 'token_config' },
ErrorTypes: { NO_USER_KEY: 'NO_USER_KEY', NO_BASE_URL: 'NO_BASE_URL' },
envVarRegex: /\$\{([^}]+)\}/,
FetchTokenConfig: {},
extractEnvVariable: jest.fn((value) => value),
}));
jest.mock('@librechat/agents', () => ({
Providers: { OLLAMA: 'ollama' },
}));
jest.mock('~/server/services/UserService', () => ({
getUserKeyValues: jest.fn(),
checkUserKeyExpiry: jest.fn(),
}));
jest.mock('~/server/services/Config', () => ({
getCustomEndpointConfig: jest.fn().mockResolvedValue({
apiKey: 'test-key',
baseURL: 'https://test.com',
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
models: { default: ['test-model'] },
}),
}));
jest.mock('~/server/services/ModelService', () => ({
fetchModels: jest.fn(),
}));
jest.mock('~/app/clients/OpenAIClient', () => {
return jest.fn().mockImplementation(() => ({
options: {},
}));
});
jest.mock('~/server/utils', () => ({
isUserProvided: jest.fn().mockReturnValue(false),
}));
jest.mock('~/cache/getLogStores', () =>
jest.fn().mockReturnValue({
get: jest.fn(),
}),
);
describe('custom/initializeClient', () => {
const mockRequest = {
body: { endpoint: 'test-endpoint' },
user: { id: 'user-123', email: 'test@example.com' },
app: { locals: {} },
};
const mockResponse = {};
beforeEach(() => {
jest.clearAllMocks();
});
it('calls resolveHeaders with headers and user', async () => {
const { resolveHeaders } = require('@librechat/api');
await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true });
expect(resolveHeaders).toHaveBeenCalledWith(
{ 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
{ id: 'user-123', email: 'test@example.com' },
);
});
it('throws if endpoint config is missing', async () => {
const { getCustomEndpointConfig } = require('~/server/services/Config');
getCustomEndpointConfig.mockResolvedValueOnce(null);
await expect(
initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }),
).rejects.toThrow('Config not found for the test-endpoint custom endpoint.');
});
it('throws if user is missing', async () => {
await expect(
initializeClient({
req: { ...mockRequest, user: undefined },
res: mockResponse,
optionsOnly: true,
}),
).rejects.toThrow("Cannot read properties of undefined (reading 'id')");
});
});

View File

@@ -1,6 +1,7 @@
const { getGoogleConfig, isEnabled } = require('@librechat/api');
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getLLMConfig } = require('~/server/services/Endpoints/google/llm');
const { isEnabled } = require('~/server/utils');
const { GoogleClient } = require('~/app');
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
@@ -64,7 +65,7 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
if (overrideModel) {
clientOptions.modelOptions.model = overrideModel;
}
return getGoogleConfig(credentials, clientOptions);
return getLLMConfig(credentials, clientOptions);
}
const client = new GoogleClient(credentials, clientOptions);

View File

@@ -1,15 +1,13 @@
import { Providers } from '@librechat/agents';
import { googleSettings, AuthKeys } from 'librechat-data-provider';
import type { GoogleClientOptions, VertexAIClientOptions } from '@librechat/agents';
import type * as t from '~/types';
import { isEnabled } from '~/utils';
const { Providers } = require('@librechat/agents');
const { AuthKeys } = require('librechat-data-provider');
const { isEnabled } = require('~/server/utils');
function getThresholdMapping(model: string) {
function getThresholdMapping(model) {
const gemini1Pattern = /gemini-(1\.0|1\.5|pro$|1\.0-pro|1\.5-pro|1\.5-flash-001)/;
const restrictedPattern = /(gemini-(1\.5-flash-8b|2\.0|exp)|learnlm)/;
if (gemini1Pattern.test(model)) {
return (value: string) => {
return (value) => {
if (value === 'OFF') {
return 'BLOCK_NONE';
}
@@ -18,7 +16,7 @@ function getThresholdMapping(model: string) {
}
if (restrictedPattern.test(model)) {
return (value: string) => {
return (value) => {
if (value === 'OFF' || value === 'HARM_BLOCK_THRESHOLD_UNSPECIFIED') {
return 'BLOCK_NONE';
}
@@ -26,16 +24,19 @@ function getThresholdMapping(model: string) {
};
}
return (value: string) => value;
return (value) => value;
}
export function getSafetySettings(
model?: string,
): Array<{ category: string; threshold: string }> | undefined {
/**
*
* @param {string} model
* @returns {Array<{category: string, threshold: string}> | undefined}
*/
function getSafetySettings(model) {
if (isEnabled(process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS)) {
return undefined;
}
const mapThreshold = getThresholdMapping(model ?? '');
const mapThreshold = getThresholdMapping(model);
return [
{
@@ -73,27 +74,24 @@ export function getSafetySettings(
* Replicates core logic from GoogleClient's constructor and setOptions, plus client determination.
* Returns an object with the provider label and the final options that would be passed to createLLM.
*
* @param credentials - Either a JSON string or an object containing Google keys
* @param options - The same shape as the "GoogleClient" constructor options
* @param {string | object} credentials - Either a JSON string or an object containing Google keys
* @param {object} [options={}] - The same shape as the "GoogleClient" constructor options
*/
export function getGoogleConfig(
credentials: string | t.GoogleCredentials | undefined,
options: t.GoogleConfigOptions = {},
) {
let creds: t.GoogleCredentials = {};
function getLLMConfig(credentials, options = {}) {
// 1. Parse credentials
let creds = {};
if (typeof credentials === 'string') {
try {
creds = JSON.parse(credentials);
} catch (err: unknown) {
throw new Error(
`Error parsing string credentials: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
} catch (err) {
throw new Error(`Error parsing string credentials: ${err.message}`);
}
} else if (credentials && typeof credentials === 'object') {
creds = credentials;
}
// Extract from credentials
const serviceKeyRaw = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {};
const serviceKey =
typeof serviceKeyRaw === 'string' ? JSON.parse(serviceKeyRaw) : (serviceKeyRaw ?? {});
@@ -104,15 +102,9 @@ export function getGoogleConfig(
const reverseProxyUrl = options.reverseProxyUrl;
const authHeader = options.authHeader;
const {
thinking = googleSettings.thinking.default,
thinkingBudget = googleSettings.thinkingBudget.default,
...modelOptions
} = options.modelOptions || {};
const llmConfig: GoogleClientOptions | VertexAIClientOptions = {
...(modelOptions || {}),
model: modelOptions?.model ?? '',
/** @type {GoogleClientOptions | VertexAIClientOptions} */
let llmConfig = {
...(options.modelOptions || {}),
maxRetries: 2,
};
@@ -129,30 +121,16 @@ export function getGoogleConfig(
// If we have a GCP project => Vertex AI
if (project_id && provider === Providers.VERTEXAI) {
(llmConfig as VertexAIClientOptions).authOptions = {
/** @type {VertexAIClientOptions['authOptions']} */
llmConfig.authOptions = {
credentials: { ...serviceKey },
projectId: project_id,
};
(llmConfig as VertexAIClientOptions).location = process.env.GOOGLE_LOC || 'us-central1';
llmConfig.location = process.env.GOOGLE_LOC || 'us-central1';
} else if (apiKey && provider === Providers.GOOGLE) {
llmConfig.apiKey = apiKey;
}
const shouldEnableThinking =
thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1);
if (shouldEnableThinking && provider === Providers.GOOGLE) {
(llmConfig as GoogleClientOptions).thinkingConfig = {
thinkingBudget: thinking ? thinkingBudget : googleSettings.thinkingBudget.default,
includeThoughts: Boolean(thinking),
};
} else if (shouldEnableThinking && provider === Providers.VERTEXAI) {
(llmConfig as VertexAIClientOptions).thinkingBudget = thinking
? thinkingBudget
: googleSettings.thinkingBudget.default;
(llmConfig as VertexAIClientOptions).includeThoughts = Boolean(thinking);
}
/*
let legacyOptions = {};
// Filter out any "examples" that are empty
@@ -174,11 +152,11 @@ export function getGoogleConfig(
*/
if (reverseProxyUrl) {
(llmConfig as GoogleClientOptions).baseUrl = reverseProxyUrl;
llmConfig.baseUrl = reverseProxyUrl;
}
if (authHeader) {
(llmConfig as GoogleClientOptions).customHeaders = {
llmConfig.customHeaders = {
Authorization: `Bearer ${apiKey}`,
};
}
@@ -191,3 +169,8 @@ export function getGoogleConfig(
llmConfig,
};
}
module.exports = {
getLLMConfig,
getSafetySettings,
};

View File

@@ -1,6 +1,3 @@
const { sleep } = require('@librechat/agents');
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
Constants,
StepTypes,
@@ -11,8 +8,9 @@ const {
} = require('librechat-data-provider');
const { retrieveAndProcessFile } = require('~/server/services/Files/process');
const { processRequiredActions } = require('~/server/services/ToolService');
const { createOnProgress, sendMessage, sleep } = require('~/server/utils');
const { processMessages } = require('~/server/services/Threads');
const { createOnProgress } = require('~/server/utils');
const { logger } = require('~/config');
/**
* Implements the StreamRunManager functionality for managing the streaming
@@ -128,7 +126,7 @@ class StreamRunManager {
conversationId: this.finalMessage.conversationId,
};
sendEvent(this.res, contentData);
sendMessage(this.res, contentData);
}
/* <------------------ Misc. Helpers ------------------> */
@@ -304,7 +302,7 @@ class StreamRunManager {
for (const d of delta[key]) {
if (typeof d === 'object' && !Object.prototype.hasOwnProperty.call(d, 'index')) {
logger.warn("Expected an object with an 'index' for array updates but got:", d);
logger.warn('Expected an object with an \'index\' for array updates but got:', d);
continue;
}

View File

@@ -7,9 +7,9 @@ const {
defaultAssistantsVersion,
defaultAgentCapabilities,
} = require('librechat-data-provider');
const { sendEvent } = require('@librechat/api');
const { Providers } = require('@librechat/agents');
const partialRight = require('lodash/partialRight');
const { sendMessage } = require('./streamResponse');
/** Helper function to escape special characters in regex
* @param {string} string - The string to escape.
@@ -37,7 +37,7 @@ const createOnProgress = (
basePayload.text = basePayload.text + chunk;
const payload = Object.assign({}, basePayload, rest);
sendEvent(res, payload);
sendMessage(res, payload);
if (_onProgress) {
_onProgress(payload);
}
@@ -50,7 +50,7 @@ const createOnProgress = (
const sendIntermediateMessage = (res, payload, extraTokens = '') => {
basePayload.text = basePayload.text + extraTokens;
const message = Object.assign({}, basePayload, payload);
sendEvent(res, message);
sendMessage(res, message);
if (i === 0) {
basePayload.initial = false;
}

View File

@@ -1,9 +1,11 @@
const streamResponse = require('./streamResponse');
const removePorts = require('./removePorts');
const countTokens = require('./countTokens');
const handleText = require('./handleText');
const sendEmail = require('./sendEmail');
const queue = require('./queue');
const files = require('./files');
const math = require('./math');
/**
* Check if email configuration is set
@@ -26,6 +28,7 @@ function checkEmailConfig() {
}
module.exports = {
...streamResponse,
checkEmailConfig,
...handleText,
countTokens,
@@ -33,4 +36,5 @@ module.exports = {
sendEmail,
...files,
...queue,
math,
};

View File

@@ -5,14 +5,14 @@
* If the input is not a string or contains invalid characters, an error is thrown.
* If the evaluated result is not a number, an error is thrown.
*
* @param str - The mathematical expression to evaluate, or a number.
* @param fallbackValue - The default value to return if the input is not a string or number, or if the evaluated result is not a number.
* @param {string|number} str - The mathematical expression to evaluate, or a number.
* @param {number} [fallbackValue] - The default value to return if the input is not a string or number, or if the evaluated result is not a number.
*
* @returns The result of the evaluated expression or the input number.
* @returns {number} The result of the evaluated expression or the input number.
*
* @throws Throws an error if the input is not a string or number, contains invalid characters, or does not evaluate to a number.
* @throws {Error} Throws an error if the input is not a string or number, contains invalid characters, or does not evaluate to a number.
*/
export function math(str: string | number, fallbackValue?: number): number {
function math(str, fallbackValue) {
const fallback = typeof fallbackValue !== 'undefined' && typeof fallbackValue === 'number';
if (typeof str !== 'string' && typeof str === 'number') {
return str;
@@ -43,3 +43,5 @@ export function math(str: string | number, fallbackValue?: number): number {
return value;
}
module.exports = math;

View File

@@ -1,9 +1,31 @@
const crypto = require('crypto');
const { logger } = require('@librechat/data-schemas');
const { parseConvo } = require('librechat-data-provider');
const { sendEvent, handleError } = require('@librechat/api');
const { saveMessage, getMessages } = require('~/models/Message');
const { getConvo } = require('~/models/Conversation');
const { logger } = require('~/config');
/**
* Sends error data in Server Sent Events format and ends the response.
* @param {object} res - The server response.
* @param {string} message - The error message.
*/
const handleError = (res, message) => {
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
res.end();
};
/**
* Sends message data in Server Sent Events format.
* @param {Express.Response} res - - The server response.
* @param {string | Object} message - The message to be sent.
* @param {'message' | 'error' | 'cancel'} event - [Optional] The type of event. Default is 'message'.
*/
const sendMessage = (res, message, event = 'message') => {
if (typeof message === 'string' && message.length === 0) {
return;
}
res.write(`event: ${event}\ndata: ${JSON.stringify(message)}\n\n`);
};
/**
* Processes an error with provided options, saves the error message and sends a corresponding SSE response
@@ -69,7 +91,7 @@ const sendError = async (req, res, options, callback) => {
convo = parseConvo(errorMessage);
}
return sendEvent(res, {
return sendMessage(res, {
final: true,
requestMessage: query?.[0] ? query[0] : requestMessage,
responseMessage: errorMessage,
@@ -98,10 +120,12 @@ const sendResponse = (req, res, data, errorMessage) => {
if (errorMessage) {
return sendError(req, res, { ...data, text: errorMessage });
}
return sendEvent(res, data);
return sendMessage(res, data);
};
module.exports = {
sendError,
sendResponse,
handleError,
sendMessage,
sendError,
};

View File

@@ -1,9 +1,11 @@
const loadYaml = require('./loadYaml');
const tokenHelpers = require('./tokens');
const deriveBaseURL = require('./deriveBaseURL');
const extractBaseURL = require('./extractBaseURL');
const findMessageContent = require('./findMessageContent');
module.exports = {
loadYaml,
deriveBaseURL,
extractBaseURL,
...tokenHelpers,

13
api/utils/loadYaml.js Normal file
View File

@@ -0,0 +1,13 @@
const fs = require('fs');
const yaml = require('js-yaml');
function loadYaml(filepath) {
try {
let fileContents = fs.readFileSync(filepath, 'utf8');
return yaml.load(fileContents);
} catch (e) {
return e;
}
}
module.exports = loadYaml;

BIN
bun.lockb

Binary file not shown.

View File

@@ -109,6 +109,9 @@
"tailwindcss-radix": "^2.8.0",
"zod": "^3.22.4"
},
"peerDependencies": {
"jotai": "^2.12.5"
},
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.22.15",
"@babel/preset-env": "^7.22.15",

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type RenderProp<
P = React.HTMLAttributes<any> & {
ref?: React.Ref<any>;

View File

@@ -1,7 +1,6 @@
import React, { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Input, Label, OGDialog, Button } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { Button, Input, Label, OGDialog, OGDialogTemplate } from '~/components';
import { useLocalize } from '~/hooks';
export interface ConfigFieldDetail {

View File

@@ -3,7 +3,7 @@ import { SettingsIcon } from 'lucide-react';
import { Constants } from 'librechat-data-provider';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog';
import MCPConfigDialog, { type ConfigFieldDetail } from './MCPConfigDialog';
import { useToastContext, useBadgeRowContext } from '~/Providers';
import MultiSelect from '~/components/ui/MultiSelect';
import { MCPIcon } from '~/components/svg';

View File

@@ -12,7 +12,6 @@ import {
InputNumber,
SelectDropDown,
HoverCardTrigger,
MultiSelectDropDown,
} from '~/components/ui';
import {
removeFocusOutlines,
@@ -25,6 +24,7 @@ import {
} from '~/utils';
import OptionHoverAlt from '~/components/SidePanel/Parameters/OptionHover';
import { useLocalize, useDebouncedInput } from '~/hooks';
import MultiSelectDropDown from '~/components/Input/ModelSelect/MultiSelectDropDown';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
import store from '~/store';
@@ -165,7 +165,7 @@ export default function Settings({
)}
className={cn(
defaultTextProps,
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ',
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
)}
/>
</div>

View File

@@ -1,4 +1,5 @@
import React, { useState, useRef } from 'react';
import { Wrench, ArrowRight } from 'lucide-react';
import {
Listbox,
ListboxButton,
@@ -7,12 +8,11 @@ import {
ListboxOption,
Transition,
} from '@headlessui/react';
import { Wrench, ArrowRight } from 'lucide-react';
import { CheckMark } from '~/components/svg';
import useOnClickOutside from '~/hooks/useOnClickOutside';
import { useMultiSearch } from './MultiSearch';
import { cn } from '~/utils/';
import type { TPlugin } from 'librechat-data-provider';
import { useMultiSearch } from '~/components/MultiSearch';
import { useOnClickOutside } from '~/hooks';
import { CheckMark } from '~/svgs';
import { cn } from '~/utils/';
export type TMultiSelectDropDownProps = {
title?: string;
@@ -142,7 +142,7 @@ function MultiSelectDropDown({
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -2,7 +2,7 @@ import { Wrench } from 'lucide-react';
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
import type { TPlugin } from 'librechat-data-provider';
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
import { useMultiSearch } from './MultiSearch';
import { useMultiSearch } from '~/components';
import { cn } from '~/utils/';
type SelectDropDownProps = {
@@ -32,8 +32,6 @@ function MultiSelectPop({
optionValueKey = 'value',
searchPlaceholder,
}: SelectDropDownProps) {
// const localize = useLocalize();
const title = _title;
const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins'];
@@ -54,14 +52,14 @@ function MultiSelectPop({
<button
data-testid="select-dropdown-button"
className={cn(
'relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 dark:bg-gray-800 sm:text-sm',
'relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor font-normal',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
)}
>
{' '}
{showLabel && (
<label className="block text-xs text-gray-700 dark:text-gray-500 ">{title}</label>
<label className="block text-xs text-gray-700 dark:text-gray-500">{title}</label>
)}
<span className="inline-flex" id={excludeIds[2]}>
<span
@@ -73,7 +71,7 @@ function MultiSelectPop({
{/* {!showLabel && title.length > 0 && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)} */}
<span className="flex items-center gap-1 ">
<span className="flex items-center gap-1">
<div className="flex gap-1">
{value.length === 0 && 'None selected'}
{value.map((v, i) => (
@@ -98,7 +96,7 @@ function MultiSelectPop({
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -4,15 +4,10 @@ import { useState, useEffect, useMemo } from 'react';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TPlugin } from 'librechat-data-provider';
import type { TModelSelectProps } from '~/common';
import {
Button,
MultiSelectPop,
SelectDropDown,
SelectDropDownPop,
MultiSelectDropDown,
} from '~/components/ui';
import { Button, SelectDropDown, SelectDropDownPop, MultiSelectDropDown } from '~/components/ui';
import { useSetIndexOptions, useAuthContext, useMediaQuery, useLocalize } from '~/hooks';
import { cn, cardStyle, selectPlugins, processPlugins } from '~/utils';
import MultiSelectPop from './MultiSelectPop';
import store from '~/store';
export default function PluginsByIndex({

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
import { useMultiSearch } from '~/components';
import type { Option } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
import { useMultiSearch } from './MultiSearch';
type SelectDropDownProps = {
id?: string;

View File

@@ -1,26 +1,28 @@
import { useCallback, useState, useMemo, useEffect } from 'react';
import { Link } from 'react-router-dom';
import debounce from 'lodash/debounce';
import { useRecoilValue } from 'recoil';
import { Link } from 'react-router-dom';
import { TrashIcon, MessageSquare, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
import {
OGDialog,
OGDialogTemplate,
OGDialogTrigger,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
TooltipAnchor,
DataTable,
Spinner,
Button,
Label,
Spinner,
} from '~/components';
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useMediaQuery } from '~/hooks';
import DataTable from '~/components/ui/DataTable';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { formatDate } from '~/utils';
import store from '~/store';
const PAGE_SIZE = 25;
@@ -36,6 +38,7 @@ export default function SharedLinks() {
const localize = useLocalize();
const { showToast } = useToastContext();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const isSearchEnabled = useRecoilValue(store.search);
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
@@ -308,6 +311,7 @@ export default function SharedLinks() {
onFilterChange={debouncedFilterChange}
filterValue={queryParams.search}
isLoading={isLoading}
enableSearch={isSearchEnabled}
/>
</OGDialogContent>
</OGDialog>

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useMemo, useEffect } from 'react';
import debounce from 'lodash/debounce';
import { useRecoilValue } from 'recoil';
import { TrashIcon, ArchiveRestore, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
import type { ConversationListParams, TConversation } from 'librechat-data-provider';
import {
@@ -11,6 +12,7 @@ import {
Label,
TooltipAnchor,
Spinner,
DataTable,
} from '~/components';
import {
useArchiveConvoMutation,
@@ -19,10 +21,10 @@ import {
} from '~/data-provider';
import { useLocalize, useMediaQuery } from '~/hooks';
import { MinimalIcon } from '~/components/Endpoints';
import DataTable from '~/components/ui/DataTable';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { formatDate } from '~/utils';
import store from '~/store';
const DEFAULT_PARAMS: ConversationListParams = {
isArchived: true,
@@ -39,7 +41,7 @@ export default function ArchivedChatsTable({
const localize = useLocalize();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const { showToast } = useToastContext();
const isSearchEnabled = useRecoilValue(store.search);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
const [deleteConversation, setDeleteConversation] = useState<TConversation | null>(null);
@@ -272,6 +274,7 @@ export default function ArchivedChatsTable({
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
showCheckboxes={false}
enableSearch={isSearchEnabled}
/>
<OGDialog open={isDeleteOpen} onOpenChange={onOpenChange}>

View File

@@ -6,7 +6,6 @@ import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-quer
import type { TUpdateUserPlugins } from 'librechat-data-provider';
import { Button, Input, Label } from '~/components/ui';
import { useGetStartupConfig } from '~/data-provider';
import { useAddToolMutation } from '~/data-provider/Tools';
import MCPPanelSkeleton from './MCPPanelSkeleton';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
@@ -18,12 +17,6 @@ interface ServerConfigWithVars {
};
}
interface AddToolFormData {
name: string;
description: string;
type: 'function' | 'code_interpreter' | 'file_search';
}
export default function MCPPanel() {
const localize = useLocalize();
const { showToast } = useToastContext();
@@ -31,7 +24,6 @@ export default function MCPPanel() {
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
null,
);
const [showAddToolForm, setShowAddToolForm] = useState(true);
const mcpServerDefinitions = useMemo(() => {
if (!startupConfig?.mcpServers) {
@@ -95,25 +87,19 @@ export default function MCPPanel() {
const handleGoBackToList = () => {
setSelectedServerNameForEditing(null);
setShowAddToolForm(false);
};
const handleShowAddToolForm = () => {
setShowAddToolForm(true);
setSelectedServerNameForEditing(null);
};
if (startupConfigLoading) {
return <MCPPanelSkeleton />;
}
// if (mcpServerDefinitions.length === 0) {
// return (
// <div className="p-4 text-center text-sm text-gray-500">
// {localize('com_sidepanel_mcp_no_servers_with_vars')}
// </div>
// );
// }
if (mcpServerDefinitions.length === 0) {
return (
<div className="p-4 text-center text-sm text-gray-500">
{localize('com_sidepanel_mcp_no_servers_with_vars')}
</div>
);
}
if (selectedServerNameForEditing) {
// Editing View
@@ -152,32 +138,10 @@ export default function MCPPanel() {
/>
</div>
);
} else if (showAddToolForm) {
// Add Tool Form View
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<Button
variant="outline"
onClick={handleGoBackToList}
className="mb-3 flex items-center px-3 py-2 text-sm"
>
<ChevronLeft className="mr-1 h-4 w-4" />
{localize('com_ui_back')}
</Button>
<h3 className="mb-3 text-lg font-medium">{localize('com_ui_add_tool')}</h3>
<AddToolForm onCancel={handleGoBackToList} />
</div>
);
} else {
// Server List View
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-lg font-medium">{localize('com_ui_mcp_servers')}</h3>
<Button variant="outline" onClick={handleShowAddToolForm} className="text-sm">
{localize('com_ui_add_tool')}
</Button>
</div>
<div className="space-y-2">
{mcpServerDefinitions.map((server) => (
<Button
@@ -287,131 +251,3 @@ function MCPVariableEditor({ server, onSave, onRevoke, isSubmitting }: MCPVariab
</form>
);
}
interface AddToolFormProps {
onCancel: () => void;
}
function AddToolForm({ onCancel }: AddToolFormProps) {
const localize = useLocalize();
const { showToast } = useToastContext();
const addToolMutation = useAddToolMutation({
onSuccess: (data) => {
showToast({
message: localize('com_ui_tool_added_success', { '0': data.function?.name || 'Unknown' }),
status: 'success',
});
onCancel();
},
onError: (error) => {
console.error('Error adding tool:', error);
showToast({
message: localize('com_ui_tool_add_error'),
status: 'error',
});
},
});
const {
control,
handleSubmit,
formState: { errors, isDirty },
} = useForm<AddToolFormData>({
defaultValues: {
name: '',
description: '',
type: 'function',
},
});
const onFormSubmit = (data: AddToolFormData) => {
addToolMutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-4 mt-2 space-y-4">
<div className="space-y-2">
<Label htmlFor="tool-name" className="text-sm font-medium">
{localize('com_ui_tool_name')}
</Label>
<Controller
name="name"
control={control}
rules={{ required: localize('com_ui_tool_name_required') }}
render={({ field }) => (
<Input
id="tool-name"
type="text"
{...field}
placeholder={localize('com_ui_enter_tool_name')}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
/>
)}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="tool-description" className="text-sm font-medium">
{localize('com_ui_description')}
</Label>
<Controller
name="description"
control={control}
rules={{ required: localize('com_ui_description_required') }}
render={({ field }) => (
<Input
id="tool-description"
type="text"
{...field}
placeholder={localize('com_ui_enter_description')}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
/>
)}
/>
{errors.description && <p className="text-xs text-red-500">{errors.description.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="tool-type" className="text-sm font-medium">
{localize('com_ui_tool_type')}
</Label>
<Controller
name="type"
control={control}
render={({ field }) => (
<select
id="tool-type"
{...field}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
>
<option value="function">{localize('com_ui_function')}</option>
<option value="code_interpreter">{localize('com_ui_code_interpreter')}</option>
<option value="file_search">{localize('com_ui_file_search')}</option>
</select>
)}
/>
{errors.type && <p className="text-xs text-red-500">{errors.type.message}</p>}
</div>
<div className="flex justify-end gap-2 pt-2">
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={addToolMutation.isLoading}
>
{localize('com_ui_cancel')}
</Button>
<Button
type="submit"
className="bg-green-500 text-white hover:bg-green-600"
disabled={addToolMutation.isLoading || !isDirty}
>
{addToolMutation.isLoading ? localize('com_ui_saving') : localize('com_ui_save')}
</Button>
</div>
</form>
);
}

View File

@@ -46,10 +46,6 @@ function DynamicInput({
setInputValue(e, !isNaN(Number(e.target.value)));
};
const placeholderText = placeholderCode
? localize(placeholder as TranslationKeys) || placeholder
: placeholder;
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
@@ -80,7 +76,11 @@ function DynamicInput({
disabled={readonly}
value={inputValue ?? defaultValue ?? ''}
onChange={handleInputChange}
placeholder={placeholderText}
placeholder={
placeholderCode
? localize(placeholder as TranslationKeys) || placeholder
: placeholder
}
className={cn(
'flex h-10 max-h-10 w-full resize-none border-none bg-surface-secondary px-3 py-2',
)}

View File

@@ -29,7 +29,7 @@ function OptionHover({
<HoverCardPortal>
<HoverCardContent side={side} className={`z-[999] w-80 ${className}`} sideOffset={sideOffset}>
<div className="space-y-2">
<p className="whitespace-pre-wrap text-sm text-text-secondary">{text}</p>
<p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>
</div>
</HoverCardContent>
</HoverCardPortal>

View File

@@ -1,111 +0,0 @@
import React from 'react';
import { Search } from 'lucide-react';
import { cn } from '~/utils';
const AnimatedSearchInput = ({
value,
onChange,
isSearching: searching,
placeholder,
}: {
value?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
isSearching?: boolean;
placeholder: string;
}) => {
const isSearching = searching === true;
const hasValue = value != null && value.length > 0;
return (
<div className="relative w-full">
<div className="relative rounded-lg transition-all duration-500 ease-in-out">
<div className="relative">
{/* Icon on the left */}
<div className="absolute left-3 top-1/2 z-50 -translate-y-1/2">
<Search
className={cn(
`
h-4 w-4 transition-all duration-500 ease-in-out`,
isSearching && hasValue ? 'text-blue-400' : 'text-gray-400',
)}
/>
</div>
{/* Input field */}
<input
type="text"
value={value}
onChange={onChange}
placeholder={placeholder}
className={`
peer relative z-20 w-full rounded-lg bg-surface-secondary px-10
py-2 outline-none ring-0 backdrop-blur-sm transition-all
duration-500 ease-in-out placeholder:text-gray-400
focus:outline-none focus:ring-0
`}
/>
{/* Gradient overlay */}
<div
className={`
pointer-events-none absolute inset-0 z-20 rounded-lg
bg-gradient-to-r from-blue-500/20 via-purple-500/20 to-blue-500/20
transition-all duration-500 ease-in-out
${isSearching && hasValue ? 'opacity-100 blur-sm' : 'opacity-0 blur-none'}
`}
/>
{/* Animated loading indicator */}
<div
className={`
absolute right-3 top-1/2 z-20 -translate-y-1/2
transition-all duration-500 ease-in-out
${isSearching && hasValue ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}
`}
>
<div className="relative h-2 w-2">
<div className="absolute inset-0 animate-ping rounded-full bg-blue-500/60" />
<div className="absolute inset-0 rounded-full bg-blue-500" />
</div>
</div>
</div>
</div>
{/* Outer glow effect */}
<div
className={`
absolute -inset-8 -z-10
transition-all duration-700 ease-in-out
${isSearching && hasValue ? 'scale-105 opacity-100' : 'scale-100 opacity-0'}
`}
>
<div className="absolute inset-0">
<div
className={`
bg-gradient-radial absolute inset-0 from-blue-500/10 to-transparent
transition-opacity duration-700 ease-in-out
${isSearching && hasValue ? 'animate-pulse-slow opacity-100' : 'opacity-0'}
`}
/>
<div
className={`
absolute inset-0 bg-gradient-to-r from-purple-500/5 via-blue-500/5 to-purple-500/5
blur-xl transition-all duration-700 ease-in-out
${isSearching && hasValue ? 'animate-gradient-x opacity-100' : 'opacity-0'}
`}
/>
</div>
</div>
<div
className={`
absolute inset-0 -z-20 scale-100 bg-gradient-to-r from-blue-500/10
via-purple-500/10 to-blue-500/10 opacity-0 blur-xl
transition-all duration-500 ease-in-out
peer-focus:scale-105 peer-focus:opacity-100
`}
/>
</div>
);
};
export default AnimatedSearchInput;

View File

@@ -1,5 +0,0 @@
import { useDelayedRender } from '~/hooks';
const DelayedRender = ({ delay, children }) => useDelayedRender(delay)(() => children);
export default DelayedRender;

View File

@@ -1,26 +0,0 @@
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '~/utils';
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & { onDoubleClick?: () => void }
>(({ className, onDoubleClick, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full cursor-pointer touch-none select-none items-center',
className,
)}
onDoubleClick={onDoubleClick}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View File

@@ -4,7 +4,7 @@ import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useAcceptTermsMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { OGDialog } from '~/components/ui';
import { OGDialog } from '~/components';
import { useLocalize } from '~/hooks';
const TermsAndConditionsModal = ({
@@ -73,7 +73,7 @@ const TermsAndConditionsModal = ({
main={
<section
// Motivation: This is a dialog, so its content should be focusable
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className="max-h-[60vh] overflow-y-auto p-4"
aria-label={localize('com_ui_terms_and_conditions')}

View File

@@ -40,44 +40,3 @@ export const useToolCallMutation = <T extends t.ToolId>(
},
);
};
/**
* Interface for creating a new tool
*/
interface CreateToolData {
name: string;
description: string;
type: 'function' | 'code_interpreter' | 'file_search';
metadata?: Record<string, unknown>;
}
/**
* Mutation hook for adding a new tool to the system
* Note: Requires corresponding backend implementation of dataService.createTool
*/
export const useAddToolMutation = (
// options?:
// {
// onMutate?: (variables: CreateToolData) => void | Promise<unknown>;
// onError?: (error: Error, variables: CreateToolData, context: unknown) => void;
// onSuccess?: (data: t.Tool, variables: CreateToolData, context: unknown) => void;
// }
options?: t.MutationOptions<Record<string, unknown>, CreateToolData>,
): UseMutationResult<Record<string, unknown>, Error, CreateToolData> => {
const queryClient = useQueryClient();
return useMutation(
(toolData: CreateToolData) => {
return dataService.createTool(toolData);
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (data, variables, context) => {
// Invalidate tools list to trigger refetch
queryClient.invalidateQueries([QueryKeys.tools]);
return options?.onSuccess?.(data, variables, context);
},
},
);
};

View File

@@ -194,7 +194,7 @@ export const useConversationTagsQuery = (
/**
* Hook for getting all available tools for Assistants
*/
export const useAvailableToolsQuery = <TData = t.TPlugin[]>( // <-- this one
export const useAvailableToolsQuery = <TData = t.TPlugin[]>(
endpoint: t.AssistantsEndpoint | EModelEndpoint.agents,
config?: UseQueryOptions<t.TPlugin[], unknown, TData>,
): QueryObserverResult<TData> => {

View File

@@ -152,20 +152,20 @@ export default function useSideNavLinks({
});
}
// if (
// startupConfig?.mcpServers &&
// Object.values(startupConfig.mcpServers).some(
// (server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
// )
// ) {
links.push({
title: 'com_nav_setting_mcp',
label: '',
icon: MCPIcon,
id: 'mcp-settings',
Component: MCPPanel,
});
// }
if (
startupConfig?.mcpServers &&
Object.values(startupConfig.mcpServers).some(
(server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
)
) {
links.push({
title: 'com_nav_setting_mcp',
label: '',
icon: MCPIcon,
id: 'mcp-settings',
Component: MCPPanel,
});
}
links.push({
title: 'com_sidepanel_hide_panel',

View File

@@ -14,24 +14,19 @@ export * from './Prompts';
export * from './Roles';
export * from './SSE';
export * from './AuthContext';
export * from './ThemeContext';
export * from './ScreenshotContext';
export * from './ApiErrorBoundaryContext';
export * from './Endpoint';
export type { TranslationKeys } from './useLocalize';
export { default as useToast } from './useToast';
export { default as useTimeout } from './useTimeout';
export { default as useNewConvo } from './useNewConvo';
export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useChatBadges } from './useChatBadges';
export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage';
export { default as useDocumentTitle } from './useDocumentTitle';
export { default as useDelayedRender } from './useDelayedRender';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useSpeechToText } from './Input/useSpeechToText';
export { default as useTextToSpeech } from './Input/useTextToSpeech';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';

View File

@@ -1,5 +1,3 @@
// Translation.spec.ts
import i18n from './i18n';
import English from './en/translation.json';
import French from './fr/translation.json';

View File

@@ -207,8 +207,6 @@
"com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.",
"com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).",
"com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.",
"com_endpoint_google_thinking": "Enables or disables reasoning. This setting is only supported by certain models (2.5 series). For older models, this setting may have no effect.",
"com_endpoint_google_thinking_budget": "Guides the number of thinking tokens the model uses. The actual amount may exceed or fall below this value depending on the prompt.\n\nThis setting is only supported by certain models (2.5 series). Gemini 2.5 Pro supports 128-32,768 tokens. Gemini 2.5 Flash supports 0-24,576 tokens. Gemini 2.5 Flash Lite supports 512-24,576 tokens.\n\nLeave blank or set to \"-1\" to let the model automatically decide when and how much to think. By default, Gemini 2.5 Flash Lite does not think.",
"com_endpoint_instructions_assistants": "Override Instructions",
"com_endpoint_instructions_assistants_placeholder": "Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.",
"com_endpoint_max_output_tokens": "Max Output Tokens",
@@ -584,7 +582,6 @@
"com_ui_auth_url": "Authorization URL",
"com_ui_authentication": "Authentication",
"com_ui_authentication_type": "Authentication Type",
"com_ui_auto": "Auto",
"com_ui_available_tools": "Available Tools",
"com_ui_avatar": "Avatar",
"com_ui_azure": "Azure",

View File

@@ -14,7 +14,7 @@ import {
FileMapContext,
SetConvoProvider,
} from '~/Providers';
import TermsAndConditionsModal from '~/components/ui/TermsAndConditionsModal';
import TermsAndConditionsModal from '~/components/ui'; // TODO: remove this comment after updating the import path of all components
import { useUserTermsQuery, useGetStartupConfig } from '~/data-provider';
import { Nav, MobileNav } from '~/components/Nav';
import { useHealthCheck } from '~/data-provider';

View File

@@ -35,5 +35,5 @@
"test/setupTests.js",
"env.d.ts",
"../config/translations/**/*.ts"
]
, "../packages/client/src/hooks/useDelayedRender.tsx" ]
}

View File

@@ -73,8 +73,6 @@ interface:
bookmarks: true
multiConvo: true
agents: true
# Temporary chat retention period in hours (default: 720, min: 1, max: 8760)
# temporaryChatRetention: 1
# Example Cloudflare turnstile (optional)
#turnstile:

552
package-lock.json generated
View File

@@ -64,7 +64,7 @@
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.46",
"@librechat/agents": "^2.4.42",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@node-saml/passport-saml": "^5.0.0",
@@ -1351,6 +1351,33 @@
}
}
},
"api/node_modules/@librechat/agents": {
"version": "2.4.42",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.42.tgz",
"integrity": "sha512-52ux2PeEAV79yr6/h6GN3omlpqX6H0FYl6qwjJ6gT04MMko/imnLd3bQrX0gm3i0KL5ygHbRjQeonONKjJayHw==",
"license": "MIT",
"dependencies": {
"@langchain/anthropic": "^0.3.23",
"@langchain/aws": "^0.1.11",
"@langchain/community": "^0.3.47",
"@langchain/core": "^0.3.60",
"@langchain/deepseek": "^0.0.2",
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/langgraph": "^0.3.4",
"@langchain/mistralai": "^0.2.1",
"@langchain/ollama": "^0.2.3",
"@langchain/openai": "^0.5.14",
"@langchain/xai": "^0.0.3",
"cheerio": "^1.0.0",
"dotenv": "^16.4.7",
"https-proxy-agent": "^7.0.6",
"nanoid": "^3.3.7"
},
"engines": {
"node": ">=14.0.0"
}
},
"api/node_modules/@smithy/abort-controller": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz",
@@ -2625,58 +2652,9 @@
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2"
}
},
"client/node_modules/@ariakit/react": {
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.15.tgz",
"integrity": "sha512-0V2LkNPFrGRT+SEIiObx/LQjR6v3rR+mKEDUu/3tq7jfCZ+7+6Q6EMR1rFaK+XMkaRY1RWUcj/rRDWAUWnsDww==",
"dependencies": {
"@ariakit/react-core": "0.4.15"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ariakit"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"client/node_modules/@ariakit/react-core": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.17.tgz",
"integrity": "sha512-kFF6n+gC/5CRQIyaMTFoBPio2xUe0k9rZhMNdUobWRmc/twfeLVkODx+8UVYaNyKilTge8G0JFqwvFKku/jKEw==",
"license": "MIT",
"dependencies": {
"@ariakit/core": "0.4.15",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"client/node_modules/@ariakit/react-core/node_modules/@ariakit/core": {
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz",
"integrity": "sha512-vvxmZvkNhiisKM+Y1TbGMUfVVchV/sWu9F0xw0RYADXcimWPK31dd9JnIZs/OQ5pwAryAHmERHwuGQVESkSjwQ==",
"license": "MIT"
},
"client/node_modules/@ariakit/react/node_modules/@ariakit/react-core": {
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.15.tgz",
"integrity": "sha512-Up8+U97nAPJdyUh9E8BCEhJYTA+eVztWpHoo1R9zZfHd4cnBWAg5RHxEmMH+MamlvuRxBQA71hFKY/735fDg+A==",
"license": "MIT",
"dependencies": {
"@ariakit/core": "0.4.14",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
"jotai": "^2.12.5"
}
},
"client/node_modules/@babel/compat-data": {
@@ -4547,6 +4525,27 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"client/node_modules/class-variance-authority": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.6.1.tgz",
"integrity": "sha512-eurOEGc7YVx3majOrOb099PNKgO3KnKSApOprXI4BTq6bcfbqbQXPN2u+rPPmIJ2di23bMwhk0SxCCthBmszEQ==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "1.2.1"
},
"funding": {
"url": "https://joebell.co.uk"
}
},
"client/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"client/node_modules/core-js-compat": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
@@ -4578,6 +4577,33 @@
"node": ">=6"
}
},
"client/node_modules/framer-motion": {
"version": "11.18.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
"integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.18.1",
"motion-utils": "^11.18.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"client/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -4588,6 +4614,15 @@
"node": ">=4"
}
},
"client/node_modules/lucide-react": {
"version": "0.394.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.394.0.tgz",
"integrity": "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
"client/node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@@ -4610,16 +4645,6 @@
"node": ">=0.10.0"
}
},
"client/node_modules/react-resizable-panels": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.2.tgz",
"integrity": "sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"client/node_modules/ts-jest": {
"version": "29.2.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
@@ -4794,9 +4819,42 @@
}
},
"node_modules/@ariakit/core": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.14.tgz",
"integrity": "sha512-hpzZvyYzGhP09S9jW1XGsU/FD5K3BKsH1eG/QJ8rfgEeUdPS7BvHPt5lHbOeJ2cMrRzBEvsEzLi1ivfDifHsVA=="
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz",
"integrity": "sha512-vvxmZvkNhiisKM+Y1TbGMUfVVchV/sWu9F0xw0RYADXcimWPK31dd9JnIZs/OQ5pwAryAHmERHwuGQVESkSjwQ==",
"license": "MIT"
},
"node_modules/@ariakit/react": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.17.tgz",
"integrity": "sha512-HQaIboE2axtlncJz1hRTaiQfJ1GGjhdtNcAnPwdjvl2RybfmlHowIB+HTVBp36LzroKPs/M4hPCxk7XTaqRZGg==",
"license": "MIT",
"dependencies": {
"@ariakit/react-core": "0.4.17"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ariakit"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@ariakit/react-core": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.17.tgz",
"integrity": "sha512-kFF6n+gC/5CRQIyaMTFoBPio2xUe0k9rZhMNdUobWRmc/twfeLVkODx+8UVYaNyKilTge8G0JFqwvFKku/jKEw==",
"license": "MIT",
"dependencies": {
"@ariakit/core": "0.4.15",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@aws-crypto/crc32": {
"version": "3.0.0",
@@ -15608,6 +15666,25 @@
"tslib": "^2.4.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
@@ -16886,21 +16963,23 @@
}
},
"node_modules/@headlessui/react": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.2.tgz",
"integrity": "sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz",
"integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1"
"@react-aria/focus": "^3.20.2",
"@react-aria/interactions": "^3.25.0",
"@tanstack/react-virtual": "^3.13.9",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@humanfs/core": {
@@ -19126,9 +19205,9 @@
}
},
"node_modules/@langchain/langgraph": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.3.5.tgz",
"integrity": "sha512-7astlgnp6BdMQJqmr+cbDgR10FYWNCaDLnbfEDHpqhKCCajU59m5snOdl4Vtu5UM6V2k3lgatNqWoflBtxhIyg==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.3.4.tgz",
"integrity": "sha512-Vuja8Qtu3Zjx7k4fK7Cnw+p8gtvIRPciWp9btPhAs3aUo6aBgOJOZVcK5Ii3mHfEHK/aQmRElR0x/u/YwykOrg==",
"license": "MIT",
"dependencies": {
"@langchain/langgraph-checkpoint": "~0.0.18",
@@ -19436,10 +19515,11 @@
}
},
"node_modules/@librechat/agents": {
"version": "2.4.46",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.46.tgz",
"integrity": "sha512-zR27U19/WGF3HN64oBbiaFgjjWHaF7BjYzRFWzQKEkk+iEzCe59IpuEZUizQ54YcY02nhhh6S3MNUjhAJwMYVA==",
"version": "2.4.41",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.41.tgz",
"integrity": "sha512-kYmdk5WVRp0qZxTx6BuGCs4l0Ir9iBLLx4ZY4/1wxr80al5/vq3P8wbgGdKMeO2qTu4ZaT4RyWRQYWBg5HDkUQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@langchain/anthropic": "^0.3.23",
"@langchain/aws": "^0.1.11",
@@ -19467,6 +19547,7 @@
"resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.47.tgz",
"integrity": "sha512-Vo42kAfkXpTFSevhEkeqqE55az8NyQgDktCbitXYuhipNbFYx08XVvqEDkFkB20MM/Z7u+cvLb+DxCqnKuH0CQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@langchain/openai": ">=0.2.0 <0.6.0",
"@langchain/weaviate": "^0.2.0",
@@ -19988,10 +20069,11 @@
}
},
"node_modules/@librechat/agents/node_modules/@langchain/openai": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.5.15.tgz",
"integrity": "sha512-ANadEHyAj5sufQpz+SOPpKbyoMcTLhnh8/d+afbSPUqWsIMPpEFX3HoSY3nrBPG6l4NQQNG5P5oHb4SdC8+YIg==",
"version": "0.5.14",
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.5.14.tgz",
"integrity": "sha512-0GEj5K/qi1MRuZ4nE7NvyI4jTG+RSewLZqsExUwRukWdeqmkPNHGrogTa5ZDt7eaJxAaY7EgLC5ZnvCM3L1oug==",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tiktoken": "^1.0.12",
"openai": "^5.3.0",
@@ -20009,6 +20091,7 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14"
}
@@ -20018,6 +20101,7 @@
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"peer": true,
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
@@ -20027,10 +20111,11 @@
}
},
"node_modules/@librechat/agents/node_modules/openai": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.7.0.tgz",
"integrity": "sha512-zXWawZl6J/P5Wz57/nKzVT3kJQZvogfuyuNVCdEp4/XU2UNrjL7SsuNpWAyLZbo6HVymwmnfno9toVzBhelygA==",
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.5.1.tgz",
"integrity": "sha512-5i19097mGotHA1eFsM6Tjd/tJ8uo9sa5Ysv4Q6bKJ2vtN6rc0MzMrUefXnLXYAJcmMQrC1Efhj0AvfIkXrQamw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"openai": "bin/cli"
},
@@ -20056,6 +20141,7 @@
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"peer": true,
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -20068,6 +20154,10 @@
"resolved": "api",
"link": true
},
"node_modules/@librechat/client": {
"resolved": "packages/client",
"link": true
},
"node_modules/@librechat/data-schemas": {
"resolved": "packages/data-schemas",
"link": true
@@ -22693,49 +22783,43 @@
}
},
"node_modules/@react-aria/focus": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz",
"integrity": "sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==",
"version": "3.20.5",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz",
"integrity": "sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.21.3",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@react-aria/interactions": "^3.25.3",
"@react-aria/utils": "^3.29.1",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/focus/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.21.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz",
"integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==",
"version": "3.25.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz",
"integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.4",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@react-aria/ssr": "^3.9.9",
"@react-aria/utils": "^3.29.1",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz",
"integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==",
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz",
"integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
@@ -22744,32 +22828,25 @@
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz",
"integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==",
"version": "3.29.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz",
"integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.4",
"@react-stately/utils": "^3.10.1",
"@react-types/shared": "^3.23.1",
"@react-aria/ssr": "^3.9.9",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.10.7",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/utils/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-dnd/asap": {
@@ -22879,25 +22956,34 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz",
"integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==",
"version": "3.10.7",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz",
"integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz",
"integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==",
"version": "3.30.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz",
"integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@redis/bloom": {
@@ -25146,12 +25232,12 @@
}
},
"node_modules/@swc/helpers": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz",
"integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==",
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.4.0"
"tslib": "^2.8.0"
}
},
"node_modules/@tanstack/match-sorter-utils": {
@@ -25245,20 +25331,20 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.2.tgz",
"integrity": "sha512-g78+DA29K0ByAfDkuibfLQqDshf8Aha/zcyEZ+huAX/yS/TWj/CUiEY4IJfDrFacdxIFmsLm0u4VtsLSKTngRw==",
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.8.2"
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
@@ -25274,9 +25360,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.2.tgz",
"integrity": "sha512-ffpN6kTaPGwQPoWMcBAHbdv2ZCpj1SugldoYAcY0C4xH+Pej1KCOEUisNeEgbUnXOp8Y/4q6wGPu2tFHthOIQw==",
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -27908,14 +27994,16 @@
"dev": true
},
"node_modules/class-variance-authority": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.6.1.tgz",
"integrity": "sha512-eurOEGc7YVx3majOrOb099PNKgO3KnKSApOprXI4BTq6bcfbqbQXPN2u+rPPmIJ2di23bMwhk0SxCCthBmszEQ==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"clsx": "1.2.1"
"clsx": "^2.1.1"
},
"funding": {
"url": "https://joebell.co.uk"
"url": "https://polar.sh/cva"
}
},
"node_modules/classnames": {
@@ -28071,9 +28159,10 @@
}
},
"node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -31465,22 +31554,22 @@
}
},
"node_modules/framer-motion": {
"version": "11.5.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz",
"integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==",
"version": "10.18.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.18.0.tgz",
"integrity": "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
@@ -32013,7 +32102,8 @@
"node_modules/hamt_plus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
"integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
"integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==",
"license": "MIT"
},
"node_modules/handlebars": {
"version": "4.7.8",
@@ -34441,6 +34531,28 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/jotai": {
"version": "2.12.5",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.5.tgz",
"integrity": "sha512-G8m32HW3lSmcz/4mbqx0hgJIQ0ekndKWiYP7kWVKi0p6saLXdSoye+FZiOFyonnd7Q482LCzm8sMDl7Ar1NWDw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=17.0.0",
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/js-base64": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz",
@@ -35801,9 +35913,11 @@
"license": "ISC"
},
"node_modules/lucide-react": {
"version": "0.394.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.394.0.tgz",
"integrity": "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw==",
"version": "0.263.1",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz",
"integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==",
"license": "ISC",
"peer": true,
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
@@ -37482,6 +37596,21 @@
"color-name": "^1.1.4"
}
},
"node_modules/motion-dom": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
"integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^11.18.1"
}
},
"node_modules/motion-utils": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
"integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
"license": "MIT"
},
"node_modules/mpath": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
@@ -40879,6 +41008,16 @@
}
}
},
"node_modules/react-resizable-panels": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.3.tgz",
"integrity": "sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/react-router": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz",
@@ -40988,6 +41127,15 @@
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-virtualized/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -41092,6 +41240,7 @@
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
"integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
"license": "MIT",
"dependencies": {
"hamt_plus": "1.0.2"
},
@@ -44917,11 +45066,12 @@
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/utf8": {
@@ -46534,7 +46684,7 @@
},
"packages/api": {
"name": "@librechat/api",
"version": "1.2.5",
"version": "1.2.4",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.21.5",
@@ -46566,14 +46716,13 @@
"typescript": "^5.0.4"
},
"peerDependencies": {
"@librechat/agents": "^2.4.46",
"@librechat/agents": "^2.4.41",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.12.3",
"axios": "^1.8.2",
"diff": "^7.0.0",
"eventsource": "^3.0.2",
"express": "^4.21.2",
"js-yaml": "^4.1.0",
"keyv": "^5.3.2",
"librechat-data-provider": "*",
"node-fetch": "2.7.0",
@@ -46657,6 +46806,61 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/client": {
"name": "@librechat/client",
"version": "0.1.0",
"dependencies": {
"@ariakit/react": "^0.4.17",
"@headlessui/react": "^2.2.4",
"react-resizable-panels": "^3.0.3"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"rollup": "^4.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"@radix-ui/react-separator": "^1.0.0",
"@radix-ui/react-slot": "^1.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.0.0",
"framer-motion": "^10.0.0",
"lucide-react": "^0.263.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tailwind-merge": "^1.14.0"
}
},
"packages/client/node_modules/@rollup/plugin-typescript": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
"integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"packages/data-provider": {
"name": "librechat-data-provider",
"version": "0.7.88",

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/api",
"version": "1.2.5",
"version": "1.2.4",
"type": "commonjs",
"description": "MCP services for LibreChat",
"main": "dist/index.js",
@@ -69,14 +69,13 @@
"registry": "https://registry.npmjs.org/"
},
"peerDependencies": {
"@librechat/agents": "^2.4.46",
"@librechat/agents": "^2.4.41",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.12.3",
"axios": "^1.8.2",
"diff": "^7.0.0",
"eventsource": "^3.0.2",
"express": "^4.21.2",
"js-yaml": "^4.1.0",
"keyv": "^5.3.2",
"librechat-data-provider": "*",
"node-fetch": "2.7.0",

View File

@@ -46,10 +46,7 @@ export async function createRun({
customHandlers?: Record<GraphEvents, EventHandler>;
}): Promise<Run<IState>> {
const provider =
(providerEndpointMap[
agent.provider as keyof typeof providerEndpointMap
] as unknown as Providers) ?? agent.provider;
providerEndpointMap[agent.provider as keyof typeof providerEndpointMap] ?? agent.provider;
const llmConfig: t.RunLLMConfig = Object.assign(
{
provider,
@@ -69,9 +66,7 @@ export async function createRun({
}
let reasoningKey: 'reasoning_content' | 'reasoning' | undefined;
if (provider === Providers.GOOGLE) {
reasoningKey = 'reasoning';
} else if (
if (
llmConfig.configuration?.baseURL?.includes(KnownEndpoints.openrouter) ||
(agent.endpoint && agent.endpoint.toLowerCase().includes(KnownEndpoints.openrouter))
) {

View File

@@ -1 +0,0 @@
export * from './llm';

View File

@@ -1,2 +1 @@
export * from './google';
export * from './openai';

View File

@@ -1,9 +1,9 @@
import { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } from 'librechat-data-provider';
import type {
LLMConfigOptions,
UserKeyValues,
OpenAIOptionsResult,
OpenAIConfigOptions,
InitializeOpenAIOptionsParams,
OpenAIOptionsResult,
} from '~/types';
import { createHandleLLMNewToken } from '~/utils/generators';
import { getAzureCredentials } from '~/utils/azure';
@@ -64,7 +64,7 @@ export const initializeOpenAI = async ({
? userValues?.baseURL
: baseURLOptions[endpoint as keyof typeof baseURLOptions];
const clientOptions: OpenAIConfigOptions = {
const clientOptions: LLMConfigOptions = {
proxy: PROXY ?? undefined,
reverseProxyUrl: baseURL || undefined,
streaming: true,
@@ -135,7 +135,7 @@ export const initializeOpenAI = async ({
user: req.user.id,
};
const finalClientOptions: OpenAIConfigOptions = {
const finalClientOptions: LLMConfigOptions = {
...clientOptions,
modelOptions,
};

View File

@@ -13,7 +13,7 @@ import { isEnabled } from '~/utils/common';
*/
export function getOpenAIConfig(
apiKey: string,
options: t.OpenAIConfigOptions = {},
options: t.LLMConfigOptions = {},
endpoint?: string | null,
): t.LLMConfigResult {
const {

View File

@@ -2,7 +2,6 @@
export * from './mcp/manager';
export * from './mcp/oauth';
export * from './mcp/auth';
export * from './mcp/add';
/* Utilities */
export * from './mcp/utils';
export * from './utils';

View File

@@ -1,81 +0,0 @@
import { Request, Response } from 'express';
import { logger } from '@librechat/data-schemas';
interface CreateToolRequest extends Request {
body: {
name: string;
description: string;
type: 'function' | 'code_interpreter' | 'file_search';
metadata?: Record<string, unknown>;
};
user?: {
id: string;
};
}
/**
* Add a new tool to the system
* @route POST /agents/tools/add
* @param {object} req.body - Request body containing tool data
* @param {string} req.body.name - Tool name
* @param {string} req.body.description - Tool description
* @param {string} req.body.type - Tool type (function, code_interpreter, file_search)
* @param {object} [req.body.metadata] - Optional metadata
* @returns {object} Created tool object
*/
export const addTool = async (req: CreateToolRequest, res: Response) => {
try {
const { name, description, type, metadata } = req.body;
// Log the incoming request for development
logger.info(
'Add Tool Request:' +
JSON.stringify({
name,
description,
type,
metadata,
userId: req.user?.id,
}),
);
// Validate required fields
if (!name || !description || !type) {
return res.status(400).json({
error: 'Missing required fields: name, description, and type are required',
});
}
// Validate tool type
const validTypes = ['function', 'code_interpreter', 'file_search'];
if (!validTypes.includes(type)) {
return res.status(400).json({
error: `Invalid tool type. Must be one of: ${validTypes.join(', ')}`,
});
}
// For now, return a mock successful response
// TODO: Implement actual tool creation logic
const mockTool = {
id: `tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type,
function: {
name,
description,
},
metadata: metadata || {},
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
logger.info('Tool created successfully:' + JSON.stringify(mockTool));
res.status(201).json(mockTool);
} catch (error) {
logger.error('Error adding tool:', error);
res.status(500).json({
error: 'Internal server error while adding tool',
message: error instanceof Error ? error.message : 'Unknown error',
});
}
};

View File

@@ -7,7 +7,6 @@ const RECOGNIZED_PROVIDERS = new Set([
'xai',
'deepseek',
'ollama',
'bedrock',
]);
const CONTENT_ARRAY_PROVIDERS = new Set(['google', 'anthropic', 'openai']);

View File

@@ -1,24 +0,0 @@
import { z } from 'zod';
import { AuthKeys, googleBaseSchema } from 'librechat-data-provider';
export type GoogleParameters = z.infer<typeof googleBaseSchema>;
export type GoogleCredentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]?: string;
[AuthKeys.GOOGLE_API_KEY]?: string;
};
/**
* Configuration options for the getLLMConfig function
*/
export interface GoogleConfigOptions {
modelOptions?: Partial<GoogleParameters>;
reverseProxyUrl?: string;
defaultQuery?: Record<string, string | undefined>;
headers?: Record<string, string>;
proxy?: string;
streaming?: boolean;
authHeader?: boolean;
addParams?: Record<string, unknown>;
dropParams?: string[];
}

View File

@@ -1,6 +1,5 @@
export * from './azure';
export * from './events';
export * from './google';
export * from './mistral';
export * from './openai';
export * from './run';

View File

@@ -9,7 +9,7 @@ export type OpenAIParameters = z.infer<typeof openAISchema>;
/**
* Configuration options for the getLLMConfig function
*/
export interface OpenAIConfigOptions {
export interface LLMConfigOptions {
modelOptions?: Partial<OpenAIParameters>;
reverseProxyUrl?: string;
defaultQuery?: Record<string, string | undefined>;

View File

@@ -1,9 +1,8 @@
import type { Providers } from '@librechat/agents';
import type { AgentModelParameters } from 'librechat-data-provider';
import type { AgentModelParameters, EModelEndpoint } from 'librechat-data-provider';
import type { OpenAIConfiguration } from './openai';
export type RunLLMConfig = {
provider: Providers;
provider: EModelEndpoint;
streaming: boolean;
streamUsage: boolean;
usage?: boolean;

View File

@@ -314,116 +314,4 @@ describe('resolveHeaders', () => {
'Dot-Header': 'dot-value',
});
});
// Additional comprehensive tests for all user field placeholders
it('should replace all allowed user field placeholders', () => {
const user = {
id: 'abc',
name: 'Test User',
username: 'testuser',
email: 'me@example.com',
provider: 'google',
role: 'admin',
googleId: 'gid',
facebookId: 'fbid',
openidId: 'oid',
samlId: 'sid',
ldapId: 'lid',
githubId: 'ghid',
discordId: 'dcid',
appleId: 'aid',
emailVerified: true,
twoFactorEnabled: false,
termsAccepted: true,
};
const headers = {
'X-User-ID': '{{LIBRECHAT_USER_ID}}',
'X-User-Name': '{{LIBRECHAT_USER_NAME}}',
'X-User-Username': '{{LIBRECHAT_USER_USERNAME}}',
'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}',
'X-User-Provider': '{{LIBRECHAT_USER_PROVIDER}}',
'X-User-Role': '{{LIBRECHAT_USER_ROLE}}',
'X-User-GoogleId': '{{LIBRECHAT_USER_GOOGLEID}}',
'X-User-FacebookId': '{{LIBRECHAT_USER_FACEBOOKID}}',
'X-User-OpenIdId': '{{LIBRECHAT_USER_OPENIDID}}',
'X-User-SamlId': '{{LIBRECHAT_USER_SAMLID}}',
'X-User-LdapId': '{{LIBRECHAT_USER_LDAPID}}',
'X-User-GithubId': '{{LIBRECHAT_USER_GITHUBID}}',
'X-User-DiscordId': '{{LIBRECHAT_USER_DISCORDID}}',
'X-User-AppleId': '{{LIBRECHAT_USER_APPLEID}}',
'X-User-EmailVerified': '{{LIBRECHAT_USER_EMAILVERIFIED}}',
'X-User-TwoFactorEnabled': '{{LIBRECHAT_USER_TWOFACTORENABLED}}',
'X-User-TermsAccepted': '{{LIBRECHAT_USER_TERMSACCEPTED}}',
};
const result = resolveHeaders(headers, user);
expect(result['X-User-ID']).toBe('abc');
expect(result['X-User-Name']).toBe('Test User');
expect(result['X-User-Username']).toBe('testuser');
expect(result['X-User-Email']).toBe('me@example.com');
expect(result['X-User-Provider']).toBe('google');
expect(result['X-User-Role']).toBe('admin');
expect(result['X-User-GoogleId']).toBe('gid');
expect(result['X-User-FacebookId']).toBe('fbid');
expect(result['X-User-OpenIdId']).toBe('oid');
expect(result['X-User-SamlId']).toBe('sid');
expect(result['X-User-LdapId']).toBe('lid');
expect(result['X-User-GithubId']).toBe('ghid');
expect(result['X-User-DiscordId']).toBe('dcid');
expect(result['X-User-AppleId']).toBe('aid');
expect(result['X-User-EmailVerified']).toBe('true');
expect(result['X-User-TwoFactorEnabled']).toBe('false');
expect(result['X-User-TermsAccepted']).toBe('true');
});
it('should handle multiple placeholders in one value', () => {
const user = { id: 'abc', email: 'me@example.com' };
const headers = {
'X-Multi': 'User: {{LIBRECHAT_USER_ID}}, Env: ${TEST_API_KEY}, Custom: {{MY_CUSTOM}}',
};
const customVars = { MY_CUSTOM: 'custom-value' };
const result = resolveHeaders(headers, user, customVars);
expect(result['X-Multi']).toBe('User: abc, Env: test-api-key-value, Custom: custom-value');
});
it('should leave unknown placeholders unchanged', () => {
const user = { id: 'abc' };
const headers = {
'X-Unknown': '{{SOMETHING_NOT_RECOGNIZED}}',
'X-Known': '{{LIBRECHAT_USER_ID}}',
};
const result = resolveHeaders(headers, user);
expect(result['X-Unknown']).toBe('{{SOMETHING_NOT_RECOGNIZED}}');
expect(result['X-Known']).toBe('abc');
});
it('should handle a mix of all types', () => {
const user = {
id: 'abc',
email: 'me@example.com',
emailVerified: true,
twoFactorEnabled: false,
};
const headers = {
'X-User': '{{LIBRECHAT_USER_ID}}',
'X-Env': '${TEST_API_KEY}',
'X-Custom': '{{MY_CUSTOM}}',
'X-Multi': 'ID: {{LIBRECHAT_USER_ID}}, ENV: ${TEST_API_KEY}, CUSTOM: {{MY_CUSTOM}}',
'X-Unknown': '{{NOT_A_REAL_PLACEHOLDER}}',
'X-Empty': '',
'X-Boolean': '{{LIBRECHAT_USER_EMAILVERIFIED}}',
};
const customVars = { MY_CUSTOM: 'custom-value' };
const result = resolveHeaders(headers, user, customVars);
expect(result['X-User']).toBe('abc');
expect(result['X-Env']).toBe('test-api-key-value');
expect(result['X-Custom']).toBe('custom-value');
expect(result['X-Multi']).toBe('ID: abc, ENV: test-api-key-value, CUSTOM: custom-value');
expect(result['X-Unknown']).toBe('{{NOT_A_REAL_PLACEHOLDER}}');
expect(result['X-Empty']).toBe('');
expect(result['X-Boolean']).toBe('true');
});
});

View File

@@ -14,13 +14,3 @@ export function sendEvent(res: ServerResponse, event: ServerSentEvent): void {
}
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
}
/**
* Sends error data in Server Sent Events format and ends the response.
* @param res - The server response.
* @param message - The error message.
*/
export function handleError(res: ServerResponse, message: string): void {
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
res.end();
}

View File

@@ -6,8 +6,5 @@ export * from './events';
export * from './files';
export * from './generators';
export * from './llm';
export * from './math';
export * from './openid';
export * from './tempChatRetention';
export { default as Tokenizer } from './tokenizer';
export * from './yaml';

View File

@@ -1,133 +0,0 @@
import {
MIN_RETENTION_HOURS,
MAX_RETENTION_HOURS,
DEFAULT_RETENTION_HOURS,
getTempChatRetentionHours,
createTempChatExpirationDate,
} from './tempChatRetention';
import type { TCustomConfig } from 'librechat-data-provider';
describe('tempChatRetention', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.TEMP_CHAT_RETENTION_HOURS;
});
afterAll(() => {
process.env = originalEnv;
});
describe('getTempChatRetentionHours', () => {
it('should return default retention hours when no config or env var is set', () => {
const result = getTempChatRetentionHours();
expect(result).toBe(DEFAULT_RETENTION_HOURS);
});
it('should use environment variable when set', () => {
process.env.TEMP_CHAT_RETENTION_HOURS = '48';
const result = getTempChatRetentionHours();
expect(result).toBe(48);
});
it('should use config value when set', () => {
const config: Partial<TCustomConfig> = {
interface: {
temporaryChatRetention: 12,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(12);
});
it('should prioritize config over environment variable', () => {
process.env.TEMP_CHAT_RETENTION_HOURS = '48';
const config: Partial<TCustomConfig> = {
interface: {
temporaryChatRetention: 12,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(12);
});
it('should enforce minimum retention period', () => {
const config: Partial<TCustomConfig> = {
interface: {
temporaryChatRetention: 0,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(MIN_RETENTION_HOURS);
});
it('should enforce maximum retention period', () => {
const config: Partial<TCustomConfig> = {
interface: {
temporaryChatRetention: 10000,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(MAX_RETENTION_HOURS);
});
it('should handle invalid environment variable', () => {
process.env.TEMP_CHAT_RETENTION_HOURS = 'invalid';
const result = getTempChatRetentionHours();
expect(result).toBe(DEFAULT_RETENTION_HOURS);
});
it('should handle invalid config value', () => {
const config: Partial<TCustomConfig> = {
interface: {
temporaryChatRetention: 'invalid' as unknown as number,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(DEFAULT_RETENTION_HOURS);
});
});
describe('createTempChatExpirationDate', () => {
it('should create expiration date with default retention period', () => {
const result = createTempChatExpirationDate();
const expectedDate = new Date();
expectedDate.setHours(expectedDate.getHours() + DEFAULT_RETENTION_HOURS);
// Allow for small time differences in test execution
const timeDiff = Math.abs(result.getTime() - expectedDate.getTime());
expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference
});
it('should create expiration date with custom retention period', () => {
const config: Partial<TCustomConfig> = {
interface: {
temporaryChatRetention: 12,
},
};
const result = createTempChatExpirationDate(config);
const expectedDate = new Date();
expectedDate.setHours(expectedDate.getHours() + 12);
// Allow for small time differences in test execution
const timeDiff = Math.abs(result.getTime() - expectedDate.getTime());
expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference
});
it('should return a Date object', () => {
const result = createTempChatExpirationDate();
expect(result).toBeInstanceOf(Date);
});
it('should return a future date', () => {
const now = new Date();
const result = createTempChatExpirationDate();
expect(result.getTime()).toBeGreaterThan(now.getTime());
});
});
});

View File

@@ -1,77 +0,0 @@
import { logger } from '@librechat/data-schemas';
import type { TCustomConfig } from 'librechat-data-provider';
/**
* Default retention period for temporary chats in hours
*/
export const DEFAULT_RETENTION_HOURS = 24 * 30; // 30 days
/**
* Minimum allowed retention period in hours
*/
export const MIN_RETENTION_HOURS = 1;
/**
* Maximum allowed retention period in hours (1 year = 8760 hours)
*/
export const MAX_RETENTION_HOURS = 8760;
/**
* Gets the temporary chat retention period from environment variables or config
* @param config - The custom configuration object
* @returns The retention period in hours
*/
export function getTempChatRetentionHours(config?: Partial<TCustomConfig> | null): number {
let retentionHours = DEFAULT_RETENTION_HOURS;
// Check environment variable first
if (process.env.TEMP_CHAT_RETENTION_HOURS) {
const envValue = parseInt(process.env.TEMP_CHAT_RETENTION_HOURS, 10);
if (!isNaN(envValue)) {
retentionHours = envValue;
} else {
logger.warn(
`Invalid TEMP_CHAT_RETENTION_HOURS environment variable: ${process.env.TEMP_CHAT_RETENTION_HOURS}. Using default: ${DEFAULT_RETENTION_HOURS} hours.`,
);
}
}
// Check config file (takes precedence over environment variable)
if (config?.interface?.temporaryChatRetention !== undefined) {
const configValue = config.interface.temporaryChatRetention;
if (typeof configValue === 'number' && !isNaN(configValue)) {
retentionHours = configValue;
} else {
logger.warn(
`Invalid temporaryChatRetention in config: ${configValue}. Using ${retentionHours} hours.`,
);
}
}
// Validate the retention period
if (retentionHours < MIN_RETENTION_HOURS) {
logger.warn(
`Temporary chat retention period ${retentionHours} is below minimum ${MIN_RETENTION_HOURS} hours. Using minimum value.`,
);
retentionHours = MIN_RETENTION_HOURS;
} else if (retentionHours > MAX_RETENTION_HOURS) {
logger.warn(
`Temporary chat retention period ${retentionHours} exceeds maximum ${MAX_RETENTION_HOURS} hours. Using maximum value.`,
);
retentionHours = MAX_RETENTION_HOURS;
}
return retentionHours;
}
/**
* Creates an expiration date for temporary chats
* @param config - The custom configuration object
* @returns The expiration date
*/
export function createTempChatExpirationDate(config?: Partial<TCustomConfig>): Date {
const retentionHours = getTempChatRetentionHours(config);
const expiredAt = new Date();
expiredAt.setHours(expiredAt.getHours() + retentionHours);
return expiredAt;
}

View File

@@ -1,11 +0,0 @@
import fs from 'fs';
import yaml from 'js-yaml';
export function loadYaml(filepath: string) {
try {
const fileContents = fs.readFileSync(filepath, 'utf8');
return yaml.load(fileContents);
} catch (e) {
return e;
}
}

View File

@@ -0,0 +1,44 @@
{
"name": "@librechat/client",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/index.js"
}
},
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rollup -c rollup.config.mjs",
"dev": "rollup -c rollup.config.mjs -w",
"clean": "rm -rf dist"
},
"peerDependencies": {
"@radix-ui/react-separator": "^1.0.0",
"@radix-ui/react-slot": "^1.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.0.0",
"framer-motion": "^10.0.0",
"lucide-react": "^0.263.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"rollup": "^4.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"@ariakit/react": "^0.4.17",
"@headlessui/react": "^2.2.4",
"react-resizable-panels": "^3.0.3"
}
}

View File

@@ -0,0 +1,40 @@
// ESM bundler config for React components
import resolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import alias from '@rollup/plugin-alias';
import { fileURLToPath } from 'url';
import { dirname, resolve as pathResolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default {
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'es',
preserveModules: true,
},
external: [
'react',
'react-dom',
'react/jsx-runtime',
'@radix-ui/react-separator',
'@radix-ui/react-slot',
'class-variance-authority',
'clsx',
'framer-motion',
'lucide-react',
'tailwind-merge',
],
plugins: [
alias({
entries: [{ find: '~', replacement: pathResolve(__dirname, 'src') }],
}),
resolve(),
typescript({
tsconfig: './tsconfig.json',
jsx: 'react-jsx',
}),
],
};

View File

@@ -0,0 +1,6 @@
export interface AnnounceOptions {
message: string;
isStatus?: boolean;
}
export const MESSAGE_UPDATE_INTERVAL = 7000;

View File

@@ -0,0 +1,33 @@
import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
import type { OptionWithIcon, ExtendedFile } from './types';
export type TAgentOption = OptionWithIcon &
Agent & {
knowledge_files?: Array<[string, ExtendedFile]>;
context_files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>;
};
export type TAgentCapabilities = {
[AgentCapabilities.web_search]: boolean;
[AgentCapabilities.file_search]: boolean;
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean;
};
export type AgentForm = {
agent?: TAgentOption;
id: string;
name: string | null;
description: string | null;
instructions: string | null;
model: string | null;
model_parameters: AgentModelParameters;
tools?: string[];
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
recursion_limit?: number;
} & TAgentCapabilities;

View File

@@ -0,0 +1,27 @@
export interface CodeBlock {
id: string;
language: string;
content: string;
}
export interface Artifact {
id: string;
lastUpdateTime: number;
index?: number;
messageId?: string;
identifier?: string;
language?: string;
content?: string;
title?: string;
type?: string;
}
export type ArtifactFiles =
| {
'App.tsx': string;
'index.tsx': string;
'/components/ui/MermaidDiagram.tsx': string;
}
| Partial<{
[x: string]: string | undefined;
}>;

View File

@@ -0,0 +1,31 @@
import { Capabilities, EModelEndpoint } from 'librechat-data-provider';
import type { Assistant, AssistantsEndpoint } from 'librechat-data-provider';
import type { Option, ExtendedFile } from './types';
export type ActionsEndpoint = AssistantsEndpoint | EModelEndpoint.agents;
export type TAssistantOption =
| string
| (Option &
Assistant & {
files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>;
});
export type Actions = {
[Capabilities.code_interpreter]: boolean;
[Capabilities.image_vision]: boolean;
[Capabilities.retrieval]: boolean;
};
export type AssistantForm = {
assistant: TAssistantOption;
id: string;
name: string | null;
description: string | null;
instructions: string | null;
conversation_starters: string[];
model: string;
functions: string[];
append_current_datetime: boolean;
} & Actions;

View File

@@ -0,0 +1,10 @@
export {
TShowToast,
Option,
OptionWithIcon,
DropdownValueSetter,
MentionOption,
NotificationSeverity,
} from './types';
export { MenuItemProps } from './menus';

View File

@@ -0,0 +1,26 @@
import {
AuthorizationTypeEnum,
AuthTypeEnum,
TokenExchangeMethodEnum,
} from 'librechat-data-provider';
import { MCPForm } from '~/common/types';
export const defaultMCPFormValues: MCPForm = {
type: AuthTypeEnum.None,
saved_auth_fields: false,
api_key: '',
authorization_type: AuthorizationTypeEnum.Basic,
custom_auth_header: '',
oauth_client_id: '',
oauth_client_secret: '',
authorization_url: '',
client_url: '',
scope: '',
token_exchange_method: TokenExchangeMethodEnum.DefaultPost,
name: '',
description: '',
url: '',
tools: [],
icon: '',
trust: false,
};

View File

@@ -0,0 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type RenderProp<
P = React.HTMLAttributes<any> & {
ref?: React.Ref<any>;
},
> = (props: P) => React.ReactNode;
export interface MenuItemProps {
id?: string;
label?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => void;
icon?: React.ReactNode;
kbd?: string;
show?: boolean;
disabled?: boolean;
separate?: boolean;
hideOnClick?: boolean;
dialog?: React.ReactElement;
ref?: React.Ref<any>;
render?:
| RenderProp<React.HTMLAttributes<any> & { ref?: React.Ref<any> | undefined }>
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
| undefined;
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { TModelSpec, TStartupConfig } from 'librechat-data-provider';
export interface Endpoint {
value: string;
label: string;
hasModels: boolean;
models?: Array<{ name: string; isGlobal?: boolean }>;
icon: React.ReactNode;
agentNames?: Record<string, string>;
assistantNames?: Record<string, string>;
modelIcons?: Record<string, string | undefined>;
}
export interface SelectedValues {
endpoint: string | null;
model: string | null;
modelSpec: string | null;
}
export interface ModelSelectorProps {
startupConfig: TStartupConfig | undefined;
}

View File

@@ -0,0 +1,6 @@
import type { AuthType } from 'librechat-data-provider';
export type ApiKeyFormData = {
apiKey: string;
authType?: string | AuthType;
};

View File

@@ -0,0 +1,624 @@
import { RefObject } from 'react';
import { FileSources, EModelEndpoint } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type * as InputNumberPrimitive from 'rc-input-number';
import type { PrimitiveAtom, WritableAtom } from 'jotai';
import type { ColumnDef } from '@tanstack/react-table';
import type * as t from 'librechat-data-provider';
import type { LucideIcon } from 'lucide-react';
import type { TranslationKeys } from '~/hooks';
export type CodeBarProps = {
lang: string;
error?: boolean;
plugin?: boolean;
blockIndex?: number;
allowExecution?: boolean;
codeRef: RefObject<HTMLElement>;
};
export enum PromptsEditorMode {
SIMPLE = 'simple',
ADVANCED = 'advanced',
}
export enum STTEndpoints {
browser = 'browser',
external = 'external',
}
export enum TTSEndpoints {
browser = 'browser',
external = 'external',
}
export type AudioChunk = {
audio: string;
isFinal: boolean;
alignment: {
char_start_times_ms: number[];
chars_durations_ms: number[];
chars: string[];
};
normalizedAlignment: {
char_start_times_ms: number[];
chars_durations_ms: number[];
chars: string[];
};
};
export type BadgeItem = {
id: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
label: string;
atom: PrimitiveAtom<boolean> | WritableAtom<boolean, [boolean], void>;
isAvailable: boolean;
};
export type AssistantListItem = {
id: string;
name: string;
metadata: t.Assistant['metadata'];
model: string;
};
export type AgentListItem = {
id: string;
name: string;
avatar: t.Agent['avatar'];
};
export type TPluginMap = Record<string, t.TPlugin>;
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
export type LocalizeFunction = (
phraseKey: TranslationKeys,
options?: Record<string, string | number>,
) => string;
export type ChatFormValues = { text: string };
export const mainTextareaId = 'prompt-textarea';
export const globalAudioId = 'global-audio';
export enum IconContext {
landing = 'landing',
menuItem = 'menu-item',
nav = 'nav',
message = 'message',
}
export type IconMapProps = {
className?: string;
iconURL?: string;
context?: 'landing' | 'menu-item' | 'nav' | 'message';
endpoint?: string | null;
endpointType?: string;
assistantName?: string;
agentName?: string;
avatar?: string;
size?: number;
};
export type IconComponent = React.ComponentType<IconMapProps>;
export type AgentIconComponent = React.ComponentType<AgentIconMapProps>;
export type IconComponentTypes = IconComponent | AgentIconComponent;
export type IconsRecord = {
[key in t.EModelEndpoint | 'unknown' | string]: IconComponentTypes | null | undefined;
};
export type AgentIconMapProps = IconMapProps & { agentName?: string };
export type NavLink = {
title: TranslationKeys;
label?: string;
icon: LucideIcon | React.FC;
Component?: React.ComponentType;
onClick?: (e?: React.MouseEvent) => void;
variant?: 'default' | 'ghost';
id: string;
};
export interface NavProps {
isCollapsed: boolean;
links: NavLink[];
resize?: (size: number) => void;
defaultActive?: string;
}
export interface DataColumnMeta {
meta:
| {
size: number | string;
}
| undefined;
}
export enum Panel {
advanced = 'advanced',
builder = 'builder',
actions = 'actions',
model = 'model',
version = 'version',
mcp = 'mcp',
}
export type FileSetter =
| GenericSetter<Map<string, ExtendedFile>>
| React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
export type ActionAuthForm = {
/* General */
type: t.AuthTypeEnum;
saved_auth_fields: boolean;
/* API key */
api_key: string; // not nested
authorization_type: t.AuthorizationTypeEnum;
custom_auth_header: string;
/* OAuth */
oauth_client_id: string; // not nested
oauth_client_secret: string; // not nested
authorization_url: string;
client_url: string;
scope: string;
token_exchange_method: t.TokenExchangeMethodEnum;
};
export type MCPForm = ActionAuthForm & {
name?: string;
description?: string;
url?: string;
tools?: string[];
icon?: string;
trust?: boolean;
};
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
metadata: t.ActionMetadata | null;
};
export type AssistantPanelProps = {
index?: number;
action?: ActionWithNullableMetadata;
actions?: t.Action[];
assistant_id?: string;
activePanel?: string;
endpoint: t.AssistantsEndpoint;
version: number | string;
documentsMap: Map<string, t.AssistantDocument> | null;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};
export type AgentPanelProps = {
index?: number;
agent_id?: string;
activePanel?: string;
mcp?: t.MCP;
mcps?: t.MCP[];
action?: t.Action;
actions?: t.Action[];
createMutation: UseMutationResult<t.Agent, Error, t.AgentCreateParams>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
endpointsConfig?: t.TEndpointsConfig;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
agentsConfig?: t.TAgentsEndpoint | null;
};
export type AgentPanelContextType = {
action?: t.Action;
actions?: t.Action[];
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
mcp?: t.MCP;
mcps?: t.MCP[];
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
tools: t.AgentToolType[];
activePanel?: string;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
agent_id?: string;
};
export type AgentModelPanelProps = {
agent_id?: string;
providers: Option[];
models: Record<string, string[] | undefined>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
export type TSetOption = t.TSetOption;
export type TSetExample = (
i: number,
type: string,
newValue: number | string | boolean | null,
) => void;
export type OnInputNumberChange = InputNumberPrimitive.InputNumberProps['onChange'];
export const defaultDebouncedDelay = 450;
export enum ESide {
Top = 'top',
Right = 'right',
Bottom = 'bottom',
Left = 'left',
}
export enum NotificationSeverity {
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
ERROR = 'error',
}
export type TShowToast = {
message: string;
severity?: NotificationSeverity;
showIcon?: boolean;
duration?: number;
status?: 'error' | 'success' | 'warning' | 'info';
};
export type TBaseSettingsProps = {
conversation: t.TConversation | t.TPreset | null;
className?: string;
isPreset?: boolean;
readonly?: boolean;
};
export type TSettingsProps = TBaseSettingsProps & {
setOption: TSetOption;
};
export type TModels = {
models: string[];
showAbove?: boolean;
popover?: boolean;
};
export type TModelSelectProps = TSettingsProps & TModels;
export type TEditPresetProps = {
open: boolean;
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
preset: t.TPreset;
title?: string;
};
export type TSetOptions = (options: Record<string, unknown>) => void;
export type TSetOptionsPayload = {
setOption: TSetOption;
setExample: TSetExample;
addExample: () => void;
removeExample: () => void;
setAgentOption: TSetOption;
// getConversation: () => t.TConversation | t.TPreset | null;
checkPluginSelection: (value: string) => boolean;
setTools: (newValue: string, remove?: boolean) => void;
setOptions?: TSetOptions;
};
export type TPresetItemProps = {
preset: t.TPreset;
value: t.TPreset;
onSelect: (preset: t.TPreset) => void;
onChangePreset: (preset: t.TPreset) => void;
onDeletePreset: (preset: t.TPreset) => void;
};
export type TOnClick = (e: React.MouseEvent<HTMLButtonElement>) => void;
export type TGenButtonProps = {
onClick: TOnClick;
};
export type TAskProps = {
text: string;
overrideConvoId?: string;
overrideUserMessageId?: string;
parentMessageId?: string | null;
conversationId?: string | null;
messageId?: string | null;
clientTimestamp?: string;
};
export type TOptions = {
editedMessageId?: string | null;
editedText?: string | null;
isRegenerate?: boolean;
isContinued?: boolean;
isEdited?: boolean;
overrideMessages?: t.TMessage[];
/** This value is only true when the user submits a message with "Save & Submit" for a user-created message */
isResubmission?: boolean;
/** Currently only utilized when `isResubmission === true`, uses that message's currently attached files */
overrideFiles?: t.TMessage['files'];
};
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
export type TMessageProps = {
conversation?: t.TConversation | null;
messageId?: string | null;
message?: t.TMessage;
messagesTree?: t.TMessage[];
currentEditId: string | number | null;
isSearchView?: boolean;
siblingIdx?: number;
siblingCount?: number;
setCurrentEditId?: React.Dispatch<React.SetStateAction<string | number | null>> | null;
setSiblingIdx?: ((value: number) => void | React.Dispatch<React.SetStateAction<number>>) | null;
};
export type TMessageIcon = { endpoint?: string | null; isCreatedByUser?: boolean } & Pick<
t.TConversation,
'modelLabel'
> &
Pick<t.TMessage, 'model' | 'iconURL'>;
export type TInitialProps = {
text: string;
edit: boolean;
error: boolean;
unfinished: boolean;
isSubmitting: boolean;
isLast: boolean;
};
export type TAdditionalProps = {
ask: TAskFunction;
message: t.TMessage;
isCreatedByUser: boolean;
siblingIdx: number;
enterEdit: (cancel: boolean) => void;
setSiblingIdx: (value: number) => void;
};
export type TMessageContentProps = TInitialProps & TAdditionalProps;
export type TText = Pick<TInitialProps, 'text'> & { className?: string };
export type TEditProps = Pick<TInitialProps, 'isSubmitting'> &
Omit<TAdditionalProps, 'isCreatedByUser' | 'siblingIdx'> & {
text?: string;
index?: number;
siblingIdx: number | null;
};
export type TDisplayProps = TText &
Pick<TAdditionalProps, 'isCreatedByUser' | 'message'> & {
showCursor?: boolean;
};
export type TConfigProps = {
userKey: string;
setUserKey: React.Dispatch<React.SetStateAction<string>>;
endpoint: t.EModelEndpoint | string;
};
export type TDangerButtonProps = {
id: string;
confirmClear: boolean;
className?: string;
disabled?: boolean;
showText?: boolean;
mutation?: UseMutationResult<unknown>;
onClick: () => void;
infoTextCode: TranslationKeys;
actionTextCode: TranslationKeys;
dataTestIdInitial: string;
dataTestIdConfirm: string;
infoDescriptionCode?: TranslationKeys;
confirmActionTextCode?: TranslationKeys;
};
export type TDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export type TPluginStoreDialogProps = {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
};
export type TResError = {
response: { data: { message: string } };
message: string;
};
export type TAuthContext = {
user: t.TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
error: string | undefined;
login: (data: t.TLoginUser) => void;
logout: (redirect?: string) => void;
setError: React.Dispatch<React.SetStateAction<string | undefined>>;
roles?: Record<string, t.TRole | null | undefined>;
};
export type TUserContext = {
user?: t.TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
redirect?: string;
};
export type TAuthConfig = {
loginRedirect: string;
test?: boolean;
};
export type IconProps = Pick<t.TMessage, 'isCreatedByUser' | 'model'> &
Pick<t.TConversation, 'chatGptLabel' | 'modelLabel'> & {
size?: number;
button?: boolean;
iconURL?: string;
message?: boolean;
className?: string;
iconClassName?: string;
endpoint?: t.EModelEndpoint | string | null;
endpointType?: t.EModelEndpoint | null;
assistantName?: string;
agentName?: string;
error?: boolean;
};
export type Option = Record<string, unknown> & {
label?: string;
value: string | number | null;
};
export type StringOption = Option & { value: string | null };
export type VoiceOption = {
value: string;
label: string;
};
export type TMessageAudio = {
isLast?: boolean;
index: number;
messageId: string;
content: string;
className?: string;
renderButton?: (props: {
onClick: (e?: React.MouseEvent<HTMLButtonElement>) => void;
title: string;
icon: React.ReactNode;
isActive?: boolean;
isVisible?: boolean;
isDisabled?: boolean;
className?: string;
}) => React.ReactNode;
};
export type OptionWithIcon = Option & { icon?: React.ReactNode };
export type DropdownValueSetter = (value: string | Option | OptionWithIcon) => void;
export type MentionOption = OptionWithIcon & {
type: string;
value: string;
description?: string;
};
export type PromptOption = MentionOption & {
id: string;
};
export type TOptionSettings = {
showExamples?: boolean;
isCodeChat?: boolean;
};
export interface ExtendedFile {
file?: File;
file_id: string;
temp_file_id?: string;
type?: string;
filepath?: string;
filename?: string;
width?: number;
height?: number;
size: number;
preview?: string;
progress: number;
source?: FileSources;
attached?: boolean;
embedded?: boolean;
tool_resource?: string;
metadata?: t.TFile['metadata'];
}
export interface ModelItemProps {
modelName: string;
endpoint: EModelEndpoint;
isSelected: boolean;
onSelect: () => void;
onNavigateBack: () => void;
icon?: JSX.Element;
className?: string;
}
export type ContextType = {
navVisible: boolean;
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
};
export interface SwitcherProps {
endpoint?: t.EModelEndpoint | null;
endpointKeyProvided: boolean;
isCollapsed: boolean;
}
export type TLoginLayoutContext = {
startupConfig: t.TStartupConfig | null;
startupConfigError: unknown;
isFetching: boolean;
error: string | null;
setError: React.Dispatch<React.SetStateAction<string | null>>;
headerText: string;
setHeaderText: React.Dispatch<React.SetStateAction<string>>;
};
export type NewConversationParams = {
template?: Partial<t.TConversation>;
preset?: Partial<t.TPreset>;
modelsData?: t.TModelsConfig;
buildDefault?: boolean;
keepLatestMessage?: boolean;
keepAddedConvos?: boolean;
disableParams?: boolean;
};
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;
export type TBaseResData = {
plugin?: t.TResPlugin;
final?: boolean;
initial?: boolean;
previousMessages?: t.TMessage[];
conversation: t.TConversation;
conversationId?: string;
runMessages?: t.TMessage[];
};
export type TResData = TBaseResData & {
requestMessage: t.TMessage;
responseMessage: t.TMessage;
};
export type TFinalResData = Omit<TBaseResData, 'conversation'> & {
conversation: Partial<t.TConversation> & Pick<t.TConversation, 'conversationId'>;
requestMessage?: t.TMessage;
responseMessage?: t.TMessage;
};
export type TVectorStore = {
_id: string;
object: 'vector_store';
created_at: string | Date;
name: string;
bytes?: number;
file_counts?: {
in_progress: number;
completed: number;
failed: number;
cancelled: number;
total: number;
};
};
export type TThread = { id: string; createdAt: string };
declare global {
interface Window {
google_tag_manager?: unknown;
}
}

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import { cn } from '~/utils';
const Accordion = AccordionPrimitive.Root;
@@ -28,7 +27,7 @@ const AccordionTrigger = React.forwardRef<
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200" />
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '../../utils';
import { cn } from '~/utils';
const AlertDialog = AlertDialogPrimitive.Root;

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { Search } from 'lucide-react';
import { cn } from '~/utils';
const AnimatedSearchInput = ({
value,
onChange,
isSearching: searching,
placeholder,
}: {
value?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
isSearching?: boolean;
placeholder: string;
}) => {
const isSearching = searching === true;
const hasValue = value != null && value.length > 0;
return (
<div className="relative w-full">
<div className="relative rounded-lg transition-all duration-500 ease-in-out">
<div className="relative">
{/* Icon on the left */}
<div className="absolute left-3 top-1/2 z-50 -translate-y-1/2">
<Search
className={cn(
`h-4 w-4 transition-all duration-500 ease-in-out`,
isSearching && hasValue ? 'text-blue-400' : 'text-gray-400',
)}
/>
</div>
{/* Input field */}
<input
type="text"
value={value}
onChange={onChange}
placeholder={placeholder}
className={`peer relative z-20 w-full rounded-lg bg-surface-secondary px-10 py-2 outline-none ring-0 backdrop-blur-sm transition-all duration-500 ease-in-out placeholder:text-gray-400 focus:outline-none focus:ring-0`}
/>
{/* Gradient overlay */}
<div
className={`pointer-events-none absolute inset-0 z-20 rounded-lg bg-gradient-to-r from-blue-500/20 via-purple-500/20 to-blue-500/20 transition-all duration-500 ease-in-out ${isSearching && hasValue ? 'opacity-100 blur-sm' : 'opacity-0 blur-none'} `}
/>
{/* Animated loading indicator */}
<div
className={`absolute right-3 top-1/2 z-20 -translate-y-1/2 transition-all duration-500 ease-in-out ${isSearching && hasValue ? 'scale-100 opacity-100' : 'scale-0 opacity-0'} `}
>
<div className="relative h-2 w-2">
<div className="absolute inset-0 animate-ping rounded-full bg-blue-500/60" />
<div className="absolute inset-0 rounded-full bg-blue-500" />
</div>
</div>
</div>
</div>
{/* Outer glow effect */}
<div
className={`absolute -inset-8 -z-10 transition-all duration-700 ease-in-out ${isSearching && hasValue ? 'scale-105 opacity-100' : 'scale-100 opacity-0'} `}
>
<div className="absolute inset-0">
<div
className={`bg-gradient-radial absolute inset-0 from-blue-500/10 to-transparent transition-opacity duration-700 ease-in-out ${isSearching && hasValue ? 'animate-pulse-slow opacity-100' : 'opacity-0'} `}
/>
<div
className={`absolute inset-0 bg-gradient-to-r from-purple-500/5 via-blue-500/5 to-purple-500/5 blur-xl transition-all duration-700 ease-in-out ${isSearching && hasValue ? 'animate-gradient-x opacity-100' : 'opacity-0'} `}
/>
</div>
</div>
<div
className={`absolute inset-0 -z-20 scale-100 bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-blue-500/10 opacity-0 blur-xl transition-all duration-500 ease-in-out peer-focus:scale-105 peer-focus:opacity-100`}
/>
</div>
);
};
export default AnimatedSearchInput;

View File

@@ -26,7 +26,7 @@
position: relative;
}
.animated-tab[data-state="active"] {
.animated-tab[data-state='active'] {
border-bottom-color: transparent !important;
}

View File

@@ -1,12 +1,15 @@
import type React from 'react';
import { X, Plus } from 'lucide-react';
import { motion } from 'framer-motion';
import type { ButtonHTMLAttributes } from 'react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '~/utils';
interface BadgeProps extends ButtonHTMLAttributes<HTMLButtonElement> {
interface BadgeProps
extends Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'
> {
icon?: LucideIcon;
label: string;
id?: string;
@@ -71,7 +74,7 @@ export default function Badge({
}}
whileTap={{ scale: isDragging ? 1.1 : isDisabled ? 1 : 0.97 }}
transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }}
{...props}
{...(props as React.ComponentProps<typeof motion.button>)}
>
{Icon && <Icon className={cn('relative h-5 w-5 md:h-4 md:w-4', !label && 'mx-auto')} />}
<span className="relative hidden md:inline">{label}</span>

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '~/utils';
const Breadcrumb = React.forwardRef<
@@ -17,7 +16,7 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWi
<ol
ref={ref}
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5',
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className,
)}
{...props}
@@ -44,7 +43,7 @@ const BreadcrumbLink = React.forwardRef<
return (
<Comp
ref={ref}
className={cn('hover:text-foreground transition-colors', className)}
className={cn('transition-colors hover:text-foreground', className)}
{...props}
/>
);
@@ -58,7 +57,7 @@ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWit
role="link"
aria-disabled="true"
aria-current="page"
className={cn('text-foreground font-normal', className)}
className={cn('font-normal text-foreground', className)}
{...props}
/>
),

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