🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes (#6553)

* 🔧 feat: Enhance logging configuration for production and debug environments

* 🔒 feat: Implement encryption and decryption functions for sensitive values in ActionService with URL encoding/decoding

* refactor: optimize action service for agent tools

* refactor: optimize action processing for Assistants API

* fix: handle case where agent is not found in loadAgent function

* refactor: improve error handling in API calls by throwing new Error with logAxiosError output

* chore: bump @librechat/agents to 2.3.95, fixes "Invalid tool call structure: No preceding AIMessage with tool_call_ids"

* refactor: enhance error logging in logAxiosError function to include response status

* refactor: remove unused useModelSelection hook from Endpoint

* refactor: add support for assistants in useSelectorEffects hook

* refactor: replace string easing with imported easings in Landing component

* chore: remove duplicate translation

* refactor: update model selection logic and improve localization for UI elements

* refactor: replace endpoint value checks with helper functions for agents and assistants

* refactor: optimize display value logic and utilize useMemo for performance improvements

* refactor: clean up imports and optimize display/icon value logic in endpoint components, fix spec selection

* refactor: enhance error logging in axios utility to include stack traces for better debugging

* refactor: update logging configuration to use DEBUG_LOGGING and streamline log level handling

* refactor: adjust className for export menu button to improve layout consistency and remove unused title prop from ShareButton

* refactor: update import path for logAxiosError utility to improve module organization and clarity

* refactor: implement debounced search value setter in ModelSelectorContext for improved performance
This commit is contained in:
Danny Avila 2025-03-26 14:10:52 -04:00 committed by GitHub
parent 801b602e27
commit 299cabd6ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 970 additions and 1135 deletions

View file

@ -4,7 +4,11 @@ require('winston-daily-rotate-file');
const logDir = path.join(__dirname, '..', 'logs');
const { NODE_ENV } = process.env;
const { NODE_ENV, DEBUG_LOGGING = false } = process.env;
const useDebugLogging =
(typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING?.toLowerCase() === 'true') ||
DEBUG_LOGGING === true;
const levels = {
error: 0,
@ -36,9 +40,10 @@ const fileFormat = winston.format.combine(
winston.format.splat(),
);
const logLevel = useDebugLogging ? 'debug' : 'error';
const transports = [
new winston.transports.DailyRotateFile({
level: 'debug',
level: logLevel,
filename: `${logDir}/meiliSync-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
@ -48,14 +53,6 @@ const transports = [
}),
];
// if (NODE_ENV !== 'production') {
// transports.push(
// new winston.transports.Console({
// format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
// }),
// );
// }
const consoleFormat = winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),

View file

@ -5,7 +5,7 @@ const { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat } = requi
const logDir = path.join(__dirname, '..', 'logs');
const { NODE_ENV, DEBUG_LOGGING = true, DEBUG_CONSOLE = false, CONSOLE_JSON = false } = process.env;
const { NODE_ENV, DEBUG_LOGGING = true, CONSOLE_JSON = false, DEBUG_CONSOLE = false } = process.env;
const useConsoleJson =
(typeof CONSOLE_JSON === 'string' && CONSOLE_JSON?.toLowerCase() === 'true') ||
@ -15,6 +15,10 @@ const useDebugConsole =
(typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE?.toLowerCase() === 'true') ||
DEBUG_CONSOLE === true;
const useDebugLogging =
(typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING?.toLowerCase() === 'true') ||
DEBUG_LOGGING === true;
const levels = {
error: 0,
warn: 1,
@ -57,28 +61,9 @@ const transports = [
maxFiles: '14d',
format: fileFormat,
}),
// new winston.transports.DailyRotateFile({
// level: 'info',
// filename: `${logDir}/info-%DATE%.log`,
// datePattern: 'YYYY-MM-DD',
// zippedArchive: true,
// maxSize: '20m',
// maxFiles: '14d',
// }),
];
// if (NODE_ENV !== 'production') {
// transports.push(
// new winston.transports.Console({
// format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
// }),
// );
// }
if (
(typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING?.toLowerCase() === 'true') ||
DEBUG_LOGGING === true
) {
if (useDebugLogging) {
transports.push(
new winston.transports.DailyRotateFile({
level: 'debug',
@ -107,10 +92,16 @@ const consoleFormat = winston.format.combine(
}),
);
// Determine console log level
let consoleLogLevel = 'info';
if (useDebugConsole) {
consoleLogLevel = 'debug';
}
if (useDebugConsole) {
transports.push(
new winston.transports.Console({
level: 'debug',
level: consoleLogLevel,
format: useConsoleJson
? winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json())
: winston.format.combine(fileFormat, debugTraverse),
@ -119,14 +110,14 @@ if (useDebugConsole) {
} else if (useConsoleJson) {
transports.push(
new winston.transports.Console({
level: 'info',
level: consoleLogLevel,
format: winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()),
}),
);
} else {
transports.push(
new winston.transports.Console({
level: 'info',
level: consoleLogLevel,
format: consoleFormat,
}),
);

View file

@ -46,6 +46,10 @@ const loadAgent = async ({ req, agent_id }) => {
id: agent_id,
});
if (!agent) {
return null;
}
if (agent.author.toString() === req.user.id) {
return agent;
}

View file

@ -49,7 +49,7 @@
"@langchain/google-genai": "^0.1.11",
"@langchain/google-vertexai": "^0.2.2",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.3.94",
"@librechat/agents": "^2.3.95",
"@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2",

View file

@ -13,7 +13,6 @@ const {
actionDomainSeparator,
} = require('librechat-data-provider');
const { refreshAccessToken } = require('~/server/services/TokenService');
const { isActionDomainAllowed } = require('~/server/services/domains');
const { logger, getFlowStateManager, sendEvent } = require('~/config');
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
const { getActions, deleteActions } = require('~/models/Action');
@ -130,6 +129,7 @@ async function loadActionSets(searchParams) {
* @param {string | undefined} [params.name] - The name of the tool.
* @param {string | undefined} [params.description] - The description for the tool.
* @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition
* @param {{ oauth_client_id?: string; oauth_client_secret?: string; }} params.encrypted - The encrypted values for the action.
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
*/
async function createActionTool({
@ -140,17 +140,8 @@ async function createActionTool({
zodSchema,
name,
description,
encrypted,
}) {
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
if (!isDomainAllowed) {
return null;
}
const encrypted = {
oauth_client_id: action.metadata.oauth_client_id,
oauth_client_secret: action.metadata.oauth_client_secret,
};
action.metadata = await decryptMetadata(action.metadata);
/** @type {(toolInput: Object | string, config: GraphRunnableConfig) => Promise<unknown>} */
const _call = async (toolInput, config) => {
try {
@ -308,9 +299,8 @@ async function createActionTool({
}
return response.data;
} catch (error) {
const logMessage = `API call to ${action.metadata.domain} failed`;
logAxiosError({ message: logMessage, error });
throw error;
const message = `API call to ${action.metadata.domain} failed:`;
return logAxiosError({ message, error });
}
};
@ -327,6 +317,27 @@ async function createActionTool({
};
}
/**
* Encrypts a sensitive value.
* @param {string} value
* @returns {Promise<string>}
*/
async function encryptSensitiveValue(value) {
// Encode API key to handle special characters like ":"
const encodedValue = encodeURIComponent(value);
return await encryptV2(encodedValue);
}
/**
* Decrypts a sensitive value.
* @param {string} value
* @returns {Promise<string>}
*/
async function decryptSensitiveValue(value) {
const decryptedValue = await decryptV2(value);
return decodeURIComponent(decryptedValue);
}
/**
* Encrypts sensitive metadata values for an action.
*
@ -339,17 +350,19 @@ async function encryptMetadata(metadata) {
// ServiceHttp
if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) {
if (metadata.api_key) {
encryptedMetadata.api_key = await encryptV2(metadata.api_key);
encryptedMetadata.api_key = await encryptSensitiveValue(metadata.api_key);
}
}
// OAuth
else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) {
if (metadata.oauth_client_id) {
encryptedMetadata.oauth_client_id = await encryptV2(metadata.oauth_client_id);
encryptedMetadata.oauth_client_id = await encryptSensitiveValue(metadata.oauth_client_id);
}
if (metadata.oauth_client_secret) {
encryptedMetadata.oauth_client_secret = await encryptV2(metadata.oauth_client_secret);
encryptedMetadata.oauth_client_secret = await encryptSensitiveValue(
metadata.oauth_client_secret,
);
}
}
@ -368,17 +381,19 @@ async function decryptMetadata(metadata) {
// ServiceHttp
if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) {
if (metadata.api_key) {
decryptedMetadata.api_key = await decryptV2(metadata.api_key);
decryptedMetadata.api_key = await decryptSensitiveValue(metadata.api_key);
}
}
// OAuth
else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) {
if (metadata.oauth_client_id) {
decryptedMetadata.oauth_client_id = await decryptV2(metadata.oauth_client_id);
decryptedMetadata.oauth_client_id = await decryptSensitiveValue(metadata.oauth_client_id);
}
if (metadata.oauth_client_secret) {
decryptedMetadata.oauth_client_secret = await decryptV2(metadata.oauth_client_secret);
decryptedMetadata.oauth_client_secret = await decryptSensitiveValue(
metadata.oauth_client_secret,
);
}
}

View file

@ -32,11 +32,12 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
const response = await axios(options);
return response;
} catch (error) {
logAxiosError({
message: `Error downloading code environment file stream: ${error.message}`,
error,
});
throw new Error(`Error downloading file: ${error.message}`);
throw new Error(
logAxiosError({
message: `Error downloading code environment file stream: ${error.message}`,
error,
}),
);
}
}
@ -89,11 +90,12 @@ async function uploadCodeEnvFile({ req, stream, filename, apiKey, entity_id = ''
return `${fileIdentifier}?entity_id=${entity_id}`;
} catch (error) {
logAxiosError({
message: `Error uploading code environment file: ${error.message}`,
error,
});
throw new Error(`Error uploading code environment file: ${error.message}`);
throw new Error(
logAxiosError({
message: `Error uploading code environment file: ${error.message}`,
error,
}),
);
}
}

View file

@ -5,7 +5,7 @@ const FormData = require('form-data');
const { FileSources, envVarRegex, extractEnvVariable } = require('librechat-data-provider');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { logger, createAxiosInstance } = require('~/config');
const { logAxiosError } = require('~/utils');
const { logAxiosError } = require('~/utils/axios');
const axios = createAxiosInstance();
@ -194,8 +194,7 @@ const uploadMistralOCR = async ({ req, file, file_id, entity_id }) => {
};
} catch (error) {
const message = 'Error uploading document to Mistral OCR API';
logAxiosError({ error, message });
throw new Error(message);
throw new Error(logAxiosError({ error, message }));
}
};

View file

@ -29,9 +29,6 @@ const mockAxios = {
jest.mock('axios', () => mockAxios);
jest.mock('fs');
jest.mock('~/utils', () => ({
logAxiosError: jest.fn(),
}));
jest.mock('~/config', () => ({
logger: {
error: jest.fn(),
@ -494,9 +491,6 @@ describe('MistralOCR Service', () => {
}),
).rejects.toThrow('Error uploading document to Mistral OCR API');
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
const { logAxiosError } = require('~/utils');
expect(logAxiosError).toHaveBeenCalled();
});
it('should handle single page documents without page numbering', async () => {

View file

@ -55,8 +55,7 @@ async function retrieveRun({ thread_id, run_id, timeout, openai }) {
return response.data;
} catch (error) {
const message = '[retrieveRun] Failed to retrieve run data:';
logAxiosError({ message, error });
throw error;
throw new Error(logAxiosError({ message, error }));
}
}

View file

@ -93,11 +93,12 @@ const refreshAccessToken = async ({
return response.data;
} catch (error) {
const message = 'Error refreshing OAuth tokens';
logAxiosError({
message,
error,
});
throw new Error(message);
throw new Error(
logAxiosError({
message,
error,
}),
);
}
};
@ -156,11 +157,12 @@ const getAccessToken = async ({
return response.data;
} catch (error) {
const message = 'Error exchanging OAuth code';
logAxiosError({
message,
error,
});
throw new Error(message);
throw new Error(
logAxiosError({
message,
error,
}),
);
}
};

View file

@ -15,9 +15,15 @@ const {
AgentCapabilities,
validateAndParseOpenAPISpec,
} = require('librechat-data-provider');
const {
loadActionSets,
createActionTool,
decryptMetadata,
domainParser,
} = require('./ActionService');
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
const { createYouTubeTools, manifestToolMap, toolkits } = require('~/app/clients/tools');
const { loadActionSets, createActionTool, domainParser } = require('./ActionService');
const { isActionDomainAllowed } = require('~/server/services/domains');
const { getEndpointsConfig } = require('~/server/services/Config');
const { recordUsage } = require('~/server/services/Threads');
const { loadTools } = require('~/app/clients/tools/util');
@ -315,58 +321,95 @@ async function processRequiredActions(client, requiredActions) {
if (!tool) {
// throw new Error(`Tool ${currentAction.tool} not found.`);
// Load all action sets once if not already loaded
if (!actionSets.length) {
actionSets =
(await loadActionSets({
assistant_id: client.req.body.assistant_id,
})) ?? [];
// Process all action sets once
// Map domains to their processed action sets
const processedDomains = new Map();
const domainMap = new Map();
for (const action of actionSets) {
const domain = await domainParser(client.req, action.metadata.domain, true);
domainMap.set(domain, action);
// Check if domain is allowed
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
if (!isDomainAllowed) {
continue;
}
// Validate and parse OpenAPI spec
const validationResult = validateAndParseOpenAPISpec(action.metadata.raw_spec);
if (!validationResult.spec) {
throw new Error(
`Invalid spec: user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
);
}
// Process the OpenAPI spec
const { requestBuilders } = openapiToFunction(validationResult.spec);
// Store encrypted values for OAuth flow
const encrypted = {
oauth_client_id: action.metadata.oauth_client_id,
oauth_client_secret: action.metadata.oauth_client_secret,
};
// Decrypt metadata
const decryptedAction = { ...action };
decryptedAction.metadata = await decryptMetadata(action.metadata);
processedDomains.set(domain, {
action: decryptedAction,
requestBuilders,
encrypted,
});
// Store builders for reuse
ActionBuildersMap[action.metadata.domain] = requestBuilders;
}
// Update actionSets reference to use the domain map
actionSets = { domainMap, processedDomains };
}
let actionSet = null;
// Find the matching domain for this tool
let currentDomain = '';
for (let action of actionSets) {
const domain = await domainParser(client.req, action.metadata.domain, true);
for (const domain of actionSets.domainMap.keys()) {
if (currentAction.tool.includes(domain)) {
currentDomain = domain;
actionSet = action;
break;
}
}
if (!actionSet) {
if (!currentDomain || !actionSets.processedDomains.has(currentDomain)) {
// TODO: try `function` if no action set is found
// throw new Error(`Tool ${currentAction.tool} not found.`);
continue;
}
let builders = ActionBuildersMap[actionSet.metadata.domain];
if (!builders) {
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
if (!validationResult.spec) {
throw new Error(
`Invalid spec: user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
);
}
const { requestBuilders } = openapiToFunction(validationResult.spec);
ActionToolMap[actionSet.metadata.domain] = requestBuilders;
builders = requestBuilders;
}
const { action, requestBuilders, encrypted } = actionSets.processedDomains.get(currentDomain);
const functionName = currentAction.tool.replace(`${actionDelimiter}${currentDomain}`, '');
const requestBuilder = builders[functionName];
const requestBuilder = requestBuilders[functionName];
if (!requestBuilder) {
// throw new Error(`Tool ${currentAction.tool} not found.`);
continue;
}
// We've already decrypted the metadata, so we can pass it directly
tool = await createActionTool({
req: client.req,
res: client.res,
action: actionSet,
action,
requestBuilder,
// Note: intentionally not passing zodSchema, name, and description for assistants API
encrypted, // Pass the encrypted values for OAuth flow
});
if (!tool) {
logger.warn(
@ -511,7 +554,62 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
};
}
let actionSets = [];
const actionSets = (await loadActionSets({ agent_id: agent.id })) ?? [];
if (actionSets.length === 0) {
if (_agentTools.length > 0 && agentTools.length === 0) {
logger.warn(`No tools found for the specified tool calls: ${_agentTools.join(', ')}`);
}
return {
tools: agentTools,
toolContextMap,
};
}
// Process each action set once (validate spec, decrypt metadata)
const processedActionSets = new Map();
const domainMap = new Map();
for (const action of actionSets) {
const domain = await domainParser(req, action.metadata.domain, true);
domainMap.set(domain, action);
// Check if domain is allowed (do this once per action set)
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
if (!isDomainAllowed) {
continue;
}
// Validate and parse OpenAPI spec once per action set
const validationResult = validateAndParseOpenAPISpec(action.metadata.raw_spec);
if (!validationResult.spec) {
continue;
}
const encrypted = {
oauth_client_id: action.metadata.oauth_client_id,
oauth_client_secret: action.metadata.oauth_client_secret,
};
// Decrypt metadata once per action set
const decryptedAction = { ...action };
decryptedAction.metadata = await decryptMetadata(action.metadata);
// Process the OpenAPI spec once per action set
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
validationResult.spec,
true,
);
processedActionSets.set(domain, {
action: decryptedAction,
requestBuilders,
functionSignatures,
zodSchemas,
encrypted,
});
}
// Now map tools to the processed action sets
const ActionToolMap = {};
for (const toolName of _agentTools) {
@ -519,55 +617,47 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
continue;
}
if (!actionSets.length) {
actionSets = (await loadActionSets({ agent_id: agent.id })) ?? [];
}
let actionSet = null;
// Find the matching domain for this tool
let currentDomain = '';
for (let action of actionSets) {
const domain = await domainParser(req, action.metadata.domain, true);
for (const domain of domainMap.keys()) {
if (toolName.includes(domain)) {
currentDomain = domain;
actionSet = action;
break;
}
}
if (!actionSet) {
if (!currentDomain || !processedActionSets.has(currentDomain)) {
continue;
}
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
if (validationResult.spec) {
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
validationResult.spec,
true,
);
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
const requestBuilder = requestBuilders[functionName];
const zodSchema = zodSchemas[functionName];
const { action, encrypted, zodSchemas, requestBuilders, functionSignatures } =
processedActionSets.get(currentDomain);
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
const requestBuilder = requestBuilders[functionName];
const zodSchema = zodSchemas[functionName];
if (requestBuilder) {
const tool = await createActionTool({
req,
res,
action: actionSet,
requestBuilder,
zodSchema,
name: toolName,
description: functionSig.description,
});
if (!tool) {
logger.warn(
`Invalid action: user: ${req.user.id} | agent_id: ${agent.id} | toolName: ${toolName}`,
);
throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`);
}
agentTools.push(tool);
ActionToolMap[toolName] = tool;
if (requestBuilder) {
const tool = await createActionTool({
req,
res,
action,
requestBuilder,
zodSchema,
encrypted,
name: toolName,
description: functionSig.description,
});
if (!tool) {
logger.warn(
`Invalid action: user: ${req.user.id} | agent_id: ${agent.id} | toolName: ${toolName}`,
);
throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`);
}
agentTools.push(tool);
ActionToolMap[toolName] = tool;
}
}

View file

@ -6,32 +6,41 @@ const { logger } = require('~/config');
* @param {Object} options - The options object.
* @param {string} options.message - The custom message to be logged.
* @param {import('axios').AxiosError} options.error - The Axios error object.
* @returns {string} The log message.
*/
const logAxiosError = ({ message, error }) => {
let logMessage = message;
try {
const stack = error.stack || 'No stack trace available';
if (error.response?.status) {
const { status, headers, data } = error.response;
logger.error(`${message} The server responded with status ${status}: ${error.message}`, {
logMessage = `${message} The server responded with status ${status}: ${error.message}`;
logger.error(logMessage, {
status,
headers,
data,
stack,
});
} else if (error.request) {
const { method, url } = error.config || {};
logger.error(
`${message} No response received for ${method ? method.toUpperCase() : ''} ${url || ''}: ${error.message}`,
{ requestInfo: { method, url } },
);
logMessage = `${message} No response received for ${method ? method.toUpperCase() : ''} ${url || ''}: ${error.message}`;
logger.error(logMessage, {
requestInfo: { method, url },
stack,
});
} else if (error?.message?.includes('Cannot read properties of undefined (reading \'status\')')) {
logger.error(
`${message} It appears the request timed out or was unsuccessful: ${error.message}`,
);
logMessage = `${message} It appears the request timed out or was unsuccessful: ${error.message}`;
logger.error(logMessage, { stack });
} else {
logger.error(`${message} An error occurred while setting up the request: ${error.message}`);
logMessage = `${message} An error occurred while setting up the request: ${error.message}`;
logger.error(logMessage, { stack });
}
} catch (err) {
logger.error(`Error in logAxiosError: ${err.message}`);
logMessage = `Error in logAxiosError: ${err.message}`;
logger.error(logMessage, { stack: err.stack || 'No stack trace available' });
}
return logMessage;
};
module.exports = { logAxiosError };

View file

@ -79,7 +79,7 @@ export default function ExportAndShareMenu({
<Ariakit.MenuButton
id="export-menu-button"
aria-label="Export options"
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
className="inline-flex size-10 flex-shrink-0 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
>
<Upload
className="icon-md text-text-secondary"
@ -103,7 +103,6 @@ export default function ExportAndShareMenu({
<ShareButton
triggerRef={shareButtonRef}
conversationId={conversation.conversationId ?? ''}
title={conversation.title ?? ''}
open={showShareDialog}
onOpenChange={setShowShareDialog}
/>

View file

@ -1,6 +1,6 @@
import { useMemo, useCallback, useState, useEffect } from 'react';
import { easings } from '@react-spring/web';
import { useMemo, useCallback } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { BirthdayIcon, TooltipAnchor, SplitText } from '~/components';
@ -117,7 +117,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
textAlign="center"
animationFrom={{ opacity: 0, transform: 'translate3d(0,50px,0)' }}
animationTo={{ opacity: 1, transform: 'translate3d(0,0,0)' }}
easing="easeOutCubic"
easing={easings.easeOutCubic}
threshold={0}
rootMargin="0px"
/>
@ -131,7 +131,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
textAlign="center"
animationFrom={{ opacity: 0, transform: 'translate3d(0,50px,0)' }}
animationTo={{ opacity: 1, transform: 'translate3d(0,0,0)' }}
easing="easeOutCubic"
easing={easings.easeOutCubic}
threshold={0}
rootMargin="0px"
/>

View file

@ -1,10 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import type { ModelSelectorProps } from '~/common';
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components';
import { getSelectedIcon, getDisplayValue } from './utils';
import { CustomMenu as Menu } from './CustomMenu';
import DialogManager from './DialogManager';
import { getSelectedIcon } from './utils';
import { useLocalize } from '~/hooks';
function ModelSelectorContent() {
@ -22,7 +22,6 @@ function ModelSelectorContent() {
// Functions
setSearchValue,
getDisplayValue,
setSelectedValues,
// Dialog
keyDialogOpen,
@ -30,18 +29,31 @@ function ModelSelectorContent() {
keyDialogEndpoint,
} = useModelSelectorContext();
const selectedIcon = getSelectedIcon({
mappedEndpoints: mappedEndpoints ?? [],
selectedValues,
modelSpecs,
endpointsConfig,
});
const selectedDisplayValue = getDisplayValue();
const selectedIcon = useMemo(
() =>
getSelectedIcon({
mappedEndpoints: mappedEndpoints ?? [],
selectedValues,
modelSpecs,
endpointsConfig,
}),
[mappedEndpoints, selectedValues, modelSpecs, endpointsConfig],
);
const selectedDisplayValue = useMemo(
() =>
getDisplayValue({
localize,
modelSpecs,
selectedValues,
mappedEndpoints,
}),
[localize, modelSpecs, selectedValues, mappedEndpoints],
);
const trigger = (
<button
className="my-1 flex h-10 w-full max-w-[70vw] items-center justify-center gap-2 rounded-xl border border-border-light bg-surface-secondary px-3 py-2 text-sm text-text-primary hover:bg-surface-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
aria-label={localize('com_endpoint_select_model')}
aria-label={localize('com_ui_select_model')}
>
{selectedIcon && React.isValidElement(selectedIcon) && (
<div className="flex flex-shrink-0 items-center justify-center overflow-hidden">

View file

@ -1,9 +1,10 @@
import React, { startTransition, createContext, useContext, useState, useMemo } from 'react';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import debounce from 'lodash/debounce';
import React, { createContext, useContext, useState, useMemo } from 'react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { Endpoint, SelectedValues } from '~/common';
import { useAgentsMapContext, useAssistantsMapContext, useChatContext } from '~/Providers';
import { useEndpoints, useSelectorEffects, useKeyDialog, useLocalize } from '~/hooks';
import { useEndpoints, useSelectorEffects, useKeyDialog } from '~/hooks';
import useSelectMention from '~/hooks/Input/useSelectMention';
import { useGetEndpointsQuery } from '~/data-provider';
import { filterItems } from './utils';
@ -22,7 +23,6 @@ type ModelSelectorContextType = {
endpointsConfig: t.TEndpointsConfig;
// Functions
getDisplayValue: () => string;
endpointRequiresUserKey: (endpoint: string) => boolean;
setSelectedValues: React.Dispatch<React.SetStateAction<SelectedValues>>;
setSearchValue: (value: string) => void;
@ -53,7 +53,6 @@ export function ModelSelectorProvider({
modelSpecs,
interfaceConfig,
}: ModelSelectorProviderProps) {
const localize = useLocalize();
const agentsMap = useAgentsMapContext();
const assistantsMap = useAssistantsMapContext();
const { data: endpointsConfig } = useGetEndpointsQuery();
@ -101,10 +100,13 @@ export function ModelSelectorProvider({
}, [searchValue, modelSpecs, mappedEndpoints, agentsMap, assistantsMap]);
// Functions
const setSearchValue = (value: string) => {
startTransition(() => setSearchValueState(value));
};
const setDebouncedSearchValue = useMemo(
() =>
debounce((value: string) => {
setSearchValueState(value);
}, 200),
[],
);
const setEndpointSearchValue = (endpoint: string, value: string) => {
setEndpointSearchValues((prev) => ({
...prev,
@ -113,10 +115,16 @@ export function ModelSelectorProvider({
};
const handleSelectSpec = (spec: t.TModelSpec) => {
let model = spec.preset.model ?? null;
onSelectSpec?.(spec);
if (isAgentsEndpoint(spec.preset.endpoint)) {
model = spec.preset.agent_id ?? '';
} else if (isAssistantsEndpoint(spec.preset.endpoint)) {
model = spec.preset.assistant_id ?? '';
}
setSelectedValues({
endpoint: spec.preset.endpoint,
model: spec.preset.model ?? null,
model,
modelSpec: spec.name,
});
};
@ -154,46 +162,6 @@ export function ModelSelectorProvider({
});
};
const getDisplayValue = () => {
if (selectedValues.modelSpec) {
const spec = modelSpecs.find((s) => s.name === selectedValues.modelSpec);
return spec?.label || localize('com_endpoint_select_model');
}
if (selectedValues.model && selectedValues.endpoint) {
const endpoint = mappedEndpoints.find((e) => e.value === selectedValues.endpoint);
if (!endpoint) {
return localize('com_endpoint_select_model');
}
if (
endpoint.value === EModelEndpoint.agents &&
endpoint.agentNames &&
endpoint.agentNames[selectedValues.model]
) {
return endpoint.agentNames[selectedValues.model];
}
if (
(endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
endpoint.assistantNames &&
endpoint.assistantNames[selectedValues.model]
) {
return endpoint.assistantNames[selectedValues.model];
}
return selectedValues.model;
}
if (selectedValues.endpoint) {
const endpoint = mappedEndpoints.find((e) => e.value === selectedValues.endpoint);
return endpoint?.label || localize('com_endpoint_select_model');
}
return localize('com_endpoint_select_model');
};
const value = {
// State
searchValue,
@ -208,14 +176,13 @@ export function ModelSelectorProvider({
endpointsConfig,
// Functions
setSearchValue,
getDisplayValue,
handleSelectSpec,
handleSelectModel,
setSelectedValues,
handleSelectEndpoint,
setEndpointSearchValue,
endpointRequiresUserKey,
setSearchValue: setDebouncedSearchValue,
// Dialog
...keyProps,
};

View file

@ -116,17 +116,15 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
</div>
}
>
{(endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
endpoint.models === undefined ? (
<div className="flex items-center justify-center p-2">
<Spinner />
</div>
) : filteredModels ? (
renderEndpointModels(endpoint, endpoint.models || [], selectedModel, filteredModels)
) : (
endpoint.models && renderEndpointModels(endpoint, endpoint.models, selectedModel)
)}
{isAssistantsEndpoint(endpoint.value) && endpoint.models === undefined ? (
<div className="flex items-center justify-center p-2">
<Spinner />
</div>
) : filteredModels ? (
renderEndpointModels(endpoint, endpoint.models || [], selectedModel, filteredModels)
) : (
endpoint.models && renderEndpointModels(endpoint, endpoint.models, selectedModel)
)}
</Menu>
);
} else {

View file

@ -1,5 +1,5 @@
import React from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
import { useModelSelectorContext } from '../ModelSelectorContext';
import { CustomMenuItem as MenuItem } from '../CustomMenu';
@ -16,18 +16,12 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
const avatarUrl = endpoint?.modelIcons?.[modelId ?? ''] || null;
// Use custom names if available
if (
endpoint &&
modelId &&
endpoint.value === EModelEndpoint.agents &&
endpoint.agentNames?.[modelId]
) {
if (endpoint && modelId && isAgentsEndpoint(endpoint.value) && endpoint.agentNames?.[modelId]) {
modelName = endpoint.agentNames[modelId];
} else if (
endpoint &&
modelId &&
(endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
isAssistantsEndpoint(endpoint.value) &&
endpoint.assistantNames?.[modelId]
) {
modelName = endpoint.assistantNames[modelId];
@ -44,9 +38,7 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
<div className="flex h-5 w-5 items-center justify-center overflow-hidden rounded-full">
<img src={avatarUrl} alt={modelName ?? ''} className="h-full w-full object-cover" />
</div>
) : (endpoint.value === EModelEndpoint.agents ||
endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
) : (isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value)) &&
endpoint.icon ? (
<div className="flex h-5 w-5 items-center justify-center overflow-hidden rounded-full">
{endpoint.icon}

View file

@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TModelSpec } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
import { useModelSelectorContext } from '../ModelSelectorContext';
@ -105,14 +105,13 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
: endpoint.models.filter((modelId) => {
let modelName = modelId;
if (
endpoint.value === EModelEndpoint.agents &&
isAgentsEndpoint(endpoint.value) &&
endpoint.agentNames &&
endpoint.agentNames[modelId]
) {
modelName = endpoint.agentNames[modelId];
} else if (
(endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
isAssistantsEndpoint(endpoint.value) &&
endpoint.assistantNames &&
endpoint.assistantNames[modelId]
) {
@ -138,14 +137,13 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
{filteredModels.map((modelId) => {
let modelName = modelId;
if (
endpoint.value === EModelEndpoint.agents &&
isAgentsEndpoint(endpoint.value) &&
endpoint.agentNames &&
endpoint.agentNames[modelId]
) {
modelName = endpoint.agentNames[modelId];
} else if (
(endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
isAssistantsEndpoint(endpoint.value) &&
endpoint.assistantNames &&
endpoint.assistantNames[modelId]
) {

View file

@ -1,12 +1,13 @@
import React from 'react';
import { Bot } from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TModelSpec,
TAgentsMap,
TAssistantsMap,
TEndpointsConfig,
TModelSpec,
} from 'librechat-data-provider';
import type { useLocalize } from '~/hooks';
import SpecIcon from '~/components/Chat/Menus/Endpoints/components/SpecIcon';
import { Endpoint, SelectedValues } from '~/common';
@ -39,17 +40,13 @@ export function filterItems<
return true;
}
if (item.value === EModelEndpoint.agents && agentsMap && modelId in agentsMap) {
if (isAgentsEndpoint(item.value) && agentsMap && modelId in agentsMap) {
const agentName = agentsMap[modelId]?.name;
return typeof agentName === 'string' && agentName.toLowerCase().includes(searchTermLower);
}
if (
(item.value === EModelEndpoint.assistants ||
item.value === EModelEndpoint.azureAssistants) &&
assistantsMap
) {
const endpoint = item.value;
if (isAssistantsEndpoint(item.value) && assistantsMap) {
const endpoint = item.value ?? '';
const assistant = assistantsMap[endpoint][modelId];
if (assistant && typeof assistant.name === 'string') {
return assistant.name.toLowerCase().includes(searchTermLower);
@ -80,11 +77,10 @@ export function filterModels(
return models.filter((modelId) => {
let modelName = modelId;
if (endpoint.value === EModelEndpoint.agents && agentsMap && agentsMap[modelId]) {
if (isAgentsEndpoint(endpoint.value) && agentsMap && agentsMap[modelId]) {
modelName = agentsMap[modelId].name || modelId;
} else if (
(endpoint.value === EModelEndpoint.assistants ||
endpoint.value === EModelEndpoint.azureAssistants) &&
isAssistantsEndpoint(endpoint.value) &&
assistantsMap &&
assistantsMap[endpoint.value]
) {
@ -160,3 +156,52 @@ export function getSelectedIcon({
return null;
}
export const getDisplayValue = ({
localize,
mappedEndpoints,
selectedValues,
modelSpecs,
}: {
localize: ReturnType<typeof useLocalize>;
selectedValues: SelectedValues;
mappedEndpoints: Endpoint[];
modelSpecs: TModelSpec[];
}) => {
if (selectedValues.modelSpec) {
const spec = modelSpecs.find((s) => s.name === selectedValues.modelSpec);
return spec?.label || spec?.name || localize('com_ui_select_model');
}
if (selectedValues.model && selectedValues.endpoint) {
const endpoint = mappedEndpoints.find((e) => e.value === selectedValues.endpoint);
if (!endpoint) {
return localize('com_ui_select_model');
}
if (
isAgentsEndpoint(endpoint.value) &&
endpoint.agentNames &&
endpoint.agentNames[selectedValues.model]
) {
return endpoint.agentNames[selectedValues.model];
}
if (
isAssistantsEndpoint(endpoint.value) &&
endpoint.assistantNames &&
endpoint.assistantNames[selectedValues.model]
) {
return endpoint.assistantNames[selectedValues.model];
}
return selectedValues.model;
}
if (selectedValues.endpoint) {
const endpoint = mappedEndpoints.find((e) => e.value === selectedValues.endpoint);
return endpoint?.label || localize('com_ui_select_model');
}
return localize('com_ui_select_model');
};

View file

@ -1,4 +1,3 @@
export { default as useKeyDialog } from './useKeyDialog';
export { default as useModelSelection } from './useModels';
export { default as useEndpoints } from './useEndpoints';
export { default as useSelectorEffects } from './useSelectorEffects';

View file

@ -1,277 +0,0 @@
import { useCallback, useRef, useContext, useMemo } from 'react';
import { EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import { getConvoSwitchLogic } from '~/utils';
import { mainTextareaId } from '~/common';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useSetIndexOptions, useDefaultConvo } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery } from '~/data-provider';
import store from '~/store';
export const useModelSelection = () => {
const { setOption } = useSetIndexOptions();
const getDefaultConversation = useDefaultConvo();
const { conversation, newConversation, index } = useChatContext();
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
const modularChat = useRecoilValue(store.modularChat);
const assistantsMapResult = useAssistantsMapContext();
const assistantsMap = useMemo(() => assistantsMapResult ?? {}, [assistantsMapResult]);
const timeoutIdRef = useRef<NodeJS.Timeout | undefined>(undefined);
const setAgentId = useCallback(
(agentId: string) => {
setOption('agent_id')(agentId);
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}${index}`, agentId);
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = setTimeout(() => {
const textarea = document.getElementById(mainTextareaId);
if (textarea) {
textarea.focus();
}
}, 150);
},
[setOption, index, timeoutIdRef],
);
const setAssistantId = useCallback(
(endpoint: string, assistantId: string) => {
const assistant = assistantsMap[endpoint]?.[assistantId];
if (assistant) {
setOption('model')(assistant.model);
setOption('assistant_id')(assistantId);
localStorage.setItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}${endpoint}`, assistantId);
}
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = setTimeout(() => {
const textarea = document.getElementById(mainTextareaId);
if (textarea) {
textarea.focus();
}
}, 150);
},
[setOption, index, assistantsMap, timeoutIdRef],
);
const setModel = useCallback(
(model: string) => {
setOption('model')(model);
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = setTimeout(() => {
const textarea = document.getElementById(mainTextareaId);
if (textarea) {
textarea.focus();
}
}, 150);
},
[setOption, timeoutIdRef],
);
const handleModelSelect = useCallback(
(ep: EModelEndpoint, selectedModel: string) => {
if (ep === EModelEndpoint.assistants) {
if (conversation?.endpoint === ep) {
setAssistantId(ep, selectedModel);
return;
}
const { template } = getConvoSwitchLogic({
newEndpoint: ep,
modularChat: false,
conversation,
endpointsConfig,
});
const assistant = assistantsMap[ep]?.[selectedModel];
const currentConvo = getDefaultConversation({
conversation: {
...conversation,
endpoint: ep,
assistant_id: selectedModel,
model: assistant?.model || '',
},
preset: {
...template,
endpoint: ep,
assistant_id: selectedModel,
model: assistant?.model || '',
},
});
newConversation({
template: currentConvo,
preset: currentConvo,
keepLatestMessage: true,
});
return;
}
if (ep === EModelEndpoint.agents) {
if (conversation?.endpoint === ep) {
setAgentId(selectedModel);
return;
}
const { template } = getConvoSwitchLogic({
newEndpoint: ep,
modularChat: false,
conversation,
endpointsConfig,
});
const currentConvo = getDefaultConversation({
conversation: { ...conversation, endpoint: ep, agent_id: selectedModel },
preset: { ...template, endpoint: ep, agent_id: selectedModel },
});
newConversation({
template: currentConvo,
preset: currentConvo,
keepLatestMessage: true,
});
return;
}
const {
template,
shouldSwitch,
isNewModular,
newEndpointType,
isCurrentModular,
isExistingConversation,
} = getConvoSwitchLogic({
newEndpoint: ep,
modularChat,
conversation,
endpointsConfig,
});
const isModular = isCurrentModular && isNewModular && shouldSwitch;
if (isExistingConversation && isModular) {
template.endpointType = newEndpointType;
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}), endpointType: template.endpointType },
preset: template,
});
newConversation({
template: currentConvo,
preset: currentConvo,
keepLatestMessage: true,
keepAddedConvos: true,
});
return;
}
newConversation({
template: { ...(template as any) },
keepAddedConvos: isModular,
});
setModel(selectedModel);
},
[
conversation,
endpointsConfig,
modularChat,
newConversation,
getDefaultConversation,
setModel,
setAgentId,
setAssistantId,
assistantsMap,
],
);
const handleEndpointSelect = useCallback(
(ep: string, hasModels: boolean, agents: any[], assistants: any[], modelsData: any) => {
if (hasModels) {
if (conversation?.endpoint !== ep) {
const newEndpoint = ep as EModelEndpoint;
const { template } = getConvoSwitchLogic({
newEndpoint,
modularChat: false,
conversation,
endpointsConfig,
});
let initialModel = '';
let initialAgentId = '';
let initialAssistantId = '';
if (newEndpoint === EModelEndpoint.agents && agents.length > 0) {
initialAgentId = agents[0].id;
} else if (newEndpoint === EModelEndpoint.assistants && assistants.length > 0) {
initialAssistantId = assistants[0].id;
initialModel = assistantsMap[newEndpoint]?.[initialAssistantId]?.model || '';
} else if (modelsData && modelsData[newEndpoint] && modelsData[newEndpoint].length > 0) {
initialModel = modelsData[newEndpoint][0];
}
const currentConvo = getDefaultConversation({
conversation: {
...conversation,
endpoint: newEndpoint,
model: initialModel,
agent_id: initialAgentId,
assistant_id: initialAssistantId,
},
preset: {
...template,
endpoint: newEndpoint,
model: initialModel,
agent_id: initialAgentId,
assistant_id: initialAssistantId,
},
});
newConversation({
template: currentConvo,
preset: currentConvo,
keepLatestMessage: true,
});
}
return;
}
if (!hasModels) {
const newEndpoint = ep as EModelEndpoint;
const { template } = getConvoSwitchLogic({
newEndpoint,
modularChat: false,
conversation,
endpointsConfig,
});
const currentConvo = getDefaultConversation({
conversation: { ...conversation, endpoint: newEndpoint },
preset: { ...template, endpoint: newEndpoint },
});
newConversation({
template: currentConvo,
preset: currentConvo,
keepLatestMessage: true,
});
}
},
[
conversation,
endpointsConfig,
newConversation,
getDefaultConversation,
assistantsMap,
modularChat,
],
);
return {
handleModelSelect,
handleEndpointSelect,
setAgentId,
setAssistantId,
setModel,
};
};
export default useModelSelection;

View file

@ -21,22 +21,51 @@ export default function useSelectorEffects({
const agents: t.Agent[] = useMemo(() => {
return Object.values(agentsMap ?? {}) as t.Agent[];
}, [agentsMap]);
const { agent_id: selectedAgentId = null, endpoint } = conversation ?? {};
const {
agent_id: selectedAgentId = null,
assistant_id: selectedAssistantId = null,
endpoint,
} = conversation ?? {};
const assistants: t.Assistant[] = useMemo(() => {
if (!isAssistantsEndpoint(endpoint)) {
return [];
}
return Object.values(assistantsMap?.[endpoint ?? ''] ?? {}) as t.Assistant[];
}, [assistantsMap, endpoint]);
useEffect(() => {
if (!isAgentsEndpoint(endpoint as string)) {
return;
}
if (selectedAgentId == null && agents.length > 0) {
let agent_id = localStorage.getItem(`${LocalStorageKeys.AGENT_ID_PREFIX}${index}`);
if (agent_id == null) {
agent_id = agents[0].id;
agent_id = agents[0]?.id;
}
const agent = agentsMap?.[agent_id];
if (agent !== undefined && isAgentsEndpoint(endpoint as string) === true) {
if (agent !== undefined) {
setOption('model')('');
setOption('agent_id')(agent_id);
}
}
}, [index, agents, selectedAgentId, agentsMap, endpoint, setOption]);
useEffect(() => {
if (!isAssistantsEndpoint(endpoint as string)) {
return;
}
if (selectedAssistantId == null && assistants.length > 0) {
let assistant_id = localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}`);
if (assistant_id == null) {
assistant_id = assistants[0]?.id;
}
const assistant = assistantsMap?.[endpoint ?? '']?.[assistant_id];
if (assistant !== undefined) {
setOption('model')('');
setOption('assistant_id')(assistant_id);
}
}
}, [index, assistants, selectedAssistantId, assistantsMap, endpoint, setOption]);
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -64,7 +93,7 @@ export default function useSelectorEffects({
debouncedSetSelectedValues({
endpoint: conversation.endpoint || '',
model: conversation.agent_id ?? '',
modelSpec: '',
modelSpec: conversation.spec || '',
});
return;
} else if (isAssistantsEndpoint(conversation?.endpoint)) {

View file

@ -249,7 +249,6 @@
"com_endpoint_search_var": "Search {{0}}...",
"com_endpoint_search_models": "Search models...",
"com_endpoint_search_endpoint_models": "Search {{0}} models...",
"com_endpoint_select_model": "Select a model",
"com_endpoint_set_custom_name": "Set a custom name, in case you can find this preset",
"com_endpoint_skip_hover": "Enable skipping the completion step, which reviews the final answer and generated steps",
"com_endpoint_stop": "Stop Sequences",

View file

@ -1,4 +1,4 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { ExtendedEndpoint } from '~/common';
export const filterMenuItems = (
@ -17,7 +17,7 @@ export const filterMenuItems = (
return mappedEndpoints
.map((ep) => {
if (ep.hasModels) {
if (ep.value === EModelEndpoint.agents) {
if (isAgentsEndpoint(ep.value)) {
const filteredAgents = agents.filter((agent) =>
agent.name?.toLowerCase().includes(lowercaseSearchTerm),
);
@ -32,7 +32,7 @@ export const filterMenuItems = (
};
}
return null;
} else if (ep.value === EModelEndpoint.assistants) {
} else if (isAssistantsEndpoint(ep.value)) {
const filteredAssistants = assistants.filter((assistant) =>
assistant.name?.toLowerCase().includes(lowercaseSearchTerm),
);

1134
package-lock.json generated

File diff suppressed because it is too large Load diff