Files
chat/packages/api/dist/index.js
T
hanzo-dev ec32864a00 refactor(chat): eliminate /api/ prefix -> /v1/chat/* (namespaced, no-collision)
App REST surface moves from /api/* to /v1/chat/* in lockstep across server
mounts, client URL builders, SSE stream paths, OAuth redirect_uris, telemetry
route-grouping, RUM proxy, and the nginx bridge.

Namespace /v1/chat/* (not flat /v1/) to avoid colliding with the OpenAI-compat
inference surface (/v1/chat/completions, /v1/models -> router_backend) and to
match the canonical chat/openapi.yaml. Social OIDC login (/oauth/*) and /health
are unchanged (login-safe).

Levers:
- server: api/server/index.js 26 app.use('/v1/chat/*') mounts (+ experimental.js)
- client: packages/data-provider/src/api-endpoints.ts (75 builders) + config.ts
Lockstep: checkBan matcher, Files/Code download path, actions/mcp CSRF cookie
paths, ActionService + mcp/oauth handler redirect_uris, admin OpenID callback,
telemetry middleware/sdk/stream, rum proxy path, vite dev proxy + PWA denylist,
robots.txt, .env.example, pr-preview health-path (/api/health -> /health).

Third-party APIs chat CALLS are untouched (Ollama, Wolfram, Discord, GitHub
Enterprise, Mistral, OpenRouter, DataDog). Live-surface /api/: 683 -> 0.
Also untracks runtime log artifacts (api/logs, client/junit.xml; already gitignored).
2026-07-03 13:55:43 -07:00

46199 lines
1.7 MiB
Plaintext

'use strict';
var dataSchemas = require('@librechat/data-schemas');
var librechatDataProvider = require('librechat-data-provider');
var axios$1 = require('axios');
var path = require('path');
var crypto$1 = require('node:crypto');
var fs = require('fs');
var promises = require('fs/promises');
var fetch$1 = require('node-fetch');
var agents = require('@librechat/agents');
var mathjs = require('mathjs');
var tiktoken = require('tiktoken');
var yaml = require('js-yaml');
var z = require('zod');
var identity = require('@azure/identity');
var firebase = require('firebase/app');
var storage = require('firebase/storage');
var clientS3 = require('@aws-sdk/client-s3');
var promises$1 = require('node:dns/promises');
var crypto$2 = require('crypto');
var dns = require('node:dns');
var http = require('node:http');
var https = require('node:https');
var keyv = require('keyv');
var IoRedis = require('ioredis');
var redis = require('@keyv/redis');
var keyvFile = require('keyv-file');
var mongoose = require('mongoose');
var events = require('events');
var mongodb = require('mongodb');
var createMemoryStore = require('memorystore');
var rateLimitRedis = require('rate-limit-redis');
var session = require('express-session');
var connectRedis = require('connect-redis');
var auth_js$1 = require('@modelcontextprotocol/sdk/shared/auth.js');
var auth_js = require('@modelcontextprotocol/sdk/client/auth.js');
var undici = require('undici');
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
var websocket_js = require('@modelcontextprotocol/sdk/client/websocket.js');
var types_js = require('@modelcontextprotocol/sdk/types.js');
var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
var jwt = require('jsonwebtoken');
var prompts = require('@langchain/core/prompts');
var messages = require('@langchain/core/messages');
var tools = require('@langchain/core/tools');
var require$$0$1 = require('buffer');
var require$$0 = require('stream');
var require$$2 = require('util');
var FormData = require('form-data');
var httpsProxyAgent = require('https-proxy-agent');
var url = require('url');
var googleAuthLibrary = require('google-auth-library');
var vertexSdk = require('@anthropic-ai/vertex-sdk');
var nodeHttpHandler = require('@smithy/node-http-handler');
var clientBedrockRuntime = require('@aws-sdk/client-bedrock-runtime');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
/**
* Logs Axios errors based on the error object and a custom message.
* @param options - The options object.
* @param options.message - The custom message to be logged.
* @param options.error - The Axios error object.
* @returns The log message.
*/
const logAxiosError = ({ message, error, }) => {
var _a;
let logMessage = message;
try {
const stack = error != null
? (error === null || error === void 0 ? void 0 : error.stack) || 'No stack trace available'
: 'No stack trace available';
const errorMessage = error != null
? (error === null || error === void 0 ? void 0 : error.message) || 'No error message available'
: 'No error message available';
if (axios$1.isAxiosError(error) && error.response && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status)) {
const { status, headers, data } = error.response;
logMessage = `${message} The server responded with status ${status}: ${error.message}`;
dataSchemas.logger.error(logMessage, {
status,
headers,
data,
stack,
});
}
else if (axios$1.isAxiosError(error) && error.request) {
const { method, url } = error.config || {};
logMessage = `${message} No response received for ${method ? method.toUpperCase() : ''} ${url || ''}: ${error.message}`;
dataSchemas.logger.error(logMessage, {
requestInfo: { method, url },
stack,
});
}
else if (errorMessage === null || errorMessage === void 0 ? void 0 : errorMessage.includes("Cannot read properties of undefined (reading 'status')")) {
logMessage = `${message} It appears the request timed out or was unsuccessful: ${errorMessage}`;
dataSchemas.logger.error(logMessage, { stack });
}
else {
logMessage = `${message} An error occurred while setting up the request: ${errorMessage}`;
dataSchemas.logger.error(logMessage, { stack });
}
}
catch (err) {
logMessage = `Error in logAxiosError: ${err.message}`;
dataSchemas.logger.error(logMessage, { stack: err.stack || 'No stack trace available' });
}
return logMessage;
};
/**
* Creates and configures an Axios instance with optional proxy settings.
* @returns A configured Axios instance
* @throws If there's an issue creating the Axios instance or parsing the proxy URL
*/
function createAxiosInstance() {
const instance = axios$1.create();
if (process.env.proxy) {
try {
const url = new URL(process.env.proxy);
const proxyConfig = {
host: url.hostname.replace(/^\[|\]$/g, ''),
protocol: url.protocol.replace(':', ''),
};
if (url.port) {
proxyConfig.port = parseInt(url.port, 10);
}
instance.defaults.proxy = proxyConfig;
}
catch (error) {
console.error('Error parsing proxy URL:', error);
throw new Error(`Invalid proxy URL: ${process.env.proxy}`);
}
}
return instance;
}
/**
* Checks if the given value is truthy by being either the boolean `true` or a string
* that case-insensitively matches 'true'.
*
* @param value - The value to check.
* @returns Returns `true` if the value is the boolean `true` or a case-insensitive
* match for the string 'true', otherwise returns `false`.
* @example
*
* isEnabled("True"); // returns true
* isEnabled("TRUE"); // returns true
* isEnabled(true); // returns true
* isEnabled("false"); // returns false
* isEnabled(false); // returns false
* isEnabled(null); // returns false
* isEnabled(); // returns false
*/
function isEnabled(value) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
return value.toLowerCase().trim() === 'true';
}
return false;
}
/**
* Checks if the provided value is 'user_provided'.
*
* @param value - The value to check.
* @returns - Returns true if the value is 'user_provided', otherwise false.
*/
const isUserProvided = (value) => value === librechatDataProvider.AuthType.USER_PROVIDED;
/**
* @param values
*/
function optionalChainWithEmptyCheck(...values) {
for (const value of values) {
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return values[values.length - 1];
}
/**
* Escapes special characters in a string for use in a regular expression.
* @param str - The string to escape.
* @returns The escaped string safe for use in RegExp.
*/
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Sanitizes the model name to be used in the URL by removing or replacing disallowed characters.
* @param modelName - The model name to be sanitized.
* @returns The sanitized model name.
*/
const sanitizeModelName = (modelName) => {
// Replace periods with empty strings and other disallowed characters as needed.
return modelName.replace(/\./g, '');
};
/**
* Generates the Azure OpenAI API endpoint URL.
* @param params - The parameters object.
* @param params.azureOpenAIApiInstanceName - The Azure OpenAI API instance name.
* @param params.azureOpenAIApiDeploymentName - The Azure OpenAI API deployment name.
* @returns The complete endpoint URL for the Azure OpenAI API.
*/
const genAzureEndpoint = ({ azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName, }) => {
// Support both old (.openai.azure.com) and new (.cognitiveservices.azure.com) endpoint formats
// If instanceName already includes a full domain, use it as-is
if (azureOpenAIApiInstanceName.includes('.azure.com')) {
return `https://${azureOpenAIApiInstanceName}/openai/deployments/${azureOpenAIApiDeploymentName}`;
}
// Legacy format for backward compatibility
return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}`;
};
/**
* Generates the Azure OpenAI API chat completion endpoint URL with the API version.
* If both deploymentName and modelName are provided, modelName takes precedence.
* @param azureConfig - The Azure configuration object.
* @param azureConfig.azureOpenAIApiInstanceName - The Azure OpenAI API instance name.
* @param azureConfig.azureOpenAIApiDeploymentName - The Azure OpenAI API deployment name (optional).
* @param azureConfig.azureOpenAIApiVersion - The Azure OpenAI API version.
* @param modelName - The model name to be included in the deployment name (optional).
* @param client - The API Client class for optionally setting properties (optional).
* @returns The complete chat completion endpoint URL for the Azure OpenAI API.
* @throws Error if neither azureOpenAIApiDeploymentName nor modelName is provided.
*/
const genAzureChatCompletion = ({ azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName, azureOpenAIApiVersion, }, modelName, client) => {
// Determine the deployment segment of the URL based on provided modelName or azureOpenAIApiDeploymentName
let deploymentSegment;
if (isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME) && modelName) {
const sanitizedModelName = sanitizeModelName(modelName);
deploymentSegment = sanitizedModelName;
if (client && typeof client === 'object') {
client.azure.azureOpenAIApiDeploymentName = sanitizedModelName;
}
}
else if (azureOpenAIApiDeploymentName) {
deploymentSegment = azureOpenAIApiDeploymentName;
}
else if (!process.env.AZURE_OPENAI_BASEURL) {
throw new Error('Either a model name with the `AZURE_USE_MODEL_AS_DEPLOYMENT_NAME` setting or a deployment name must be provided if `AZURE_OPENAI_BASEURL` is omitted.');
}
else {
deploymentSegment = '';
}
return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${deploymentSegment}/chat/completions?api-version=${azureOpenAIApiVersion}`;
};
/**
* Retrieves the Azure OpenAI API credentials from environment variables.
* @returns An object containing the Azure OpenAI API credentials.
*/
const getAzureCredentials = () => {
var _a;
return {
azureOpenAIApiKey: (_a = process.env.AZURE_API_KEY) !== null && _a !== void 0 ? _a : process.env.AZURE_OPENAI_API_KEY,
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
};
};
/**
* Constructs a URL by replacing placeholders in the baseURL with values from the azure object.
* It specifically looks for '${INSTANCE_NAME}' and '${DEPLOYMENT_NAME}' within the baseURL and replaces
* them with 'azureOpenAIApiInstanceName' and 'azureOpenAIApiDeploymentName' from the azure object.
* If the respective azure property is not provided, the placeholder is replaced with an empty string.
*
* @param params - The parameters object.
* @param params.baseURL - The baseURL to inspect for replacement placeholders.
* @param params.azureOptions - The azure options object containing the instance and deployment names.
* @returns The complete baseURL with credentials injected for the Azure OpenAI API.
*/
function constructAzureURL({ baseURL, azureOptions, }) {
var _a, _b;
let finalURL = baseURL;
// Replace INSTANCE_NAME and DEPLOYMENT_NAME placeholders with actual values if available
if (azureOptions) {
finalURL = finalURL.replace('${INSTANCE_NAME}', (_a = azureOptions.azureOpenAIApiInstanceName) !== null && _a !== void 0 ? _a : '');
finalURL = finalURL.replace('${DEPLOYMENT_NAME}', (_b = azureOptions.azureOpenAIApiDeploymentName) !== null && _b !== void 0 ? _b : '');
}
return finalURL;
}
function filterMalformedContentParts(contentParts) {
if (!Array.isArray(contentParts)) {
return contentParts;
}
return contentParts.filter((part) => {
if (!part || typeof part !== 'object') {
return false;
}
const { type } = part;
if (type === librechatDataProvider.ContentTypes.TOOL_CALL) {
return 'tool_call' in part && part.tool_call != null && typeof part.tool_call === 'object';
}
return true;
});
}
/**
* Check if email configuration is set
* @returns Returns `true` if either Mailgun or SMTP is properly configured
*/
function checkEmailConfig() {
const hasMailgunConfig = !!process.env.MAILGUN_API_KEY && !!process.env.MAILGUN_DOMAIN && !!process.env.EMAIL_FROM;
const hasSMTPConfig = (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM;
return hasMailgunConfig || hasSMTPConfig;
}
function isFederatedTokens(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
return 'access_token' in obj || 'id_token' in obj || 'expires_at' in obj;
}
const OPENID_TOKEN_FIELDS = [
'ACCESS_TOKEN',
'ID_TOKEN',
'USER_ID',
'USER_EMAIL',
'USER_NAME',
'EXPIRES_AT',
];
/**
* Placeholder for Microsoft Graph API access token.
* This placeholder is resolved asynchronously via OBO (On-Behalf-Of) flow
* and requires special handling outside the synchronous processMCPEnv pipeline.
*/
const GRAPH_TOKEN_PLACEHOLDER = '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}';
/**
* Default Microsoft Graph API scopes for OBO token exchange.
* Can be overridden via GRAPH_API_SCOPES environment variable.
*/
const DEFAULT_GRAPH_SCOPES = 'https://graph.microsoft.com/.default';
function extractOpenIDTokenInfo(user) {
if (!user) {
return null;
}
try {
if (user.provider !== 'openid' && !user.openidId) {
return null;
}
const tokenInfo = {};
if ('federatedTokens' in user && isFederatedTokens(user.federatedTokens)) {
const tokens = user.federatedTokens;
dataSchemas.logger.debug('[extractOpenIDTokenInfo] Found federatedTokens:', {
has_access_token: !!tokens.access_token,
has_id_token: !!tokens.id_token,
has_refresh_token: !!tokens.refresh_token,
expires_at: tokens.expires_at,
});
tokenInfo.accessToken = tokens.access_token;
tokenInfo.idToken = tokens.id_token;
tokenInfo.expiresAt = tokens.expires_at;
}
else if ('openidTokens' in user && isFederatedTokens(user.openidTokens)) {
const tokens = user.openidTokens;
dataSchemas.logger.debug('[extractOpenIDTokenInfo] Found openidTokens');
tokenInfo.accessToken = tokens.access_token;
tokenInfo.idToken = tokens.id_token;
tokenInfo.expiresAt = tokens.expires_at;
}
tokenInfo.userId = user.openidId || user.id;
tokenInfo.userEmail = user.email;
tokenInfo.userName = user.name || user.username;
if (tokenInfo.idToken) {
try {
const payload = JSON.parse(Buffer.from(tokenInfo.idToken.split('.')[1], 'base64').toString());
tokenInfo.claims = payload;
if (payload.sub)
tokenInfo.userId = payload.sub;
if (payload.email)
tokenInfo.userEmail = payload.email;
if (payload.name)
tokenInfo.userName = payload.name;
if (payload.exp)
tokenInfo.expiresAt = payload.exp;
}
catch (jwtError) {
dataSchemas.logger.warn('Could not parse ID token claims:', jwtError);
}
}
return tokenInfo;
}
catch (error) {
dataSchemas.logger.error('Error extracting OpenID token info:', error);
return null;
}
}
function isOpenIDTokenValid(tokenInfo) {
if (!tokenInfo || !tokenInfo.accessToken) {
return false;
}
if (tokenInfo.expiresAt) {
const now = Math.floor(Date.now() / 1000);
if (now >= tokenInfo.expiresAt) {
dataSchemas.logger.warn('OpenID token has expired');
return false;
}
}
return true;
}
function processOpenIDPlaceholders(value, tokenInfo) {
if (!tokenInfo || typeof value !== 'string') {
return value;
}
let processedValue = value;
for (const field of OPENID_TOKEN_FIELDS) {
const placeholder = `{{LIBRECHAT_OPENID_${field}}}`;
if (!processedValue.includes(placeholder)) {
continue;
}
let replacementValue = '';
switch (field) {
case 'ACCESS_TOKEN':
replacementValue = tokenInfo.accessToken || '';
break;
case 'ID_TOKEN':
replacementValue = tokenInfo.idToken || '';
break;
case 'USER_ID':
replacementValue = tokenInfo.userId || '';
break;
case 'USER_EMAIL':
replacementValue = tokenInfo.userEmail || '';
break;
case 'USER_NAME':
replacementValue = tokenInfo.userName || '';
break;
case 'EXPIRES_AT':
replacementValue = tokenInfo.expiresAt ? String(tokenInfo.expiresAt) : '';
break;
}
processedValue = processedValue.replace(new RegExp(placeholder, 'g'), replacementValue);
}
const genericPlaceholder = '{{LIBRECHAT_OPENID_TOKEN}}';
if (processedValue.includes(genericPlaceholder)) {
const replacementValue = tokenInfo.accessToken || '';
processedValue = processedValue.replace(new RegExp(genericPlaceholder, 'g'), replacementValue);
}
return processedValue;
}
function createBearerAuthHeader(tokenInfo) {
if (!tokenInfo || !tokenInfo.accessToken) {
return '';
}
return `Bearer ${tokenInfo.accessToken}`;
}
function isOpenIDAvailable() {
const openidClientId = process.env.OPENID_CLIENT_ID;
const openidClientSecret = process.env.OPENID_CLIENT_SECRET;
const openidIssuer = process.env.OPENID_ISSUER;
return !!(openidClientId && openidClientSecret && openidIssuer);
}
/**
* List of allowed user fields that can be used in MCP environment variables.
* These are non-sensitive string/boolean fields from the IUser interface.
*/
const ALLOWED_USER_FIELDS = [
'id',
'name',
'username',
'email',
'provider',
'role',
'googleId',
'facebookId',
'openidId',
'samlId',
'ldapId',
'githubId',
'discordId',
'appleId',
'emailVerified',
'twoFactorEnabled',
'termsAccepted',
];
/**
* Encodes a string value to be safe for HTTP headers.
* HTTP headers are restricted to ASCII characters (0-255) per the Fetch API standard.
* Non-ASCII characters with Unicode values > 255 are Base64 encoded with 'b64:' prefix.
*
* NOTE: This is a LibreChat-specific encoding scheme to work around Fetch API limitations.
* MCP servers receiving headers with the 'b64:' prefix should:
* 1. Detect the 'b64:' prefix in header values
* 2. Remove the prefix and Base64-decode the remaining string
* 3. Use the decoded UTF-8 string as the actual value
*
* Example decoding (Node.js):
* if (headerValue.startsWith('b64:')) {
* const decoded = Buffer.from(headerValue.slice(4), 'base64').toString('utf8');
* }
*
* @param value - The string value to encode
* @returns ASCII-safe string (encoded if necessary)
*
* @example
* encodeHeaderValue("José") // Returns "José" (é = 233, safe)
* encodeHeaderValue("Marić") // Returns "b64:TWFyacSH" (ć = 263, needs encoding)
*/
function encodeHeaderValue(value) {
// Handle non-string or empty values
if (!value || typeof value !== 'string') {
return '';
}
// Check if string contains extended Unicode characters (> 255)
// Characters 0-255 (ASCII + Latin-1) are safe and don't need encoding
// Characters > 255 (e.g., ć=263, đ=272, ł=322) need Base64 encoding
// eslint-disable-next-line no-control-regex
const hasExtendedUnicode = /[^\u0000-\u00FF]/.test(value);
if (!hasExtendedUnicode) {
return value; // Safe to pass through
}
// Encode to Base64 for extended Unicode characters
const base64 = Buffer.from(value, 'utf8').toString('base64');
return `b64:${base64}`;
}
/**
* Creates a safe user object containing only allowed fields.
* Preserves federatedTokens for OpenID token template variable resolution.
*
* @param user - The user object to extract safe fields from
* @returns A new object containing only allowed fields plus federatedTokens if present
*/
function createSafeUser(user) {
if (!user) {
return {};
}
const safeUser = {};
for (const field of ALLOWED_USER_FIELDS) {
if (field in user) {
safeUser[field] = user[field];
}
}
if ('federatedTokens' in user) {
safeUser.federatedTokens = user.federatedTokens;
}
return safeUser;
}
/**
* List of allowed request body fields that can be used in header placeholders.
* These are common fields from the request body that are safe to expose in headers.
*/
const ALLOWED_BODY_FIELDS = ['conversationId', 'parentMessageId', 'messageId'];
/**
* Processes a string value to replace user field placeholders.
* When isHeader is true, non-ASCII characters in certain fields are Base64 encoded.
*
* @param value - The string value to process
* @param user - The user object
* @param isHeader - Whether this value will be used in an HTTP header
* @returns The processed string with placeholders replaced (and encoded if necessary)
*/
function processUserPlaceholders(value, user, isHeader = false) {
if (!user || typeof value !== 'string') {
return value;
}
for (const field of ALLOWED_USER_FIELDS) {
const placeholder = `{{LIBRECHAT_USER_${field.toUpperCase()}}}`;
if (typeof value !== 'string' || !value.includes(placeholder)) {
continue;
}
const fieldValue = user[field];
// Skip replacement if field doesn't exist in user object
if (!(field in user)) {
continue;
}
// Special case for 'id' field: skip if undefined or empty
if (field === 'id' && (fieldValue === undefined || fieldValue === '')) {
continue;
}
let replacementValue = fieldValue == null ? '' : String(fieldValue);
// Encode non-ASCII characters when used in headers
// Fields like name, username, email can contain non-ASCII characters
// that would cause ByteString conversion errors in the Fetch API
if (isHeader) {
const fieldsToEncode = ['name', 'username', 'email'];
if (fieldsToEncode.includes(field)) {
replacementValue = encodeHeaderValue(replacementValue);
}
}
value = value.replace(new RegExp(placeholder, 'g'), replacementValue);
}
return value;
}
/**
* Replaces request body field placeholders within a string.
* Recognized placeholders: `{{LIBRECHAT_BODY_<FIELD>}}` where `<FIELD>` ∈ ALLOWED_BODY_FIELDS.
* If a body field is absent or null/undefined, it is replaced with an empty string.
*
* @param value - The string value to process
* @param body - The request body object
* @returns The processed string with placeholders replaced
*/
function processBodyPlaceholders(value, body) {
// Type guard: ensure value is a string
if (typeof value !== 'string') {
return value;
}
for (const field of ALLOWED_BODY_FIELDS) {
const placeholder = `{{LIBRECHAT_BODY_${field.toUpperCase()}}}`;
if (!value.includes(placeholder)) {
continue;
}
const fieldValue = body[field];
const replacementValue = fieldValue == null ? '' : String(fieldValue);
value = value.replace(new RegExp(placeholder, 'g'), replacementValue);
}
return value;
}
/**
* Processes a single string value by replacing various types of placeholders
*
* @param originalValue - The original string value to process
* @param customUserVars - Optional custom user variables to replace placeholders
* @param user - Optional user object for replacing user field placeholders
* @param body - Optional request body object for replacing body field placeholders
* @param isHeader - Whether this value will be used in an HTTP header (enables encoding)
* @returns The processed string with all placeholders replaced
*/
function processSingleValue({ originalValue, customUserVars, user, body = undefined, isHeader = false, }) {
// Type guard: ensure we're working with a string
if (typeof originalValue !== 'string') {
return String(originalValue);
}
let value = originalValue;
if (customUserVars) {
for (const [varName, varVal] of Object.entries(customUserVars)) {
/** Escaped varName for use in regex to avoid issues with special characters */
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const placeholderRegex = new RegExp(`\\{\\{${escapedVarName}\\}\\}`, 'g');
value = value.replace(placeholderRegex, varVal);
}
}
value = processUserPlaceholders(value, user, isHeader);
const openidTokenInfo = extractOpenIDTokenInfo(user);
if (openidTokenInfo && isOpenIDTokenValid(openidTokenInfo)) {
value = processOpenIDPlaceholders(value, openidTokenInfo);
}
if (body) {
value = processBodyPlaceholders(value, body);
}
value = librechatDataProvider.extractEnvVariable(value);
return value;
}
/**
* Recursively processes an object to replace environment variables in string values
* @param params - Processing parameters
* @param params.options - The MCP options to process
* @param params.user - The user object containing all user fields
* @param params.customUserVars - vars that user set in settings
* @param params.body - the body of the request that is being processed
* @returns - The processed object with environment variables replaced
*/
function processMCPEnv(params) {
const { options, user, customUserVars, body } = params;
if (options === null || options === undefined) {
return options;
}
const newObj = structuredClone(options);
// Apply admin-provided API key to headers at runtime
// Note: User-provided keys use {{MCP_API_KEY}} placeholder in headers,
// which is processed later via customUserVars replacement
if ('apiKey' in newObj && newObj.apiKey) {
const apiKeyConfig = newObj.apiKey;
if (apiKeyConfig.source === 'admin' && apiKeyConfig.key) {
const { key, authorization_type, custom_header } = apiKeyConfig;
const headerName = authorization_type === 'custom' ? custom_header || 'X-Api-Key' : 'Authorization';
let headerValue = key;
if (authorization_type === 'basic') {
headerValue = `Basic ${key}`;
}
else if (authorization_type === 'bearer') {
headerValue = `Bearer ${key}`;
}
// Initialize headers if needed and add the API key header (overwrites if header already exists)
const objWithHeaders = newObj;
if (!objWithHeaders.headers) {
objWithHeaders.headers = {};
}
objWithHeaders.headers[headerName] = headerValue;
}
}
if ('env' in newObj && newObj.env) {
const processedEnv = {};
for (const [key, originalValue] of Object.entries(newObj.env)) {
processedEnv[key] = processSingleValue({ originalValue, customUserVars, user, body });
}
newObj.env = processedEnv;
}
if ('args' in newObj && newObj.args) {
const processedArgs = [];
for (const originalValue of newObj.args) {
processedArgs.push(processSingleValue({ originalValue, customUserVars, user, body }));
}
newObj.args = processedArgs;
}
// Process headers if they exist (for WebSocket, SSE, StreamableHTTP types)
// Note: `env` and `headers` are on different branches of the MCPOptions union type.
if ('headers' in newObj && newObj.headers) {
const processedHeaders = {};
for (const [key, originalValue] of Object.entries(newObj.headers)) {
processedHeaders[key] = processSingleValue({
originalValue,
customUserVars,
user,
body,
isHeader: true, // Important: Enable header encoding
});
}
newObj.headers = processedHeaders;
}
// Process URL if it exists (for WebSocket, SSE, StreamableHTTP types)
if ('url' in newObj && newObj.url) {
newObj.url = processSingleValue({ originalValue: newObj.url, customUserVars, user, body });
}
// Process OAuth configuration if it exists (for all transport types)
if ('oauth' in newObj && newObj.oauth) {
const processedOAuth = {};
for (const [key, originalValue] of Object.entries(newObj.oauth)) {
// Only process string values for environment variables
// token_exchange_method is an enum and shouldn't be processed
if (typeof originalValue === 'string') {
processedOAuth[key] = processSingleValue({ originalValue, customUserVars, user, body });
}
else {
processedOAuth[key] = originalValue;
}
}
newObj.oauth = processedOAuth;
}
return newObj;
}
/**
* Recursively processes a value, replacing placeholders in strings while preserving structure
* @param value - The value to process (can be string, number, boolean, array, object, etc.)
* @param options - Processing options
* @returns The processed value with the same structure
*/
function processValue(value, options) {
if (typeof value === 'string') {
return processSingleValue({
originalValue: value,
customUserVars: options.customUserVars,
user: options.user,
body: options.body,
});
}
if (Array.isArray(value)) {
return value.map((item) => processValue(item, options));
}
if (value !== null && typeof value === 'object') {
const processed = {};
for (const [key, val] of Object.entries(value)) {
processed[key] = processValue(val, options);
}
return processed;
}
return value;
}
/**
* Recursively resolves placeholders in a nested object structure while preserving types.
* Only processes string values - leaves numbers, booleans, arrays, and nested objects intact.
*
* @param options - Configuration object
* @param options.obj - The object to process
* @param options.user - Optional user object for replacing user field placeholders
* @param options.body - Optional request body object for replacing body field placeholders
* @param options.customUserVars - Optional custom user variables to replace placeholders
* @returns The processed object with placeholders replaced in string values
*/
function resolveNestedObject(options) {
const { obj, user, body, customUserVars } = options !== null && options !== void 0 ? options : {};
if (!obj) {
return obj;
}
return processValue(obj, {
customUserVars,
user: user,
body,
});
}
/**
* Resolves header values by replacing user placeholders, body variables, custom variables, and environment variables.
* Automatically encodes non-ASCII characters for header safety.
*
* @param options - Optional configuration object
* @param options.headers - The headers object to process
* @param options.user - Optional user object for replacing user field placeholders (can be partial with just id)
* @param options.body - Optional request body object for replacing body field placeholders
* @param options.customUserVars - Optional custom user variables to replace placeholders
* @returns The processed headers with all placeholders replaced
*/
function resolveHeaders(options) {
const { headers, user, body, customUserVars } = options !== null && options !== void 0 ? options : {};
const inputHeaders = headers !== null && headers !== void 0 ? headers : {};
const resolvedHeaders = Object.assign({}, inputHeaders);
if (inputHeaders && typeof inputHeaders === 'object' && !Array.isArray(inputHeaders)) {
Object.keys(inputHeaders).forEach((key) => {
resolvedHeaders[key] = processSingleValue({
originalValue: inputHeaders[key],
customUserVars,
user: user,
body,
isHeader: true, // Important: Enable header encoding
});
});
}
return resolvedHeaders;
}
/**
* Sends message data in Server Sent Events format.
* @param res - The server response.
* @param event - The message event.
* @param event.event - The type of event.
* @param event.data - The message to be sent.
*/
function sendEvent(res, event) {
if (typeof event.data === 'string' && event.data.length === 0) {
return;
}
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.
*/
function handleError(res, message) {
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
res.end();
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __values(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
}
function __await(v) {
return this instanceof __await ? (this.v = v, this) : new __await(v);
}
function __asyncGenerator(thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
}
function __asyncValues(o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* Sanitize a filename by removing any directory components, replacing non-alphanumeric characters
* @param inputName
*/
function sanitizeFilename(inputName) {
// Remove any directory components
let name = path.basename(inputName);
// Replace any non-alphanumeric characters except for '.' and '-'
name = name.replace(/[^a-zA-Z0-9.-]/g, '_');
// Ensure the name doesn't start with a dot (hidden file in Unix-like systems)
if (name.startsWith('.') || name === '') {
name = '_' + name;
}
// Limit the length of the filename
const MAX_LENGTH = 255;
if (name.length > MAX_LENGTH) {
const ext = path.extname(name);
const nameWithoutExt = path.basename(name, ext);
name =
nameWithoutExt.slice(0, MAX_LENGTH - ext.length - 7) +
'-' +
crypto$1.randomBytes(3).toString('hex') +
ext;
}
return name;
}
/**
* Reads a file asynchronously. Uses streaming for large files to avoid memory issues.
*
* @param filePath - Path to the file to read
* @param options - Options for reading the file
* @returns Promise resolving to the file contents and size
* @throws Error if the file cannot be read
*/
function readFileAsString(filePath_1) {
return __awaiter(this, arguments, void 0, function* (filePath, options = {}) {
var _a, e_1, _b, _c;
const { encoding = 'utf8', streamThreshold = 10 * 1024 * 1024, // 10MB
highWaterMark = 64 * 1024, // 64KB
fileSize, } = options;
// Get file size if not provided
const bytes = fileSize !== null && fileSize !== void 0 ? fileSize : (yield promises.stat(filePath)).size;
// For large files, use streaming to avoid memory issues
if (bytes > streamThreshold) {
const chunks = [];
const stream = fs.createReadStream(filePath, {
encoding,
highWaterMark,
});
try {
for (var _d = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _d = true) {
_c = stream_1_1.value;
_d = false;
const chunk = _c;
chunks.push(chunk);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
}
finally { if (e_1) throw e_1.error; }
}
return { content: chunks.join(''), bytes };
}
// For smaller files, read directly
const content = yield promises.readFile(filePath, encoding);
return { content, bytes };
});
}
/**
* Reads a file as a Buffer asynchronously. Uses streaming for large files.
*
* @param filePath - Path to the file to read
* @param options - Options for reading the file
* @returns Promise resolving to the file contents and size
* @throws Error if the file cannot be read
*/
function readFileAsBuffer(filePath_1) {
return __awaiter(this, arguments, void 0, function* (filePath, options = {}) {
var _a, e_2, _b, _c;
const { streamThreshold = 10 * 1024 * 1024, // 10MB
highWaterMark = 64 * 1024, // 64KB
fileSize, } = options;
// Get file size if not provided
const bytes = fileSize !== null && fileSize !== void 0 ? fileSize : (yield promises.stat(filePath)).size;
// For large files, use streaming to avoid memory issues
if (bytes > streamThreshold) {
const chunks = [];
const stream = fs.createReadStream(filePath, {
highWaterMark,
});
try {
for (var _d = true, stream_2 = __asyncValues(stream), stream_2_1; stream_2_1 = yield stream_2.next(), _a = stream_2_1.done, !_a; _d = true) {
_c = stream_2_1.value;
_d = false;
const chunk = _c;
chunks.push(chunk);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (!_d && !_a && (_b = stream_2.return)) yield _b.call(stream_2);
}
finally { if (e_2) throw e_2.error; }
}
return { content: Buffer.concat(chunks), bytes };
}
// For smaller files, read directly
const content = yield promises.readFile(filePath);
return { content, bytes };
});
}
/**
* Reads a JSON file asynchronously
*
* @param filePath - Path to the JSON file to read
* @param options - Options for reading the file
* @returns Promise resolving to the parsed JSON object
* @throws Error if the file cannot be read or parsed
*/
function readJsonFile(filePath_1) {
return __awaiter(this, arguments, void 0, function* (filePath, options = {}) {
const { content } = yield readFileAsString(filePath, Object.assign(Object.assign({}, options), { encoding: 'utf8' }));
return JSON.parse(content);
});
}
/**
* Makes a function to make HTTP request and logs the process.
* @param params
* @param params.directEndpoint - Whether to use a direct endpoint.
* @param params.reverseProxyUrl - The reverse proxy URL to use for the request.
* @returns A promise that resolves to the response of the fetch request.
*/
function createFetch({ directEndpoint = false, reverseProxyUrl = '', }) {
/**
* Makes an HTTP request and logs the process.
* @param url - The URL to make the request to. Can be a string or a Request object.
* @param init - Optional init options for the request.
* @returns A promise that resolves to the response of the fetch request.
*/
return function (_url, init) {
return __awaiter(this, void 0, void 0, function* () {
let url = _url;
if (directEndpoint) {
url = reverseProxyUrl;
}
dataSchemas.logger.debug(`Making request to ${url}`);
if (typeof Bun !== 'undefined') {
return yield fetch$1(url, init);
}
return yield fetch$1(url, init);
});
};
}
/**
* Creates event handlers for stream events that don't capture client references
* @param res - The response object to send events to
* @returns Object containing handler functions
*/
function createStreamEventHandlers(res) {
return {
[agents.GraphEvents.ON_RUN_STEP]: function (event) {
if (res) {
sendEvent(res, event);
}
},
[agents.GraphEvents.ON_MESSAGE_DELTA]: function (event) {
if (res) {
sendEvent(res, event);
}
},
[agents.GraphEvents.ON_REASONING_DELTA]: function (event) {
if (res) {
sendEvent(res, event);
}
},
};
}
function createHandleLLMNewToken(streamRate) {
return function () {
return __awaiter(this, void 0, void 0, function* () {
if (streamRate) {
yield agents.sleep(streamRate);
}
});
};
}
/**
* Pre-computed regex for matching the Graph token placeholder.
* Escapes curly braces in the placeholder string for safe regex use.
*/
const GRAPH_TOKEN_REGEX = new RegExp(GRAPH_TOKEN_PLACEHOLDER.replace(/[{}]/g, '\\$&'), 'g');
/**
* Checks if a string contains the Graph token placeholder.
* @param value - The string to check
* @returns True if the placeholder is present
*/
function containsGraphTokenPlaceholder(value) {
return typeof value === 'string' && value.includes(GRAPH_TOKEN_PLACEHOLDER);
}
/**
* Checks if any value in a record contains the Graph token placeholder.
* @param record - The record to check (e.g., headers, env vars)
* @returns True if any value contains the placeholder
*/
function recordContainsGraphTokenPlaceholder(record) {
if (!record || typeof record !== 'object') {
return false;
}
return Object.values(record).some(containsGraphTokenPlaceholder);
}
/**
* Checks if MCP options contain the Graph token placeholder in headers, env, or url.
* @param options - The MCP options object
* @returns True if any field contains the placeholder
*/
function mcpOptionsContainGraphTokenPlaceholder(options) {
if (options.url && containsGraphTokenPlaceholder(options.url)) {
return true;
}
if (recordContainsGraphTokenPlaceholder(options.headers)) {
return true;
}
if (recordContainsGraphTokenPlaceholder(options.env)) {
return true;
}
return false;
}
/**
* Asynchronously resolves Graph token placeholders in a string.
* This function must be called before the synchronous processMCPEnv pipeline.
*
* @param value - The string containing the placeholder
* @param options - Options including user and graph token resolver
* @returns The string with Graph token placeholder replaced
*/
function resolveGraphTokenPlaceholder(value, options) {
return __awaiter(this, void 0, void 0, function* () {
if (!containsGraphTokenPlaceholder(value)) {
return value;
}
const { user, graphTokenResolver, scopes } = options;
if (!user || !graphTokenResolver) {
dataSchemas.logger.warn('[resolveGraphTokenPlaceholder] User or graphTokenResolver not provided, cannot resolve Graph token');
return value;
}
const tokenInfo = extractOpenIDTokenInfo(user);
if (!tokenInfo || !isOpenIDTokenValid(tokenInfo)) {
dataSchemas.logger.warn('[resolveGraphTokenPlaceholder] No valid OpenID token available for Graph token exchange');
return value;
}
if (!tokenInfo.accessToken) {
dataSchemas.logger.warn('[resolveGraphTokenPlaceholder] No access token available for OBO exchange');
return value;
}
try {
const graphScopes = scopes || process.env.GRAPH_API_SCOPES || DEFAULT_GRAPH_SCOPES;
const graphTokenResponse = yield graphTokenResolver(user, tokenInfo.accessToken, graphScopes, true);
if (graphTokenResponse === null || graphTokenResponse === void 0 ? void 0 : graphTokenResponse.access_token) {
return value.replace(GRAPH_TOKEN_REGEX, graphTokenResponse.access_token);
}
dataSchemas.logger.warn('[resolveGraphTokenPlaceholder] Graph token exchange did not return an access token');
return value;
}
catch (error) {
dataSchemas.logger.error('[resolveGraphTokenPlaceholder] Failed to exchange token for Graph API:', error);
return value;
}
});
}
/**
* Asynchronously resolves Graph token placeholders in a record of string values.
*
* @param record - The record containing placeholders (e.g., headers)
* @param options - Options including user and graph token resolver
* @returns The record with Graph token placeholders replaced
*/
function resolveGraphTokensInRecord(record, options) {
return __awaiter(this, void 0, void 0, function* () {
if (!record || typeof record !== 'object') {
return record;
}
if (!recordContainsGraphTokenPlaceholder(record)) {
return record;
}
const resolved = {};
for (const [key, value] of Object.entries(record)) {
resolved[key] = yield resolveGraphTokenPlaceholder(value, options);
}
return resolved;
});
}
/**
* Pre-processes MCP options to resolve Graph token placeholders.
* This must be called before processMCPEnv since Graph token resolution is async.
*
* @param options - The MCP options object
* @param graphOptions - Options for Graph token resolution
* @returns The options with Graph token placeholders resolved
*/
function preProcessGraphTokens(options, graphOptions) {
return __awaiter(this, void 0, void 0, function* () {
if (!mcpOptionsContainGraphTokenPlaceholder(options)) {
return options;
}
const result = Object.assign({}, options);
if (result.url && containsGraphTokenPlaceholder(result.url)) {
result.url = yield resolveGraphTokenPlaceholder(result.url, graphOptions);
}
if (result.headers) {
result.headers = yield resolveGraphTokensInRecord(result.headers, graphOptions);
}
if (result.env) {
result.env = yield resolveGraphTokensInRecord(result.env, graphOptions);
}
return result;
});
}
/**
* Gets the base path from the DOMAIN_CLIENT environment variable.
* This is useful for constructing URLs when LibreChat is served from a subdirectory.
* @returns {string} The base path (e.g., '/librechat' or '')
*/
function getBasePath() {
if (!process.env.DOMAIN_CLIENT) {
return '';
}
try {
const clientUrl = new URL(process.env.DOMAIN_CLIENT);
// Keep consistent with the logic in api/server/index.js
const baseHref = clientUrl.pathname.endsWith('/')
? clientUrl.pathname.slice(0, -1) // Remove trailing slash for path construction
: clientUrl.pathname;
return baseHref === '/' ? '' : baseHref;
}
catch (error) {
dataSchemas.logger.warn('Error parsing DOMAIN_CLIENT for base path:', error);
return '';
}
}
/**
* Load Google service key from file path, URL, or stringified JSON
* @param keyPath - The path to the service key file, URL to fetch it from, or stringified JSON
* @returns The parsed service key object or null if failed
*/
function loadServiceKey(keyPath) {
return __awaiter(this, void 0, void 0, function* () {
if (!keyPath) {
return null;
}
let serviceKey;
// Check if it's base64 encoded (common pattern for storing in env vars)
if (keyPath.trim().match(/^[A-Za-z0-9+/]+=*$/)) {
try {
const decoded = Buffer.from(keyPath.trim(), 'base64').toString('utf-8');
// Try to parse the decoded string as JSON
serviceKey = JSON.parse(decoded);
}
catch (_a) {
// Not base64 or not valid JSON after decoding, continue with other methods
// Silent failure - not critical
}
}
// Check if it's a stringified JSON (starts with '{')
if (!serviceKey && keyPath.trim().startsWith('{')) {
try {
serviceKey = JSON.parse(keyPath);
}
catch (error) {
dataSchemas.logger.error('Failed to parse service key from stringified JSON', error);
return null;
}
}
// Check if it's a URL
else if (!serviceKey && /^https?:\/\//.test(keyPath)) {
try {
const response = yield axios$1.get(keyPath);
serviceKey = response.data;
}
catch (error) {
dataSchemas.logger.error(`Failed to fetch the service key from URL: ${keyPath}`, error);
return null;
}
}
else if (!serviceKey) {
// It's a file path
try {
const absolutePath = path.isAbsolute(keyPath) ? keyPath : path.resolve(keyPath);
const { content: fileContent } = yield readFileAsString(absolutePath);
serviceKey = JSON.parse(fileContent);
}
catch (error) {
dataSchemas.logger.error(`Failed to load service key from file: ${keyPath}`, error);
return null;
}
}
// If the response is a string (e.g., from a URL that returns JSON as text), parse it
if (typeof serviceKey === 'string') {
try {
serviceKey = JSON.parse(serviceKey);
}
catch (parseError) {
dataSchemas.logger.error(`Failed to parse service key JSON from ${keyPath}`, parseError);
return null;
}
}
// Validate the service key has required fields
if (!serviceKey || typeof serviceKey !== 'object') {
dataSchemas.logger.error(`Invalid service key format from ${keyPath}`);
return null;
}
// Fix private key formatting if needed
const key = serviceKey;
if (key.private_key && typeof key.private_key === 'string') {
// Replace escaped newlines with actual newlines
// When JSON.parse processes "\\n", it becomes "\n" (single backslash + n)
// When JSON.parse processes "\n", it becomes an actual newline character
key.private_key = key.private_key.replace(/\\n/g, '\n');
// Also handle the String.raw`\n` case mentioned in Stack Overflow
key.private_key = key.private_key.split(String.raw `\n`).join('\n');
// Ensure proper PEM format
if (!key.private_key.includes('\n')) {
// If no newlines are present, try to format it properly
const privateKeyMatch = key.private_key.match(/^(-----BEGIN [A-Z ]+-----)(.*)(-----END [A-Z ]+-----)$/);
if (privateKeyMatch) {
const [, header, body, footer] = privateKeyMatch;
// Add newlines after header and before footer
key.private_key = `${header}\n${body}\n${footer}`;
}
}
}
return key;
});
}
/**
* Checks if a user key has expired based on the provided expiration date and endpoint.
* If the key has expired, it throws an Error with details including the type of error,
* the expiration date, and the endpoint.
*
* @param expiresAt - The expiration date of the user key in a format that can be parsed by the Date constructor
* @param endpoint - The endpoint associated with the user key to be checked
* @throws Error if the user key has expired. The error message is a stringified JSON object
* containing the type of error (`ErrorTypes.EXPIRED_USER_KEY`), the expiration date in the local string format, and the endpoint.
*/
function checkUserKeyExpiry(expiresAt, endpoint) {
const expiresAtDate = new Date(expiresAt);
if (expiresAtDate < new Date()) {
const errorMessage = JSON.stringify({
type: librechatDataProvider.ErrorTypes.EXPIRED_USER_KEY,
expiredAt: expiresAtDate.toLocaleString(),
endpoint,
});
throw new Error(errorMessage);
}
}
/**
* Unescapes LaTeX preprocessing done by the frontend preprocessLaTeX function.
* This reverses the escaping of currency dollar signs and other LaTeX transformations.
*
* The frontend escapes dollar signs for proper LaTeX rendering (e.g., $14 → \\$14),
* but the database stores the original unescaped versions. This function reverses
* that transformation to match database content.
*
* @param text - The escaped text from the frontend
* @returns The unescaped text matching the database format
*/
function unescapeLaTeX(text) {
if (!text || typeof text !== 'string') {
return text;
}
// Unescape currency dollar signs (\\$ or \$ → $)
// This is the main transformation done by preprocessLaTeX for currency
let result = text.replace(/\\\\?\$/g, '$');
// Unescape mhchem notation if present
// Convert $$\\ce{...}$$ back to $\ce{...}$
result = result.replace(/\$\$\\\\ce\{([^}]*)\}\$\$/g, '$\\ce{$1}$');
result = result.replace(/\$\$\\\\pu\{([^}]*)\}\$\$/g, '$\\pu{$1}$');
return result;
}
/**
* Separates LibreChat-specific parameters from model options
* @param options - The combined options object
*/
function extractLibreChatParams(options) {
var _a;
if (!options) {
return {
modelOptions: {},
resendFiles: librechatDataProvider.librechat.resendFiles.default,
};
}
const modelOptions = Object.assign({}, options);
const resendFiles = (_a = (delete modelOptions.resendFiles, options.resendFiles)) !== null && _a !== void 0 ? _a : librechatDataProvider.librechat.resendFiles.default;
const promptPrefix = (delete modelOptions.promptPrefix, options.promptPrefix);
const maxContextTokens = (delete modelOptions.maxContextTokens, options.maxContextTokens);
const fileTokenLimit = (delete modelOptions.fileTokenLimit, options.fileTokenLimit);
const modelLabel = (delete modelOptions.modelLabel, options.modelLabel);
return {
modelOptions: modelOptions,
maxContextTokens,
fileTokenLimit,
promptPrefix,
resendFiles,
modelLabel,
};
}
/**
* Evaluates a mathematical expression provided as a string and returns the result.
*
* If the input is already a number, it returns the number as is.
* 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.
*
* Uses mathjs for safe expression evaluation instead of eval().
*
* @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.
*
* @returns 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.
*/
function math(str, fallbackValue) {
const fallback = fallbackValue != null;
if (typeof str !== 'string' && typeof str === 'number') {
return str;
}
else if (typeof str !== 'string') {
if (fallback) {
return fallbackValue;
}
throw new Error(`str is ${typeof str}, but should be a string`);
}
const validStr = /^[+\-\d.\s*/%()]+$/.test(str);
if (!validStr) {
if (fallback) {
return fallbackValue;
}
throw new Error('Invalid characters in string');
}
try {
const value = mathjs.evaluate(str);
if (typeof value !== 'number') {
if (fallback) {
return fallbackValue;
}
throw new Error(`[math] str did not evaluate to a number but to a ${typeof value}`);
}
return value;
}
catch (error) {
if (fallback) {
return fallbackValue;
}
const originalMessage = error instanceof Error ? error.message : String(error);
throw new Error(`[math] Error while evaluating mathematical expression: ${originalMessage}`);
}
}
/**
* Helper function to safely log sensitive data when debug mode is enabled
* @param obj - Object to stringify
* @param maxLength - Maximum length of the stringified output
* @returns Stringified object with sensitive data masked
*/
function safeStringify(obj, maxLength = 1000) {
try {
const str = JSON.stringify(obj, (key, value) => {
// Mask sensitive values
if (key === 'client_secret' ||
key === 'Authorization' ||
key.toLowerCase().includes('token') ||
key.toLowerCase().includes('password')) {
return typeof value === 'string' && value.length > 6
? `${value.substring(0, 3)}...${value.substring(value.length - 3)}`
: '***MASKED***';
}
return value;
});
if (str && str.length > maxLength) {
return `${str.substring(0, maxLength)}... (truncated)`;
}
return str;
}
catch (error) {
return `[Error stringifying object: ${error.message}]`;
}
}
/**
* Helper to log headers without revealing sensitive information
* @param headers - Headers object to log
* @returns Stringified headers with sensitive data masked
*/
function logHeaders(headers) {
const headerObj = {};
if (!headers || typeof headers.entries !== 'function') {
return 'No headers available';
}
for (const [key, value] of headers.entries()) {
if (key.toLowerCase() === 'authorization' || key.toLowerCase().includes('secret')) {
headerObj[key] = '***MASKED***';
}
else {
headerObj[key] = value;
}
}
return safeStringify(headerObj);
}
/**
* Wraps a promise with a timeout. If the promise doesn't resolve/reject within
* the specified time, it will be rejected with a timeout error.
*
* @param promise - The promise to wrap with a timeout
* @param timeoutMs - Timeout duration in milliseconds
* @param errorMessage - Custom error message for timeout (optional)
* @param logger - Optional logger function to log timeout errors (e.g., console.warn, logger.warn)
* @returns Promise that resolves/rejects with the original promise or times out
*
* @example
* ```typescript
* const result = await withTimeout(
* fetchData(),
* 5000,
* 'Failed to fetch data within 5 seconds',
* console.warn
* );
* ```
*/
function withTimeout(promise, timeoutMs, errorMessage, logger) {
return __awaiter(this, void 0, void 0, function* () {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
const error = new Error(errorMessage !== null && errorMessage !== void 0 ? errorMessage : `Operation timed out after ${timeoutMs}ms`);
if (logger)
logger(error.message, error);
reject(error);
}, timeoutMs);
});
try {
return yield Promise.race([promise, timeoutPromise]);
}
finally {
clearTimeout(timeoutId);
}
});
}
/**
* Sanitizes LLM-generated chat titles by removing <think>...</think> reasoning blocks.
*
* This function strips out all reasoning blocks (with optional attributes and newlines)
* and returns a clean title. If the result is empty, a fallback is returned.
*
* @param rawTitle - The raw LLM-generated title string, potentially containing <think> blocks.
* @returns A sanitized title string, never empty (fallback used if needed).
*/
function sanitizeTitle(rawTitle) {
const DEFAULT_FALLBACK = 'Untitled Conversation';
// Step 1: Input Validation
if (!rawTitle || typeof rawTitle !== 'string') {
return DEFAULT_FALLBACK;
}
// Step 2: Build and apply the regex to remove all <think>...</think> blocks
const thinkBlockRegex = /<think\b[^>]*>[\s\S]*?<\/think>/gi;
const cleaned = rawTitle.replace(thinkBlockRegex, '');
// Step 3: Normalize whitespace (collapse multiple spaces/newlines to single space)
const normalized = cleaned.replace(/\s+/g, ' ');
// Step 4: Trim leading and trailing whitespace
const trimmed = normalized.trim();
// Step 5: Return trimmed result or fallback if empty
return trimmed.length > 0 ? trimmed : DEFAULT_FALLBACK;
}
/**
* Default retention period for temporary chats in hours
*/
const DEFAULT_RETENTION_HOURS = 24 * 30; // 30 days
/**
* Minimum allowed retention period in hours
*/
const MIN_RETENTION_HOURS = 1;
/**
* Maximum allowed retention period in hours (1 year = 8760 hours)
*/
const MAX_RETENTION_HOURS = 8760;
/**
* Gets the temporary chat retention period from environment variables or config
* @param interfaceConfig - The custom configuration object
* @returns The retention period in hours
*/
function getTempChatRetentionHours(interfaceConfig) {
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 {
dataSchemas.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 ((interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.temporaryChatRetention) !== undefined) {
const configValue = interfaceConfig.temporaryChatRetention;
if (typeof configValue === 'number' && !isNaN(configValue)) {
retentionHours = configValue;
}
else {
dataSchemas.logger.warn(`Invalid temporaryChatRetention in config: ${configValue}. Using ${retentionHours} hours.`);
}
}
// Validate the retention period
if (retentionHours < MIN_RETENTION_HOURS) {
dataSchemas.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) {
dataSchemas.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 interfaceConfig - The custom configuration object
* @returns The expiration date
*/
function createTempChatExpirationDate(interfaceConfig) {
const retentionHours = getTempChatRetentionHours(interfaceConfig);
return new Date(Date.now() + retentionHours * 60 * 60 * 1000);
}
/**
* Safety buffer multiplier applied to character position estimates during truncation.
*
* We use 98% (0.98) rather than 100% to intentionally undershoot the target on the first attempt.
* This is necessary because:
* - Token density varies across text (some regions may have more tokens per character than the average)
* - The ratio-based estimate assumes uniform token distribution, which is rarely true
* - Undershooting is safer than overshooting: exceeding the limit requires another iteration,
* while being slightly under is acceptable
* - In practice, this buffer reduces refinement iterations from 2-3 down to 0-1 in most cases
*
* @example
* // If text has 1000 chars and 250 tokens (4 chars/token average), targeting 100 tokens:
* // Without buffer: estimate = 1000 * (100/250) = 400 chars → might yield 105 tokens (over!)
* // With 0.98 buffer: estimate = 400 * 0.98 = 392 chars → likely yields 97-99 tokens (safe)
*/
const TRUNCATION_SAFETY_BUFFER = 0.98;
/**
* Processes text content by counting tokens and truncating if it exceeds the specified limit.
* Uses ratio-based estimation to minimize expensive tokenCountFn calls.
*
* @param text - The text content to process
* @param tokenLimit - The maximum number of tokens allowed
* @param tokenCountFn - Function to count tokens (can be sync or async)
* @returns Promise resolving to object with processed text, token count, and truncation status
*
* @remarks
* This function uses a ratio-based estimation algorithm instead of binary search.
* Binary search would require O(log n) tokenCountFn calls (~17 for 100k chars),
* while this approach typically requires only 2-3 calls for a 90%+ reduction in CPU usage.
*/
function processTextWithTokenLimit(_a) {
return __awaiter(this, arguments, void 0, function* ({ text, tokenLimit, tokenCountFn, }) {
const originalTokenCount = yield tokenCountFn(text);
if (originalTokenCount <= tokenLimit) {
return {
text,
tokenCount: originalTokenCount,
wasTruncated: false,
};
}
dataSchemas.logger.debug(`[textTokenLimiter] Text content exceeds token limit: ${originalTokenCount} > ${tokenLimit}, truncating...`);
const ratio = tokenLimit / originalTokenCount;
let charPosition = Math.floor(text.length * ratio * TRUNCATION_SAFETY_BUFFER);
let truncatedText = text.substring(0, charPosition);
let tokenCount = yield tokenCountFn(truncatedText);
const maxIterations = 5;
let iterations = 0;
while (tokenCount > tokenLimit && iterations < maxIterations && charPosition > 0) {
const overageRatio = tokenLimit / tokenCount;
charPosition = Math.floor(charPosition * overageRatio * TRUNCATION_SAFETY_BUFFER);
truncatedText = text.substring(0, charPosition);
tokenCount = yield tokenCountFn(truncatedText);
iterations++;
}
dataSchemas.logger.warn(`[textTokenLimiter] Text truncated from ${originalTokenCount} to ${tokenCount} tokens (limit: ${tokenLimit})`);
return {
text: truncatedText,
tokenCount,
wasTruncated: true,
};
});
}
class Tokenizer {
constructor() {
this.tokenizersCache = {};
this.tokenizerCallsCount = 0;
}
getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
let tokenizer;
if (this.tokenizersCache[encoding]) {
tokenizer = this.tokenizersCache[encoding];
}
else {
if (isModelName) {
tokenizer = tiktoken.encoding_for_model(encoding, extendSpecialTokens);
}
else {
tokenizer = tiktoken.get_encoding(encoding, extendSpecialTokens);
}
this.tokenizersCache[encoding] = tokenizer;
}
return tokenizer;
}
freeAndResetAllEncoders() {
try {
Object.keys(this.tokenizersCache).forEach((key) => {
if (this.tokenizersCache[key]) {
this.tokenizersCache[key].free();
delete this.tokenizersCache[key];
}
});
this.tokenizerCallsCount = 1;
}
catch (error) {
dataSchemas.logger.error('[Tokenizer] Free and reset encoders error', error);
}
}
resetTokenizersIfNecessary() {
var _a;
if (this.tokenizerCallsCount >= 25) {
if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.debug) {
dataSchemas.logger.debug('[Tokenizer] freeAndResetAllEncoders: reached 25 encodings, resetting...');
}
this.freeAndResetAllEncoders();
}
this.tokenizerCallsCount++;
}
getTokenCount(text, encoding = 'cl100k_base') {
this.resetTokenizersIfNecessary();
try {
const tokenizer = this.getTokenizer(encoding);
return tokenizer.encode(text, 'all').length;
}
catch (error) {
dataSchemas.logger.error('[Tokenizer] Error getting token count:', error);
this.freeAndResetAllEncoders();
const tokenizer = this.getTokenizer(encoding);
return tokenizer.encode(text, 'all').length;
}
}
}
const TokenizerSingleton = new Tokenizer();
/**
* Counts the number of tokens in a given text using tiktoken.
* This is an async wrapper around Tokenizer.getTokenCount for compatibility.
* @param text - The text to be tokenized. Defaults to an empty string if not provided.
* @returns The number of tokens in the provided text.
*/
function countTokens() {
return __awaiter(this, arguments, void 0, function* (text = '') {
return TokenizerSingleton.getTokenCount(text, 'cl100k_base');
});
}
function loadYaml(filepath) {
try {
const fileContents = fs.readFileSync(filepath, 'utf8');
return yaml.load(fileContents);
}
catch (e) {
return e;
}
}
/**
* Normalizes an error-like object into an HTTP status and message.
* Ensures we always respond with a valid numeric status to avoid UI hangs.
*/
function normalizeHttpError(err, fallbackStatus = 400) {
let status = fallbackStatus;
if (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number') {
status = err.status;
}
let message = 'An error occurred.';
if (err &&
typeof err === 'object' &&
'message' in err &&
typeof err.message === 'string' &&
err.message.length > 0) {
message = err.message;
}
return { status, message };
}
/**
* Model Token Configuration Maps
*
* IMPORTANT: Key Ordering for Pattern Matching
* ============================================
* The `findMatchingPattern` function iterates through object keys in REVERSE order
* (last-defined keys are checked first) and uses `modelName.includes(key)` for matching.
*
* This means:
* 1. BASE PATTERNS must be defined FIRST (e.g., "kimi", "moonshot")
* 2. SPECIFIC PATTERNS must be defined AFTER their base patterns (e.g., "kimi-k2", "kimi-k2.5")
*
* Example ordering for Kimi models:
* kimi: 262144, // Base pattern - checked last
* 'kimi-k2': 262144, // More specific - checked before "kimi"
* 'kimi-k2.5': 262144, // Most specific - checked first
*
* Why this matters:
* - Model name "kimi-k2.5" contains both "kimi" and "kimi-k2" as substrings
* - If "kimi" were checked first, it would incorrectly match "kimi-k2.5"
* - By defining specific patterns AFTER base patterns, they're checked first in reverse iteration
*
* When adding new model families:
* 1. Define the base/generic pattern first
* 2. Define increasingly specific patterns after
* 3. Ensure no pattern is a substring of another that should match differently
*/
const openAIModels = {
'o4-mini': 200000,
'o3-mini': 195000, // -5000 from max
o3: 200000,
o1: 195000, // -5000 from max
'o1-mini': 127500, // -500 from max
'o1-preview': 127500, // -500 from max
'gpt-4': 8187, // -5 from max
'gpt-4-0613': 8187, // -5 from max
'gpt-4-32k': 32758, // -10 from max
'gpt-4-32k-0314': 32758, // -10 from max
'gpt-4-32k-0613': 32758, // -10 from max
'gpt-4-1106': 127500, // -500 from max
'gpt-4-0125': 127500, // -500 from max
'gpt-4.5': 127500, // -500 from max
'gpt-4.1': 1047576,
'gpt-4.1-mini': 1047576,
'gpt-4.1-nano': 1047576,
'gpt-5': 400000,
'gpt-5.1': 400000,
'gpt-5.2': 400000,
'gpt-5-mini': 400000,
'gpt-5-nano': 400000,
'gpt-5-pro': 400000,
'gpt-4o': 127500, // -500 from max
'gpt-4o-mini': 127500, // -500 from max
'gpt-4o-2024-05-13': 127500, // -500 from max
'gpt-4-turbo': 127500, // -500 from max
'gpt-4-vision': 127500, // -500 from max
'gpt-3.5-turbo': 16375, // -10 from max
'gpt-3.5-turbo-0613': 4092, // -5 from max
'gpt-3.5-turbo-0301': 4092, // -5 from max
'gpt-3.5-turbo-16k': 16375, // -10 from max
'gpt-3.5-turbo-16k-0613': 16375, // -10 from max
'gpt-3.5-turbo-1106': 16375, // -10 from max
'gpt-3.5-turbo-0125': 16375, // -10 from max
};
const mistralModels = {
'mistral-': 31990, // -10 from max
'mistral-7b': 31990, // -10 from max
'mistral-small': 31990, // -10 from max
'mixtral-8x7b': 31990, // -10 from max
'mixtral-8x22b': 65536,
'mistral-large': 131000,
'mistral-large-2402': 127500,
'mistral-large-2407': 127500,
'mistral-nemo': 131000,
'pixtral-large': 131000,
'mistral-saba': 32000,
codestral: 256000,
'ministral-8b': 131000,
'ministral-3b': 131000,
};
const cohereModels = {
'command-light': 4086, // -10 from max
'command-light-nightly': 8182, // -10 from max
command: 4086, // -10 from max
'command-nightly': 8182, // -10 from max
'command-text': 4086, // -10 from max
'command-r': 127500, // -500 from max
'command-r-plus': 127500, // -500 from max
};
const googleModels = {
/* Max I/O is combined so we subtract the amount from max response tokens for actual total */
gemma: 8196,
'gemma-2': 32768,
'gemma-3': 32768,
'gemma-3-27b': 131072,
gemini: 30720, // -2048 from max
'gemini-pro-vision': 12288,
'gemini-exp': 2000000,
'gemini-3': 1000000, // 1M input tokens, 64k output tokens
'gemini-3-pro-image': 1000000,
'gemini-3.1': 1000000, // 1M input tokens, 64k output tokens
'gemini-2.5': 1000000, // 1M input tokens, 64k output tokens
'gemini-2.5-pro': 1000000,
'gemini-2.5-flash': 1000000,
'gemini-2.5-flash-image': 1000000,
'gemini-2.5-flash-lite': 1000000,
'gemini-2.0': 2000000,
'gemini-2.0-flash': 1000000,
'gemini-2.0-flash-lite': 1000000,
'gemini-1.5': 1000000,
'gemini-1.5-flash': 1000000,
'gemini-1.5-flash-8b': 1000000,
'text-bison-32k': 32758, // -10 from max
'chat-bison-32k': 32758, // -10 from max
'code-bison-32k': 32758, // -10 from max
'codechat-bison-32k': 32758,
/* Codey, -5 from max: 6144 */
'code-': 6139,
'codechat-': 6139,
/* PaLM2, -5 from max: 8192 */
'text-': 8187,
'chat-': 8187,
};
const anthropicModels = {
'claude-': 100000,
'claude-instant': 100000,
'claude-2': 100000,
'claude-2.1': 200000,
'claude-3': 200000,
'claude-3-haiku': 200000,
'claude-3-sonnet': 200000,
'claude-3-opus': 200000,
'claude-3.5-haiku': 200000,
'claude-3-5-haiku': 200000,
'claude-3-5-sonnet': 200000,
'claude-3.5-sonnet': 200000,
'claude-3-7-sonnet': 200000,
'claude-3.7-sonnet': 200000,
'claude-3-5-sonnet-latest': 200000,
'claude-3.5-sonnet-latest': 200000,
'claude-haiku-4-5': 200000,
'claude-sonnet-4': 1000000,
'claude-sonnet-4-6': 1000000,
'claude-4': 200000,
'claude-opus-4': 200000,
'claude-opus-4-5': 200000,
'claude-opus-4-6': 1000000,
};
const deepseekModels = {
deepseek: 128000,
'deepseek-chat': 128000,
'deepseek-reasoner': 128000,
'deepseek-r1': 128000,
'deepseek-v3': 128000,
'deepseek.r1': 128000,
};
const moonshotModels = {
// Base patterns (check last due to reverse iteration)
kimi: 262144,
moonshot: 131072,
// kimi-k2 series (specific patterns)
'kimi-latest': 128000,
'kimi-k2': 262144,
'kimi-k2.5': 262144,
'kimi-k2-turbo': 262144,
'kimi-k2-turbo-preview': 262144,
'kimi-k2-0905': 262144,
'kimi-k2-0905-preview': 262144,
'kimi-k2-0711': 131072,
'kimi-k2-0711-preview': 131072,
'kimi-k2-thinking': 262144,
'kimi-k2-thinking-turbo': 262144,
// moonshot-v1 series (specific patterns)
'moonshot-v1': 131072,
'moonshot-v1-auto': 131072,
'moonshot-v1-8k': 8192,
'moonshot-v1-8k-vision': 8192,
'moonshot-v1-8k-vision-preview': 8192,
'moonshot-v1-32k': 32768,
'moonshot-v1-32k-vision': 32768,
'moonshot-v1-32k-vision-preview': 32768,
'moonshot-v1-128k': 131072,
'moonshot-v1-128k-vision': 131072,
'moonshot-v1-128k-vision-preview': 131072,
// Bedrock moonshot models
'moonshot.kimi': 262144,
'moonshot.kimi-k2': 262144,
'moonshot.kimi-k2.5': 262144,
'moonshot.kimi-k2-thinking': 262144,
'moonshot.kimi-k2-0711': 131072,
'moonshotai.kimi': 262144,
'moonshotai.kimi-k2.5': 262144,
};
const metaModels = {
// Basic patterns
llama3: 8000,
llama2: 4000,
'llama-3': 8000,
'llama-2': 4000,
// llama3.x pattern
'llama3.1': 127500,
'llama3.2': 127500,
'llama3.3': 127500,
// llama3-x pattern
'llama3-1': 127500,
'llama3-2': 127500,
'llama3-3': 127500,
// llama-3.x pattern
'llama-3.1': 127500,
'llama-3.2': 127500,
'llama-3.3': 127500,
// llama3.x:Nb pattern
'llama3.1:405b': 127500,
'llama3.1:70b': 127500,
'llama3.1:8b': 127500,
'llama3.2:1b': 127500,
'llama3.2:3b': 127500,
'llama3.2:11b': 127500,
'llama3.2:90b': 127500,
'llama3.3:70b': 127500,
// llama3-x-Nb pattern
'llama3-1-405b': 127500,
'llama3-1-70b': 127500,
'llama3-1-8b': 127500,
'llama3-2-1b': 127500,
'llama3-2-3b': 127500,
'llama3-2-11b': 127500,
'llama3-2-90b': 127500,
'llama3-3-70b': 127500,
// llama-3.x-Nb pattern
'llama-3.1-405b': 127500,
'llama-3.1-70b': 127500,
'llama-3.1-8b': 127500,
'llama-3.2-1b': 127500,
'llama-3.2-3b': 127500,
'llama-3.2-11b': 127500,
'llama-3.2-90b': 127500,
'llama-3.3-70b': 127500,
// Original llama2/3 patterns
'llama3-70b': 8000,
'llama3-8b': 8000,
'llama2-70b': 4000,
'llama2-13b': 4000,
'llama3:70b': 8000,
'llama3:8b': 8000,
'llama2:70b': 4000,
};
const qwenModels = {
qwen: 32000,
'qwen2.5': 32000,
'qwen-turbo': 1000000,
'qwen-plus': 131000,
'qwen-max': 32000,
'qwq-32b': 32000,
// Qwen3 models
qwen3: 40960, // Qwen3 base pattern (using qwen3-4b context)
'qwen3-8b': 128000,
'qwen3-14b': 40960,
'qwen3-30b-a3b': 40960,
'qwen3-32b': 40960,
'qwen3-235b-a22b': 40960,
// Qwen3 VL (Vision-Language) models
'qwen3-vl-8b-thinking': 256000,
'qwen3-vl-8b-instruct': 262144,
'qwen3-vl-30b-a3b': 262144,
'qwen3-vl-235b-a22b': 131072,
// Qwen3 specialized models
'qwen3-max': 256000,
'qwen3-coder': 262144,
'qwen3-coder-30b-a3b': 262144,
'qwen3-coder-plus': 128000,
'qwen3-coder-flash': 128000,
'qwen3-next-80b-a3b': 262144,
};
const ai21Models = {
'j2-mid': 8182, // -10 from max
'j2-ultra': 8182, // -10 from max
'jamba-instruct': 255500, // -500 from max
};
const amazonModels = {
// Amazon Titan models
'titan-text-lite': 4000,
'titan-text-express': 8000,
'titan-text-premier': 31500, // -500 from max
// Amazon Nova models
// https://aws.amazon.com/ai/generative-ai/nova/
'nova-micro': 127000, // -1000 from max
'nova-lite': 295000, // -5000 from max
'nova-pro': 295000, // -5000 from max
'nova-premier': 995000, // -5000 from max
};
const openAIBedrockModels = {
'openai.gpt-oss-20b': 128000,
'openai.gpt-oss-120b': 128000,
};
const bedrockModels = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, anthropicModels), mistralModels), cohereModels), deepseekModels), moonshotModels), metaModels), ai21Models), amazonModels), openAIBedrockModels);
const xAIModels = {
grok: 131072,
'grok-beta': 131072,
'grok-vision-beta': 8192,
'grok-2': 131072,
'grok-2-latest': 131072,
'grok-2-1212': 131072,
'grok-2-vision': 32768,
'grok-2-vision-latest': 32768,
'grok-2-vision-1212': 32768,
'grok-3': 131072,
'grok-3-fast': 131072,
'grok-3-mini': 131072,
'grok-3-mini-fast': 131072,
'grok-4': 256000, // 256K context
'grok-4-fast': 2000000, // 2M context
'grok-4-1-fast': 2000000, // 2M context (covers reasoning & non-reasoning variants)
'grok-code-fast': 256000, // 256K context
};
const aggregateModels = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, openAIModels), googleModels), bedrockModels), xAIModels), qwenModels), {
// GPT-OSS
'gpt-oss': 131000, 'gpt-oss:20b': 131000, 'gpt-oss-20b': 131000, 'gpt-oss:120b': 131000, 'gpt-oss-120b': 131000,
// GLM models (Zhipu AI)
glm4: 128000, 'glm-4': 128000, 'glm-4-32b': 128000, 'glm-4.5': 131000, 'glm-4.5-air': 131000, 'glm-4.5v': 66000, 'glm-4.6': 200000 });
const maxTokensMap = {
[librechatDataProvider.EModelEndpoint.azureOpenAI]: openAIModels,
[librechatDataProvider.EModelEndpoint.openAI]: aggregateModels,
[librechatDataProvider.EModelEndpoint.agents]: aggregateModels,
[librechatDataProvider.EModelEndpoint.custom]: aggregateModels,
[librechatDataProvider.EModelEndpoint.google]: googleModels,
[librechatDataProvider.EModelEndpoint.anthropic]: anthropicModels,
[librechatDataProvider.EModelEndpoint.bedrock]: bedrockModels,
};
const modelMaxOutputs = {
o1: 32268, // -500 from max: 32,768
'o1-mini': 65136, // -500 from max: 65,536
'o1-preview': 32268, // -500 from max: 32,768
'gpt-5': 128000,
'gpt-5.1': 128000,
'gpt-5.2': 128000,
'gpt-5-mini': 128000,
'gpt-5-nano': 128000,
'gpt-5-pro': 128000,
'gpt-oss-20b': 131000,
'gpt-oss-120b': 131000,
system_default: 32000,
};
/** Outputs from https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-names */
const anthropicMaxOutputs = {
'claude-3-haiku': 4096,
'claude-3-sonnet': 4096,
'claude-3-opus': 4096,
'claude-haiku-4-5': 64000,
'claude-sonnet-4': 64000,
'claude-sonnet-4-6': 64000,
'claude-opus-4': 32000,
'claude-opus-4-5': 64000,
'claude-opus-4-6': 128000,
'claude-3.5-sonnet': 8192,
'claude-3-5-sonnet': 8192,
'claude-3.7-sonnet': 128000,
'claude-3-7-sonnet': 128000,
};
/** Outputs from https://api-docs.deepseek.com/quick_start/pricing */
const deepseekMaxOutputs = {
deepseek: 8000, // deepseek-chat default: 4K, max: 8K
'deepseek-chat': 8000,
'deepseek-reasoner': 64000, // default: 32K, max: 64K
'deepseek-r1': 64000,
'deepseek-v3': 8000,
'deepseek.r1': 64000,
};
const maxOutputTokensMap = {
[librechatDataProvider.EModelEndpoint.anthropic]: anthropicMaxOutputs,
[librechatDataProvider.EModelEndpoint.azureOpenAI]: modelMaxOutputs,
[librechatDataProvider.EModelEndpoint.openAI]: Object.assign(Object.assign({}, modelMaxOutputs), deepseekMaxOutputs),
[librechatDataProvider.EModelEndpoint.custom]: Object.assign(Object.assign({}, modelMaxOutputs), deepseekMaxOutputs),
};
/**
* Finds the first matching pattern in the tokens map.
* @param {string} modelName
* @param {Record<string, number> | EndpointTokenConfig} tokensMap
* @returns {string|null}
*/
function findMatchingPattern(modelName, tokensMap) {
const keys = Object.keys(tokensMap);
const lowerModelName = modelName.toLowerCase();
for (let i = keys.length - 1; i >= 0; i--) {
const modelKey = keys[i];
if (lowerModelName.includes(modelKey)) {
return modelKey;
}
}
return null;
}
/**
* Retrieves a token value for a given model name from a tokens map.
*
* @param modelName - The name of the model to look up.
* @param tokensMap - The map of model names to token values.
* @param [key='context'] - The key to look up in the tokens map.
* @returns The token value for the given model or undefined if no match is found.
*/
function getModelTokenValue(modelName, tokensMap, key = 'context') {
if (typeof modelName !== 'string' || !tokensMap) {
return undefined;
}
const value = tokensMap[modelName];
if (typeof value === 'number') {
return value;
}
if (value === null || value === void 0 ? void 0 : value.context) {
return value.context;
}
const matchedPattern = findMatchingPattern(modelName, tokensMap);
if (matchedPattern) {
const result = tokensMap[matchedPattern];
if (typeof result === 'number') {
return result;
}
const tokenValue = result === null || result === void 0 ? void 0 : result[key];
if (typeof tokenValue === 'number') {
return tokenValue;
}
return tokensMap.system_default;
}
return tokensMap.system_default;
}
/**
* Retrieves the maximum tokens for a given model name.
*
* @param modelName - The name of the model to look up.
* @param endpoint - The endpoint (default is 'openAI').
* @param [endpointTokenConfig] - Token Config for current endpoint to use for max tokens lookup
* @returns The maximum tokens for the given model or undefined if no match is found.
*/
function getModelMaxTokens(modelName, endpoint = librechatDataProvider.EModelEndpoint.openAI, endpointTokenConfig) {
const tokensMap = endpointTokenConfig !== null && endpointTokenConfig !== void 0 ? endpointTokenConfig : maxTokensMap[endpoint];
return getModelTokenValue(modelName, tokensMap);
}
/**
* Retrieves the maximum output tokens for a given model name.
*
* @param modelName - The name of the model to look up.
* @param endpoint - The endpoint (default is 'openAI').
* @param [endpointTokenConfig] - Token Config for current endpoint to use for max tokens lookup
* @returns The maximum output tokens for the given model or undefined if no match is found.
*/
function getModelMaxOutputTokens(modelName, endpoint = librechatDataProvider.EModelEndpoint.openAI, endpointTokenConfig) {
const tokensMap = endpointTokenConfig !== null && endpointTokenConfig !== void 0 ? endpointTokenConfig : maxOutputTokensMap[endpoint];
return getModelTokenValue(modelName, tokensMap, 'output');
}
/**
* Retrieves the model name key for a given model name input. If the exact model name isn't found,
* it searches for partial matches within the model name, checking keys in reverse order.
*
* @param modelName - The name of the model to look up.
* @param endpoint - The endpoint (default is 'openAI').
* @returns The model name key for the given model; returns input if no match is found and is string.
*
* @example
* matchModelName('gpt-4-32k-0613'); // Returns 'gpt-4-32k-0613'
* matchModelName('gpt-4-32k-unknown'); // Returns 'gpt-4-32k'
* matchModelName('unknown-model'); // Returns undefined
*/
function matchModelName(modelName, endpoint = librechatDataProvider.EModelEndpoint.openAI) {
if (typeof modelName !== 'string') {
return undefined;
}
const tokensMap = maxTokensMap[endpoint];
if (!tokensMap) {
return modelName;
}
if (tokensMap[modelName]) {
return modelName;
}
const matchedPattern = findMatchingPattern(modelName, tokensMap);
return matchedPattern || modelName;
}
const modelSchema = z.object({
id: z.string(),
pricing: z.object({
prompt: z.string(),
completion: z.string(),
}),
context_length: z.number(),
});
const inputSchema = z.object({
data: z.array(modelSchema),
});
/**
* Processes a list of model data from an API and organizes it into structured data based on URL and specifics of rates and context.
* @param {{ data: Array<z.infer<typeof modelSchema>> }} input The input object containing base URL and data fetched from the API.
* @returns {EndpointTokenConfig} The processed model data.
*/
function processModelData(input) {
const validationResult = inputSchema.safeParse(input);
if (!validationResult.success) {
throw new Error('Invalid input data');
}
const { data } = validationResult.data;
/** @type {EndpointTokenConfig} */
const tokenConfig = {};
for (const model of data) {
const modelKey = model.id;
if (modelKey === 'openrouter/auto') {
model.pricing = {
prompt: '0.00001',
completion: '0.00003',
};
}
const prompt = parseFloat(model.pricing.prompt) * 1000000;
const completion = parseFloat(model.pricing.completion) * 1000000;
tokenConfig[modelKey] = {
prompt,
completion,
context: model.context_length,
};
}
return tokenConfig;
}
const tiktokenModels = new Set([
'text-davinci-003',
'text-davinci-002',
'text-davinci-001',
'text-curie-001',
'text-babbage-001',
'text-ada-001',
'davinci',
'curie',
'babbage',
'ada',
'code-davinci-002',
'code-davinci-001',
'code-cushman-002',
'code-cushman-001',
'davinci-codex',
'cushman-codex',
'text-davinci-edit-001',
'code-davinci-edit-001',
'text-embedding-ada-002',
'text-similarity-davinci-001',
'text-similarity-curie-001',
'text-similarity-babbage-001',
'text-similarity-ada-001',
'text-search-davinci-doc-001',
'text-search-curie-doc-001',
'text-search-babbage-doc-001',
'text-search-ada-doc-001',
'code-search-babbage-code-001',
'code-search-ada-code-001',
'gpt2',
'gpt-4',
'gpt-4-0314',
'gpt-4-32k',
'gpt-4-32k-0314',
'gpt-3.5-turbo',
'gpt-3.5-turbo-0301',
]);
/**
* Extracts a valid OpenAI baseURL from a given string, matching "url/v1," followed by an optional suffix.
* The suffix can be one of several predefined values (e.g., 'openai', 'azure-openai', etc.),
* accommodating different proxy patterns like Cloudflare, LiteLLM, etc.
* Returns the original URL if no valid pattern is found.
*
* Examples:
* - `https://open.ai/v1/chat` -> `https://open.ai/v1`
* - `https://open.ai/v1/chat/completions` -> `https://open.ai/v1`
* - `https://gateway.ai.cloudflare.com/v1/account/gateway/azure-openai/completions` -> `https://gateway.ai.cloudflare.com/v1/account/gateway/azure-openai`
* - `https://open.ai/v1/hi/openai` -> `https://open.ai/v1/hi/openai`
* - `https://api.example.com/v1/replicate` -> `https://api.example.com/v1/replicate`
*
* @param url - The URL to be processed.
* @returns The matched pattern or input if no match is found.
*/
function extractBaseURL(url) {
var _a;
if (!url || typeof url !== 'string') {
return undefined;
}
if (url.startsWith(librechatDataProvider.CohereConstants.API_URL)) {
return null;
}
if (!url.includes('/v1')) {
return url;
}
const v1Index = url.indexOf('/v1');
let baseUrl = url.substring(0, v1Index + 3);
const openai = 'openai';
const suffixes = [
'azure-openai',
openai,
'aws-bedrock',
'anthropic',
'cohere',
'deepseek',
'google-ai-studio',
'google-vertex-ai',
'grok',
'groq',
'mistral',
'openrouter',
'perplexity-ai',
'replicate',
'huggingface',
'workers-ai',
'aws-bedrock',
];
const suffixUsed = suffixes.find((suffix) => url.includes(`/${suffix}`));
if (suffixUsed === 'azure-openai') {
return url.split(/\/(chat|completion)/)[0];
}
const openaiIndex = url.indexOf(`/${openai}`, v1Index + 3);
const suffixIndex = suffixUsed === openai ? openaiIndex : url.indexOf(`/${suffixUsed}`, v1Index + 3);
if (openaiIndex === v1Index + 3) {
const nextSlashIndex = url.indexOf('/', openaiIndex + 7);
if (nextSlashIndex === -1) {
baseUrl = url.substring(0, openaiIndex + 7);
}
else {
baseUrl = url.substring(0, nextSlashIndex);
}
}
else if (suffixIndex > 0) {
baseUrl = url.substring(0, suffixIndex + ((_a = suffixUsed === null || suffixUsed === void 0 ? void 0 : suffixUsed.length) !== null && _a !== void 0 ? _a : 0) + 1);
}
return baseUrl;
}
/**
* Extracts the base URL (protocol + hostname + port) from the provided URL.
* Used primarily for Ollama endpoints to derive the host.
* @param fullURL - The full URL.
* @returns The base URL (protocol://hostname:port).
*/
function deriveBaseURL(fullURL) {
try {
const parsedUrl = new URL(fullURL);
const protocol = parsedUrl.protocol;
const hostname = parsedUrl.hostname;
const port = parsedUrl.port;
if (!protocol || !hostname) {
return fullURL;
}
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
}
catch (error) {
dataSchemas.logger.error('Failed to derive base URL', error);
return fullURL;
}
}
/** Fields to strip from files before client transmission */
const FILE_STRIP_FIELDS = ['text', '_id', '__v'];
/** Fields to strip from messages before client transmission */
const MESSAGE_STRIP_FIELDS = ['fileContext'];
/**
* Strips large/unnecessary fields from a file object before transmitting to client.
* Use this within existing loops when building file arrays to avoid extra iterations.
*
* @param file - The file object to sanitize
* @returns A new file object without the stripped fields
*
* @example
* // Use in existing file processing loop:
* for (const attachment of client.options.attachments) {
* if (messageFiles.has(attachment.file_id)) {
* userMessage.files.push(sanitizeFileForTransmit(attachment));
* }
* }
*/
function sanitizeFileForTransmit(file) {
const sanitized = Object.assign({}, file);
for (const field of FILE_STRIP_FIELDS) {
delete sanitized[field];
}
return sanitized;
}
/**
* Sanitizes a message object before transmitting to client.
* Removes large fields like `fileContext` and strips `text` from embedded files.
*
* @param message - The message object to sanitize
* @returns A new message object safe for client transmission
*
* @example
* sendEvent(res, {
* final: true,
* requestMessage: sanitizeMessageForTransmit(userMessage),
* responseMessage: response,
* });
*/
function sanitizeMessageForTransmit(message) {
if (!message) {
return message;
}
const sanitized = Object.assign({}, message);
// Remove message-level fields
for (const field of MESSAGE_STRIP_FIELDS) {
delete sanitized[field];
}
// Always create a new array when files exist to maintain full immutability
if (Array.isArray(sanitized.files)) {
sanitized.files = sanitized.files.map((file) => sanitizeFileForTransmit(file));
}
return sanitized;
}
/**
* Extracts thread message IDs and file IDs in a single O(n) pass.
* Builds a Map for O(1) lookups, then traverses the thread collecting both IDs.
*
* @param messages - All messages in the conversation (should be queried with select for efficiency)
* @param parentMessageId - The ID of the parent message to start traversal from
* @returns Object containing messageIds and fileIds arrays
*/
function getThreadData(messages, parentMessageId) {
const result = { messageIds: [], fileIds: [] };
if (!messages || messages.length === 0 || !parentMessageId) {
return result;
}
/** Build Map for O(1) lookups instead of O(n) .find() calls */
const messageMap = new Map();
for (const msg of messages) {
messageMap.set(msg.messageId, msg);
}
const fileIdSet = new Set();
const visitedIds = new Set();
let currentId = parentMessageId;
/** Single traversal: collect message IDs and file IDs together */
while (currentId) {
if (visitedIds.has(currentId)) {
break;
}
visitedIds.add(currentId);
const message = messageMap.get(currentId);
if (!message) {
break;
}
result.messageIds.push(message.messageId);
/** Collect file IDs from this message */
if (message.files) {
for (const file of message.files) {
if (file.file_id) {
fileIdSet.add(file.file_id);
}
}
}
currentId = message.parentMessageId === librechatDataProvider.Constants.NO_PARENT ? null : message.parentMessageId;
}
result.fileIds = Array.from(fileIdSet);
return result;
}
/**
* Retrieves the balance configuration object.
* Supports legacy env vars (CHECK_BALANCE, START_BALANCE) and
* Hanzo billing env vars (HANZO_BILLING_ENABLED, HANZO_SIGNUP_CREDIT, etc.).
*/
function getBalanceConfig(appConfig) {
var _a;
const isLegacyEnabled = isEnabled(process.env.CHECK_BALANCE);
const isHanzoEnabled = isEnabled(process.env.HANZO_BILLING_ENABLED);
const startBalance = process.env.START_BALANCE;
// Hanzo billing env vars: HANZO_SIGNUP_CREDIT is in USD, convert to tokenCredits
const hanzoSignupCredit = process.env.HANZO_SIGNUP_CREDIT;
const hanzoExpiryDays = process.env.HANZO_SIGNUP_CREDIT_EXPIRY_DAYS;
const hanzoMinBalance = process.env.HANZO_MIN_BALANCE;
const envConfig = librechatDataProvider.removeNullishValues({
enabled: isLegacyEnabled || isHanzoEnabled,
startBalance: startBalance != null && startBalance ? parseInt(startBalance, 10) : undefined,
});
// Apply Hanzo env var overrides (USD to tokenCredits conversion)
if (hanzoSignupCredit != null && hanzoSignupCredit !== '') {
const usd = parseFloat(hanzoSignupCredit);
if (!isNaN(usd)) {
envConfig.startBalance = Math.round(usd * 1000000);
}
}
if (hanzoExpiryDays != null && hanzoExpiryDays !== '') {
const days = parseInt(hanzoExpiryDays, 10);
if (!isNaN(days)) {
envConfig.creditExpiryDays = days;
}
}
if (hanzoMinBalance != null && hanzoMinBalance !== '') {
const usd = parseFloat(hanzoMinBalance);
if (!isNaN(usd)) {
envConfig.minBalance = Math.round(usd * 1000000);
}
}
// Commerce integration settings
const commerceUrl = process.env.COMMERCE_API_URL || process.env.COMMERCE_ENDPOINT;
if (commerceUrl) {
envConfig.commerce = {
enabled: true,
endpoint: commerceUrl,
token: process.env.COMMERCE_API_TOKEN || process.env.COMMERCE_TOKEN || '',
};
}
// Trial credit config (white-label overrides)
const trialUsd = process.env.HANZO_TRIAL_CREDIT_USD;
const trialExpiry = process.env.HANZO_TRIAL_EXPIRY_DAYS;
const trialModels = process.env.HANZO_TRIAL_ALLOWED_MODELS;
const trialRateLimit = process.env.HANZO_TRIAL_RATE_LIMIT;
if (trialUsd || trialExpiry || trialModels || trialRateLimit) {
envConfig.trial = Object.assign(Object.assign(Object.assign(Object.assign({}, (trialUsd ? { amountUsd: parseFloat(trialUsd) } : {})), (trialExpiry ? { expiryDays: parseInt(trialExpiry, 10) } : {})), (trialModels ? { allowedModels: trialModels.split(',').map((m) => m.trim()) } : {})), (trialRateLimit ? { rateLimit: parseInt(trialRateLimit, 10) } : {}));
}
// Paid credit config
const paidModels = process.env.HANZO_PAID_ALLOWED_MODELS;
if (paidModels) {
envConfig.paid = {
allowedModels: paidModels.split(',').map((m) => m.trim()),
};
}
if (!appConfig) {
return envConfig;
}
const appBalance = (_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig['balance']) !== null && _a !== void 0 ? _a : {};
return Object.assign(Object.assign(Object.assign({}, envConfig), appBalance), {
// Deep merge commerce/trial/paid from appConfig if present
commerce: Object.assign(Object.assign({}, envConfig.commerce), appBalance.commerce), trial: Object.assign(Object.assign({}, envConfig.trial), appBalance.trial), paid: Object.assign(Object.assign({}, envConfig.paid), appBalance.paid) });
}
/**
* Retrieves the transactions configuration object
* */
function getTransactionsConfig(appConfig) {
var _a;
const defaultConfig = { enabled: true };
if (!appConfig) {
return defaultConfig;
}
const transactionsConfig = (_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig['transactions']) !== null && _a !== void 0 ? _a : defaultConfig;
const balanceConfig = getBalanceConfig(appConfig);
// If balance is enabled but transactions are disabled, force transactions to be enabled
// and log a warning
if ((balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.enabled) && !transactionsConfig.enabled) {
dataSchemas.logger.warn('Configuration warning: transactions.enabled=false is incompatible with balance.enabled=true. ' +
'Transactions will be enabled to ensure balance tracking works correctly.');
return Object.assign(Object.assign({}, transactionsConfig), { enabled: true });
}
return transactionsConfig;
}
const getCustomEndpointConfig = ({ endpoint, appConfig, }) => {
var _a, _b;
if (!appConfig) {
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
}
const customEndpoints = (_b = (_a = appConfig.endpoints) === null || _a === void 0 ? void 0 : _a[librechatDataProvider.EModelEndpoint.custom]) !== null && _b !== void 0 ? _b : [];
return customEndpoints.find((endpointConfig) => librechatDataProvider.normalizeEndpointName(endpointConfig.name) === librechatDataProvider.normalizeEndpointName(endpoint));
};
function hasCustomUserVars(appConfig) {
const mcpServers = appConfig === null || appConfig === void 0 ? void 0 : appConfig.mcpConfig;
return Object.values(mcpServers !== null && mcpServers !== void 0 ? mcpServers : {}).some((server) => server === null || server === void 0 ? void 0 : server.customUserVars);
}
const hasValidAgent = (agent) => !!agent &&
(('id' in agent && !!agent.id) ||
('provider' in agent && 'model' in agent && !!agent.provider && !!agent.model));
const isDisabled = (config) => !config || config.disabled === true;
function loadMemoryConfig(config) {
var _a;
if (!config)
return undefined;
if (isDisabled(config))
return config;
if (!hasValidAgent(config.agent)) {
return Object.assign(Object.assign({}, config), { disabled: true });
}
const charLimit = (_a = librechatDataProvider.memorySchema.shape.charLimit.safeParse(config.charLimit).data) !== null && _a !== void 0 ? _a : 10000;
return Object.assign(Object.assign({}, config), { charLimit });
}
function isMemoryEnabled(config) {
if (isDisabled(config))
return false;
return hasValidAgent(config.agent);
}
/**
* Checks if a permission type has explicit configuration
*/
function hasExplicitConfig(interfaceConfig, permissionType) {
switch (permissionType) {
case librechatDataProvider.PermissionTypes.PROMPTS:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.prompts) !== undefined;
case librechatDataProvider.PermissionTypes.BOOKMARKS:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.bookmarks) !== undefined;
case librechatDataProvider.PermissionTypes.MEMORIES:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.memories) !== undefined;
case librechatDataProvider.PermissionTypes.MULTI_CONVO:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.multiConvo) !== undefined;
case librechatDataProvider.PermissionTypes.AGENTS:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.agents) !== undefined;
case librechatDataProvider.PermissionTypes.TEMPORARY_CHAT:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.temporaryChat) !== undefined;
case librechatDataProvider.PermissionTypes.RUN_CODE:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.runCode) !== undefined;
case librechatDataProvider.PermissionTypes.WEB_SEARCH:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.webSearch) !== undefined;
case librechatDataProvider.PermissionTypes.PEOPLE_PICKER:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.peoplePicker) !== undefined;
case librechatDataProvider.PermissionTypes.MARKETPLACE:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.marketplace) !== undefined;
case librechatDataProvider.PermissionTypes.FILE_SEARCH:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.fileSearch) !== undefined;
case librechatDataProvider.PermissionTypes.FILE_CITATIONS:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.fileCitations) !== undefined;
case librechatDataProvider.PermissionTypes.MCP_SERVERS:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.mcpServers) !== undefined;
case librechatDataProvider.PermissionTypes.REMOTE_AGENTS:
return (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.remoteAgents) !== undefined;
default:
return false;
}
}
function updateInterfacePermissions(_a) {
return __awaiter(this, arguments, void 0, function* ({ appConfig, getRoleByName, updateAccessPermissions, }) {
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58;
const loadedInterface = appConfig === null || appConfig === void 0 ? void 0 : appConfig.interfaceConfig;
if (!loadedInterface) {
return;
}
/** Configured values for interface object structure */
const interfaceConfig = (_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.config) === null || _b === void 0 ? void 0 : _b.interface;
const memoryConfig = (_c = appConfig === null || appConfig === void 0 ? void 0 : appConfig.config) === null || _c === void 0 ? void 0 : _c.memory;
const memoryEnabled = isMemoryEnabled(memoryConfig);
/** Check if memory is explicitly disabled (memory.disabled === true) */
const isMemoryExplicitlyDisabled = (memoryConfig === null || memoryConfig === void 0 ? void 0 : memoryConfig.disabled) === true;
/** Check if memory should be enabled (explicitly enabled or valid config) */
const shouldEnableMemory = (memoryConfig === null || memoryConfig === void 0 ? void 0 : memoryConfig.disabled) === false ||
(memoryConfig && memoryEnabled && memoryConfig.disabled === undefined);
/** Check if personalization is enabled (defaults to true if memory is configured and enabled) */
const isPersonalizationEnabled = memoryConfig && memoryEnabled && memoryConfig.personalize !== false;
/** Helper to get permission value with proper precedence */
const getPermissionValue = (configValue, roleDefault, schemaDefault) => {
if (configValue !== undefined)
return configValue;
if (roleDefault !== undefined)
return roleDefault;
return schemaDefault;
};
const defaults = librechatDataProvider.getConfigDefaults().interface;
// Permission precedence order:
// 1. Explicit user configuration (from librechat.yaml)
// 2. Role-specific defaults (from roleDefaults)
// 3. Interface schema defaults (from interfaceSchema.default())
for (const roleName of [librechatDataProvider.SystemRoles.USER, librechatDataProvider.SystemRoles.ADMIN]) {
const defaultPerms = (_d = librechatDataProvider.roleDefaults[roleName]) === null || _d === void 0 ? void 0 : _d.permissions;
const existingRole = yield getRoleByName(roleName);
const existingPermissions = existingRole === null || existingRole === void 0 ? void 0 : existingRole.permissions;
const permissionsToUpdate = {};
/**
* Helper to add permission if it should be updated
*/
const addPermissionIfNeeded = (permType, permissions) => {
var _a;
const permTypeExists = existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[permType];
const isExplicitlyConfigured = interfaceConfig && hasExplicitConfig(interfaceConfig, permType);
const isMemoryDisabled = permType === librechatDataProvider.PermissionTypes.MEMORIES && isMemoryExplicitlyDisabled;
const isMemoryReenabling = permType === librechatDataProvider.PermissionTypes.MEMORIES &&
shouldEnableMemory &&
((_a = existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.MEMORIES]) === null || _a === void 0 ? void 0 : _a[librechatDataProvider.Permissions.USE]) === false;
// Only update if: doesn't exist OR explicitly configured OR memory state change
if (!permTypeExists || isExplicitlyConfigured || isMemoryDisabled || isMemoryReenabling) {
permissionsToUpdate[permType] = permissions;
if (!permTypeExists) {
dataSchemas.logger.debug(`Role '${roleName}': Setting up default permissions for '${permType}'`);
}
else if (isExplicitlyConfigured) {
dataSchemas.logger.debug(`Role '${roleName}': Applying explicit config for '${permType}'`);
}
else if (isMemoryDisabled) {
dataSchemas.logger.debug(`Role '${roleName}': Disabling memories as memory.disabled is true`);
}
else if (isMemoryReenabling) {
dataSchemas.logger.debug(`Role '${roleName}': Re-enabling memories due to valid memory configuration`);
}
}
else {
dataSchemas.logger.debug(`Role '${roleName}': Preserving existing permissions for '${permType}'`);
}
};
const getConfigUse = (config) => typeof config === 'boolean' ? config : config === null || config === void 0 ? void 0 : config.use;
const getConfigCreate = (config) => typeof config === 'boolean' ? undefined : config === null || config === void 0 ? void 0 : config.create;
const getConfigShare = (config) => typeof config === 'boolean' ? undefined : config === null || config === void 0 ? void 0 : config.share;
const getConfigPublic = (config) => typeof config === 'boolean' ? undefined : config === null || config === void 0 ? void 0 : config.public;
// Get default values (for backward compat when config is boolean)
const promptsDefaultUse = typeof defaults.prompts === 'boolean' ? defaults.prompts : (_e = defaults.prompts) === null || _e === void 0 ? void 0 : _e.use;
const agentsDefaultUse = typeof defaults.agents === 'boolean' ? defaults.agents : (_f = defaults.agents) === null || _f === void 0 ? void 0 : _f.use;
const promptsDefaultCreate = typeof defaults.prompts === 'object' ? (_g = defaults.prompts) === null || _g === void 0 ? void 0 : _g.create : undefined;
const agentsDefaultCreate = typeof defaults.agents === 'object' ? (_h = defaults.agents) === null || _h === void 0 ? void 0 : _h.create : undefined;
const promptsDefaultShare = typeof defaults.prompts === 'object' ? (_j = defaults.prompts) === null || _j === void 0 ? void 0 : _j.share : undefined;
const agentsDefaultShare = typeof defaults.agents === 'object' ? (_k = defaults.agents) === null || _k === void 0 ? void 0 : _k.share : undefined;
const promptsDefaultPublic = typeof defaults.prompts === 'object' ? (_l = defaults.prompts) === null || _l === void 0 ? void 0 : _l.public : undefined;
const agentsDefaultPublic = typeof defaults.agents === 'object' ? (_m = defaults.agents) === null || _m === void 0 ? void 0 : _m.public : undefined;
const allPermissions = {
[librechatDataProvider.PermissionTypes.PROMPTS]: Object.assign(Object.assign({ [librechatDataProvider.Permissions.USE]: getPermissionValue(getConfigUse(loadedInterface.prompts), (_o = defaultPerms[librechatDataProvider.PermissionTypes.PROMPTS]) === null || _o === void 0 ? void 0 : _o[librechatDataProvider.Permissions.USE], promptsDefaultUse) }, ((typeof (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.prompts) === 'object' && 'create' in interfaceConfig.prompts) ||
!(existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.PROMPTS])
? {
[librechatDataProvider.Permissions.CREATE]: getPermissionValue(getConfigCreate(loadedInterface.prompts), (_p = defaultPerms[librechatDataProvider.PermissionTypes.PROMPTS]) === null || _p === void 0 ? void 0 : _p[librechatDataProvider.Permissions.CREATE], promptsDefaultCreate !== null && promptsDefaultCreate !== void 0 ? promptsDefaultCreate : true),
}
: {})), ((typeof (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.prompts) === 'object' &&
('share' in interfaceConfig.prompts || 'public' in interfaceConfig.prompts)) ||
!(existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.PROMPTS])
? {
[librechatDataProvider.Permissions.SHARE]: getPermissionValue(getConfigShare(loadedInterface.prompts), (_q = defaultPerms[librechatDataProvider.PermissionTypes.PROMPTS]) === null || _q === void 0 ? void 0 : _q[librechatDataProvider.Permissions.SHARE], promptsDefaultShare),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue(getConfigPublic(loadedInterface.prompts), (_r = defaultPerms[librechatDataProvider.PermissionTypes.PROMPTS]) === null || _r === void 0 ? void 0 : _r[librechatDataProvider.Permissions.SHARE_PUBLIC], promptsDefaultPublic),
}
: {})),
[librechatDataProvider.PermissionTypes.BOOKMARKS]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.bookmarks, (_s = defaultPerms[librechatDataProvider.PermissionTypes.BOOKMARKS]) === null || _s === void 0 ? void 0 : _s[librechatDataProvider.Permissions.USE], defaults.bookmarks),
},
[librechatDataProvider.PermissionTypes.MEMORIES]: Object.assign(Object.assign(Object.assign(Object.assign({ [librechatDataProvider.Permissions.USE]: (() => {
var _a;
if (isMemoryExplicitlyDisabled)
return false;
if (shouldEnableMemory)
return true;
return getPermissionValue(loadedInterface.memories, (_a = defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES]) === null || _a === void 0 ? void 0 : _a[librechatDataProvider.Permissions.USE], defaults.memories);
})() }, (((_t = defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES]) === null || _t === void 0 ? void 0 : _t[librechatDataProvider.Permissions.CREATE]) !== undefined && {
[librechatDataProvider.Permissions.CREATE]: isMemoryExplicitlyDisabled
? false
: defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES][librechatDataProvider.Permissions.CREATE],
})), (((_u = defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES]) === null || _u === void 0 ? void 0 : _u[librechatDataProvider.Permissions.READ]) !== undefined && {
[librechatDataProvider.Permissions.READ]: isMemoryExplicitlyDisabled
? false
: defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES][librechatDataProvider.Permissions.READ],
})), (((_v = defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES]) === null || _v === void 0 ? void 0 : _v[librechatDataProvider.Permissions.UPDATE]) !== undefined && {
[librechatDataProvider.Permissions.UPDATE]: isMemoryExplicitlyDisabled
? false
: defaultPerms[librechatDataProvider.PermissionTypes.MEMORIES][librechatDataProvider.Permissions.UPDATE],
})), { [librechatDataProvider.Permissions.OPT_OUT]: isMemoryExplicitlyDisabled
? false
: isPersonalizationEnabled || undefined }),
[librechatDataProvider.PermissionTypes.MULTI_CONVO]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.multiConvo, (_w = defaultPerms[librechatDataProvider.PermissionTypes.MULTI_CONVO]) === null || _w === void 0 ? void 0 : _w[librechatDataProvider.Permissions.USE], defaults.multiConvo),
},
[librechatDataProvider.PermissionTypes.AGENTS]: Object.assign(Object.assign({ [librechatDataProvider.Permissions.USE]: getPermissionValue(getConfigUse(loadedInterface.agents), (_x = defaultPerms[librechatDataProvider.PermissionTypes.AGENTS]) === null || _x === void 0 ? void 0 : _x[librechatDataProvider.Permissions.USE], agentsDefaultUse) }, ((typeof (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.agents) === 'object' && 'create' in interfaceConfig.agents) ||
!(existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.AGENTS])
? {
[librechatDataProvider.Permissions.CREATE]: getPermissionValue(getConfigCreate(loadedInterface.agents), (_y = defaultPerms[librechatDataProvider.PermissionTypes.AGENTS]) === null || _y === void 0 ? void 0 : _y[librechatDataProvider.Permissions.CREATE], agentsDefaultCreate !== null && agentsDefaultCreate !== void 0 ? agentsDefaultCreate : true),
}
: {})), ((typeof (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.agents) === 'object' &&
('share' in interfaceConfig.agents || 'public' in interfaceConfig.agents)) ||
!(existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.AGENTS])
? {
[librechatDataProvider.Permissions.SHARE]: getPermissionValue(getConfigShare(loadedInterface.agents), (_z = defaultPerms[librechatDataProvider.PermissionTypes.AGENTS]) === null || _z === void 0 ? void 0 : _z[librechatDataProvider.Permissions.SHARE], agentsDefaultShare),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue(getConfigPublic(loadedInterface.agents), (_0 = defaultPerms[librechatDataProvider.PermissionTypes.AGENTS]) === null || _0 === void 0 ? void 0 : _0[librechatDataProvider.Permissions.SHARE_PUBLIC], agentsDefaultPublic),
}
: {})),
[librechatDataProvider.PermissionTypes.TEMPORARY_CHAT]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.temporaryChat, (_1 = defaultPerms[librechatDataProvider.PermissionTypes.TEMPORARY_CHAT]) === null || _1 === void 0 ? void 0 : _1[librechatDataProvider.Permissions.USE], defaults.temporaryChat),
},
[librechatDataProvider.PermissionTypes.RUN_CODE]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.runCode, (_2 = defaultPerms[librechatDataProvider.PermissionTypes.RUN_CODE]) === null || _2 === void 0 ? void 0 : _2[librechatDataProvider.Permissions.USE], defaults.runCode),
},
[librechatDataProvider.PermissionTypes.WEB_SEARCH]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.webSearch, (_3 = defaultPerms[librechatDataProvider.PermissionTypes.WEB_SEARCH]) === null || _3 === void 0 ? void 0 : _3[librechatDataProvider.Permissions.USE], defaults.webSearch),
},
[librechatDataProvider.PermissionTypes.PEOPLE_PICKER]: {
[librechatDataProvider.Permissions.VIEW_USERS]: getPermissionValue((_4 = loadedInterface.peoplePicker) === null || _4 === void 0 ? void 0 : _4.users, (_5 = defaultPerms[librechatDataProvider.PermissionTypes.PEOPLE_PICKER]) === null || _5 === void 0 ? void 0 : _5[librechatDataProvider.Permissions.VIEW_USERS], (_6 = defaults.peoplePicker) === null || _6 === void 0 ? void 0 : _6.users),
[librechatDataProvider.Permissions.VIEW_GROUPS]: getPermissionValue((_7 = loadedInterface.peoplePicker) === null || _7 === void 0 ? void 0 : _7.groups, (_8 = defaultPerms[librechatDataProvider.PermissionTypes.PEOPLE_PICKER]) === null || _8 === void 0 ? void 0 : _8[librechatDataProvider.Permissions.VIEW_GROUPS], (_9 = defaults.peoplePicker) === null || _9 === void 0 ? void 0 : _9.groups),
[librechatDataProvider.Permissions.VIEW_ROLES]: getPermissionValue((_10 = loadedInterface.peoplePicker) === null || _10 === void 0 ? void 0 : _10.roles, (_11 = defaultPerms[librechatDataProvider.PermissionTypes.PEOPLE_PICKER]) === null || _11 === void 0 ? void 0 : _11[librechatDataProvider.Permissions.VIEW_ROLES], (_12 = defaults.peoplePicker) === null || _12 === void 0 ? void 0 : _12.roles),
},
[librechatDataProvider.PermissionTypes.MARKETPLACE]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue((_13 = loadedInterface.marketplace) === null || _13 === void 0 ? void 0 : _13.use, (_14 = defaultPerms[librechatDataProvider.PermissionTypes.MARKETPLACE]) === null || _14 === void 0 ? void 0 : _14[librechatDataProvider.Permissions.USE], (_15 = defaults.marketplace) === null || _15 === void 0 ? void 0 : _15.use),
},
[librechatDataProvider.PermissionTypes.FILE_SEARCH]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.fileSearch, (_16 = defaultPerms[librechatDataProvider.PermissionTypes.FILE_SEARCH]) === null || _16 === void 0 ? void 0 : _16[librechatDataProvider.Permissions.USE], defaults.fileSearch),
},
[librechatDataProvider.PermissionTypes.FILE_CITATIONS]: {
[librechatDataProvider.Permissions.USE]: getPermissionValue(loadedInterface.fileCitations, (_17 = defaultPerms[librechatDataProvider.PermissionTypes.FILE_CITATIONS]) === null || _17 === void 0 ? void 0 : _17[librechatDataProvider.Permissions.USE], defaults.fileCitations),
},
[librechatDataProvider.PermissionTypes.MCP_SERVERS]: Object.assign({ [librechatDataProvider.Permissions.USE]: getPermissionValue((_18 = loadedInterface.mcpServers) === null || _18 === void 0 ? void 0 : _18.use, (_19 = defaultPerms[librechatDataProvider.PermissionTypes.MCP_SERVERS]) === null || _19 === void 0 ? void 0 : _19[librechatDataProvider.Permissions.USE], (_20 = defaults.mcpServers) === null || _20 === void 0 ? void 0 : _20.use), [librechatDataProvider.Permissions.CREATE]: getPermissionValue((_21 = loadedInterface.mcpServers) === null || _21 === void 0 ? void 0 : _21.create, (_22 = defaultPerms[librechatDataProvider.PermissionTypes.MCP_SERVERS]) === null || _22 === void 0 ? void 0 : _22[librechatDataProvider.Permissions.CREATE], (_23 = defaults.mcpServers) === null || _23 === void 0 ? void 0 : _23.create) }, ((typeof (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.mcpServers) === 'object' &&
('share' in interfaceConfig.mcpServers || 'public' in interfaceConfig.mcpServers)) ||
!(existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.MCP_SERVERS])
? {
[librechatDataProvider.Permissions.SHARE]: getPermissionValue((_24 = loadedInterface.mcpServers) === null || _24 === void 0 ? void 0 : _24.share, (_25 = defaultPerms[librechatDataProvider.PermissionTypes.MCP_SERVERS]) === null || _25 === void 0 ? void 0 : _25[librechatDataProvider.Permissions.SHARE], (_26 = defaults.mcpServers) === null || _26 === void 0 ? void 0 : _26.share),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue((_27 = loadedInterface.mcpServers) === null || _27 === void 0 ? void 0 : _27.public, (_28 = defaultPerms[librechatDataProvider.PermissionTypes.MCP_SERVERS]) === null || _28 === void 0 ? void 0 : _28[librechatDataProvider.Permissions.SHARE_PUBLIC], (_29 = defaults.mcpServers) === null || _29 === void 0 ? void 0 : _29.public),
}
: {})),
[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]: Object.assign({ [librechatDataProvider.Permissions.USE]: getPermissionValue((_30 = loadedInterface.remoteAgents) === null || _30 === void 0 ? void 0 : _30.use, (_31 = defaultPerms[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]) === null || _31 === void 0 ? void 0 : _31[librechatDataProvider.Permissions.USE], (_32 = defaults.remoteAgents) === null || _32 === void 0 ? void 0 : _32.use), [librechatDataProvider.Permissions.CREATE]: getPermissionValue((_33 = loadedInterface.remoteAgents) === null || _33 === void 0 ? void 0 : _33.create, (_34 = defaultPerms[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]) === null || _34 === void 0 ? void 0 : _34[librechatDataProvider.Permissions.CREATE], (_35 = defaults.remoteAgents) === null || _35 === void 0 ? void 0 : _35.create) }, ((typeof (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.remoteAgents) === 'object' &&
('share' in interfaceConfig.remoteAgents || 'public' in interfaceConfig.remoteAgents)) ||
!(existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[librechatDataProvider.PermissionTypes.REMOTE_AGENTS])
? {
[librechatDataProvider.Permissions.SHARE]: getPermissionValue((_36 = loadedInterface.remoteAgents) === null || _36 === void 0 ? void 0 : _36.share, (_37 = defaultPerms[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]) === null || _37 === void 0 ? void 0 : _37[librechatDataProvider.Permissions.SHARE], (_38 = defaults.remoteAgents) === null || _38 === void 0 ? void 0 : _38.share),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue((_39 = loadedInterface.remoteAgents) === null || _39 === void 0 ? void 0 : _39.public, (_40 = defaultPerms[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]) === null || _40 === void 0 ? void 0 : _40[librechatDataProvider.Permissions.SHARE_PUBLIC], (_41 = defaults.remoteAgents) === null || _41 === void 0 ? void 0 : _41.public),
}
: {})),
};
// Check and add each permission type if needed
for (const [permType, permissions] of Object.entries(allPermissions)) {
addPermissionIfNeeded(permType, permissions);
}
const shareBackfill = [
[
librechatDataProvider.PermissionTypes.PROMPTS,
{
[librechatDataProvider.Permissions.SHARE]: getPermissionValue(getConfigShare(loadedInterface.prompts), (_42 = defaultPerms[librechatDataProvider.PermissionTypes.PROMPTS]) === null || _42 === void 0 ? void 0 : _42[librechatDataProvider.Permissions.SHARE], promptsDefaultShare),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue(getConfigPublic(loadedInterface.prompts), (_43 = defaultPerms[librechatDataProvider.PermissionTypes.PROMPTS]) === null || _43 === void 0 ? void 0 : _43[librechatDataProvider.Permissions.SHARE_PUBLIC], promptsDefaultPublic),
},
],
[
librechatDataProvider.PermissionTypes.AGENTS,
{
[librechatDataProvider.Permissions.SHARE]: getPermissionValue(getConfigShare(loadedInterface.agents), (_44 = defaultPerms[librechatDataProvider.PermissionTypes.AGENTS]) === null || _44 === void 0 ? void 0 : _44[librechatDataProvider.Permissions.SHARE], agentsDefaultShare),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue(getConfigPublic(loadedInterface.agents), (_45 = defaultPerms[librechatDataProvider.PermissionTypes.AGENTS]) === null || _45 === void 0 ? void 0 : _45[librechatDataProvider.Permissions.SHARE_PUBLIC], agentsDefaultPublic),
},
],
[
librechatDataProvider.PermissionTypes.MCP_SERVERS,
{
[librechatDataProvider.Permissions.SHARE]: getPermissionValue((_46 = loadedInterface.mcpServers) === null || _46 === void 0 ? void 0 : _46.share, (_47 = defaultPerms[librechatDataProvider.PermissionTypes.MCP_SERVERS]) === null || _47 === void 0 ? void 0 : _47[librechatDataProvider.Permissions.SHARE], (_48 = defaults.mcpServers) === null || _48 === void 0 ? void 0 : _48.share),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue((_49 = loadedInterface.mcpServers) === null || _49 === void 0 ? void 0 : _49.public, (_50 = defaultPerms[librechatDataProvider.PermissionTypes.MCP_SERVERS]) === null || _50 === void 0 ? void 0 : _50[librechatDataProvider.Permissions.SHARE_PUBLIC], (_51 = defaults.mcpServers) === null || _51 === void 0 ? void 0 : _51.public),
},
],
[
librechatDataProvider.PermissionTypes.REMOTE_AGENTS,
{
[librechatDataProvider.Permissions.SHARE]: getPermissionValue((_52 = loadedInterface.remoteAgents) === null || _52 === void 0 ? void 0 : _52.share, (_53 = defaultPerms[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]) === null || _53 === void 0 ? void 0 : _53[librechatDataProvider.Permissions.SHARE], (_54 = defaults.remoteAgents) === null || _54 === void 0 ? void 0 : _54.share),
[librechatDataProvider.Permissions.SHARE_PUBLIC]: getPermissionValue((_55 = loadedInterface.remoteAgents) === null || _55 === void 0 ? void 0 : _55.public, (_56 = defaultPerms[librechatDataProvider.PermissionTypes.REMOTE_AGENTS]) === null || _56 === void 0 ? void 0 : _56[librechatDataProvider.Permissions.SHARE_PUBLIC], (_57 = defaults.remoteAgents) === null || _57 === void 0 ? void 0 : _57.public),
},
],
];
for (const [permType, shareDefaults] of shareBackfill) {
const existingPerms = existingPermissions === null || existingPermissions === void 0 ? void 0 : existingPermissions[permType];
// Skip permission types that don't exist yet — addPermissionIfNeeded already handles those
if (!existingPerms) {
continue;
}
const missingFields = {};
for (const [field, value] of Object.entries(shareDefaults)) {
if (value !== undefined &&
existingPerms[field] === undefined &&
// Don't clobber a value already queued by addPermissionIfNeeded (e.g. explicit config)
((_58 = permissionsToUpdate[permType]) === null || _58 === void 0 ? void 0 : _58[field]) === undefined) {
missingFields[field] = value;
}
}
if (Object.keys(missingFields).length > 0) {
dataSchemas.logger.debug(`Role '${roleName}': Backfilling missing share fields for '${permType}': ${Object.keys(missingFields).join(', ')}`);
// Merge into any update already queued by addPermissionIfNeeded, or create a new entry
permissionsToUpdate[permType] = Object.assign(Object.assign({}, permissionsToUpdate[permType]), missingFields);
}
}
// Update permissions if any need updating
if (Object.keys(permissionsToUpdate).length > 0) {
yield updateAccessPermissions(roleName, permissionsToUpdate, existingRole);
}
}
});
}
let blobServiceClient = null;
let azureWarningLogged = false;
/**
* Initializes the Azure Blob Service client.
* This function establishes a connection by checking if a connection string is provided.
* If available, the connection string is used; otherwise, Managed Identity (via DefaultAzureCredential) is utilized.
* Note: Container creation (and its public access settings) is handled later in the CRUD functions.
* @returns The initialized client, or null if the required configuration is missing.
*/
const initializeAzureBlobService = () => __awaiter(void 0, void 0, void 0, function* () {
if (blobServiceClient) {
return blobServiceClient;
}
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
if (connectionString) {
const { BlobServiceClient } = yield import('@azure/storage-blob');
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
dataSchemas.logger.info('Azure Blob Service initialized using connection string');
}
else {
const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
if (!accountName) {
if (!azureWarningLogged) {
dataSchemas.logger.error('[initializeAzureBlobService] Azure Blob Service not initialized. Connection string missing and AZURE_STORAGE_ACCOUNT_NAME not provided.');
azureWarningLogged = true;
}
return null;
}
const url = `https://${accountName}.blob.core.windows.net`;
const credential = new identity.DefaultAzureCredential();
const { BlobServiceClient } = yield import('@azure/storage-blob');
blobServiceClient = new BlobServiceClient(url, credential);
dataSchemas.logger.info('Azure Blob Service initialized using Managed Identity');
}
return blobServiceClient;
});
/**
* Retrieves the Azure ContainerClient for the given container name.
* @param [containerName=process.env.AZURE_CONTAINER_NAME || 'files'] - The container name.
* @returns The Azure ContainerClient.
*/
const getAzureContainerClient = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (containerName = process.env.AZURE_CONTAINER_NAME || 'files') {
const serviceClient = yield initializeAzureBlobService();
return serviceClient ? serviceClient.getContainerClient(containerName) : null;
});
let firebaseInitCount = 0;
let firebaseApp = null;
const initializeFirebase = () => {
if (firebaseApp) {
return firebaseApp;
}
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
if (Object.values(firebaseConfig).some((value) => !value)) {
if (firebaseInitCount === 0) {
dataSchemas.logger.info('[Optional] Firebase CDN not initialized. To enable, set FIREBASE_API_KEY, FIREBASE_AUTH_DOMAIN, FIREBASE_PROJECT_ID, FIREBASE_STORAGE_BUCKET, FIREBASE_MESSAGING_SENDER_ID, and FIREBASE_APP_ID environment variables.');
}
firebaseInitCount++;
return null;
}
firebaseApp = firebase.initializeApp(firebaseConfig);
dataSchemas.logger.info('Firebase CDN initialized');
return firebaseApp;
};
const getFirebaseStorage = () => {
const app = initializeFirebase();
return app ? storage.getStorage(app) : null;
};
let s3 = null;
/**
* Initializes and returns an instance of the AWS S3 client.
*
* If AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are provided, they will be used.
* Otherwise, the AWS SDK's default credentials chain (including IRSA) is used.
*
* If AWS_ENDPOINT_URL is provided, it will be used as the endpoint.
*
* @returns An instance of S3Client if the region is provided; otherwise, null.
*/
const initializeS3 = () => {
if (s3) {
return s3;
}
const region = process.env.AWS_REGION;
if (!region) {
dataSchemas.logger.error('[initializeS3] AWS_REGION is not set. Cannot initialize S3.');
return null;
}
// Read the custom endpoint if provided.
const endpoint = process.env.AWS_ENDPOINT_URL;
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
const config = Object.assign(Object.assign({ region }, (endpoint ? { endpoint } : {})), (isEnabled(process.env.AWS_FORCE_PATH_STYLE) ? { forcePathStyle: true } : {}));
if (accessKeyId && secretAccessKey) {
s3 = new clientS3.S3Client(Object.assign(Object.assign({}, config), { credentials: { accessKeyId, secretAccessKey } }));
dataSchemas.logger.info('[initializeS3] S3 initialized with provided credentials.');
}
else {
// When using IRSA, credentials are automatically provided via the IAM Role attached to the ServiceAccount.
s3 = new clientS3.S3Client(config);
dataSchemas.logger.info('[initializeS3] S3 initialized using default credentials (IRSA).');
}
return s3;
};
/**
* Initializes file storage clients based on the configured file strategy.
* This should be called after loading the app configuration.
* @param {Object} options
* @param {AppConfig} options.appConfig - The application configuration
*/
function initializeFileStorage(appConfig) {
const { fileStrategy } = appConfig;
if (fileStrategy === librechatDataProvider.FileSources.firebase) {
initializeFirebase();
}
else if (fileStrategy === librechatDataProvider.FileSources.azure_blob) {
initializeAzureBlobService().catch((error) => {
dataSchemas.logger.error('Error initializing Azure Blob Service:', error);
});
}
else if (fileStrategy === librechatDataProvider.FileSources.s3) {
initializeS3();
}
}
/**
*
* @param rateLimits
*/
const handleRateLimits = (rateLimits) => {
if (!rateLimits) {
return;
}
const rateLimitKeys = {
fileUploads: librechatDataProvider.RateLimitPrefix.FILE_UPLOAD,
conversationsImport: librechatDataProvider.RateLimitPrefix.IMPORT,
tts: librechatDataProvider.RateLimitPrefix.TTS,
stt: librechatDataProvider.RateLimitPrefix.STT,
};
Object.entries(rateLimitKeys).forEach(([key, prefix]) => {
const rateLimit = rateLimits[key];
if (rateLimit) {
setRateLimitEnvVars(prefix, rateLimit);
}
});
};
/**
* Set environment variables for rate limit configurations
*
* @param prefix - Prefix for environment variable names
* @param rateLimit - Rate limit configuration object
*/
const setRateLimitEnvVars = (prefix, rateLimit) => {
const envVarsMapping = {
ipMax: `${prefix}_IP_MAX`,
ipWindowInMinutes: `${prefix}_IP_WINDOW`,
userMax: `${prefix}_USER_MAX`,
userWindowInMinutes: `${prefix}_USER_WINDOW`,
};
Object.entries(envVarsMapping).forEach(([key, envVar]) => {
const value = rateLimit[key];
if (value !== undefined) {
process.env[envVar] = value.toString();
}
});
};
const secretDefaults = {
JWT_SECRET: '16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef',
JWT_REFRESH_SECRET: 'eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418',
};
const deprecatedVariables = [
{
key: 'CHECK_BALANCE',
description: 'Please use the `balance` field in the `librechat.yaml` config file instead.\nMore info: https://librechat.ai/docs/configuration/librechat_yaml/object_structure/balance#overview',
},
{
key: 'START_BALANCE',
description: 'Please use the `balance` field in the `librechat.yaml` config file instead.\nMore info: https://librechat.ai/docs/configuration/librechat_yaml/object_structure/balance#overview',
},
{
key: 'GOOGLE_API_KEY',
description: 'Please use the `GOOGLE_SEARCH_API_KEY` environment variable for the Google Search Tool instead.',
},
];
const deprecatedAzureVariables = [
/* "related to" precedes description text */
{ key: 'AZURE_OPENAI_DEFAULT_MODEL', description: 'setting a default model' },
{ key: 'AZURE_OPENAI_MODELS', description: 'setting models' },
{
key: 'AZURE_USE_MODEL_AS_DEPLOYMENT_NAME',
description: 'using model names as deployment names',
},
{ key: 'AZURE_API_KEY', description: 'setting a single Azure API key' },
{ key: 'AZURE_OPENAI_API_INSTANCE_NAME', description: 'setting a single Azure instance name' },
{
key: 'AZURE_OPENAI_API_DEPLOYMENT_NAME',
description: 'setting a single Azure deployment name',
},
{ key: 'AZURE_OPENAI_API_VERSION', description: 'setting a single Azure API version' },
{
key: 'AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME',
description: 'setting a single Azure completions deployment name',
},
{
key: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME',
description: 'setting a single Azure embeddings deployment name',
},
{
key: 'PLUGINS_USE_AZURE',
description: 'using Azure for Plugins',
},
];
const conflictingAzureVariables = [
{
key: 'INSTANCE_NAME',
},
{
key: 'DEPLOYMENT_NAME',
},
];
/**
* Checks the password reset configuration for security issues.
*/
function checkPasswordReset() {
const emailEnabled = checkEmailConfig();
const passwordResetAllowed = isEnabled(process.env.ALLOW_PASSWORD_RESET);
if (!emailEnabled && passwordResetAllowed) {
dataSchemas.logger.warn(`❗❗❗
Password reset is enabled with \`ALLOW_PASSWORD_RESET\` but email service is not configured.
This setup is insecure as password reset links will be issued with a recognized email.
Please configure email service for secure password reset functionality.
https://www.librechat.ai/docs/configuration/authentication/email
❗❗❗`);
}
}
/**
* Checks environment variables for default secrets and deprecated variables.
* Logs warnings for any default secret values being used and for usage of deprecated variables.
* Advises on replacing default secrets and updating deprecated variables.
* @param {Object} options
* @param {Function} options.isEnabled - Function to check if a feature is enabled
* @param {Function} options.checkEmailConfig - Function to check email configuration
*/
function checkVariables() {
let hasDefaultSecrets = false;
for (const [key, value] of Object.entries(secretDefaults)) {
if (process.env[key] === value) {
dataSchemas.logger.warn(`Default value for ${key} is being used.`);
if (!hasDefaultSecrets) {
hasDefaultSecrets = true;
}
}
}
if (hasDefaultSecrets) {
dataSchemas.logger.info('Please replace any default secret values.');
dataSchemas.logger.info(`\u200B
For your convenience, use this tool to generate your own secret values:
https://www.librechat.ai/toolkit/creds_generator
\u200B`);
}
deprecatedVariables.forEach(({ key, description }) => {
if (process.env[key]) {
dataSchemas.logger.warn(`The \`${key}\` environment variable is deprecated. ${description}`);
}
});
checkPasswordReset();
}
/**
* Checks the health of auxiliary API's by attempting a fetch request to their respective `/health` endpoints.
* Logs information or warning based on the API's availability and response.
*/
function checkHealth() {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = yield fetch(`${process.env.RAG_API_URL}/health`);
if ((response === null || response === void 0 ? void 0 : response.ok) && (response === null || response === void 0 ? void 0 : response.status) === 200) {
dataSchemas.logger.info(`RAG API is running and reachable at ${process.env.RAG_API_URL}.`);
}
}
catch (_a) {
dataSchemas.logger.warn(`RAG API is either not running or not reachable at ${process.env.RAG_API_URL}, you may experience errors with file uploads.`);
}
});
}
/**
* Checks for the usage of deprecated and conflicting Azure variables.
* Logs warnings for any deprecated or conflicting environment variables found, indicating potential issues with `azureOpenAI` endpoint configuration.
*/
function checkAzureVariables() {
deprecatedAzureVariables.forEach(({ key, description }) => {
if (process.env[key]) {
dataSchemas.logger.warn(`The \`${key}\` environment variable (related to ${description}) should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you will experience conflicts and errors.`);
}
});
conflictingAzureVariables.forEach(({ key }) => {
if (process.env[key]) {
dataSchemas.logger.warn(`The \`${key}\` environment variable should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you may experience with the defined placeholders for mapping to the current model grouping using the same name.`);
}
});
}
function checkInterfaceConfig(appConfig) {
var _a, _b, _c, _d;
const interfaceConfig = appConfig.interfaceConfig;
let i = 0;
const logSettings = () => {
var _a;
// log interface object and model specs object (without list) for reference
dataSchemas.logger.warn(`\`interface\` settings:\n${JSON.stringify(interfaceConfig, null, 2)}`);
dataSchemas.logger.warn(`\`modelSpecs\` settings:\n${JSON.stringify(Object.assign(Object.assign({}, ((_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig.modelSpecs) !== null && _a !== void 0 ? _a : {})), { list: undefined }), null, 2)}`);
};
// warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs.
if (((_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig.modelSpecs) === null || _a === void 0 ? void 0 : _a.prioritize) && (interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.presets)) {
dataSchemas.logger.warn("Note: Prioritizing model specs can conflict with default presets if a default preset is set. It's recommended to disable presets from the interface or disable use of a default preset.");
if (i === 0)
i++;
}
// warn about config.modelSpecs.enforce if true and if any of these, endpointsMenu, modelSelect, presets, or parameters are enabled, that enforcing model specs can conflict with these options.
if (((_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.modelSpecs) === null || _b === void 0 ? void 0 : _b.enforce) &&
((interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.endpointsMenu) ||
(interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.modelSelect) ||
(interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.presets) ||
(interfaceConfig === null || interfaceConfig === void 0 ? void 0 : interfaceConfig.parameters))) {
dataSchemas.logger.warn("Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It's recommended to disable these options from the interface or disable enforcing model specs.");
if (i === 0)
i++;
}
// warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior.
if (((_c = appConfig === null || appConfig === void 0 ? void 0 : appConfig.modelSpecs) === null || _c === void 0 ? void 0 : _c.enforce) && !((_d = appConfig === null || appConfig === void 0 ? void 0 : appConfig.modelSpecs) === null || _d === void 0 ? void 0 : _d.prioritize)) {
dataSchemas.logger.warn("Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It's recommended to enable prioritizing model specs if enforcing them.");
if (i === 0)
i++;
}
if (i > 0) {
logSettings();
}
}
/**
* Performs startup checks including environment variable validation and health checks.
* This should be called during application startup before initializing services.
* @param [appConfig] - The application configuration object.
*/
function performStartupChecks(appConfig) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
checkVariables();
if ((_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _a === void 0 ? void 0 : _a.azureOpenAI) {
checkAzureVariables();
}
if (appConfig) {
checkInterfaceConfig(appConfig);
}
if (appConfig === null || appConfig === void 0 ? void 0 : appConfig.config) {
checkConfig(appConfig.config);
}
if ((_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.config) === null || _b === void 0 ? void 0 : _b.webSearch) {
checkWebSearchConfig(appConfig.config.webSearch);
}
if ((_c = appConfig === null || appConfig === void 0 ? void 0 : appConfig.config) === null || _c === void 0 ? void 0 : _c.rateLimits) {
handleRateLimits(appConfig.config.rateLimits);
}
yield checkHealth();
});
}
/**
* Performs basic checks on the loaded config object.
* @param config - The loaded custom configuration.
*/
function checkConfig(config) {
if (config.version !== librechatDataProvider.Constants.CONFIG_VERSION) {
dataSchemas.logger.info(`\nOutdated Config version: ${config.version}
Latest version: ${librechatDataProvider.Constants.CONFIG_VERSION}
Check out the Config changelogs for the latest options and features added.
https://www.librechat.ai/changelog\n\n`);
}
}
/**
* Checks web search configuration values to ensure they are environment variable references.
* Warns if actual API keys or URLs are used instead of environment variable references.
* Logs debug information for properly configured environment variable references.
* @param webSearchConfig - The loaded web search configuration object.
*/
function checkWebSearchConfig(webSearchConfig) {
if (!webSearchConfig) {
return;
}
dataSchemas.webSearchKeys.forEach((key) => {
const value = webSearchConfig[key];
if (typeof value === 'string') {
const varName = librechatDataProvider.extractVariableName(value);
if (varName) {
// This is a proper environment variable reference
const actualValue = process.env[varName];
if (actualValue) {
dataSchemas.logger.debug(`Web search ${key}: Using environment variable ${varName} with value set`);
}
else {
dataSchemas.logger.debug(`Web search ${key}: Using environment variable ${varName} (not set in environment, user provided value)`);
}
}
else {
// This is not an environment variable reference - warn user
dataSchemas.logger.warn(`❗ Web search configuration error: ${key} contains an actual value instead of an environment variable reference.
Current value: "${value.substring(0, 10)}..."
This is incorrect! You should use environment variable references in your librechat.yaml file, such as:
${key}: "\${YOUR_ENV_VAR_NAME}"
Then set the actual API key in your .env file or environment variables.
More info: https://www.librechat.ai/docs/configuration/librechat_yaml/web_search`);
}
}
});
}
/**
* @param email
* @param allowedDomains
*/
function isEmailDomainAllowed(email, allowedDomains) {
var _a;
/** If no domain restrictions are configured, allow all */
if (!allowedDomains || !Array.isArray(allowedDomains) || !allowedDomains.length) {
return true;
}
/** If restrictions exist, validate email format */
if (!email) {
return false;
}
const domain = (_a = email.split('@')[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
if (!domain) {
return false;
}
return allowedDomains.some((allowedDomain) => (allowedDomain === null || allowedDomain === void 0 ? void 0 : allowedDomain.toLowerCase()) === domain);
}
/** Checks if IPv4 octets fall within private, reserved, or link-local ranges */
function isPrivateIPv4(a, b, c) {
if (a === 127) {
return true;
}
if (a === 10) {
return true;
}
if (a === 172 && b >= 16 && b <= 31) {
return true;
}
if (a === 192 && b === 168) {
return true;
}
if (a === 169 && b === 254) {
return true;
}
if (a === 0 && b === 0 && c === 0) {
return true;
}
return false;
}
/**
* Checks if an IP address belongs to a private, reserved, or link-local range.
* Handles IPv4, IPv6, and IPv4-mapped IPv6 addresses (::ffff:A.B.C.D).
*/
function isPrivateIP(ip) {
const normalized = ip.toLowerCase().trim();
const mappedMatch = normalized.match(/^::ffff:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
if (mappedMatch) {
const [, a, b, c] = mappedMatch.map(Number);
return isPrivateIPv4(a, b, c);
}
const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
if (ipv4Match) {
const [, a, b, c] = ipv4Match.map(Number);
return isPrivateIPv4(a, b, c);
}
const ipv6 = normalized.replace(/^\[|\]$/g, '');
if (ipv6 === '::1' ||
ipv6 === '::' ||
ipv6.startsWith('fc') ||
ipv6.startsWith('fd') ||
ipv6.startsWith('fe80')) {
return true;
}
return false;
}
/**
* Resolves a hostname via DNS and checks if any resolved address is a private/reserved IP.
* Detects DNS-based SSRF bypasses (e.g., nip.io wildcard DNS, attacker-controlled nameservers).
* Fails open: returns false if DNS resolution fails, since hostname-only checks still apply
* and the actual HTTP request would also fail.
*/
function resolveHostnameSSRF(hostname) {
return __awaiter(this, void 0, void 0, function* () {
const normalizedHost = hostname.toLowerCase().trim();
if (/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(normalizedHost)) {
return false;
}
const ipv6Check = normalizedHost.replace(/^\[|\]$/g, '');
if (ipv6Check.includes(':')) {
return false;
}
try {
const addresses = yield promises$1.lookup(hostname, { all: true });
return addresses.some((entry) => isPrivateIP(entry.address));
}
catch (_a) {
return false;
}
});
}
/**
* SSRF Protection: Checks if a hostname/IP is a potentially dangerous internal target.
* Blocks private IPs, localhost, cloud metadata IPs, and common internal hostnames.
* @param hostname - The hostname or IP to check
* @returns true if the target is blocked (SSRF risk), false if safe
*/
function isSSRFTarget(hostname) {
const normalizedHost = hostname.toLowerCase().trim();
if (normalizedHost === 'localhost' ||
normalizedHost === 'localhost.localdomain' ||
normalizedHost.endsWith('.localhost')) {
return true;
}
if (isPrivateIP(normalizedHost)) {
return true;
}
// Block common internal Docker/Kubernetes service names
const internalHostnames = [
'rag_api',
'rag-api',
'api',
'redis',
'mongodb',
'mongo',
'postgres',
'postgresql',
'mysql',
'database',
'db',
'elasticsearch',
'kibana',
'grafana',
'prometheus',
'rabbitmq',
'kafka',
'zookeeper',
'consul',
'vault',
'etcd',
'minio',
'internal',
'backend',
'metadata', // Common metadata service name
];
if (internalHostnames.includes(normalizedHost)) {
return true;
}
// Block .internal and .local TLDs (common in internal networks)
if (normalizedHost.endsWith('.internal') || normalizedHost.endsWith('.local')) {
return true;
}
return false;
}
/** Checks if a string starts with a recognized protocol prefix */
function hasRecognizedProtocol(domain) {
return (domain.startsWith('http://') ||
domain.startsWith('https://') ||
domain.startsWith('ws://') ||
domain.startsWith('wss://'));
}
/**
* Parses a domain specification into its components.
* Supports formats:
* - `example.com` (any protocol, any port)
* - `https://example.com` (https only, any port)
* - `https://example.com:443` (https only, port 443)
* - `wss://ws.example.com` (secure WebSocket only)
* - `*.example.com` (wildcard subdomain)
* @param domain - Domain specification string
* @returns ParsedDomainSpec or null if invalid
*/
function parseDomainSpec(domain) {
try {
let normalizedDomain = domain.toLowerCase().trim();
// Early return for obviously invalid formats (protocol-only strings)
const emptyProtocols = ['http://', 'https://', 'ws://', 'wss://'];
if (emptyProtocols.includes(normalizedDomain)) {
return null;
}
// Check for wildcard prefix before parsing
const isWildcard = normalizedDomain.startsWith('*.');
// Check if it has a recognized protocol (http, https, ws, wss)
const hasProtocol = hasRecognizedProtocol(normalizedDomain);
// Check if port was explicitly specified (e.g., :443, :8080)
// Need to check before URL parsing because URL normalizes default ports
const portMatch = normalizedDomain.match(/:(\d+)(\/|$|\?)/);
const explicitPort = portMatch !== null;
const explicitPortValue = portMatch ? portMatch[1] : null;
// If no protocol, add one temporarily for URL parsing
if (!hasProtocol) {
normalizedDomain = `https://${normalizedDomain}`;
}
const url = new URL(normalizedDomain);
// Additional validation that hostname isn't just protocol
if (!url.hostname || emptyProtocols.some((p) => url.hostname === p.replace('://', ''))) {
return null;
}
const hostname = url.hostname.replace(/^www\./i, '');
return {
hostname,
protocol: hasProtocol ? url.protocol : null,
// Use the explicitly specified port, or null if no port was specified
port: explicitPort ? explicitPortValue : null,
explicitPort,
isWildcard,
};
}
catch (_a) {
return null;
}
}
/**
* Checks if hostname matches an allowed pattern (supports wildcards).
*/
function hostnameMatches(inputHostname, allowedSpec) {
if (allowedSpec.isWildcard) {
// Extract base domain from wildcard (e.g., "*.example.com" -> "example.com")
const baseDomain = allowedSpec.hostname.replace(/^\*\./, '');
return inputHostname === baseDomain || inputHostname.endsWith(`.${baseDomain}`);
}
return inputHostname === allowedSpec.hostname;
}
/** Protocol sets for different use cases */
const HTTP_PROTOCOLS = ['http:', 'https:'];
const MCP_PROTOCOLS = ['http:', 'https:', 'ws:', 'wss:'];
/**
* Core domain validation logic with configurable protocol support.
* SECURITY: When no allowedDomains is configured, blocks SSRF-prone targets.
* @param domain - The domain to check (can include protocol/port)
* @param allowedDomains - List of allowed domain patterns
* @param supportedProtocols - Protocols to accept (others are rejected)
*/
function isDomainAllowedCore(domain, allowedDomains, supportedProtocols) {
return __awaiter(this, void 0, void 0, function* () {
const inputSpec = parseDomainSpec(domain);
if (!inputSpec) {
return false;
}
// SECURITY: Reject unsupported protocols (e.g., WebSocket for OpenAPI Actions)
if (inputSpec.protocol !== null && !supportedProtocols.includes(inputSpec.protocol)) {
return false;
}
/** If no domain restrictions configured, block SSRF targets but allow all else */
if (!Array.isArray(allowedDomains) || !allowedDomains.length) {
/** SECURITY: Block SSRF-prone targets when no allowlist is configured */
if (isSSRFTarget(inputSpec.hostname)) {
return false;
}
/** SECURITY: Resolve hostname and block if it points to a private/reserved IP */
if (yield resolveHostnameSSRF(inputSpec.hostname)) {
return false;
}
return true;
}
/** When allowedDomains is configured, check against the list with protocol/port matching */
for (const allowedDomain of allowedDomains) {
const allowedSpec = parseDomainSpec(allowedDomain);
if (!allowedSpec) {
continue;
}
// Skip allowedDomains with unsupported protocols for this context
if (allowedSpec.protocol !== null && !supportedProtocols.includes(allowedSpec.protocol)) {
continue;
}
// Check hostname match (with wildcard support)
if (!hostnameMatches(inputSpec.hostname, allowedSpec)) {
continue;
}
// If allowedSpec has protocol restriction, input must match
if (allowedSpec.protocol !== null) {
// Input must have protocol specified to match a protocol-restricted rule
if (inputSpec.protocol === null || inputSpec.protocol !== allowedSpec.protocol) {
continue;
}
}
// If allowedSpec has explicit port restriction, input must have matching explicit port
if (allowedSpec.explicitPort) {
// Input must also have an explicit port that matches
if (!inputSpec.explicitPort || inputSpec.port !== allowedSpec.port) {
continue;
}
}
// All specified constraints matched
return true;
}
return false;
});
}
/**
* Validates domain for OpenAPI Agent Actions (HTTP/HTTPS only).
* SECURITY: WebSocket protocols are NOT allowed per OpenAPI specification.
* @param domain - The domain to check (can include protocol/port)
* @param allowedDomains - List of allowed domain patterns
*/
function isActionDomainAllowed(domain, allowedDomains) {
return __awaiter(this, void 0, void 0, function* () {
if (!domain || typeof domain !== 'string') {
return false;
}
return isDomainAllowedCore(domain, allowedDomains, HTTP_PROTOCOLS);
});
}
/**
* Extracts full domain spec (protocol://hostname:port) from MCP server config URL.
* Returns the full origin for proper protocol/port matching against allowedDomains.
* Returns null for stdio transports (no URL) or invalid URLs.
* @param config - MCP server configuration (accepts any config with optional url field)
*/
function extractMCPServerDomain(config) {
const url = config.url;
// Stdio transports don't have URLs - always allowed
if (!url || typeof url !== 'string') {
return null;
}
try {
const parsedUrl = new URL(url);
// Return full origin (protocol://hostname:port) for proper domain validation
// This allows admins to restrict by protocol/port in allowedDomains
return parsedUrl.origin;
}
catch (_a) {
return null;
}
}
/**
* Validates MCP server domain against allowedDomains.
* Supports HTTP, HTTPS, WS, and WSS protocols (per MCP specification).
* Stdio transports (no URL) are always allowed.
* @param config - MCP server configuration with optional url field
* @param allowedDomains - List of allowed domains (with wildcard support)
*/
function isMCPDomainAllowed(config, allowedDomains) {
return __awaiter(this, void 0, void 0, function* () {
const domain = extractMCPServerDomain(config);
// Stdio transports don't have domains - always allowed
if (!domain) {
return true;
}
// Use MCP_PROTOCOLS (HTTP/HTTPS/WS/WSS) for MCP server validation
return isDomainAllowedCore(domain, allowedDomains, MCP_PROTOCOLS);
});
}
/**
* Finds or migrates a user for OpenID authentication
* @returns user object (with migration fields if needed), error message, and whether migration is needed
*/
function findOpenIDUser(_a) {
return __awaiter(this, arguments, void 0, function* ({ openidId, findUser, email, idOnTheSource, strategyName = 'openid', }) {
const primaryConditions = [];
if (openidId && typeof openidId === 'string') {
primaryConditions.push({ openidId });
}
if (idOnTheSource && typeof idOnTheSource === 'string') {
primaryConditions.push({ idOnTheSource });
}
let user = null;
if (primaryConditions.length > 0) {
user = yield findUser({ $or: primaryConditions });
}
if (!user && email) {
user = yield findUser({ email });
dataSchemas.logger.warn(`[${strategyName}] user ${user ? 'found' : 'not found'} with email: ${email} for openidId: ${openidId}`);
// If user found by email, check if they're allowed to use OpenID provider
if (user && user.provider && user.provider !== 'openid') {
dataSchemas.logger.warn(`[${strategyName}] Attempted OpenID login by user ${user.email}, was registered with "${user.provider}" provider`);
return { user: null, error: librechatDataProvider.ErrorTypes.AUTH_FAILED, migration: false };
}
// If user found by email but doesn't have openidId, prepare for migration
if (user && !user.openidId) {
dataSchemas.logger.info(`[${strategyName}] Preparing user ${user.email} for migration to OpenID with sub: ${openidId}`);
user.provider = 'openid';
user.openidId = openidId;
return { user, error: null, migration: true };
}
}
return { user, error: null, migration: false };
});
}
/** Default admin panel URL for local development */
const DEFAULT_ADMIN_PANEL_URL = 'http://localhost:3000';
/**
* Gets the admin panel URL from environment or falls back to default.
* @returns The admin panel URL
*/
function getAdminPanelUrl() {
return process.env.ADMIN_PANEL_URL || DEFAULT_ADMIN_PANEL_URL;
}
/**
* Serializes user data for the exchange cache.
* @param user - The authenticated user object
* @returns Serialized user data for admin panel
*/
function serializeUserForExchange(user) {
var _a, _b, _c;
const userId = String(user._id);
return {
_id: userId,
id: userId,
email: user.email,
name: (_a = user.name) !== null && _a !== void 0 ? _a : '',
username: (_b = user.username) !== null && _b !== void 0 ? _b : '',
role: (_c = user.role) !== null && _c !== void 0 ? _c : 'USER',
avatar: user.avatar,
provider: user.provider,
openidId: user.openidId,
};
}
/**
* Generates an exchange code and stores user data for admin panel OAuth flow.
* @param cache - The Keyv cache instance for storing exchange data
* @param user - The authenticated user object
* @param token - The JWT access token
* @param refreshToken - Optional refresh token for OpenID users
* @returns The generated exchange code
*/
function generateAdminExchangeCode(cache, user, token, refreshToken) {
return __awaiter(this, void 0, void 0, function* () {
const exchangeCode = crypto$2.randomBytes(32).toString('hex');
const data = {
userId: String(user._id),
user: serializeUserForExchange(user),
token,
refreshToken,
};
yield cache.set(exchangeCode, data);
dataSchemas.logger.info(`[adminExchange] Generated exchange code for user: ${user.email}`);
return exchangeCode;
});
}
/**
* Exchanges an authorization code for tokens and user data.
* The code is deleted immediately after retrieval (one-time use).
* @param cache - The Keyv cache instance for retrieving exchange data
* @param code - The authorization code to exchange
* @returns The exchange response with token, refreshToken, and user data, or null if invalid/expired
*/
function exchangeAdminCode(cache, code) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const data = (yield cache.get(code));
/** Delete immediately - one-time use */
yield cache.delete(code);
if (!data) {
dataSchemas.logger.warn('[adminExchange] Invalid or expired authorization code');
return null;
}
dataSchemas.logger.info(`[adminExchange] Exchanged code for user: ${(_a = data.user) === null || _a === void 0 ? void 0 : _a.email}`);
return {
token: data.token,
refreshToken: data.refreshToken,
user: data.user,
};
});
}
/**
* Checks if the redirect URI is for the admin panel (cross-origin).
* Uses proper URL parsing to compare origins, handling edge cases where
* both URLs might share the same prefix (e.g., localhost:3000 vs localhost:3001).
*
* @param redirectUri - The redirect URI to check.
* @param adminPanelUrl - The admin panel URL (defaults to ADMIN_PANEL_URL env var)
* @param domainClient - The main client domain
* @returns True if redirecting to admin panel (different origin from main client).
*/
function isAdminPanelRedirect(redirectUri, adminPanelUrl, domainClient) {
try {
const redirectOrigin = new URL(redirectUri).origin;
const adminOrigin = new URL(adminPanelUrl).origin;
const clientOrigin = new URL(domainClient).origin;
/** Redirect is for admin panel if it matches admin origin but not main client origin */
return redirectOrigin === adminOrigin && redirectOrigin !== clientOrigin;
}
catch (_a) {
/** If URL parsing fails, fall back to simple string comparison */
return redirectUri.startsWith(adminPanelUrl) && !redirectUri.startsWith(domainClient);
}
}
/** DNS lookup wrapper that blocks resolution to private/reserved IP addresses */
const ssrfSafeLookup = (hostname, options, callback) => {
dns.lookup(hostname, options, (err, address, family) => {
if (err) {
callback(err, '', 0);
return;
}
if (typeof address === 'string' && isPrivateIP(address)) {
const ssrfError = Object.assign(new Error(`SSRF protection: ${hostname} resolved to blocked address ${address}`), { code: 'ESSRF' });
callback(ssrfError, address, family);
return;
}
callback(null, address, family);
});
};
/** Patches an agent instance to inject SSRF-safe DNS lookup at connect time */
function withSSRFProtection(agent) {
const internal = agent;
const origCreate = internal.createConnection.bind(agent);
internal.createConnection = (options, oncreate) => {
options.lookup = ssrfSafeLookup;
return origCreate(options, oncreate);
};
return agent;
}
/**
* Creates HTTP and HTTPS agents that block TCP connections to private/reserved IP addresses.
* Provides TOCTOU-safe SSRF protection by validating the resolved IP at connect time,
* preventing DNS rebinding attacks where a hostname resolves to a public IP during
* pre-validation but to a private IP when the actual connection is made.
*/
function createSSRFSafeAgents() {
return {
httpAgent: withSSRFProtection(new http.Agent()),
httpsAgent: withSSRFProtection(new https.Agent()),
};
}
/**
* Returns undici-compatible `connect` options with SSRF-safe DNS lookup.
* Pass the result as the `connect` property when constructing an undici `Agent`.
*/
function createSSRFSafeUndiciConnect() {
return { lookup: ssrfSafeLookup };
}
class AgentApiKeyService {
constructor(deps) {
this.deps = deps;
}
validateApiKey(apiKey) {
return __awaiter(this, void 0, void 0, function* () {
return this.deps.validateAgentApiKey(apiKey);
});
}
createApiKey(params) {
return __awaiter(this, void 0, void 0, function* () {
return this.deps.createAgentApiKey(params);
});
}
listApiKeys(userId) {
return __awaiter(this, void 0, void 0, function* () {
return this.deps.listAgentApiKeys(userId);
});
}
deleteApiKey(keyId, userId) {
return __awaiter(this, void 0, void 0, function* () {
return this.deps.deleteAgentApiKey(keyId, userId);
});
}
getApiKeyById(keyId, userId) {
return __awaiter(this, void 0, void 0, function* () {
return this.deps.getAgentApiKeyById(keyId, userId);
});
}
getUserFromApiKey(apiKey) {
return __awaiter(this, void 0, void 0, function* () {
const keyValidation = yield this.validateApiKey(apiKey);
if (!keyValidation) {
return null;
}
return this.deps.findUser({ _id: keyValidation.userId });
});
}
}
function createApiKeyServiceDependencies(mongoose) {
const methods = dataSchemas.createMethods(mongoose);
return {
validateAgentApiKey: methods.validateAgentApiKey,
createAgentApiKey: methods.createAgentApiKey,
listAgentApiKeys: methods.listAgentApiKeys,
deleteAgentApiKey: methods.deleteAgentApiKey,
getAgentApiKeyById: methods.getAgentApiKeyById,
findUser: methods.findUser,
};
}
/** AGENT owners automatically have full REMOTE_AGENT permissions */
function getRemoteAgentPermissions(deps, userId, role, resourceId) {
return __awaiter(this, void 0, void 0, function* () {
const agentPerms = yield deps.getEffectivePermissions({
userId,
role,
resourceType: librechatDataProvider.ResourceType.AGENT,
resourceId,
});
if (librechatDataProvider.hasPermissions(agentPerms, librechatDataProvider.PermissionBits.SHARE)) {
return librechatDataProvider.PermissionBits.VIEW | librechatDataProvider.PermissionBits.EDIT | librechatDataProvider.PermissionBits.DELETE | librechatDataProvider.PermissionBits.SHARE;
}
return deps.getEffectivePermissions({
userId,
role,
resourceType: librechatDataProvider.ResourceType.REMOTE_AGENT,
resourceId,
});
});
}
function checkRemoteAgentAccess(params) {
return __awaiter(this, void 0, void 0, function* () {
const { userId, role, agentId, getAgent, getEffectivePermissions } = params;
const agent = yield getAgent({ id: agentId });
if (!agent) {
return { hasAccess: false, permissions: 0, agent: null };
}
const permissions = yield getRemoteAgentPermissions({ getEffectivePermissions }, userId, role, agent._id);
const hasAccess = librechatDataProvider.hasPermissions(permissions, librechatDataProvider.PermissionBits.VIEW);
return { hasAccess, permissions, agent };
});
}
function createRequireApiKeyAuth(deps) {
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: {
message: 'Missing or invalid Authorization header. Expected: Bearer <api_key>',
type: 'invalid_request_error',
code: 'missing_api_key',
},
});
}
const apiKey = authHeader.slice(7);
if (!apiKey || apiKey.trim() === '') {
return res.status(401).json({
error: {
message: 'API key is required',
type: 'invalid_request_error',
code: 'missing_api_key',
},
});
}
try {
const keyValidation = yield deps.validateAgentApiKey(apiKey);
if (!keyValidation) {
return res.status(401).json({
error: {
message: 'Invalid API key',
type: 'invalid_request_error',
code: 'invalid_api_key',
},
});
}
const user = yield deps.findUser({ _id: keyValidation.userId });
if (!user) {
return res.status(401).json({
error: {
message: 'User not found for this API key',
type: 'invalid_request_error',
code: 'invalid_api_key',
},
});
}
user.id = user._id.toString();
req.user = user;
req.apiKeyId = keyValidation.keyId;
next();
}
catch (error) {
dataSchemas.logger.error('[requireApiKeyAuth] Error validating API key:', error);
return res.status(500).json({
error: {
message: 'Internal server error during authentication',
type: 'server_error',
code: 'internal_error',
},
});
}
});
}
function createCheckRemoteAgentAccess(deps) {
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
const agentId = ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model) || ((_b = req.params) === null || _b === void 0 ? void 0 : _b.model);
if (!agentId) {
return res.status(400).json({
error: {
message: 'Model (agent ID) is required',
type: 'invalid_request_error',
code: 'missing_model',
},
});
}
try {
const agent = yield deps.getAgent({ id: agentId });
if (!agent) {
return res.status(404).json({
error: {
message: `Agent not found: ${agentId}`,
type: 'invalid_request_error',
code: 'model_not_found',
},
});
}
const userId = ((_c = req.user) === null || _c === void 0 ? void 0 : _c.id) || '';
const permissions = yield getRemoteAgentPermissions(deps, userId, (_d = req.user) === null || _d === void 0 ? void 0 : _d.role, agent._id);
if (!librechatDataProvider.hasPermissions(permissions, librechatDataProvider.PermissionBits.VIEW)) {
return res.status(403).json({
error: {
message: `No remote access to agent: ${agentId}`,
type: 'permission_error',
code: 'access_denied',
},
});
}
req.agent = agent;
req.agentPermissions = permissions;
next();
}
catch (error) {
dataSchemas.logger.error('[checkRemoteAgentAccess] Error checking agent access:', error);
return res.status(500).json({
error: {
message: 'Internal server error while checking agent access',
type: 'server_error',
code: 'internal_error',
},
});
}
});
}
function createApiKeyHandlers(deps) {
function createApiKey(req, res) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const { name, expiresAt } = req.body;
if (!name || typeof name !== 'string' || name.trim() === '') {
return res.status(400).json({
error: 'API key name is required',
});
}
const result = yield deps.createAgentApiKey({
userId: ((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) || '',
name: name.trim(),
expiresAt: expiresAt ? new Date(expiresAt) : null,
});
res.status(201).json({
id: result.id,
name: result.name,
key: result.key,
keyPrefix: result.keyPrefix,
createdAt: result.createdAt,
expiresAt: result.expiresAt,
});
}
catch (error) {
dataSchemas.logger.error('[createApiKey] Error creating API key:', error);
res.status(500).json({ error: 'Failed to create API key' });
}
});
}
function listApiKeys(req, res) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const keys = yield deps.listAgentApiKeys(((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) || '');
res.status(200).json({ keys });
}
catch (error) {
dataSchemas.logger.error('[listApiKeys] Error listing API keys:', error);
res.status(500).json({ error: 'Failed to list API keys' });
}
});
}
function getApiKey(req, res) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const key = yield deps.getAgentApiKeyById(req.params.id, ((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) || '');
if (!key) {
return res.status(404).json({ error: 'API key not found' });
}
res.status(200).json(key);
}
catch (error) {
dataSchemas.logger.error('[getApiKey] Error getting API key:', error);
res.status(500).json({ error: 'Failed to get API key' });
}
});
}
function deleteApiKey(req, res) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const deleted = yield deps.deleteAgentApiKey(req.params.id, ((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) || '');
if (!deleted) {
return res.status(404).json({ error: 'API key not found' });
}
res.status(204).send();
}
catch (error) {
dataSchemas.logger.error('[deleteApiKey] Error deleting API key:', error);
res.status(500).json({ error: 'Failed to delete API key' });
}
});
}
return {
createApiKey,
listApiKeys,
getApiKey,
deleteApiKey,
};
}
/** Enriches REMOTE_AGENT principals with implicit AGENT owners */
function enrichRemoteAgentPrincipals(deps, resourceId, principals) {
return __awaiter(this, void 0, void 0, function* () {
const { AclEntry } = deps;
const resourceObjectId = typeof resourceId === 'string' && /^[a-f\d]{24}$/i.test(resourceId)
? deps.AclEntry.base.Types.ObjectId.createFromHexString(resourceId)
: resourceId;
const agentOwnerEntries = yield AclEntry.aggregate([
{
$match: {
resourceType: librechatDataProvider.ResourceType.AGENT,
resourceId: resourceObjectId,
principalType: librechatDataProvider.PrincipalType.USER,
permBits: { $bitsAllSet: librechatDataProvider.PermissionBits.SHARE },
},
},
{
$lookup: {
from: 'users',
localField: 'principalId',
foreignField: '_id',
as: 'userInfo',
},
},
{
$project: {
principalId: 1,
userInfo: { $arrayElemAt: ['$userInfo', 0] },
},
},
]);
const enrichedPrincipals = [...principals];
const entriesToBackfill = [];
for (const entry of agentOwnerEntries) {
if (!entry.userInfo) {
continue;
}
const alreadyIncluded = enrichedPrincipals.some((p) => p.type === librechatDataProvider.PrincipalType.USER && p.id === entry.principalId.toString());
if (!alreadyIncluded) {
enrichedPrincipals.unshift({
type: librechatDataProvider.PrincipalType.USER,
id: entry.userInfo._id.toString(),
name: entry.userInfo.name || entry.userInfo.username,
email: entry.userInfo.email,
avatar: entry.userInfo.avatar,
source: 'local',
idOnTheSource: entry.userInfo.idOnTheSource || entry.userInfo._id.toString(),
accessRoleId: librechatDataProvider.AccessRoleIds.REMOTE_AGENT_OWNER,
isImplicit: true,
});
entriesToBackfill.push(entry.principalId);
}
}
return { principals: enrichedPrincipals, entriesToBackfill };
});
}
/** Backfills REMOTE_AGENT ACL entries for AGENT owners (fire-and-forget) */
function backfillRemoteAgentPermissions(deps, resourceId, entriesToBackfill) {
if (entriesToBackfill.length === 0) {
return;
}
const { AclEntry, AccessRole, logger } = deps;
const resourceObjectId = typeof resourceId === 'string' && /^[a-f\d]{24}$/i.test(resourceId)
? AclEntry.base.Types.ObjectId.createFromHexString(resourceId)
: resourceId;
AccessRole.findOne({ accessRoleId: librechatDataProvider.AccessRoleIds.REMOTE_AGENT_OWNER })
.lean()
.then((role) => {
if (!role) {
logger.error('[backfillRemoteAgentPermissions] REMOTE_AGENT_OWNER role not found');
return;
}
const bulkOps = entriesToBackfill.map((principalId) => ({
updateOne: {
filter: {
principalType: librechatDataProvider.PrincipalType.USER,
principalId,
resourceType: librechatDataProvider.ResourceType.REMOTE_AGENT,
resourceId: resourceObjectId,
},
update: {
$setOnInsert: {
principalType: librechatDataProvider.PrincipalType.USER,
principalId,
principalModel: 'User',
resourceType: librechatDataProvider.ResourceType.REMOTE_AGENT,
resourceId: resourceObjectId,
permBits: role.permBits,
roleId: role._id,
grantedBy: principalId,
grantedAt: new Date(),
},
},
upsert: true,
},
}));
return AclEntry.bulkWrite(bulkOps, { ordered: false });
})
.catch((err) => {
logger.error('[backfillRemoteAgentPermissions] Failed to backfill:', err);
});
}
/**
* MCP-specific error classes
*/
const MCPErrorCodes = {
DOMAIN_NOT_ALLOWED: 'MCP_DOMAIN_NOT_ALLOWED',
INSPECTION_FAILED: 'MCP_INSPECTION_FAILED',
};
/**
* Custom error for MCP domain restriction violations.
* Thrown when a user attempts to connect to an MCP server whose domain is not in the allowlist.
*/
class MCPDomainNotAllowedError extends Error {
constructor(domain) {
super(`Domain "${domain}" is not allowed`);
this.code = MCPErrorCodes.DOMAIN_NOT_ALLOWED;
this.statusCode = 403;
this.name = 'MCPDomainNotAllowedError';
this.domain = domain;
Object.setPrototypeOf(this, MCPDomainNotAllowedError.prototype);
}
}
/**
* Custom error for MCP server inspection failures.
* Thrown when attempting to connect/inspect an MCP server fails.
*/
class MCPInspectionFailedError extends Error {
constructor(serverName, cause) {
super(`Failed to connect to MCP server "${serverName}"`);
this.code = MCPErrorCodes.INSPECTION_FAILED;
this.statusCode = 400;
this.name = 'MCPInspectionFailedError';
this.serverName = serverName;
if (cause) {
this.cause = cause;
}
Object.setPrototypeOf(this, MCPInspectionFailedError.prototype);
}
}
/**
* Type guard to check if an error is an MCPDomainNotAllowedError
*/
function isMCPDomainNotAllowedError(error) {
return error instanceof MCPDomainNotAllowedError;
}
/**
* Type guard to check if an error is an MCPInspectionFailedError
*/
function isMCPInspectionFailedError(error) {
return error instanceof MCPInspectionFailedError;
}
var _a$2, _b$2;
// To ensure that different deployments do not interfere with each other's cache, we use a prefix for the Redis keys.
// This prefix is usually the deployment ID, which is often passed to the container or pod as an env var.
// Set REDIS_KEY_PREFIX_VAR to the env var that contains the deployment ID.
const REDIS_KEY_PREFIX_VAR = process.env.REDIS_KEY_PREFIX_VAR;
const REDIS_KEY_PREFIX = process.env.REDIS_KEY_PREFIX;
if (REDIS_KEY_PREFIX_VAR && REDIS_KEY_PREFIX) {
throw new Error('Only either REDIS_KEY_PREFIX_VAR or REDIS_KEY_PREFIX can be set.');
}
const USE_REDIS$1 = isEnabled(process.env.USE_REDIS);
if (USE_REDIS$1 && !process.env.REDIS_URI) {
throw new Error('USE_REDIS is enabled but REDIS_URI is not set.');
}
// USE_REDIS_STREAMS controls whether Redis is used for resumable stream job storage.
// Defaults to true if USE_REDIS is enabled but USE_REDIS_STREAMS is not explicitly set.
// Set to 'false' to use in-memory storage for streams while keeping Redis for other caches.
const USE_REDIS_STREAMS = process.env.USE_REDIS_STREAMS !== undefined
? isEnabled(process.env.USE_REDIS_STREAMS)
: USE_REDIS$1;
// Comma-separated list of cache namespaces that should be forced to use in-memory storage
// even when Redis is enabled. This allows selective performance optimization for specific caches.
// Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container.
// Set to empty string to force all namespaces through Redis.
const FORCED_IN_MEMORY_CACHE_NAMESPACES = process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES !== undefined
? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',')
.map((key) => key.trim())
.filter(Boolean)
: [librechatDataProvider.CacheKeys.CONFIG_STORE, librechatDataProvider.CacheKeys.APP_CONFIG];
// Validate against CacheKeys enum
if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) {
const validKeys = Object.values(librechatDataProvider.CacheKeys);
const invalidKeys = FORCED_IN_MEMORY_CACHE_NAMESPACES.filter((key) => !validKeys.includes(key));
if (invalidKeys.length > 0) {
throw new Error(`Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: ${invalidKeys.join(', ')}. Valid keys: ${validKeys.join(', ')}`);
}
}
/** Helper function to safely read Redis CA certificate from file
* @returns {string|null} The contents of the CA certificate file, or null if not set or on error
*/
const getRedisCA = () => {
const caPath = process.env.REDIS_CA;
if (!caPath) {
return null;
}
try {
if (fs.existsSync(caPath)) {
return fs.readFileSync(caPath, 'utf8');
}
else {
dataSchemas.logger.warn(`Redis CA certificate file not found: ${caPath}`);
return null;
}
}
catch (error) {
dataSchemas.logger.error(`Failed to read Redis CA certificate file '${caPath}':`, error);
return null;
}
};
const cacheConfig = {
FORCED_IN_MEMORY_CACHE_NAMESPACES,
USE_REDIS: USE_REDIS$1,
USE_REDIS_STREAMS,
REDIS_URI: process.env.REDIS_URI,
REDIS_USERNAME: process.env.REDIS_USERNAME,
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CA: getRedisCA(),
REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR !== null && REDIS_KEY_PREFIX_VAR !== void 0 ? REDIS_KEY_PREFIX_VAR : ''] || REDIS_KEY_PREFIX || '',
GLOBAL_PREFIX_SEPARATOR: '::',
REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40),
REDIS_PING_INTERVAL: math(process.env.REDIS_PING_INTERVAL, 0),
/** Max delay between reconnection attempts in ms */
REDIS_RETRY_MAX_DELAY: math(process.env.REDIS_RETRY_MAX_DELAY, 3000),
/** Max number of reconnection attempts (0 = infinite) */
REDIS_RETRY_MAX_ATTEMPTS: math(process.env.REDIS_RETRY_MAX_ATTEMPTS, 10),
/** Connection timeout in ms */
REDIS_CONNECT_TIMEOUT: math(process.env.REDIS_CONNECT_TIMEOUT, 10000),
/** Queue commands when disconnected */
REDIS_ENABLE_OFFLINE_QUEUE: isEnabled((_a$2 = process.env.REDIS_ENABLE_OFFLINE_QUEUE) !== null && _a$2 !== void 0 ? _a$2 : 'true'),
/** flag to modify redis connection by adding dnsLookup this is required when connecting to elasticache for ioredis
* see "Special Note: Aws Elasticache Clusters with TLS" on this webpage: https://www.npmjs.com/package/ioredis **/
REDIS_USE_ALTERNATIVE_DNS_LOOKUP: isEnabled(process.env.REDIS_USE_ALTERNATIVE_DNS_LOOKUP),
/** Enable redis cluster without the need of multiple URIs */
USE_REDIS_CLUSTER: isEnabled((_b$2 = process.env.USE_REDIS_CLUSTER) !== null && _b$2 !== void 0 ? _b$2 : 'false'),
CI: isEnabled(process.env.CI),
DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE),
BAN_DURATION: math(process.env.BAN_DURATION, 7200000), // 2 hours
/**
* Number of keys to delete in each batch during Redis DEL operations.
* In cluster mode, keys are deleted individually in parallel chunks to avoid CROSSSLOT errors.
* In single-node mode, keys are deleted in batches using DEL with arrays.
* Lower values reduce memory usage but increase number of Redis calls.
* @default 1000
*/
REDIS_DELETE_CHUNK_SIZE: math(process.env.REDIS_DELETE_CHUNK_SIZE, 1000),
/**
* Number of keys to update in each batch during Redis SET operations.
* In cluster mode, keys are updated individually in parallel chunks to avoid CROSSSLOT errors.
* In single-node mode, keys are updated in batches using transactions (multi/exec).
* Lower values reduce memory usage but increase number of Redis calls.
* @default 1000
*/
REDIS_UPDATE_CHUNK_SIZE: math(process.env.REDIS_UPDATE_CHUNK_SIZE, 1000),
/**
* COUNT hint for Redis SCAN operations when scanning keys by pattern.
* This is a hint to Redis about how many keys to scan in each iteration.
* Higher values can reduce round trips but increase memory usage and latency per call.
* Note: Redis may return more or fewer keys than this count depending on internal heuristics.
* @default 1000
*/
REDIS_SCAN_COUNT: math(process.env.REDIS_SCAN_COUNT, 1000),
/**
* TTL in milliseconds for MCP registry read-through cache.
* This cache reduces redundant lookups within a single request flow.
* @default 5000 (5 seconds)
*/
MCP_REGISTRY_CACHE_TTL: math(process.env.MCP_REGISTRY_CACHE_TTL, 5000),
};
var _a$1, _b$1, _c$1;
const urls = ((_a$1 = cacheConfig.REDIS_URI) === null || _a$1 === void 0 ? void 0 : _a$1.split(',').map((uri) => new URL(uri))) || [];
const username = ((_b$1 = urls === null || urls === void 0 ? void 0 : urls[0]) === null || _b$1 === void 0 ? void 0 : _b$1.username) || cacheConfig.REDIS_USERNAME;
const password = ((_c$1 = urls === null || urls === void 0 ? void 0 : urls[0]) === null || _c$1 === void 0 ? void 0 : _c$1.password) || cacheConfig.REDIS_PASSWORD;
const ca = cacheConfig.REDIS_CA;
exports.ioredisClient = null;
if (cacheConfig.USE_REDIS) {
const redisOptions = {
username: username,
password: password,
tls: ca ? { ca } : undefined,
keyPrefix: `${cacheConfig.REDIS_KEY_PREFIX}${cacheConfig.GLOBAL_PREFIX_SEPARATOR}`,
maxListeners: cacheConfig.REDIS_MAX_LISTENERS,
retryStrategy: (times) => {
if (cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 &&
times > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS) {
dataSchemas.logger.error(`ioredis giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`);
return null;
}
const base = Math.min(Math.pow(2, times) * 50, cacheConfig.REDIS_RETRY_MAX_DELAY);
const jitter = Math.floor(Math.random() * Math.min(base, 1000));
const delay = Math.min(base + jitter, cacheConfig.REDIS_RETRY_MAX_DELAY);
dataSchemas.logger.info(`ioredis reconnecting... attempt ${times}, delay ${delay}ms`);
return delay;
},
reconnectOnError: (err) => {
const targetError = 'READONLY';
if (err.message.includes(targetError)) {
dataSchemas.logger.warn('ioredis reconnecting due to READONLY error');
return 2; // Return retry delay instead of boolean
}
return false;
},
enableOfflineQueue: cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE,
connectTimeout: cacheConfig.REDIS_CONNECT_TIMEOUT,
maxRetriesPerRequest: 3,
};
exports.ioredisClient =
urls.length === 1 && !cacheConfig.USE_REDIS_CLUSTER
? new IoRedis(cacheConfig.REDIS_URI, redisOptions)
: new IoRedis.Cluster(urls.map((url) => ({ host: url.hostname, port: parseInt(url.port, 10) || 6379 })), Object.assign(Object.assign({}, (cacheConfig.REDIS_USE_ALTERNATIVE_DNS_LOOKUP
? {
dnsLookup: (address, callback) => callback(null, address),
}
: {})), { redisOptions, clusterRetryStrategy: (times) => {
if (cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 &&
times > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS) {
dataSchemas.logger.error(`ioredis cluster giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`);
return null;
}
const base = Math.min(Math.pow(2, times) * 100, cacheConfig.REDIS_RETRY_MAX_DELAY);
const jitter = Math.floor(Math.random() * Math.min(base, 1000));
const delay = Math.min(base + jitter, cacheConfig.REDIS_RETRY_MAX_DELAY);
dataSchemas.logger.info(`ioredis cluster reconnecting... attempt ${times}, delay ${delay}ms`);
return delay;
}, enableOfflineQueue: cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE }));
exports.ioredisClient.on('error', (err) => {
dataSchemas.logger.error('ioredis client error:', err);
});
exports.ioredisClient.on('connect', () => {
dataSchemas.logger.info('ioredis client connected');
});
exports.ioredisClient.on('ready', () => {
dataSchemas.logger.info('ioredis client ready');
});
exports.ioredisClient.on('reconnecting', (delay) => {
dataSchemas.logger.info(`ioredis client reconnecting in ${delay}ms`);
});
exports.ioredisClient.on('close', () => {
dataSchemas.logger.warn('ioredis client connection closed');
});
/** Ping Interval to keep the Redis server connection alive (if enabled) */
let pingInterval = null;
const clearPingInterval = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
};
if (cacheConfig.REDIS_PING_INTERVAL > 0) {
pingInterval = setInterval(() => {
if (exports.ioredisClient && exports.ioredisClient.status === 'ready') {
exports.ioredisClient.ping().catch((err) => {
dataSchemas.logger.error('ioredis ping failed:', err);
});
}
}, cacheConfig.REDIS_PING_INTERVAL * 1000);
exports.ioredisClient.on('close', clearPingInterval);
exports.ioredisClient.on('end', clearPingInterval);
}
}
exports.keyvRedisClient = null;
exports.keyvRedisClientReady = null;
if (cacheConfig.USE_REDIS) {
/**
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
* The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js
*/
const redisOptions = Object.assign({ username,
password, socket: {
tls: ca != null,
ca,
connectTimeout: cacheConfig.REDIS_CONNECT_TIMEOUT,
reconnectStrategy: (retries) => {
if (cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 &&
retries > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS) {
dataSchemas.logger.error(`@keyv/redis client giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`);
return new Error('Max reconnection attempts reached');
}
const base = Math.min(Math.pow(2, retries) * 100, cacheConfig.REDIS_RETRY_MAX_DELAY);
const jitter = Math.floor(Math.random() * Math.min(base, 1000));
const delay = Math.min(base + jitter, cacheConfig.REDIS_RETRY_MAX_DELAY);
dataSchemas.logger.info(`@keyv/redis reconnecting... attempt ${retries}, delay ${delay}ms`);
return delay;
},
}, disableOfflineQueue: !cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE }, (cacheConfig.REDIS_PING_INTERVAL > 0
? { pingInterval: cacheConfig.REDIS_PING_INTERVAL * 1000 }
: {}));
exports.keyvRedisClient =
urls.length === 1 && !cacheConfig.USE_REDIS_CLUSTER
? redis.createClient(Object.assign({ url: cacheConfig.REDIS_URI }, redisOptions))
: redis.createCluster({
rootNodes: urls.map((url) => ({ url: url.href })),
defaults: redisOptions,
});
// Add scanIterator method to cluster client for API consistency with standalone client
if (!('scanIterator' in exports.keyvRedisClient)) {
const clusterClient = exports.keyvRedisClient;
exports.keyvRedisClient.scanIterator = function (options) {
return __asyncGenerator(this, arguments, function* () {
var _a, e_1, _b, _c;
const masters = clusterClient.masters;
for (const master of masters) {
const nodeClient = yield __await(clusterClient.nodeClient(master));
try {
for (var _d = true, _e = (e_1 = void 0, __asyncValues(nodeClient.scanIterator(options))), _f; _f = yield __await(_e.next()), _a = _f.done, !_a; _d = true) {
_c = _f.value;
_d = false;
const key = _c;
yield yield __await(key);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield __await(_b.call(_e));
}
finally { if (e_1) throw e_1.error; }
}
}
});
};
}
exports.keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
exports.keyvRedisClient.on('error', (err) => {
dataSchemas.logger.error('@keyv/redis client error:', err);
});
exports.keyvRedisClient.on('connect', () => {
dataSchemas.logger.info('@keyv/redis client connected');
});
exports.keyvRedisClient.on('ready', () => {
dataSchemas.logger.info('@keyv/redis client ready');
});
exports.keyvRedisClient.on('reconnecting', () => {
dataSchemas.logger.info('@keyv/redis client reconnecting...');
});
exports.keyvRedisClient.on('disconnect', () => {
dataSchemas.logger.warn('@keyv/redis client disconnected');
});
// Start connection immediately
exports.keyvRedisClientReady = exports.keyvRedisClient.connect();
exports.keyvRedisClientReady.catch((err) => {
dataSchemas.logger.error('@keyv/redis initial connection failed:', err);
throw err;
});
}
const logFile = new keyvFile.KeyvFile({ filename: './data/logs.json' }).setMaxListeners(20);
const violationFile = new keyvFile.KeyvFile({ filename: './data/violations.json' }).setMaxListeners(20);
const storeMap = new Map();
class KeyvMongoCustom extends events.EventEmitter {
constructor(options = {}) {
super();
this.opts = Object.assign({ url: 'mongodb://127.0.0.1:27017', collection: 'keyv' }, options);
this.ttlSupport = false;
}
// Helper to access the store WITHOUT storing a promise on the instance
_getClient() {
return __awaiter(this, void 0, void 0, function* () {
const storeKey = `${this.opts.collection}:${this.opts.useGridFS ? 'gridfs' : 'collection'}`;
// If we already have the store initialized, return it directly
if (storeMap.has(storeKey)) {
return storeMap.get(storeKey);
}
// Check mongoose connection state
if (mongoose.connection.readyState !== 1) {
throw new Error('Mongoose connection not ready. Ensure connectDb() is called first.');
}
try {
const db = mongoose.connection.db;
if (!db) {
throw new Error('MongoDB database not available');
}
let client;
if (this.opts.useGridFS) {
const bucket = new mongodb.GridFSBucket(db, {
readPreference: this.opts.readPreference,
bucketName: this.opts.collection,
});
const store = db.collection(`${this.opts.collection}.files`);
client = { bucket, store, db };
}
else {
const collection = this.opts.collection || 'keyv';
const store = db.collection(collection);
client = { store, db };
}
storeMap.set(storeKey, client);
return client;
}
catch (error) {
this.emit('error', error);
throw error;
}
});
}
get(key) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
if (this.opts.useGridFS && this.isGridFSClient(client)) {
yield client.store.updateOne({
filename: key,
}, {
$set: {
'metadata.lastAccessed': new Date(),
},
});
const stream = client.bucket.openDownloadStreamByName(key);
return new Promise((resolve) => {
const resp = [];
stream.on('error', () => {
resolve(undefined);
});
stream.on('end', () => {
const data = Buffer.concat(resp).toString('utf8');
resolve(data);
});
stream.on('data', (chunk) => {
resp.push(chunk);
});
});
}
const document = yield client.store.findOne({ key: { $eq: key } });
if (!document) {
return undefined;
}
return document.value;
});
}
getMany(keys) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
if (this.opts.useGridFS) {
const promises = [];
for (const key of keys) {
promises.push(this.get(key));
}
const values = yield Promise.allSettled(promises);
const data = [];
for (const value of values) {
data.push(value.status === 'fulfilled' ? value.value : undefined);
}
return data;
}
const values = yield client.store
.find({ key: { $in: keys } })
.project({ _id: 0, value: 1, key: 1 })
.toArray();
const results = [...keys];
let i = 0;
for (const key of keys) {
const rowIndex = values.findIndex((row) => row.key === key);
results[i] = rowIndex > -1 ? values[rowIndex].value : undefined;
i++;
}
return results;
});
}
set(key, value, ttl) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
const expiresAt = typeof ttl === 'number' ? new Date(Date.now() + ttl) : null;
if (this.opts.useGridFS && this.isGridFSClient(client)) {
const stream = client.bucket.openUploadStream(key, {
metadata: {
expiresAt,
lastAccessed: new Date(),
},
});
return new Promise((resolve) => {
stream.on('finish', () => {
resolve(stream);
});
stream.end(value);
});
}
yield client.store.updateOne({ key: { $eq: key } }, { $set: { key, value, expiresAt } }, { upsert: true });
});
}
delete(key) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
if (this.opts.useGridFS && this.isGridFSClient(client)) {
try {
const bucket = new mongodb.GridFSBucket(client.db, {
bucketName: this.opts.collection,
});
const files = yield bucket.find({ filename: key }).toArray();
if (files.length > 0) {
yield client.bucket.delete(files[0]._id);
}
return true;
}
catch (_a) {
return false;
}
}
const object = yield client.store.deleteOne({ key: { $eq: key } });
return object.deletedCount > 0;
});
}
deleteMany(keys) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
if (this.opts.useGridFS && this.isGridFSClient(client)) {
const bucket = new mongodb.GridFSBucket(client.db, {
bucketName: this.opts.collection,
});
const files = yield bucket.find({ filename: { $in: keys } }).toArray();
if (files.length === 0) {
return false;
}
yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () { return client.bucket.delete(file._id); })));
return true;
}
const object = yield client.store.deleteMany({ key: { $in: keys } });
return object.deletedCount > 0;
});
}
clear() {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
if (this.opts.useGridFS && this.isGridFSClient(client)) {
try {
yield client.bucket.drop();
}
catch (error) {
// Throw error if not "namespace not found" error
const errorCode = error instanceof Error && 'code' in error ? error.code : undefined;
if (errorCode !== 26) {
throw error;
}
}
}
yield client.store.deleteMany({
key: { $regex: this.namespace ? `^${this.namespace}:*` : '' },
});
});
}
has(key) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this._getClient();
const filter = { [this.opts.useGridFS ? 'filename' : 'key']: { $eq: key } };
const document = yield client.store.countDocuments(filter, { limit: 1 });
return document !== 0;
});
}
// No-op disconnect
disconnect() {
return __awaiter(this, void 0, void 0, function* () {
// This is a no-op since we don't want to close the shared mongoose connection
return true;
});
}
isGridFSClient(client) {
return client.bucket != null;
}
}
const keyvMongo = new KeyvMongoCustom({
collection: 'logs',
});
keyvMongo.on('error', (err) => dataSchemas.logger.error('KeyvMongo connection error:', err));
/**
* Efficiently deletes multiple Redis keys with support for both cluster and single-node modes.
*
* - Cluster mode: Deletes keys in parallel chunks to avoid CROSSSLOT errors
* - Single-node mode: Uses batch DEL commands for efficiency
*
* @param client - Redis client (node or cluster)
* @param keys - Array of keys to delete
* @param chunkSize - Optional chunk size (defaults to REDIS_DELETE_CHUNK_SIZE config)
* @returns Number of keys deleted
*
* @example
* ```typescript
* const deletedCount = await batchDeleteKeys(keyvRedisClient, ['key1', 'key2', 'key3']);
* console.log(`Deleted ${deletedCount} keys`);
* ```
*/
function batchDeleteKeys(client, keys, chunkSize) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
if (keys.length === 0) {
return 0;
}
const size = chunkSize !== null && chunkSize !== void 0 ? chunkSize : cacheConfig.REDIS_DELETE_CHUNK_SIZE;
const mode = cacheConfig.USE_REDIS_CLUSTER ? 'cluster' : 'single-node';
const deletePromises = [];
if (cacheConfig.USE_REDIS_CLUSTER) {
// Cluster mode: Delete each key individually in parallel chunks to avoid CROSSSLOT errors
for (let i = 0; i < keys.length; i += size) {
const chunk = keys.slice(i, i + size);
deletePromises.push(Promise.all(chunk.map((key) => client.del(key))));
}
}
else {
// Single-node mode: Batch delete chunks using DEL with array
for (let i = 0; i < keys.length; i += size) {
const chunk = keys.slice(i, i + size);
deletePromises.push(client.del(chunk));
}
}
const results = yield Promise.all(deletePromises);
// Sum up deleted counts (cluster returns array of individual counts, single-node returns total)
const deletedCount = results.reduce((sum, count) => {
if (Array.isArray(count)) {
return sum + count.reduce((a, b) => a + b, 0);
}
return sum + count;
}, 0);
// Performance monitoring
const duration = Date.now() - startTime;
const batchCount = deletePromises.length;
if (duration > 1000) {
dataSchemas.logger.warn(`[Redis][batchDeleteKeys] Slow operation - Duration: ${duration}ms, Mode: ${mode}, Keys: ${keys.length}, Deleted: ${deletedCount}, Batches: ${batchCount}, Chunk size: ${size}`);
}
else {
dataSchemas.logger.debug(`[Redis][batchDeleteKeys] Duration: ${duration}ms, Mode: ${mode}, Keys: ${keys.length}, Deleted: ${deletedCount}, Batches: ${batchCount}`);
}
return deletedCount;
});
}
/**
* Scans Redis for keys matching a pattern and collects them into an array.
* Uses Redis SCAN to avoid blocking the server.
*
* @param client - Redis client (node or cluster) with scanIterator support
* @param pattern - Pattern to match keys (e.g., 'user:*', 'session:*:active')
* @param count - Optional SCAN COUNT hint (defaults to REDIS_SCAN_COUNT config)
* @returns Array of matching keys
*
* @example
* ```typescript
* const userKeys = await scanKeys(keyvRedisClient, 'user:*');
* const sessionKeys = await scanKeys(keyvRedisClient, 'session:*:active', 500);
* ```
*/
function scanKeys(client, pattern, count) {
return __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
const startTime = Date.now();
const keys = [];
// Type guard to check if client has scanIterator
if (!('scanIterator' in client)) {
throw new Error('Redis client does not support scanIterator');
}
const scanCount = count !== null && count !== void 0 ? count : cacheConfig.REDIS_SCAN_COUNT;
try {
for (var _d = true, _e = __asyncValues(client.scanIterator({
MATCH: pattern,
COUNT: scanCount,
})), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
_c = _f.value;
_d = false;
const key = _c;
keys.push(key);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
// Performance monitoring
const duration = Date.now() - startTime;
if (duration > 1000) {
dataSchemas.logger.warn(`[Redis][scanKeys] Slow operation - Duration: ${duration}ms, Pattern: "${pattern}", Keys found: ${keys.length}, Scan count: ${scanCount}`);
}
else {
dataSchemas.logger.debug(`[Redis][scanKeys] Duration: ${duration}ms, Pattern: "${pattern}", Keys found: ${keys.length}`);
}
return keys;
});
}
/**
* @keyv/redis exports its default class in a non-standard way:
* module.exports = { default: KeyvRedis, ... } instead of module.exports = KeyvRedis
* This breaks ES6 imports when the module is marked as external in rollup.
* We must use require() to access the .default property directly.
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports
const KeyvRedis = require('@keyv/redis').default;
/**
* Creates a cache instance using Redis or a fallback store. Suitable for general caching needs.
* @param namespace - The cache namespace.
* @param ttl - Time to live for cache entries.
* @param fallbackStore - Optional fallback store if Redis is not used.
* @returns Cache instance.
*/
const standardCache = (namespace, ttl, fallbackStore) => {
var _a;
if (exports.keyvRedisClient && !((_a = cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES) === null || _a === void 0 ? void 0 : _a.includes(namespace))) {
try {
const keyvRedis = new KeyvRedis(exports.keyvRedisClient);
const cache = new keyv.Keyv(keyvRedis, { namespace, ttl });
keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX;
keyvRedis.keyPrefixSeparator = cacheConfig.GLOBAL_PREFIX_SEPARATOR;
cache.on('error', (err) => {
dataSchemas.logger.error(`Cache error in namespace ${namespace}:`, err);
});
// Override clear() to handle namespace-aware deletion
// The default Keyv clear() doesn't respect namespace due to the workaround above
// Workaround for issue #10487 https://github.com/danny-avila/LibreChat/issues/10487
cache.clear = () => __awaiter(void 0, void 0, void 0, function* () {
// Type-safe check for Redis client with scanIterator support
if (!exports.keyvRedisClient || !('scanIterator' in exports.keyvRedisClient)) {
dataSchemas.logger.warn(`Cannot clear namespace ${namespace}: Redis scanIterator not available`);
return;
}
// Build pattern: globalPrefix::namespace:* or namespace:*
const pattern = cacheConfig.REDIS_KEY_PREFIX
? `${cacheConfig.REDIS_KEY_PREFIX}${cacheConfig.GLOBAL_PREFIX_SEPARATOR}${namespace}:*`
: `${namespace}:*`;
// Use utility functions for efficient scan and parallel deletion
const keysToDelete = yield scanKeys(exports.keyvRedisClient, pattern);
if (keysToDelete.length === 0) {
return;
}
yield batchDeleteKeys(exports.keyvRedisClient, keysToDelete);
dataSchemas.logger.debug(`Cleared ${keysToDelete.length} keys from namespace ${namespace}`);
});
return cache;
}
catch (err) {
dataSchemas.logger.error(`Failed to create Redis cache for namespace ${namespace}:`, err);
throw err;
}
}
if (fallbackStore) {
return new keyv.Keyv({ store: fallbackStore, namespace, ttl });
}
return new keyv.Keyv({ namespace, ttl });
};
/**
* Creates a cache instance for storing violation data.
* Uses a file-based fallback store if Redis is not enabled.
* @param namespace - The cache namespace for violations.
* @param ttl - Time to live for cache entries.
* @returns Cache instance for violations.
*/
const violationCache = (namespace, ttl) => {
return standardCache(`violations:${namespace}`, ttl, violationFile);
};
/**
* Creates a session cache instance using Redis or in-memory store.
* @param namespace - The session namespace.
* @param ttl - Time to live for session entries.
* @returns Session store instance.
*/
const sessionCache = (namespace, ttl) => {
namespace = namespace.endsWith(':') ? namespace : `${namespace}:`;
if (!cacheConfig.USE_REDIS) {
const MemoryStore = createMemoryStore(session);
return new MemoryStore({ ttl, checkPeriod: librechatDataProvider.Time.ONE_DAY });
}
const store = new connectRedis.RedisStore({ client: exports.ioredisClient, ttl, prefix: namespace });
if (exports.ioredisClient) {
exports.ioredisClient.on('error', (err) => {
dataSchemas.logger.error(`Session store Redis error for namespace ${namespace}:`, err);
});
}
return store;
};
/**
* Creates a rate limiter cache using Redis.
* @param prefix - The key prefix for rate limiting.
* @returns RedisStore instance or undefined if Redis is not used.
*/
const limiterCache = (prefix) => {
if (!prefix) {
throw new Error('prefix is required');
}
if (!cacheConfig.USE_REDIS) {
return undefined;
}
// Note: The `prefix` is applied by RedisStore internally to its key operations.
// The global REDIS_KEY_PREFIX is applied by ioredisClient's keyPrefix setting.
// Combined key format: `{REDIS_KEY_PREFIX}::{prefix}{identifier}`
prefix = prefix.endsWith(':') ? prefix : `${prefix}:`;
try {
const sendCommand = ((...args) => __awaiter(void 0, void 0, void 0, function* () {
if (exports.ioredisClient == null) {
throw new Error('Redis client not available');
}
try {
return yield exports.ioredisClient.call(args[0], ...args.slice(1));
}
catch (err) {
dataSchemas.logger.error('Redis command execution failed:', err);
throw err;
}
}));
return new rateLimitRedis.RedisStore({ sendCommand, prefix });
}
catch (err) {
dataSchemas.logger.error(`Failed to create Redis rate limiter for prefix ${prefix}:`, err);
return undefined;
}
};
/**
* In-memory implementation of MCP server configurations cache for single-instance deployments.
* Uses a native JavaScript Map for fast, local storage without Redis dependencies.
* Suitable for development environments or single-server production deployments.
* Does not require leader checks or distributed coordination since data is instance-local.
* Data is lost on server restart and not shared across multiple server instances.
*/
class ServerConfigsCacheInMemory {
constructor() {
this.cache = new Map();
}
add(serverName, config) {
return __awaiter(this, void 0, void 0, function* () {
if (this.cache.has(serverName))
throw new Error(`Server "${serverName}" already exists in cache. Use update() to modify existing configs.`);
const storedConfig = Object.assign(Object.assign({}, config), { updatedAt: Date.now() });
this.cache.set(serverName, storedConfig);
return { serverName, config: storedConfig };
});
}
update(serverName, config) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.cache.has(serverName))
throw new Error(`Server "${serverName}" does not exist in cache. Use add() to create new configs.`);
this.cache.set(serverName, Object.assign(Object.assign({}, config), { updatedAt: Date.now() }));
});
}
remove(serverName) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.cache.delete(serverName)) {
throw new Error(`Failed to remove server "${serverName}" in cache.`);
}
});
}
get(serverName) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.get(serverName);
});
}
getAll() {
return __awaiter(this, void 0, void 0, function* () {
return Object.fromEntries(this.cache);
});
}
reset() {
return __awaiter(this, void 0, void 0, function* () {
this.cache.clear();
});
}
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var lodash$1 = {exports: {}};
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
var lodash = lodash$1.exports;
var hasRequiredLodash;
function requireLodash () {
if (hasRequiredLodash) return lodash$1.exports;
hasRequiredLodash = 1;
(function (module, exports$1) {
(function() {
/** Used as a safe reference for `undefined` in pre-ES5 environments. */
var undefined$1;
/** Used as the semantic version number. */
var VERSION = '4.17.23';
/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;
/** Error message constants. */
var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.',
FUNC_ERROR_TEXT = 'Expected a function',
INVALID_TEMPL_VAR_ERROR_TEXT = 'Invalid `variable` option passed into `_.template`';
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** Used as the maximum memoize cache size. */
var MAX_MEMOIZE_SIZE = 500;
/** Used as the internal argument placeholder. */
var PLACEHOLDER = '__lodash_placeholder__';
/** Used to compose bitmasks for cloning. */
var CLONE_DEEP_FLAG = 1,
CLONE_FLAT_FLAG = 2,
CLONE_SYMBOLS_FLAG = 4;
/** Used to compose bitmasks for value comparisons. */
var COMPARE_PARTIAL_FLAG = 1,
COMPARE_UNORDERED_FLAG = 2;
/** Used to compose bitmasks for function metadata. */
var WRAP_BIND_FLAG = 1,
WRAP_BIND_KEY_FLAG = 2,
WRAP_CURRY_BOUND_FLAG = 4,
WRAP_CURRY_FLAG = 8,
WRAP_CURRY_RIGHT_FLAG = 16,
WRAP_PARTIAL_FLAG = 32,
WRAP_PARTIAL_RIGHT_FLAG = 64,
WRAP_ARY_FLAG = 128,
WRAP_REARG_FLAG = 256,
WRAP_FLIP_FLAG = 512;
/** Used as default options for `_.truncate`. */
var DEFAULT_TRUNC_LENGTH = 30,
DEFAULT_TRUNC_OMISSION = '...';
/** Used to detect hot functions by number of calls within a span of milliseconds. */
var HOT_COUNT = 800,
HOT_SPAN = 16;
/** Used to indicate the type of lazy iteratees. */
var LAZY_FILTER_FLAG = 1,
LAZY_MAP_FLAG = 2,
LAZY_WHILE_FLAG = 3;
/** Used as references for various `Number` constants. */
var INFINITY = 1 / 0,
MAX_SAFE_INTEGER = 9007199254740991,
MAX_INTEGER = 1.7976931348623157e+308,
NAN = 0 / 0;
/** Used as references for the maximum length and index of an array. */
var MAX_ARRAY_LENGTH = 4294967295,
MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1,
HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;
/** Used to associate wrap methods with their bit flags. */
var wrapFlags = [
['ary', WRAP_ARY_FLAG],
['bind', WRAP_BIND_FLAG],
['bindKey', WRAP_BIND_KEY_FLAG],
['curry', WRAP_CURRY_FLAG],
['curryRight', WRAP_CURRY_RIGHT_FLAG],
['flip', WRAP_FLIP_FLAG],
['partial', WRAP_PARTIAL_FLAG],
['partialRight', WRAP_PARTIAL_RIGHT_FLAG],
['rearg', WRAP_REARG_FLAG]
];
/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
arrayTag = '[object Array]',
asyncTag = '[object AsyncFunction]',
boolTag = '[object Boolean]',
dateTag = '[object Date]',
domExcTag = '[object DOMException]',
errorTag = '[object Error]',
funcTag = '[object Function]',
genTag = '[object GeneratorFunction]',
mapTag = '[object Map]',
numberTag = '[object Number]',
nullTag = '[object Null]',
objectTag = '[object Object]',
promiseTag = '[object Promise]',
proxyTag = '[object Proxy]',
regexpTag = '[object RegExp]',
setTag = '[object Set]',
stringTag = '[object String]',
symbolTag = '[object Symbol]',
undefinedTag = '[object Undefined]',
weakMapTag = '[object WeakMap]',
weakSetTag = '[object WeakSet]';
var arrayBufferTag = '[object ArrayBuffer]',
dataViewTag = '[object DataView]',
float32Tag = '[object Float32Array]',
float64Tag = '[object Float64Array]',
int8Tag = '[object Int8Array]',
int16Tag = '[object Int16Array]',
int32Tag = '[object Int32Array]',
uint8Tag = '[object Uint8Array]',
uint8ClampedTag = '[object Uint8ClampedArray]',
uint16Tag = '[object Uint16Array]',
uint32Tag = '[object Uint32Array]';
/** Used to match empty string literals in compiled template source. */
var reEmptyStringLeading = /\b__p \+= '';/g,
reEmptyStringMiddle = /\b(__p \+=) '' \+/g,
reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g;
/** Used to match HTML entities and HTML characters. */
var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g,
reUnescapedHtml = /[&<>"']/g,
reHasEscapedHtml = RegExp(reEscapedHtml.source),
reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
/** Used to match template delimiters. */
var reEscape = /<%-([\s\S]+?)%>/g,
reEvaluate = /<%([\s\S]+?)%>/g,
reInterpolate = /<%=([\s\S]+?)%>/g;
/** Used to match property names within property paths. */
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
reIsPlainProp = /^\w*$/,
rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
reHasRegExpChar = RegExp(reRegExpChar.source);
/** Used to match leading whitespace. */
var reTrimStart = /^\s+/;
/** Used to match a single whitespace character. */
var reWhitespace = /\s/;
/** Used to match wrap detail comments. */
var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,
reWrapDetails = /\{\n\/\* \[wrapped with (.+)\] \*/,
reSplitDetails = /,? & /;
/** Used to match words composed of alphanumeric characters. */
var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;
/**
* Used to validate the `validate` option in `_.template` variable.
*
* Forbids characters which could potentially change the meaning of the function argument definition:
* - "()," (modification of function parameters)
* - "=" (default value)
* - "[]{}" (destructuring of function parameters)
* - "/" (beginning of a comment)
* - whitespace
*/
var reForbiddenIdentifierChars = /[()=,{}\[\]\/\s]/;
/** Used to match backslashes in property paths. */
var reEscapeChar = /\\(\\)?/g;
/**
* Used to match
* [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components).
*/
var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g;
/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;
/** Used to detect bad signed hexadecimal string values. */
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
/** Used to detect binary string values. */
var reIsBinary = /^0b[01]+$/i;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;
/** Used to detect octal string values. */
var reIsOctal = /^0o[0-7]+$/i;
/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;
/** Used to match Latin Unicode letters (excluding mathematical operators). */
var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g;
/** Used to ensure capturing order of template delimiters. */
var reNoMatch = /($^)/;
/** Used to match unescaped characters in compiled string literals. */
var reUnescapedString = /['\n\r\u2028\u2029\\]/g;
/** Used to compose unicode character classes. */
var rsAstralRange = '\\ud800-\\udfff',
rsComboMarksRange = '\\u0300-\\u036f',
reComboHalfMarksRange = '\\ufe20-\\ufe2f',
rsComboSymbolsRange = '\\u20d0-\\u20ff',
rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange,
rsDingbatRange = '\\u2700-\\u27bf',
rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff',
rsMathOpRange = '\\xac\\xb1\\xd7\\xf7',
rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf',
rsPunctuationRange = '\\u2000-\\u206f',
rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000',
rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde',
rsVarRange = '\\ufe0e\\ufe0f',
rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange;
/** Used to compose unicode capture groups. */
var rsApos = "['\u2019]",
rsAstral = '[' + rsAstralRange + ']',
rsBreak = '[' + rsBreakRange + ']',
rsCombo = '[' + rsComboRange + ']',
rsDigits = '\\d+',
rsDingbat = '[' + rsDingbatRange + ']',
rsLower = '[' + rsLowerRange + ']',
rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']',
rsFitz = '\\ud83c[\\udffb-\\udfff]',
rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',
rsNonAstral = '[^' + rsAstralRange + ']',
rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}',
rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]',
rsUpper = '[' + rsUpperRange + ']',
rsZWJ = '\\u200d';
/** Used to compose unicode regexes. */
var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')',
rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')',
rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?',
rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?',
reOptMod = rsModifier + '?',
rsOptVar = '[' + rsVarRange + ']?',
rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',
rsOrdLower = '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])',
rsOrdUpper = '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])',
rsSeq = rsOptVar + reOptMod + rsOptJoin,
rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq,
rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';
/** Used to match apostrophes. */
var reApos = RegExp(rsApos, 'g');
/**
* Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and
* [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).
*/
var reComboMark = RegExp(rsCombo, 'g');
/** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */
var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');
/** Used to match complex or compound words. */
var reUnicodeWord = RegExp([
rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')',
rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')',
rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower,
rsUpper + '+' + rsOptContrUpper,
rsOrdUpper,
rsOrdLower,
rsDigits,
rsEmoji
].join('|'), 'g');
/** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */
var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']');
/** Used to detect strings that need a more robust regexp to match words. */
var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;
/** Used to assign default `context` object properties. */
var contextProps = [
'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array',
'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object',
'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array',
'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap',
'_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout'
];
/** Used to make template sourceURLs easier to identify. */
var templateCounter = -1;
/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
typedArrayTags[uint32Tag] = true;
typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
typedArrayTags[errorTag] = typedArrayTags[funcTag] =
typedArrayTags[mapTag] = typedArrayTags[numberTag] =
typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
typedArrayTags[setTag] = typedArrayTags[stringTag] =
typedArrayTags[weakMapTag] = false;
/** Used to identify `toStringTag` values supported by `_.clone`. */
var cloneableTags = {};
cloneableTags[argsTag] = cloneableTags[arrayTag] =
cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
cloneableTags[boolTag] = cloneableTags[dateTag] =
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
cloneableTags[int32Tag] = cloneableTags[mapTag] =
cloneableTags[numberTag] = cloneableTags[objectTag] =
cloneableTags[regexpTag] = cloneableTags[setTag] =
cloneableTags[stringTag] = cloneableTags[symbolTag] =
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;
cloneableTags[errorTag] = cloneableTags[funcTag] =
cloneableTags[weakMapTag] = false;
/** Used to map Latin Unicode letters to basic Latin letters. */
var deburredLetters = {
// Latin-1 Supplement block.
'\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A',
'\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a',
'\xc7': 'C', '\xe7': 'c',
'\xd0': 'D', '\xf0': 'd',
'\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E',
'\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e',
'\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I',
'\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i',
'\xd1': 'N', '\xf1': 'n',
'\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O',
'\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o',
'\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U',
'\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u',
'\xdd': 'Y', '\xfd': 'y', '\xff': 'y',
'\xc6': 'Ae', '\xe6': 'ae',
'\xde': 'Th', '\xfe': 'th',
'\xdf': 'ss',
// Latin Extended-A block.
'\u0100': 'A', '\u0102': 'A', '\u0104': 'A',
'\u0101': 'a', '\u0103': 'a', '\u0105': 'a',
'\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C',
'\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c',
'\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd',
'\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E',
'\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e',
'\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G',
'\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g',
'\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h',
'\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I',
'\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i',
'\u0134': 'J', '\u0135': 'j',
'\u0136': 'K', '\u0137': 'k', '\u0138': 'k',
'\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L',
'\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l',
'\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N',
'\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n',
'\u014c': 'O', '\u014e': 'O', '\u0150': 'O',
'\u014d': 'o', '\u014f': 'o', '\u0151': 'o',
'\u0154': 'R', '\u0156': 'R', '\u0158': 'R',
'\u0155': 'r', '\u0157': 'r', '\u0159': 'r',
'\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S',
'\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's',
'\u0162': 'T', '\u0164': 'T', '\u0166': 'T',
'\u0163': 't', '\u0165': 't', '\u0167': 't',
'\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U',
'\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u',
'\u0174': 'W', '\u0175': 'w',
'\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y',
'\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z',
'\u017a': 'z', '\u017c': 'z', '\u017e': 'z',
'\u0132': 'IJ', '\u0133': 'ij',
'\u0152': 'Oe', '\u0153': 'oe',
'\u0149': "'n", '\u017f': 's'
};
/** Used to map characters to HTML entities. */
var htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
/** Used to map HTML entities to characters. */
var htmlUnescapes = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'"
};
/** Used to escape characters for inclusion in compiled string literals. */
var stringEscapes = {
'\\': '\\',
"'": "'",
'\n': 'n',
'\r': 'r',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
/** Built-in method references without a dependency on `root`. */
var freeParseFloat = parseFloat,
freeParseInt = parseInt;
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
/** Detect free variable `exports`. */
var freeExports = exports$1 && !exports$1.nodeType && exports$1;
/** Detect free variable `module`. */
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;
/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports && freeGlobal.process;
/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
try {
// Use `util.types` for Node.js 10+.
var types = freeModule && freeModule.require && freeModule.require('util').types;
if (types) {
return types;
}
// Legacy `process.binding('util')` for Node.js < 10.
return freeProcess && freeProcess.binding && freeProcess.binding('util');
} catch (e) {}
}());
/* Node.js helper references. */
var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer,
nodeIsDate = nodeUtil && nodeUtil.isDate,
nodeIsMap = nodeUtil && nodeUtil.isMap,
nodeIsRegExp = nodeUtil && nodeUtil.isRegExp,
nodeIsSet = nodeUtil && nodeUtil.isSet,
nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
/*--------------------------------------------------------------------------*/
/**
* A faster alternative to `Function#apply`, this function invokes `func`
* with the `this` binding of `thisArg` and the arguments of `args`.
*
* @private
* @param {Function} func The function to invoke.
* @param {*} thisArg The `this` binding of `func`.
* @param {Array} args The arguments to invoke `func` with.
* @returns {*} Returns the result of `func`.
*/
function apply(func, thisArg, args) {
switch (args.length) {
case 0: return func.call(thisArg);
case 1: return func.call(thisArg, args[0]);
case 2: return func.call(thisArg, args[0], args[1]);
case 3: return func.call(thisArg, args[0], args[1], args[2]);
}
return func.apply(thisArg, args);
}
/**
* A specialized version of `baseAggregator` for arrays.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} setter The function to set `accumulator` values.
* @param {Function} iteratee The iteratee to transform keys.
* @param {Object} accumulator The initial aggregated object.
* @returns {Function} Returns `accumulator`.
*/
function arrayAggregator(array, setter, iteratee, accumulator) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
var value = array[index];
setter(accumulator, value, iteratee(value), array);
}
return accumulator;
}
/**
* A specialized version of `_.forEach` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns `array`.
*/
function arrayEach(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break;
}
}
return array;
}
/**
* A specialized version of `_.forEachRight` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns `array`.
*/
function arrayEachRight(array, iteratee) {
var length = array == null ? 0 : array.length;
while (length--) {
if (iteratee(array[length], length, array) === false) {
break;
}
}
return array;
}
/**
* A specialized version of `_.every` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {boolean} Returns `true` if all elements pass the predicate check,
* else `false`.
*/
function arrayEvery(array, predicate) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (!predicate(array[index], index, array)) {
return false;
}
}
return true;
}
/**
* A specialized version of `_.filter` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {Array} Returns the new filtered array.
*/
function arrayFilter(array, predicate) {
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (predicate(value, index, array)) {
result[resIndex++] = value;
}
}
return result;
}
/**
* A specialized version of `_.includes` for arrays without support for
* specifying an index to search from.
*
* @private
* @param {Array} [array] The array to inspect.
* @param {*} target The value to search for.
* @returns {boolean} Returns `true` if `target` is found, else `false`.
*/
function arrayIncludes(array, value) {
var length = array == null ? 0 : array.length;
return !!length && baseIndexOf(array, value, 0) > -1;
}
/**
* This function is like `arrayIncludes` except that it accepts a comparator.
*
* @private
* @param {Array} [array] The array to inspect.
* @param {*} target The value to search for.
* @param {Function} comparator The comparator invoked per element.
* @returns {boolean} Returns `true` if `target` is found, else `false`.
*/
function arrayIncludesWith(array, value, comparator) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (comparator(value, array[index])) {
return true;
}
}
return false;
}
/**
* A specialized version of `_.map` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the new mapped array.
*/
function arrayMap(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length,
result = Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
/**
* Appends the elements of `values` to `array`.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to append.
* @returns {Array} Returns `array`.
*/
function arrayPush(array, values) {
var index = -1,
length = values.length,
offset = array.length;
while (++index < length) {
array[offset + index] = values[index];
}
return array;
}
/**
* A specialized version of `_.reduce` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {*} [accumulator] The initial value.
* @param {boolean} [initAccum] Specify using the first element of `array` as
* the initial value.
* @returns {*} Returns the accumulated value.
*/
function arrayReduce(array, iteratee, accumulator, initAccum) {
var index = -1,
length = array == null ? 0 : array.length;
if (initAccum && length) {
accumulator = array[++index];
}
while (++index < length) {
accumulator = iteratee(accumulator, array[index], index, array);
}
return accumulator;
}
/**
* A specialized version of `_.reduceRight` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {*} [accumulator] The initial value.
* @param {boolean} [initAccum] Specify using the last element of `array` as
* the initial value.
* @returns {*} Returns the accumulated value.
*/
function arrayReduceRight(array, iteratee, accumulator, initAccum) {
var length = array == null ? 0 : array.length;
if (initAccum && length) {
accumulator = array[--length];
}
while (length--) {
accumulator = iteratee(accumulator, array[length], length, array);
}
return accumulator;
}
/**
* A specialized version of `_.some` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {boolean} Returns `true` if any element passes the predicate check,
* else `false`.
*/
function arraySome(array, predicate) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (predicate(array[index], index, array)) {
return true;
}
}
return false;
}
/**
* Gets the size of an ASCII `string`.
*
* @private
* @param {string} string The string inspect.
* @returns {number} Returns the string size.
*/
var asciiSize = baseProperty('length');
/**
* Converts an ASCII `string` to an array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the converted array.
*/
function asciiToArray(string) {
return string.split('');
}
/**
* Splits an ASCII `string` into an array of its words.
*
* @private
* @param {string} The string to inspect.
* @returns {Array} Returns the words of `string`.
*/
function asciiWords(string) {
return string.match(reAsciiWord) || [];
}
/**
* The base implementation of methods like `_.findKey` and `_.findLastKey`,
* without support for iteratee shorthands, which iterates over `collection`
* using `eachFunc`.
*
* @private
* @param {Array|Object} collection The collection to inspect.
* @param {Function} predicate The function invoked per iteration.
* @param {Function} eachFunc The function to iterate over `collection`.
* @returns {*} Returns the found element or its key, else `undefined`.
*/
function baseFindKey(collection, predicate, eachFunc) {
var result;
eachFunc(collection, function(value, key, collection) {
if (predicate(value, key, collection)) {
result = key;
return false;
}
});
return result;
}
/**
* The base implementation of `_.findIndex` and `_.findLastIndex` without
* support for iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Function} predicate The function invoked per iteration.
* @param {number} fromIndex The index to search from.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseFindIndex(array, predicate, fromIndex, fromRight) {
var length = array.length,
index = fromIndex + (fromRight ? 1 : -1);
while ((fromRight ? index-- : ++index < length)) {
if (predicate(array[index], index, array)) {
return index;
}
}
return -1;
}
/**
* The base implementation of `_.indexOf` without `fromIndex` bounds checks.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseIndexOf(array, value, fromIndex) {
return value === value
? strictIndexOf(array, value, fromIndex)
: baseFindIndex(array, baseIsNaN, fromIndex);
}
/**
* This function is like `baseIndexOf` except that it accepts a comparator.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @param {Function} comparator The comparator invoked per element.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseIndexOfWith(array, value, fromIndex, comparator) {
var index = fromIndex - 1,
length = array.length;
while (++index < length) {
if (comparator(array[index], value)) {
return index;
}
}
return -1;
}
/**
* The base implementation of `_.isNaN` without support for number objects.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
*/
function baseIsNaN(value) {
return value !== value;
}
/**
* The base implementation of `_.mean` and `_.meanBy` without support for
* iteratee shorthands.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {number} Returns the mean.
*/
function baseMean(array, iteratee) {
var length = array == null ? 0 : array.length;
return length ? (baseSum(array, iteratee) / length) : NAN;
}
/**
* The base implementation of `_.property` without support for deep paths.
*
* @private
* @param {string} key The key of the property to get.
* @returns {Function} Returns the new accessor function.
*/
function baseProperty(key) {
return function(object) {
return object == null ? undefined$1 : object[key];
};
}
/**
* The base implementation of `_.propertyOf` without support for deep paths.
*
* @private
* @param {Object} object The object to query.
* @returns {Function} Returns the new accessor function.
*/
function basePropertyOf(object) {
return function(key) {
return object == null ? undefined$1 : object[key];
};
}
/**
* The base implementation of `_.reduce` and `_.reduceRight`, without support
* for iteratee shorthands, which iterates over `collection` using `eachFunc`.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {*} accumulator The initial value.
* @param {boolean} initAccum Specify using the first or last element of
* `collection` as the initial value.
* @param {Function} eachFunc The function to iterate over `collection`.
* @returns {*} Returns the accumulated value.
*/
function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) {
eachFunc(collection, function(value, index, collection) {
accumulator = initAccum
? (initAccum = false, value)
: iteratee(accumulator, value, index, collection);
});
return accumulator;
}
/**
* The base implementation of `_.sortBy` which uses `comparer` to define the
* sort order of `array` and replaces criteria objects with their corresponding
* values.
*
* @private
* @param {Array} array The array to sort.
* @param {Function} comparer The function to define sort order.
* @returns {Array} Returns `array`.
*/
function baseSortBy(array, comparer) {
var length = array.length;
array.sort(comparer);
while (length--) {
array[length] = array[length].value;
}
return array;
}
/**
* The base implementation of `_.sum` and `_.sumBy` without support for
* iteratee shorthands.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {number} Returns the sum.
*/
function baseSum(array, iteratee) {
var result,
index = -1,
length = array.length;
while (++index < length) {
var current = iteratee(array[index]);
if (current !== undefined$1) {
result = result === undefined$1 ? current : (result + current);
}
}
return result;
}
/**
* The base implementation of `_.times` without support for iteratee shorthands
* or max array length checks.
*
* @private
* @param {number} n The number of times to invoke `iteratee`.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the array of results.
*/
function baseTimes(n, iteratee) {
var index = -1,
result = Array(n);
while (++index < n) {
result[index] = iteratee(index);
}
return result;
}
/**
* The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array
* of key-value pairs for `object` corresponding to the property names of `props`.
*
* @private
* @param {Object} object The object to query.
* @param {Array} props The property names to get values for.
* @returns {Object} Returns the key-value pairs.
*/
function baseToPairs(object, props) {
return arrayMap(props, function(key) {
return [key, object[key]];
});
}
/**
* The base implementation of `_.trim`.
*
* @private
* @param {string} string The string to trim.
* @returns {string} Returns the trimmed string.
*/
function baseTrim(string) {
return string
? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')
: string;
}
/**
* The base implementation of `_.unary` without support for storing metadata.
*
* @private
* @param {Function} func The function to cap arguments for.
* @returns {Function} Returns the new capped function.
*/
function baseUnary(func) {
return function(value) {
return func(value);
};
}
/**
* The base implementation of `_.values` and `_.valuesIn` which creates an
* array of `object` property values corresponding to the property names
* of `props`.
*
* @private
* @param {Object} object The object to query.
* @param {Array} props The property names to get values for.
* @returns {Object} Returns the array of property values.
*/
function baseValues(object, props) {
return arrayMap(props, function(key) {
return object[key];
});
}
/**
* Checks if a `cache` value for `key` exists.
*
* @private
* @param {Object} cache The cache to query.
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function cacheHas(cache, key) {
return cache.has(key);
}
/**
* Used by `_.trim` and `_.trimStart` to get the index of the first string symbol
* that is not found in the character symbols.
*
* @private
* @param {Array} strSymbols The string symbols to inspect.
* @param {Array} chrSymbols The character symbols to find.
* @returns {number} Returns the index of the first unmatched string symbol.
*/
function charsStartIndex(strSymbols, chrSymbols) {
var index = -1,
length = strSymbols.length;
while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
return index;
}
/**
* Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol
* that is not found in the character symbols.
*
* @private
* @param {Array} strSymbols The string symbols to inspect.
* @param {Array} chrSymbols The character symbols to find.
* @returns {number} Returns the index of the last unmatched string symbol.
*/
function charsEndIndex(strSymbols, chrSymbols) {
var index = strSymbols.length;
while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
return index;
}
/**
* Gets the number of `placeholder` occurrences in `array`.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} placeholder The placeholder to search for.
* @returns {number} Returns the placeholder count.
*/
function countHolders(array, placeholder) {
var length = array.length,
result = 0;
while (length--) {
if (array[length] === placeholder) {
++result;
}
}
return result;
}
/**
* Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A
* letters to basic Latin letters.
*
* @private
* @param {string} letter The matched letter to deburr.
* @returns {string} Returns the deburred letter.
*/
var deburrLetter = basePropertyOf(deburredLetters);
/**
* Used by `_.escape` to convert characters to HTML entities.
*
* @private
* @param {string} chr The matched character to escape.
* @returns {string} Returns the escaped character.
*/
var escapeHtmlChar = basePropertyOf(htmlEscapes);
/**
* Used by `_.template` to escape characters for inclusion in compiled string literals.
*
* @private
* @param {string} chr The matched character to escape.
* @returns {string} Returns the escaped character.
*/
function escapeStringChar(chr) {
return '\\' + stringEscapes[chr];
}
/**
* Gets the value at `key` of `object`.
*
* @private
* @param {Object} [object] The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function getValue(object, key) {
return object == null ? undefined$1 : object[key];
}
/**
* Checks if `string` contains Unicode symbols.
*
* @private
* @param {string} string The string to inspect.
* @returns {boolean} Returns `true` if a symbol is found, else `false`.
*/
function hasUnicode(string) {
return reHasUnicode.test(string);
}
/**
* Checks if `string` contains a word composed of Unicode symbols.
*
* @private
* @param {string} string The string to inspect.
* @returns {boolean} Returns `true` if a word is found, else `false`.
*/
function hasUnicodeWord(string) {
return reHasUnicodeWord.test(string);
}
/**
* Converts `iterator` to an array.
*
* @private
* @param {Object} iterator The iterator to convert.
* @returns {Array} Returns the converted array.
*/
function iteratorToArray(iterator) {
var data,
result = [];
while (!(data = iterator.next()).done) {
result.push(data.value);
}
return result;
}
/**
* Converts `map` to its key-value pairs.
*
* @private
* @param {Object} map The map to convert.
* @returns {Array} Returns the key-value pairs.
*/
function mapToArray(map) {
var index = -1,
result = Array(map.size);
map.forEach(function(value, key) {
result[++index] = [key, value];
});
return result;
}
/**
* Creates a unary function that invokes `func` with its argument transformed.
*
* @private
* @param {Function} func The function to wrap.
* @param {Function} transform The argument transform.
* @returns {Function} Returns the new function.
*/
function overArg(func, transform) {
return function(arg) {
return func(transform(arg));
};
}
/**
* Replaces all `placeholder` elements in `array` with an internal placeholder
* and returns an array of their indexes.
*
* @private
* @param {Array} array The array to modify.
* @param {*} placeholder The placeholder to replace.
* @returns {Array} Returns the new array of placeholder indexes.
*/
function replaceHolders(array, placeholder) {
var index = -1,
length = array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (value === placeholder || value === PLACEHOLDER) {
array[index] = PLACEHOLDER;
result[resIndex++] = index;
}
}
return result;
}
/**
* Converts `set` to an array of its values.
*
* @private
* @param {Object} set The set to convert.
* @returns {Array} Returns the values.
*/
function setToArray(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = value;
});
return result;
}
/**
* Converts `set` to its value-value pairs.
*
* @private
* @param {Object} set The set to convert.
* @returns {Array} Returns the value-value pairs.
*/
function setToPairs(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = [value, value];
});
return result;
}
/**
* A specialized version of `_.indexOf` which performs strict equality
* comparisons of values, i.e. `===`.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function strictIndexOf(array, value, fromIndex) {
var index = fromIndex - 1,
length = array.length;
while (++index < length) {
if (array[index] === value) {
return index;
}
}
return -1;
}
/**
* A specialized version of `_.lastIndexOf` which performs strict equality
* comparisons of values, i.e. `===`.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function strictLastIndexOf(array, value, fromIndex) {
var index = fromIndex + 1;
while (index--) {
if (array[index] === value) {
return index;
}
}
return index;
}
/**
* Gets the number of symbols in `string`.
*
* @private
* @param {string} string The string to inspect.
* @returns {number} Returns the string size.
*/
function stringSize(string) {
return hasUnicode(string)
? unicodeSize(string)
: asciiSize(string);
}
/**
* Converts `string` to an array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the converted array.
*/
function stringToArray(string) {
return hasUnicode(string)
? unicodeToArray(string)
: asciiToArray(string);
}
/**
* Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace
* character of `string`.
*
* @private
* @param {string} string The string to inspect.
* @returns {number} Returns the index of the last non-whitespace character.
*/
function trimmedEndIndex(string) {
var index = string.length;
while (index-- && reWhitespace.test(string.charAt(index))) {}
return index;
}
/**
* Used by `_.unescape` to convert HTML entities to characters.
*
* @private
* @param {string} chr The matched character to unescape.
* @returns {string} Returns the unescaped character.
*/
var unescapeHtmlChar = basePropertyOf(htmlUnescapes);
/**
* Gets the size of a Unicode `string`.
*
* @private
* @param {string} string The string inspect.
* @returns {number} Returns the string size.
*/
function unicodeSize(string) {
var result = reUnicode.lastIndex = 0;
while (reUnicode.test(string)) {
++result;
}
return result;
}
/**
* Converts a Unicode `string` to an array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the converted array.
*/
function unicodeToArray(string) {
return string.match(reUnicode) || [];
}
/**
* Splits a Unicode `string` into an array of its words.
*
* @private
* @param {string} The string to inspect.
* @returns {Array} Returns the words of `string`.
*/
function unicodeWords(string) {
return string.match(reUnicodeWord) || [];
}
/*--------------------------------------------------------------------------*/
/**
* Create a new pristine `lodash` function using the `context` object.
*
* @static
* @memberOf _
* @since 1.1.0
* @category Util
* @param {Object} [context=root] The context object.
* @returns {Function} Returns a new `lodash` function.
* @example
*
* _.mixin({ 'foo': _.constant('foo') });
*
* var lodash = _.runInContext();
* lodash.mixin({ 'bar': lodash.constant('bar') });
*
* _.isFunction(_.foo);
* // => true
* _.isFunction(_.bar);
* // => false
*
* lodash.isFunction(lodash.foo);
* // => false
* lodash.isFunction(lodash.bar);
* // => true
*
* // Create a suped-up `defer` in Node.js.
* var defer = _.runInContext({ 'setTimeout': setImmediate }).defer;
*/
var runInContext = (function runInContext(context) {
context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps));
/** Built-in constructor references. */
var Array = context.Array,
Date = context.Date,
Error = context.Error,
Function = context.Function,
Math = context.Math,
Object = context.Object,
RegExp = context.RegExp,
String = context.String,
TypeError = context.TypeError;
/** Used for built-in method references. */
var arrayProto = Array.prototype,
funcProto = Function.prototype,
objectProto = Object.prototype;
/** Used to detect overreaching core-js shims. */
var coreJsData = context['__core-js_shared__'];
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/** Used to generate unique IDs. */
var idCounter = 0;
/** Used to detect methods masquerading as native. */
var maskSrcKey = (function() {
var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
return uid ? ('Symbol(src)_1.' + uid) : '';
}());
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString = objectProto.toString;
/** Used to infer the `Object` constructor. */
var objectCtorString = funcToString.call(Object);
/** Used to restore the original `_` reference in `_.noConflict`. */
var oldDash = root._;
/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);
/** Built-in value references. */
var Buffer = moduleExports ? context.Buffer : undefined$1,
Symbol = context.Symbol,
Uint8Array = context.Uint8Array,
allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined$1,
getPrototype = overArg(Object.getPrototypeOf, Object),
objectCreate = Object.create,
propertyIsEnumerable = objectProto.propertyIsEnumerable,
splice = arrayProto.splice,
spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined$1,
symIterator = Symbol ? Symbol.iterator : undefined$1,
symToStringTag = Symbol ? Symbol.toStringTag : undefined$1;
var defineProperty = (function() {
try {
var func = getNative(Object, 'defineProperty');
func({}, '', {});
return func;
} catch (e) {}
}());
/** Mocked built-ins. */
var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout,
ctxNow = Date && Date.now !== root.Date.now && Date.now,
ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeCeil = Math.ceil,
nativeFloor = Math.floor,
nativeGetSymbols = Object.getOwnPropertySymbols,
nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined$1,
nativeIsFinite = context.isFinite,
nativeJoin = arrayProto.join,
nativeKeys = overArg(Object.keys, Object),
nativeMax = Math.max,
nativeMin = Math.min,
nativeNow = Date.now,
nativeParseInt = context.parseInt,
nativeRandom = Math.random,
nativeReverse = arrayProto.reverse;
/* Built-in method references that are verified to be native. */
var DataView = getNative(context, 'DataView'),
Map = getNative(context, 'Map'),
Promise = getNative(context, 'Promise'),
Set = getNative(context, 'Set'),
WeakMap = getNative(context, 'WeakMap'),
nativeCreate = getNative(Object, 'create');
/** Used to store function metadata. */
var metaMap = WeakMap && new WeakMap;
/** Used to lookup unminified function names. */
var realNames = {};
/** Used to detect maps, sets, and weakmaps. */
var dataViewCtorString = toSource(DataView),
mapCtorString = toSource(Map),
promiseCtorString = toSource(Promise),
setCtorString = toSource(Set),
weakMapCtorString = toSource(WeakMap);
/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol ? Symbol.prototype : undefined$1,
symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1,
symbolToString = symbolProto ? symbolProto.toString : undefined$1;
/*------------------------------------------------------------------------*/
/**
* Creates a `lodash` object which wraps `value` to enable implicit method
* chain sequences. Methods that operate on and return arrays, collections,
* and functions can be chained together. Methods that retrieve a single value
* or may return a primitive value will automatically end the chain sequence
* and return the unwrapped value. Otherwise, the value must be unwrapped
* with `_#value`.
*
* Explicit chain sequences, which must be unwrapped with `_#value`, may be
* enabled using `_.chain`.
*
* The execution of chained methods is lazy, that is, it's deferred until
* `_#value` is implicitly or explicitly called.
*
* Lazy evaluation allows several methods to support shortcut fusion.
* Shortcut fusion is an optimization to merge iteratee calls; this avoids
* the creation of intermediate arrays and can greatly reduce the number of
* iteratee executions. Sections of a chain sequence qualify for shortcut
* fusion if the section is applied to an array and iteratees accept only
* one argument. The heuristic for whether a section qualifies for shortcut
* fusion is subject to change.
*
* Chaining is supported in custom builds as long as the `_#value` method is
* directly or indirectly included in the build.
*
* In addition to lodash methods, wrappers have `Array` and `String` methods.
*
* The wrapper `Array` methods are:
* `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift`
*
* The wrapper `String` methods are:
* `replace` and `split`
*
* The wrapper methods that support shortcut fusion are:
* `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,
* `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,
* `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`
*
* The chainable wrapper methods are:
* `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`,
* `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`,
* `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`,
* `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`,
* `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`,
* `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`,
* `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`,
* `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`,
* `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`,
* `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`,
* `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`,
* `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`,
* `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`,
* `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`,
* `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`,
* `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`,
* `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`,
* `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`,
* `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`,
* `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`,
* `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`,
* `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`,
* `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`,
* `zipObject`, `zipObjectDeep`, and `zipWith`
*
* The wrapper methods that are **not** chainable by default are:
* `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`,
* `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`,
* `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`,
* `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`,
* `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`,
* `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`,
* `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`,
* `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`,
* `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`,
* `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`,
* `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`,
* `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`,
* `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`,
* `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`,
* `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`,
* `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`,
* `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`,
* `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`,
* `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`,
* `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`,
* `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`,
* `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`,
* `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`,
* `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`,
* `upperFirst`, `value`, and `words`
*
* @name _
* @constructor
* @category Seq
* @param {*} value The value to wrap in a `lodash` instance.
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* function square(n) {
* return n * n;
* }
*
* var wrapped = _([1, 2, 3]);
*
* // Returns an unwrapped value.
* wrapped.reduce(_.add);
* // => 6
*
* // Returns a wrapped value.
* var squares = wrapped.map(square);
*
* _.isArray(squares);
* // => false
*
* _.isArray(squares.value());
* // => true
*/
function lodash(value) {
if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
if (value instanceof LodashWrapper) {
return value;
}
if (hasOwnProperty.call(value, '__wrapped__')) {
return wrapperClone(value);
}
}
return new LodashWrapper(value);
}
/**
* The base implementation of `_.create` without support for assigning
* properties to the created object.
*
* @private
* @param {Object} proto The object to inherit from.
* @returns {Object} Returns the new object.
*/
var baseCreate = (function() {
function object() {}
return function(proto) {
if (!isObject(proto)) {
return {};
}
if (objectCreate) {
return objectCreate(proto);
}
object.prototype = proto;
var result = new object;
object.prototype = undefined$1;
return result;
};
}());
/**
* The function whose prototype chain sequence wrappers inherit from.
*
* @private
*/
function baseLodash() {
// No operation performed.
}
/**
* The base constructor for creating `lodash` wrapper objects.
*
* @private
* @param {*} value The value to wrap.
* @param {boolean} [chainAll] Enable explicit method chain sequences.
*/
function LodashWrapper(value, chainAll) {
this.__wrapped__ = value;
this.__actions__ = [];
this.__chain__ = !!chainAll;
this.__index__ = 0;
this.__values__ = undefined$1;
}
/**
* By default, the template delimiters used by lodash are like those in
* embedded Ruby (ERB) as well as ES2015 template strings. Change the
* following template settings to use alternative delimiters.
*
* @static
* @memberOf _
* @type {Object}
*/
lodash.templateSettings = {
/**
* Used to detect `data` property values to be HTML-escaped.
*
* @memberOf _.templateSettings
* @type {RegExp}
*/
'escape': reEscape,
/**
* Used to detect code to be evaluated.
*
* @memberOf _.templateSettings
* @type {RegExp}
*/
'evaluate': reEvaluate,
/**
* Used to detect `data` property values to inject.
*
* @memberOf _.templateSettings
* @type {RegExp}
*/
'interpolate': reInterpolate,
/**
* Used to reference the data object in the template text.
*
* @memberOf _.templateSettings
* @type {string}
*/
'variable': '',
/**
* Used to import variables into the compiled template.
*
* @memberOf _.templateSettings
* @type {Object}
*/
'imports': {
/**
* A reference to the `lodash` function.
*
* @memberOf _.templateSettings.imports
* @type {Function}
*/
'_': lodash
}
};
// Ensure wrappers are instances of `baseLodash`.
lodash.prototype = baseLodash.prototype;
lodash.prototype.constructor = lodash;
LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;
/*------------------------------------------------------------------------*/
/**
* Creates a lazy wrapper object which wraps `value` to enable lazy evaluation.
*
* @private
* @constructor
* @param {*} value The value to wrap.
*/
function LazyWrapper(value) {
this.__wrapped__ = value;
this.__actions__ = [];
this.__dir__ = 1;
this.__filtered__ = false;
this.__iteratees__ = [];
this.__takeCount__ = MAX_ARRAY_LENGTH;
this.__views__ = [];
}
/**
* Creates a clone of the lazy wrapper object.
*
* @private
* @name clone
* @memberOf LazyWrapper
* @returns {Object} Returns the cloned `LazyWrapper` object.
*/
function lazyClone() {
var result = new LazyWrapper(this.__wrapped__);
result.__actions__ = copyArray(this.__actions__);
result.__dir__ = this.__dir__;
result.__filtered__ = this.__filtered__;
result.__iteratees__ = copyArray(this.__iteratees__);
result.__takeCount__ = this.__takeCount__;
result.__views__ = copyArray(this.__views__);
return result;
}
/**
* Reverses the direction of lazy iteration.
*
* @private
* @name reverse
* @memberOf LazyWrapper
* @returns {Object} Returns the new reversed `LazyWrapper` object.
*/
function lazyReverse() {
if (this.__filtered__) {
var result = new LazyWrapper(this);
result.__dir__ = -1;
result.__filtered__ = true;
} else {
result = this.clone();
result.__dir__ *= -1;
}
return result;
}
/**
* Extracts the unwrapped value from its lazy wrapper.
*
* @private
* @name value
* @memberOf LazyWrapper
* @returns {*} Returns the unwrapped value.
*/
function lazyValue() {
var array = this.__wrapped__.value(),
dir = this.__dir__,
isArr = isArray(array),
isRight = dir < 0,
arrLength = isArr ? array.length : 0,
view = getView(0, arrLength, this.__views__),
start = view.start,
end = view.end,
length = end - start,
index = isRight ? end : (start - 1),
iteratees = this.__iteratees__,
iterLength = iteratees.length,
resIndex = 0,
takeCount = nativeMin(length, this.__takeCount__);
if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
return baseWrapperValue(array, this.__actions__);
}
var result = [];
outer:
while (length-- && resIndex < takeCount) {
index += dir;
var iterIndex = -1,
value = array[index];
while (++iterIndex < iterLength) {
var data = iteratees[iterIndex],
iteratee = data.iteratee,
type = data.type,
computed = iteratee(value);
if (type == LAZY_MAP_FLAG) {
value = computed;
} else if (!computed) {
if (type == LAZY_FILTER_FLAG) {
continue outer;
} else {
break outer;
}
}
}
result[resIndex++] = value;
}
return result;
}
// Ensure `LazyWrapper` is an instance of `baseLodash`.
LazyWrapper.prototype = baseCreate(baseLodash.prototype);
LazyWrapper.prototype.constructor = LazyWrapper;
/*------------------------------------------------------------------------*/
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
this.size = 0;
}
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined$1 : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined$1;
}
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate ? (data[key] !== undefined$1) : hasOwnProperty.call(data, key);
}
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = (nativeCreate && value === undefined$1) ? HASH_UNDEFINED : value;
return this;
}
// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
/*------------------------------------------------------------------------*/
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
this.size = 0;
}
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
--this.size;
return true;
}
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined$1 : data[index][1];
}
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
++this.size;
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
/*------------------------------------------------------------------------*/
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.size = 0;
this.__data__ = {
'hash': new Hash,
'map': new (Map || ListCache),
'string': new Hash
};
}
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
var result = getMapData(this, key)['delete'](key);
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;
data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}
// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
/*------------------------------------------------------------------------*/
/**
*
* Creates an array cache object to store unique values.
*
* @private
* @constructor
* @param {Array} [values] The values to cache.
*/
function SetCache(values) {
var index = -1,
length = values == null ? 0 : values.length;
this.__data__ = new MapCache;
while (++index < length) {
this.add(values[index]);
}
}
/**
* Adds `value` to the array cache.
*
* @private
* @name add
* @memberOf SetCache
* @alias push
* @param {*} value The value to cache.
* @returns {Object} Returns the cache instance.
*/
function setCacheAdd(value) {
this.__data__.set(value, HASH_UNDEFINED);
return this;
}
/**
* Checks if `value` is in the array cache.
*
* @private
* @name has
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {number} Returns `true` if `value` is found, else `false`.
*/
function setCacheHas(value) {
return this.__data__.has(value);
}
// Add methods to `SetCache`.
SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;
/*------------------------------------------------------------------------*/
/**
* Creates a stack cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Stack(entries) {
var data = this.__data__ = new ListCache(entries);
this.size = data.size;
}
/**
* Removes all key-value entries from the stack.
*
* @private
* @name clear
* @memberOf Stack
*/
function stackClear() {
this.__data__ = new ListCache;
this.size = 0;
}
/**
* Removes `key` and its value from the stack.
*
* @private
* @name delete
* @memberOf Stack
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function stackDelete(key) {
var data = this.__data__,
result = data['delete'](key);
this.size = data.size;
return result;
}
/**
* Gets the stack value for `key`.
*
* @private
* @name get
* @memberOf Stack
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function stackGet(key) {
return this.__data__.get(key);
}
/**
* Checks if a stack value for `key` exists.
*
* @private
* @name has
* @memberOf Stack
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function stackHas(key) {
return this.__data__.has(key);
}
/**
* Sets the stack `key` to `value`.
*
* @private
* @name set
* @memberOf Stack
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the stack cache instance.
*/
function stackSet(key, value) {
var data = this.__data__;
if (data instanceof ListCache) {
var pairs = data.__data__;
if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
pairs.push([key, value]);
this.size = ++data.size;
return this;
}
data = this.__data__ = new MapCache(pairs);
}
data.set(key, value);
this.size = data.size;
return this;
}
// Add methods to `Stack`.
Stack.prototype.clear = stackClear;
Stack.prototype['delete'] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;
/*------------------------------------------------------------------------*/
/**
* Creates an array of the enumerable property names of the array-like `value`.
*
* @private
* @param {*} value The value to query.
* @param {boolean} inherited Specify returning inherited property names.
* @returns {Array} Returns the array of property names.
*/
function arrayLikeKeys(value, inherited) {
var isArr = isArray(value),
isArg = !isArr && isArguments(value),
isBuff = !isArr && !isArg && isBuffer(value),
isType = !isArr && !isArg && !isBuff && isTypedArray(value),
skipIndexes = isArr || isArg || isBuff || isType,
result = skipIndexes ? baseTimes(value.length, String) : [],
length = result.length;
for (var key in value) {
if ((inherited || hasOwnProperty.call(value, key)) &&
!(skipIndexes && (
// Safari 9 has enumerable `arguments.length` in strict mode.
key == 'length' ||
// Node.js 0.10 has enumerable non-index properties on buffers.
(isBuff && (key == 'offset' || key == 'parent')) ||
// PhantomJS 2 has enumerable non-index properties on typed arrays.
(isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
// Skip index properties.
isIndex(key, length)
))) {
result.push(key);
}
}
return result;
}
/**
* A specialized version of `_.sample` for arrays.
*
* @private
* @param {Array} array The array to sample.
* @returns {*} Returns the random element.
*/
function arraySample(array) {
var length = array.length;
return length ? array[baseRandom(0, length - 1)] : undefined$1;
}
/**
* A specialized version of `_.sampleSize` for arrays.
*
* @private
* @param {Array} array The array to sample.
* @param {number} n The number of elements to sample.
* @returns {Array} Returns the random elements.
*/
function arraySampleSize(array, n) {
return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length));
}
/**
* A specialized version of `_.shuffle` for arrays.
*
* @private
* @param {Array} array The array to shuffle.
* @returns {Array} Returns the new shuffled array.
*/
function arrayShuffle(array) {
return shuffleSelf(copyArray(array));
}
/**
* This function is like `assignValue` except that it doesn't assign
* `undefined` values.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignMergeValue(object, key, value) {
if ((value !== undefined$1 && !eq(object[key], value)) ||
(value === undefined$1 && !(key in object))) {
baseAssignValue(object, key, value);
}
}
/**
* Assigns `value` to `key` of `object` if the existing value is not equivalent
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignValue(object, key, value) {
var objValue = object[key];
if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
(value === undefined$1 && !(key in object))) {
baseAssignValue(object, key, value);
}
}
/**
* Gets the index at which the `key` is found in `array` of key-value pairs.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (eq(array[length][0], key)) {
return length;
}
}
return -1;
}
/**
* Aggregates elements of `collection` on `accumulator` with keys transformed
* by `iteratee` and values set by `setter`.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} setter The function to set `accumulator` values.
* @param {Function} iteratee The iteratee to transform keys.
* @param {Object} accumulator The initial aggregated object.
* @returns {Function} Returns `accumulator`.
*/
function baseAggregator(collection, setter, iteratee, accumulator) {
baseEach(collection, function(value, key, collection) {
setter(accumulator, value, iteratee(value), collection);
});
return accumulator;
}
/**
* The base implementation of `_.assign` without support for multiple sources
* or `customizer` functions.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @returns {Object} Returns `object`.
*/
function baseAssign(object, source) {
return object && copyObject(source, keys(source), object);
}
/**
* The base implementation of `_.assignIn` without support for multiple sources
* or `customizer` functions.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @returns {Object} Returns `object`.
*/
function baseAssignIn(object, source) {
return object && copyObject(source, keysIn(source), object);
}
/**
* The base implementation of `assignValue` and `assignMergeValue` without
* value checks.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function baseAssignValue(object, key, value) {
if (key == '__proto__' && defineProperty) {
defineProperty(object, key, {
'configurable': true,
'enumerable': true,
'value': value,
'writable': true
});
} else {
object[key] = value;
}
}
/**
* The base implementation of `_.at` without support for individual paths.
*
* @private
* @param {Object} object The object to iterate over.
* @param {string[]} paths The property paths to pick.
* @returns {Array} Returns the picked elements.
*/
function baseAt(object, paths) {
var index = -1,
length = paths.length,
result = Array(length),
skip = object == null;
while (++index < length) {
result[index] = skip ? undefined$1 : get(object, paths[index]);
}
return result;
}
/**
* The base implementation of `_.clamp` which doesn't coerce arguments.
*
* @private
* @param {number} number The number to clamp.
* @param {number} [lower] The lower bound.
* @param {number} upper The upper bound.
* @returns {number} Returns the clamped number.
*/
function baseClamp(number, lower, upper) {
if (number === number) {
if (upper !== undefined$1) {
number = number <= upper ? number : upper;
}
if (lower !== undefined$1) {
number = number >= lower ? number : lower;
}
}
return number;
}
/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} bitmask The bitmask flags.
* 1 - Deep clone
* 2 - Flatten inherited properties
* 4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
* @returns {*} Returns the cloned value.
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
var result,
isDeep = bitmask & CLONE_DEEP_FLAG,
isFlat = bitmask & CLONE_FLAT_FLAG,
isFull = bitmask & CLONE_SYMBOLS_FLAG;
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined$1) {
return result;
}
if (!isObject(value)) {
return value;
}
var isArr = isArray(value);
if (isArr) {
result = initCloneArray(value);
if (!isDeep) {
return copyArray(value, result);
}
} else {
var tag = getTag(value),
isFunc = tag == funcTag || tag == genTag;
if (isBuffer(value)) {
return cloneBuffer(value, isDeep);
}
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
result = (isFlat || isFunc) ? {} : initCloneObject(value);
if (!isDeep) {
return isFlat
? copySymbolsIn(value, baseAssignIn(result, value))
: copySymbols(value, baseAssign(result, value));
}
} else {
if (!cloneableTags[tag]) {
return object ? value : {};
}
result = initCloneByTag(value, tag, isDeep);
}
}
// Check for circular references and return its corresponding clone.
stack || (stack = new Stack);
var stacked = stack.get(value);
if (stacked) {
return stacked;
}
stack.set(value, result);
if (isSet(value)) {
value.forEach(function(subValue) {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
});
} else if (isMap(value)) {
value.forEach(function(subValue, key) {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
}
var keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys);
var props = isArr ? undefined$1 : keysFunc(value);
arrayEach(props || value, function(subValue, key) {
if (props) {
key = subValue;
subValue = value[key];
}
// Recursively populate clone (susceptible to call stack limits).
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
return result;
}
/**
* The base implementation of `_.conforms` which doesn't clone `source`.
*
* @private
* @param {Object} source The object of property predicates to conform to.
* @returns {Function} Returns the new spec function.
*/
function baseConforms(source) {
var props = keys(source);
return function(object) {
return baseConformsTo(object, source, props);
};
}
/**
* The base implementation of `_.conformsTo` which accepts `props` to check.
*
* @private
* @param {Object} object The object to inspect.
* @param {Object} source The object of property predicates to conform to.
* @returns {boolean} Returns `true` if `object` conforms, else `false`.
*/
function baseConformsTo(object, source, props) {
var length = props.length;
if (object == null) {
return !length;
}
object = Object(object);
while (length--) {
var key = props[length],
predicate = source[key],
value = object[key];
if ((value === undefined$1 && !(key in object)) || !predicate(value)) {
return false;
}
}
return true;
}
/**
* The base implementation of `_.delay` and `_.defer` which accepts `args`
* to provide to `func`.
*
* @private
* @param {Function} func The function to delay.
* @param {number} wait The number of milliseconds to delay invocation.
* @param {Array} args The arguments to provide to `func`.
* @returns {number|Object} Returns the timer id or timeout object.
*/
function baseDelay(func, wait, args) {
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
return setTimeout(function() { func.apply(undefined$1, args); }, wait);
}
/**
* The base implementation of methods like `_.difference` without support
* for excluding multiple arrays or iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Array} values The values to exclude.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of filtered values.
*/
function baseDifference(array, values, iteratee, comparator) {
var index = -1,
includes = arrayIncludes,
isCommon = true,
length = array.length,
result = [],
valuesLength = values.length;
if (!length) {
return result;
}
if (iteratee) {
values = arrayMap(values, baseUnary(iteratee));
}
if (comparator) {
includes = arrayIncludesWith;
isCommon = false;
}
else if (values.length >= LARGE_ARRAY_SIZE) {
includes = cacheHas;
isCommon = false;
values = new SetCache(values);
}
outer:
while (++index < length) {
var value = array[index],
computed = iteratee == null ? value : iteratee(value);
value = (comparator || value !== 0) ? value : 0;
if (isCommon && computed === computed) {
var valuesIndex = valuesLength;
while (valuesIndex--) {
if (values[valuesIndex] === computed) {
continue outer;
}
}
result.push(value);
}
else if (!includes(values, computed, comparator)) {
result.push(value);
}
}
return result;
}
/**
* The base implementation of `_.forEach` without support for iteratee shorthands.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array|Object} Returns `collection`.
*/
var baseEach = createBaseEach(baseForOwn);
/**
* The base implementation of `_.forEachRight` without support for iteratee shorthands.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array|Object} Returns `collection`.
*/
var baseEachRight = createBaseEach(baseForOwnRight, true);
/**
* The base implementation of `_.every` without support for iteratee shorthands.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {boolean} Returns `true` if all elements pass the predicate check,
* else `false`
*/
function baseEvery(collection, predicate) {
var result = true;
baseEach(collection, function(value, index, collection) {
result = !!predicate(value, index, collection);
return result;
});
return result;
}
/**
* The base implementation of methods like `_.max` and `_.min` which accepts a
* `comparator` to determine the extremum value.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} iteratee The iteratee invoked per iteration.
* @param {Function} comparator The comparator used to compare values.
* @returns {*} Returns the extremum value.
*/
function baseExtremum(array, iteratee, comparator) {
var index = -1,
length = array.length;
while (++index < length) {
var value = array[index],
current = iteratee(value);
if (current != null && (computed === undefined$1
? (current === current && !isSymbol(current))
: comparator(current, computed)
)) {
var computed = current,
result = value;
}
}
return result;
}
/**
* The base implementation of `_.fill` without an iteratee call guard.
*
* @private
* @param {Array} array The array to fill.
* @param {*} value The value to fill `array` with.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns `array`.
*/
function baseFill(array, value, start, end) {
var length = array.length;
start = toInteger(start);
if (start < 0) {
start = -start > length ? 0 : (length + start);
}
end = (end === undefined$1 || end > length) ? length : toInteger(end);
if (end < 0) {
end += length;
}
end = start > end ? 0 : toLength(end);
while (start < end) {
array[start++] = value;
}
return array;
}
/**
* The base implementation of `_.filter` without support for iteratee shorthands.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {Array} Returns the new filtered array.
*/
function baseFilter(collection, predicate) {
var result = [];
baseEach(collection, function(value, index, collection) {
if (predicate(value, index, collection)) {
result.push(value);
}
});
return result;
}
/**
* The base implementation of `_.flatten` with support for restricting flattening.
*
* @private
* @param {Array} array The array to flatten.
* @param {number} depth The maximum recursion depth.
* @param {boolean} [predicate=isFlattenable] The function invoked per iteration.
* @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
* @param {Array} [result=[]] The initial result value.
* @returns {Array} Returns the new flattened array.
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
var index = -1,
length = array.length;
predicate || (predicate = isFlattenable);
result || (result = []);
while (++index < length) {
var value = array[index];
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
arrayPush(result, value);
}
} else if (!isStrict) {
result[result.length] = value;
}
}
return result;
}
/**
* The base implementation of `baseForOwn` which iterates over `object`
* properties returned by `keysFunc` and invokes `iteratee` for each property.
* Iteratee functions may exit iteration early by explicitly returning `false`.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {Function} keysFunc The function to get the keys of `object`.
* @returns {Object} Returns `object`.
*/
var baseFor = createBaseFor();
/**
* This function is like `baseFor` except that it iterates over properties
* in the opposite order.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {Function} keysFunc The function to get the keys of `object`.
* @returns {Object} Returns `object`.
*/
var baseForRight = createBaseFor(true);
/**
* The base implementation of `_.forOwn` without support for iteratee shorthands.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Object} Returns `object`.
*/
function baseForOwn(object, iteratee) {
return object && baseFor(object, iteratee, keys);
}
/**
* The base implementation of `_.forOwnRight` without support for iteratee shorthands.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Object} Returns `object`.
*/
function baseForOwnRight(object, iteratee) {
return object && baseForRight(object, iteratee, keys);
}
/**
* The base implementation of `_.functions` which creates an array of
* `object` function property names filtered from `props`.
*
* @private
* @param {Object} object The object to inspect.
* @param {Array} props The property names to filter.
* @returns {Array} Returns the function names.
*/
function baseFunctions(object, props) {
return arrayFilter(props, function(key) {
return isFunction(object[key]);
});
}
/**
* The base implementation of `_.get` without support for default values.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @returns {*} Returns the resolved value.
*/
function baseGet(object, path) {
path = castPath(path, object);
var index = 0,
length = path.length;
while (object != null && index < length) {
object = object[toKey(path[index++])];
}
return (index && index == length) ? object : undefined$1;
}
/**
* The base implementation of `getAllKeys` and `getAllKeysIn` which uses
* `keysFunc` and `symbolsFunc` to get the enumerable property names and
* symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Function} keysFunc The function to get the keys of `object`.
* @param {Function} symbolsFunc The function to get the symbols of `object`.
* @returns {Array} Returns the array of property names and symbols.
*/
function baseGetAllKeys(object, keysFunc, symbolsFunc) {
var result = keysFunc(object);
return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
}
/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value) {
if (value == null) {
return value === undefined$1 ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}
/**
* The base implementation of `_.gt` which doesn't coerce arguments.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if `value` is greater than `other`,
* else `false`.
*/
function baseGt(value, other) {
return value > other;
}
/**
* The base implementation of `_.has` without support for deep paths.
*
* @private
* @param {Object} [object] The object to query.
* @param {Array|string} key The key to check.
* @returns {boolean} Returns `true` if `key` exists, else `false`.
*/
function baseHas(object, key) {
return object != null && hasOwnProperty.call(object, key);
}
/**
* The base implementation of `_.hasIn` without support for deep paths.
*
* @private
* @param {Object} [object] The object to query.
* @param {Array|string} key The key to check.
* @returns {boolean} Returns `true` if `key` exists, else `false`.
*/
function baseHasIn(object, key) {
return object != null && key in Object(object);
}
/**
* The base implementation of `_.inRange` which doesn't coerce arguments.
*
* @private
* @param {number} number The number to check.
* @param {number} start The start of the range.
* @param {number} end The end of the range.
* @returns {boolean} Returns `true` if `number` is in the range, else `false`.
*/
function baseInRange(number, start, end) {
return number >= nativeMin(start, end) && number < nativeMax(start, end);
}
/**
* The base implementation of methods like `_.intersection`, without support
* for iteratee shorthands, that accepts an array of arrays to inspect.
*
* @private
* @param {Array} arrays The arrays to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of shared values.
*/
function baseIntersection(arrays, iteratee, comparator) {
var includes = comparator ? arrayIncludesWith : arrayIncludes,
length = arrays[0].length,
othLength = arrays.length,
othIndex = othLength,
caches = Array(othLength),
maxLength = Infinity,
result = [];
while (othIndex--) {
var array = arrays[othIndex];
if (othIndex && iteratee) {
array = arrayMap(array, baseUnary(iteratee));
}
maxLength = nativeMin(array.length, maxLength);
caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120))
? new SetCache(othIndex && array)
: undefined$1;
}
array = arrays[0];
var index = -1,
seen = caches[0];
outer:
while (++index < length && result.length < maxLength) {
var value = array[index],
computed = iteratee ? iteratee(value) : value;
value = (comparator || value !== 0) ? value : 0;
if (!(seen
? cacheHas(seen, computed)
: includes(result, computed, comparator)
)) {
othIndex = othLength;
while (--othIndex) {
var cache = caches[othIndex];
if (!(cache
? cacheHas(cache, computed)
: includes(arrays[othIndex], computed, comparator))
) {
continue outer;
}
}
if (seen) {
seen.push(computed);
}
result.push(value);
}
}
return result;
}
/**
* The base implementation of `_.invert` and `_.invertBy` which inverts
* `object` with values transformed by `iteratee` and set by `setter`.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} setter The function to set `accumulator` values.
* @param {Function} iteratee The iteratee to transform values.
* @param {Object} accumulator The initial inverted object.
* @returns {Function} Returns `accumulator`.
*/
function baseInverter(object, setter, iteratee, accumulator) {
baseForOwn(object, function(value, key, object) {
setter(accumulator, iteratee(value), key, object);
});
return accumulator;
}
/**
* The base implementation of `_.invoke` without support for individual
* method arguments.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the method to invoke.
* @param {Array} args The arguments to invoke the method with.
* @returns {*} Returns the result of the invoked method.
*/
function baseInvoke(object, path, args) {
path = castPath(path, object);
object = parent(object, path);
var func = object == null ? object : object[toKey(last(path))];
return func == null ? undefined$1 : apply(func, object, args);
}
/**
* The base implementation of `_.isArguments`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
*/
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
/**
* The base implementation of `_.isArrayBuffer` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.
*/
function baseIsArrayBuffer(value) {
return isObjectLike(value) && baseGetTag(value) == arrayBufferTag;
}
/**
* The base implementation of `_.isDate` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a date object, else `false`.
*/
function baseIsDate(value) {
return isObjectLike(value) && baseGetTag(value) == dateTag;
}
/**
* The base implementation of `_.isEqual` which supports partial comparisons
* and tracks traversed objects.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @param {boolean} bitmask The bitmask flags.
* 1 - Unordered comparison
* 2 - Partial comparison
* @param {Function} [customizer] The function to customize comparisons.
* @param {Object} [stack] Tracks traversed `value` and `other` objects.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
*/
function baseIsEqual(value, other, bitmask, customizer, stack) {
if (value === other) {
return true;
}
if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
return value !== value && other !== other;
}
return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
}
/**
* A specialized version of `baseIsEqual` for arrays and objects which performs
* deep comparisons and tracks traversed objects enabling objects with circular
* references to be compared.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} [stack] Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {
var objIsArr = isArray(object),
othIsArr = isArray(other),
objTag = objIsArr ? arrayTag : getTag(object),
othTag = othIsArr ? arrayTag : getTag(other);
objTag = objTag == argsTag ? objectTag : objTag;
othTag = othTag == argsTag ? objectTag : othTag;
var objIsObj = objTag == objectTag,
othIsObj = othTag == objectTag,
isSameTag = objTag == othTag;
if (isSameTag && isBuffer(object)) {
if (!isBuffer(other)) {
return false;
}
objIsArr = true;
objIsObj = false;
}
if (isSameTag && !objIsObj) {
stack || (stack = new Stack);
return (objIsArr || isTypedArray(object))
? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
: equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
}
if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
if (objIsWrapped || othIsWrapped) {
var objUnwrapped = objIsWrapped ? object.value() : object,
othUnwrapped = othIsWrapped ? other.value() : other;
stack || (stack = new Stack);
return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);
}
}
if (!isSameTag) {
return false;
}
stack || (stack = new Stack);
return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
}
/**
* The base implementation of `_.isMap` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a map, else `false`.
*/
function baseIsMap(value) {
return isObjectLike(value) && getTag(value) == mapTag;
}
/**
* The base implementation of `_.isMatch` without support for iteratee shorthands.
*
* @private
* @param {Object} object The object to inspect.
* @param {Object} source The object of property values to match.
* @param {Array} matchData The property names, values, and compare flags to match.
* @param {Function} [customizer] The function to customize comparisons.
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
*/
function baseIsMatch(object, source, matchData, customizer) {
var index = matchData.length,
length = index,
noCustomizer = !customizer;
if (object == null) {
return !length;
}
object = Object(object);
while (index--) {
var data = matchData[index];
if ((noCustomizer && data[2])
? data[1] !== object[data[0]]
: !(data[0] in object)
) {
return false;
}
}
while (++index < length) {
data = matchData[index];
var key = data[0],
objValue = object[key],
srcValue = data[1];
if (noCustomizer && data[2]) {
if (objValue === undefined$1 && !(key in object)) {
return false;
}
} else {
var stack = new Stack;
if (customizer) {
var result = customizer(objValue, srcValue, key, object, source, stack);
}
if (!(result === undefined$1
? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack)
: result
)) {
return false;
}
}
}
return true;
}
/**
* The base implementation of `_.isNative` without bad shim checks.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
*/
function baseIsNative(value) {
if (!isObject(value) || isMasked(value)) {
return false;
}
var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
return pattern.test(toSource(value));
}
/**
* The base implementation of `_.isRegExp` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a regexp, else `false`.
*/
function baseIsRegExp(value) {
return isObjectLike(value) && baseGetTag(value) == regexpTag;
}
/**
* The base implementation of `_.isSet` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a set, else `false`.
*/
function baseIsSet(value) {
return isObjectLike(value) && getTag(value) == setTag;
}
/**
* The base implementation of `_.isTypedArray` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
*/
function baseIsTypedArray(value) {
return isObjectLike(value) &&
isLength(value.length) && !!typedArrayTags[baseGetTag(value)];
}
/**
* The base implementation of `_.iteratee`.
*
* @private
* @param {*} [value=_.identity] The value to convert to an iteratee.
* @returns {Function} Returns the iteratee.
*/
function baseIteratee(value) {
// Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.
// See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.
if (typeof value == 'function') {
return value;
}
if (value == null) {
return identity;
}
if (typeof value == 'object') {
return isArray(value)
? baseMatchesProperty(value[0], value[1])
: baseMatches(value);
}
return property(value);
}
/**
* The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeys(object) {
if (!isPrototype(object)) {
return nativeKeys(object);
}
var result = [];
for (var key in Object(object)) {
if (hasOwnProperty.call(object, key) && key != 'constructor') {
result.push(key);
}
}
return result;
}
/**
* The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeysIn(object) {
if (!isObject(object)) {
return nativeKeysIn(object);
}
var isProto = isPrototype(object),
result = [];
for (var key in object) {
if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
result.push(key);
}
}
return result;
}
/**
* The base implementation of `_.lt` which doesn't coerce arguments.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if `value` is less than `other`,
* else `false`.
*/
function baseLt(value, other) {
return value < other;
}
/**
* The base implementation of `_.map` without support for iteratee shorthands.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the new mapped array.
*/
function baseMap(collection, iteratee) {
var index = -1,
result = isArrayLike(collection) ? Array(collection.length) : [];
baseEach(collection, function(value, key, collection) {
result[++index] = iteratee(value, key, collection);
});
return result;
}
/**
* The base implementation of `_.matches` which doesn't clone `source`.
*
* @private
* @param {Object} source The object of property values to match.
* @returns {Function} Returns the new spec function.
*/
function baseMatches(source) {
var matchData = getMatchData(source);
if (matchData.length == 1 && matchData[0][2]) {
return matchesStrictComparable(matchData[0][0], matchData[0][1]);
}
return function(object) {
return object === source || baseIsMatch(object, source, matchData);
};
}
/**
* The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.
*
* @private
* @param {string} path The path of the property to get.
* @param {*} srcValue The value to match.
* @returns {Function} Returns the new spec function.
*/
function baseMatchesProperty(path, srcValue) {
if (isKey(path) && isStrictComparable(srcValue)) {
return matchesStrictComparable(toKey(path), srcValue);
}
return function(object) {
var objValue = get(object, path);
return (objValue === undefined$1 && objValue === srcValue)
? hasIn(object, path)
: baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG);
};
}
/**
* The base implementation of `_.merge` without support for multiple sources.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @param {number} srcIndex The index of `source`.
* @param {Function} [customizer] The function to customize merged values.
* @param {Object} [stack] Tracks traversed source values and their merged
* counterparts.
*/
function baseMerge(object, source, srcIndex, customizer, stack) {
if (object === source) {
return;
}
baseFor(source, function(srcValue, key) {
stack || (stack = new Stack);
if (isObject(srcValue)) {
baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
}
else {
var newValue = customizer
? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)
: undefined$1;
if (newValue === undefined$1) {
newValue = srcValue;
}
assignMergeValue(object, key, newValue);
}
}, keysIn);
}
/**
* A specialized version of `baseMerge` for arrays and objects which performs
* deep merges and tracks traversed objects enabling objects with circular
* references to be merged.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @param {string} key The key of the value to merge.
* @param {number} srcIndex The index of `source`.
* @param {Function} mergeFunc The function to merge values.
* @param {Function} [customizer] The function to customize assigned values.
* @param {Object} [stack] Tracks traversed source values and their merged
* counterparts.
*/
function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
var objValue = safeGet(object, key),
srcValue = safeGet(source, key),
stacked = stack.get(srcValue);
if (stacked) {
assignMergeValue(object, key, stacked);
return;
}
var newValue = customizer
? customizer(objValue, srcValue, (key + ''), object, source, stack)
: undefined$1;
var isCommon = newValue === undefined$1;
if (isCommon) {
var isArr = isArray(srcValue),
isBuff = !isArr && isBuffer(srcValue),
isTyped = !isArr && !isBuff && isTypedArray(srcValue);
newValue = srcValue;
if (isArr || isBuff || isTyped) {
if (isArray(objValue)) {
newValue = objValue;
}
else if (isArrayLikeObject(objValue)) {
newValue = copyArray(objValue);
}
else if (isBuff) {
isCommon = false;
newValue = cloneBuffer(srcValue, true);
}
else if (isTyped) {
isCommon = false;
newValue = cloneTypedArray(srcValue, true);
}
else {
newValue = [];
}
}
else if (isPlainObject(srcValue) || isArguments(srcValue)) {
newValue = objValue;
if (isArguments(objValue)) {
newValue = toPlainObject(objValue);
}
else if (!isObject(objValue) || isFunction(objValue)) {
newValue = initCloneObject(srcValue);
}
}
else {
isCommon = false;
}
}
if (isCommon) {
// Recursively merge objects and arrays (susceptible to call stack limits).
stack.set(srcValue, newValue);
mergeFunc(newValue, srcValue, srcIndex, customizer, stack);
stack['delete'](srcValue);
}
assignMergeValue(object, key, newValue);
}
/**
* The base implementation of `_.nth` which doesn't coerce arguments.
*
* @private
* @param {Array} array The array to query.
* @param {number} n The index of the element to return.
* @returns {*} Returns the nth element of `array`.
*/
function baseNth(array, n) {
var length = array.length;
if (!length) {
return;
}
n += n < 0 ? length : 0;
return isIndex(n, length) ? array[n] : undefined$1;
}
/**
* The base implementation of `_.orderBy` without param guards.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by.
* @param {string[]} orders The sort orders of `iteratees`.
* @returns {Array} Returns the new sorted array.
*/
function baseOrderBy(collection, iteratees, orders) {
if (iteratees.length) {
iteratees = arrayMap(iteratees, function(iteratee) {
if (isArray(iteratee)) {
return function(value) {
return baseGet(value, iteratee.length === 1 ? iteratee[0] : iteratee);
};
}
return iteratee;
});
} else {
iteratees = [identity];
}
var index = -1;
iteratees = arrayMap(iteratees, baseUnary(getIteratee()));
var result = baseMap(collection, function(value, key, collection) {
var criteria = arrayMap(iteratees, function(iteratee) {
return iteratee(value);
});
return { 'criteria': criteria, 'index': ++index, 'value': value };
});
return baseSortBy(result, function(object, other) {
return compareMultiple(object, other, orders);
});
}
/**
* The base implementation of `_.pick` without support for individual
* property identifiers.
*
* @private
* @param {Object} object The source object.
* @param {string[]} paths The property paths to pick.
* @returns {Object} Returns the new object.
*/
function basePick(object, paths) {
return basePickBy(object, paths, function(value, path) {
return hasIn(object, path);
});
}
/**
* The base implementation of `_.pickBy` without support for iteratee shorthands.
*
* @private
* @param {Object} object The source object.
* @param {string[]} paths The property paths to pick.
* @param {Function} predicate The function invoked per property.
* @returns {Object} Returns the new object.
*/
function basePickBy(object, paths, predicate) {
var index = -1,
length = paths.length,
result = {};
while (++index < length) {
var path = paths[index],
value = baseGet(object, path);
if (predicate(value, path)) {
baseSet(result, castPath(path, object), value);
}
}
return result;
}
/**
* A specialized version of `baseProperty` which supports deep paths.
*
* @private
* @param {Array|string} path The path of the property to get.
* @returns {Function} Returns the new accessor function.
*/
function basePropertyDeep(path) {
return function(object) {
return baseGet(object, path);
};
}
/**
* The base implementation of `_.pullAllBy` without support for iteratee
* shorthands.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns `array`.
*/
function basePullAll(array, values, iteratee, comparator) {
var indexOf = comparator ? baseIndexOfWith : baseIndexOf,
index = -1,
length = values.length,
seen = array;
if (array === values) {
values = copyArray(values);
}
if (iteratee) {
seen = arrayMap(array, baseUnary(iteratee));
}
while (++index < length) {
var fromIndex = 0,
value = values[index],
computed = iteratee ? iteratee(value) : value;
while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {
if (seen !== array) {
splice.call(seen, fromIndex, 1);
}
splice.call(array, fromIndex, 1);
}
}
return array;
}
/**
* The base implementation of `_.pullAt` without support for individual
* indexes or capturing the removed elements.
*
* @private
* @param {Array} array The array to modify.
* @param {number[]} indexes The indexes of elements to remove.
* @returns {Array} Returns `array`.
*/
function basePullAt(array, indexes) {
var length = array ? indexes.length : 0,
lastIndex = length - 1;
while (length--) {
var index = indexes[length];
if (length == lastIndex || index !== previous) {
var previous = index;
if (isIndex(index)) {
splice.call(array, index, 1);
} else {
baseUnset(array, index);
}
}
}
return array;
}
/**
* The base implementation of `_.random` without support for returning
* floating-point numbers.
*
* @private
* @param {number} lower The lower bound.
* @param {number} upper The upper bound.
* @returns {number} Returns the random number.
*/
function baseRandom(lower, upper) {
return lower + nativeFloor(nativeRandom() * (upper - lower + 1));
}
/**
* The base implementation of `_.range` and `_.rangeRight` which doesn't
* coerce arguments.
*
* @private
* @param {number} start The start of the range.
* @param {number} end The end of the range.
* @param {number} step The value to increment or decrement by.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Array} Returns the range of numbers.
*/
function baseRange(start, end, step, fromRight) {
var index = -1,
length = nativeMax(nativeCeil((end - start) / (step || 1)), 0),
result = Array(length);
while (length--) {
result[fromRight ? length : ++index] = start;
start += step;
}
return result;
}
/**
* The base implementation of `_.repeat` which doesn't coerce arguments.
*
* @private
* @param {string} string The string to repeat.
* @param {number} n The number of times to repeat the string.
* @returns {string} Returns the repeated string.
*/
function baseRepeat(string, n) {
var result = '';
if (!string || n < 1 || n > MAX_SAFE_INTEGER) {
return result;
}
// Leverage the exponentiation by squaring algorithm for a faster repeat.
// See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details.
do {
if (n % 2) {
result += string;
}
n = nativeFloor(n / 2);
if (n) {
string += string;
}
} while (n);
return result;
}
/**
* The base implementation of `_.rest` which doesn't validate or coerce arguments.
*
* @private
* @param {Function} func The function to apply a rest parameter to.
* @param {number} [start=func.length-1] The start position of the rest parameter.
* @returns {Function} Returns the new function.
*/
function baseRest(func, start) {
return setToString(overRest(func, start, identity), func + '');
}
/**
* The base implementation of `_.sample`.
*
* @private
* @param {Array|Object} collection The collection to sample.
* @returns {*} Returns the random element.
*/
function baseSample(collection) {
return arraySample(values(collection));
}
/**
* The base implementation of `_.sampleSize` without param guards.
*
* @private
* @param {Array|Object} collection The collection to sample.
* @param {number} n The number of elements to sample.
* @returns {Array} Returns the random elements.
*/
function baseSampleSize(collection, n) {
var array = values(collection);
return shuffleSelf(array, baseClamp(n, 0, array.length));
}
/**
* The base implementation of `_.set`.
*
* @private
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to set.
* @param {*} value The value to set.
* @param {Function} [customizer] The function to customize path creation.
* @returns {Object} Returns `object`.
*/
function baseSet(object, path, value, customizer) {
if (!isObject(object)) {
return object;
}
path = castPath(path, object);
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
while (nested != null && ++index < length) {
var key = toKey(path[index]),
newValue = value;
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
return object;
}
if (index != lastIndex) {
var objValue = nested[key];
newValue = customizer ? customizer(objValue, key, nested) : undefined$1;
if (newValue === undefined$1) {
newValue = isObject(objValue)
? objValue
: (isIndex(path[index + 1]) ? [] : {});
}
}
assignValue(nested, key, newValue);
nested = nested[key];
}
return object;
}
/**
* The base implementation of `setData` without support for hot loop shorting.
*
* @private
* @param {Function} func The function to associate metadata with.
* @param {*} data The metadata.
* @returns {Function} Returns `func`.
*/
var baseSetData = !metaMap ? identity : function(func, data) {
metaMap.set(func, data);
return func;
};
/**
* The base implementation of `setToString` without support for hot loop shorting.
*
* @private
* @param {Function} func The function to modify.
* @param {Function} string The `toString` result.
* @returns {Function} Returns `func`.
*/
var baseSetToString = !defineProperty ? identity : function(func, string) {
return defineProperty(func, 'toString', {
'configurable': true,
'enumerable': false,
'value': constant(string),
'writable': true
});
};
/**
* The base implementation of `_.shuffle`.
*
* @private
* @param {Array|Object} collection The collection to shuffle.
* @returns {Array} Returns the new shuffled array.
*/
function baseShuffle(collection) {
return shuffleSelf(values(collection));
}
/**
* The base implementation of `_.slice` without an iteratee call guard.
*
* @private
* @param {Array} array The array to slice.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the slice of `array`.
*/
function baseSlice(array, start, end) {
var index = -1,
length = array.length;
if (start < 0) {
start = -start > length ? 0 : (length + start);
}
end = end > length ? length : end;
if (end < 0) {
end += length;
}
length = start > end ? 0 : ((end - start) >>> 0);
start >>>= 0;
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
/**
* The base implementation of `_.some` without support for iteratee shorthands.
*
* @private
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {boolean} Returns `true` if any element passes the predicate check,
* else `false`.
*/
function baseSome(collection, predicate) {
var result;
baseEach(collection, function(value, index, collection) {
result = predicate(value, index, collection);
return !result;
});
return !!result;
}
/**
* The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which
* performs a binary search of `array` to determine the index at which `value`
* should be inserted into `array` in order to maintain its sort order.
*
* @private
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {boolean} [retHighest] Specify returning the highest qualified index.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
*/
function baseSortedIndex(array, value, retHighest) {
var low = 0,
high = array == null ? low : array.length;
if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {
while (low < high) {
var mid = (low + high) >>> 1,
computed = array[mid];
if (computed !== null && !isSymbol(computed) &&
(retHighest ? (computed <= value) : (computed < value))) {
low = mid + 1;
} else {
high = mid;
}
}
return high;
}
return baseSortedIndexBy(array, value, identity, retHighest);
}
/**
* The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`
* which invokes `iteratee` for `value` and each element of `array` to compute
* their sort ranking. The iteratee is invoked with one argument; (value).
*
* @private
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {Function} iteratee The iteratee invoked per element.
* @param {boolean} [retHighest] Specify returning the highest qualified index.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
*/
function baseSortedIndexBy(array, value, iteratee, retHighest) {
var low = 0,
high = array == null ? 0 : array.length;
if (high === 0) {
return 0;
}
value = iteratee(value);
var valIsNaN = value !== value,
valIsNull = value === null,
valIsSymbol = isSymbol(value),
valIsUndefined = value === undefined$1;
while (low < high) {
var mid = nativeFloor((low + high) / 2),
computed = iteratee(array[mid]),
othIsDefined = computed !== undefined$1,
othIsNull = computed === null,
othIsReflexive = computed === computed,
othIsSymbol = isSymbol(computed);
if (valIsNaN) {
var setLow = retHighest || othIsReflexive;
} else if (valIsUndefined) {
setLow = othIsReflexive && (retHighest || othIsDefined);
} else if (valIsNull) {
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
} else if (valIsSymbol) {
setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);
} else if (othIsNull || othIsSymbol) {
setLow = false;
} else {
setLow = retHighest ? (computed <= value) : (computed < value);
}
if (setLow) {
low = mid + 1;
} else {
high = mid;
}
}
return nativeMin(high, MAX_ARRAY_INDEX);
}
/**
* The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without
* support for iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @returns {Array} Returns the new duplicate free array.
*/
function baseSortedUniq(array, iteratee) {
var index = -1,
length = array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index],
computed = iteratee ? iteratee(value) : value;
if (!index || !eq(computed, seen)) {
var seen = computed;
result[resIndex++] = value === 0 ? 0 : value;
}
}
return result;
}
/**
* The base implementation of `_.toNumber` which doesn't ensure correct
* conversions of binary, hexadecimal, or octal string values.
*
* @private
* @param {*} value The value to process.
* @returns {number} Returns the number.
*/
function baseToNumber(value) {
if (typeof value == 'number') {
return value;
}
if (isSymbol(value)) {
return NAN;
}
return +value;
}
/**
* The base implementation of `_.toString` which doesn't convert nullish
* values to empty strings.
*
* @private
* @param {*} value The value to process.
* @returns {string} Returns the string.
*/
function baseToString(value) {
// Exit early for strings to avoid a performance hit in some environments.
if (typeof value == 'string') {
return value;
}
if (isArray(value)) {
// Recursively convert values (susceptible to call stack limits).
return arrayMap(value, baseToString) + '';
}
if (isSymbol(value)) {
return symbolToString ? symbolToString.call(value) : '';
}
var result = (value + '');
return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
}
/**
* The base implementation of `_.uniqBy` without support for iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new duplicate free array.
*/
function baseUniq(array, iteratee, comparator) {
var index = -1,
includes = arrayIncludes,
length = array.length,
isCommon = true,
result = [],
seen = result;
if (comparator) {
isCommon = false;
includes = arrayIncludesWith;
}
else if (length >= LARGE_ARRAY_SIZE) {
var set = iteratee ? null : createSet(array);
if (set) {
return setToArray(set);
}
isCommon = false;
includes = cacheHas;
seen = new SetCache;
}
else {
seen = iteratee ? [] : result;
}
outer:
while (++index < length) {
var value = array[index],
computed = iteratee ? iteratee(value) : value;
value = (comparator || value !== 0) ? value : 0;
if (isCommon && computed === computed) {
var seenIndex = seen.length;
while (seenIndex--) {
if (seen[seenIndex] === computed) {
continue outer;
}
}
if (iteratee) {
seen.push(computed);
}
result.push(value);
}
else if (!includes(seen, computed, comparator)) {
if (seen !== result) {
seen.push(computed);
}
result.push(value);
}
}
return result;
}
/**
* The base implementation of `_.unset`.
*
* @private
* @param {Object} object The object to modify.
* @param {Array|string} path The property path to unset.
* @returns {boolean} Returns `true` if the property is deleted, else `false`.
*/
function baseUnset(object, path) {
path = castPath(path, object);
// Prevent prototype pollution, see: https://github.com/lodash/lodash/security/advisories/GHSA-xxjr-mmjv-4gpg
var index = -1,
length = path.length;
if (!length) {
return true;
}
var isRootPrimitive = object == null || (typeof object !== 'object' && typeof object !== 'function');
while (++index < length) {
var key = path[index];
// skip non-string keys (e.g., Symbols, numbers)
if (typeof key !== 'string') {
continue;
}
// Always block "__proto__" anywhere in the path if it's not expected
if (key === '__proto__' && !hasOwnProperty.call(object, '__proto__')) {
return false;
}
// Block "constructor.prototype" chains
if (key === 'constructor' &&
(index + 1) < length &&
typeof path[index + 1] === 'string' &&
path[index + 1] === 'prototype') {
// Allow ONLY when the path starts at a primitive root, e.g., _.unset(0, 'constructor.prototype.a')
if (isRootPrimitive && index === 0) {
continue;
}
return false;
}
}
var obj = parent(object, path);
return obj == null || delete obj[toKey(last(path))];
}
/**
* The base implementation of `_.update`.
*
* @private
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to update.
* @param {Function} updater The function to produce the updated value.
* @param {Function} [customizer] The function to customize path creation.
* @returns {Object} Returns `object`.
*/
function baseUpdate(object, path, updater, customizer) {
return baseSet(object, path, updater(baseGet(object, path)), customizer);
}
/**
* The base implementation of methods like `_.dropWhile` and `_.takeWhile`
* without support for iteratee shorthands.
*
* @private
* @param {Array} array The array to query.
* @param {Function} predicate The function invoked per iteration.
* @param {boolean} [isDrop] Specify dropping elements instead of taking them.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Array} Returns the slice of `array`.
*/
function baseWhile(array, predicate, isDrop, fromRight) {
var length = array.length,
index = fromRight ? length : -1;
while ((fromRight ? index-- : ++index < length) &&
predicate(array[index], index, array)) {}
return isDrop
? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))
: baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));
}
/**
* The base implementation of `wrapperValue` which returns the result of
* performing a sequence of actions on the unwrapped `value`, where each
* successive action is supplied the return value of the previous.
*
* @private
* @param {*} value The unwrapped value.
* @param {Array} actions Actions to perform to resolve the unwrapped value.
* @returns {*} Returns the resolved value.
*/
function baseWrapperValue(value, actions) {
var result = value;
if (result instanceof LazyWrapper) {
result = result.value();
}
return arrayReduce(actions, function(result, action) {
return action.func.apply(action.thisArg, arrayPush([result], action.args));
}, result);
}
/**
* The base implementation of methods like `_.xor`, without support for
* iteratee shorthands, that accepts an array of arrays to inspect.
*
* @private
* @param {Array} arrays The arrays to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of values.
*/
function baseXor(arrays, iteratee, comparator) {
var length = arrays.length;
if (length < 2) {
return length ? baseUniq(arrays[0]) : [];
}
var index = -1,
result = Array(length);
while (++index < length) {
var array = arrays[index],
othIndex = -1;
while (++othIndex < length) {
if (othIndex != index) {
result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator);
}
}
}
return baseUniq(baseFlatten(result, 1), iteratee, comparator);
}
/**
* This base implementation of `_.zipObject` which assigns values using `assignFunc`.
*
* @private
* @param {Array} props The property identifiers.
* @param {Array} values The property values.
* @param {Function} assignFunc The function to assign values.
* @returns {Object} Returns the new object.
*/
function baseZipObject(props, values, assignFunc) {
var index = -1,
length = props.length,
valsLength = values.length,
result = {};
while (++index < length) {
var value = index < valsLength ? values[index] : undefined$1;
assignFunc(result, props[index], value);
}
return result;
}
/**
* Casts `value` to an empty array if it's not an array like object.
*
* @private
* @param {*} value The value to inspect.
* @returns {Array|Object} Returns the cast array-like object.
*/
function castArrayLikeObject(value) {
return isArrayLikeObject(value) ? value : [];
}
/**
* Casts `value` to `identity` if it's not a function.
*
* @private
* @param {*} value The value to inspect.
* @returns {Function} Returns cast function.
*/
function castFunction(value) {
return typeof value == 'function' ? value : identity;
}
/**
* Casts `value` to a path array if it's not one.
*
* @private
* @param {*} value The value to inspect.
* @param {Object} [object] The object to query keys on.
* @returns {Array} Returns the cast property path array.
*/
function castPath(value, object) {
if (isArray(value)) {
return value;
}
return isKey(value, object) ? [value] : stringToPath(toString(value));
}
/**
* A `baseRest` alias which can be replaced with `identity` by module
* replacement plugins.
*
* @private
* @type {Function}
* @param {Function} func The function to apply a rest parameter to.
* @returns {Function} Returns the new function.
*/
var castRest = baseRest;
/**
* Casts `array` to a slice if it's needed.
*
* @private
* @param {Array} array The array to inspect.
* @param {number} start The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the cast slice.
*/
function castSlice(array, start, end) {
var length = array.length;
end = end === undefined$1 ? length : end;
return (!start && end >= length) ? array : baseSlice(array, start, end);
}
/**
* A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout).
*
* @private
* @param {number|Object} id The timer id or timeout object of the timer to clear.
*/
var clearTimeout = ctxClearTimeout || function(id) {
return root.clearTimeout(id);
};
/**
* Creates a clone of `buffer`.
*
* @private
* @param {Buffer} buffer The buffer to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Buffer} Returns the cloned buffer.
*/
function cloneBuffer(buffer, isDeep) {
if (isDeep) {
return buffer.slice();
}
var length = buffer.length,
result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);
buffer.copy(result);
return result;
}
/**
* Creates a clone of `arrayBuffer`.
*
* @private
* @param {ArrayBuffer} arrayBuffer The array buffer to clone.
* @returns {ArrayBuffer} Returns the cloned array buffer.
*/
function cloneArrayBuffer(arrayBuffer) {
var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
new Uint8Array(result).set(new Uint8Array(arrayBuffer));
return result;
}
/**
* Creates a clone of `dataView`.
*
* @private
* @param {Object} dataView The data view to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned data view.
*/
function cloneDataView(dataView, isDeep) {
var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
}
/**
* Creates a clone of `regexp`.
*
* @private
* @param {Object} regexp The regexp to clone.
* @returns {Object} Returns the cloned regexp.
*/
function cloneRegExp(regexp) {
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
result.lastIndex = regexp.lastIndex;
return result;
}
/**
* Creates a clone of the `symbol` object.
*
* @private
* @param {Object} symbol The symbol object to clone.
* @returns {Object} Returns the cloned symbol object.
*/
function cloneSymbol(symbol) {
return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}
/**
* Creates a clone of `typedArray`.
*
* @private
* @param {Object} typedArray The typed array to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned typed array.
*/
function cloneTypedArray(typedArray, isDeep) {
var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
}
/**
* Compares values to sort them in ascending order.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {number} Returns the sort order indicator for `value`.
*/
function compareAscending(value, other) {
if (value !== other) {
var valIsDefined = value !== undefined$1,
valIsNull = value === null,
valIsReflexive = value === value,
valIsSymbol = isSymbol(value);
var othIsDefined = other !== undefined$1,
othIsNull = other === null,
othIsReflexive = other === other,
othIsSymbol = isSymbol(other);
if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) ||
(valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) ||
(valIsNull && othIsDefined && othIsReflexive) ||
(!valIsDefined && othIsReflexive) ||
!valIsReflexive) {
return 1;
}
if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) ||
(othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) ||
(othIsNull && valIsDefined && valIsReflexive) ||
(!othIsDefined && valIsReflexive) ||
!othIsReflexive) {
return -1;
}
}
return 0;
}
/**
* Used by `_.orderBy` to compare multiple properties of a value to another
* and stable sort them.
*
* If `orders` is unspecified, all values are sorted in ascending order. Otherwise,
* specify an order of "desc" for descending or "asc" for ascending sort order
* of corresponding values.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {boolean[]|string[]} orders The order to sort by for each property.
* @returns {number} Returns the sort order indicator for `object`.
*/
function compareMultiple(object, other, orders) {
var index = -1,
objCriteria = object.criteria,
othCriteria = other.criteria,
length = objCriteria.length,
ordersLength = orders.length;
while (++index < length) {
var result = compareAscending(objCriteria[index], othCriteria[index]);
if (result) {
if (index >= ordersLength) {
return result;
}
var order = orders[index];
return result * (order == 'desc' ? -1 : 1);
}
}
// Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications
// that causes it, under certain circumstances, to provide the same value for
// `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247
// for more details.
//
// This also ensures a stable sort in V8 and other engines.
// See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details.
return object.index - other.index;
}
/**
* Creates an array that is the composition of partially applied arguments,
* placeholders, and provided arguments into a single array of arguments.
*
* @private
* @param {Array} args The provided arguments.
* @param {Array} partials The arguments to prepend to those provided.
* @param {Array} holders The `partials` placeholder indexes.
* @params {boolean} [isCurried] Specify composing for a curried function.
* @returns {Array} Returns the new array of composed arguments.
*/
function composeArgs(args, partials, holders, isCurried) {
var argsIndex = -1,
argsLength = args.length,
holdersLength = holders.length,
leftIndex = -1,
leftLength = partials.length,
rangeLength = nativeMax(argsLength - holdersLength, 0),
result = Array(leftLength + rangeLength),
isUncurried = !isCurried;
while (++leftIndex < leftLength) {
result[leftIndex] = partials[leftIndex];
}
while (++argsIndex < holdersLength) {
if (isUncurried || argsIndex < argsLength) {
result[holders[argsIndex]] = args[argsIndex];
}
}
while (rangeLength--) {
result[leftIndex++] = args[argsIndex++];
}
return result;
}
/**
* This function is like `composeArgs` except that the arguments composition
* is tailored for `_.partialRight`.
*
* @private
* @param {Array} args The provided arguments.
* @param {Array} partials The arguments to append to those provided.
* @param {Array} holders The `partials` placeholder indexes.
* @params {boolean} [isCurried] Specify composing for a curried function.
* @returns {Array} Returns the new array of composed arguments.
*/
function composeArgsRight(args, partials, holders, isCurried) {
var argsIndex = -1,
argsLength = args.length,
holdersIndex = -1,
holdersLength = holders.length,
rightIndex = -1,
rightLength = partials.length,
rangeLength = nativeMax(argsLength - holdersLength, 0),
result = Array(rangeLength + rightLength),
isUncurried = !isCurried;
while (++argsIndex < rangeLength) {
result[argsIndex] = args[argsIndex];
}
var offset = argsIndex;
while (++rightIndex < rightLength) {
result[offset + rightIndex] = partials[rightIndex];
}
while (++holdersIndex < holdersLength) {
if (isUncurried || argsIndex < argsLength) {
result[offset + holders[holdersIndex]] = args[argsIndex++];
}
}
return result;
}
/**
* Copies the values of `source` to `array`.
*
* @private
* @param {Array} source The array to copy values from.
* @param {Array} [array=[]] The array to copy values to.
* @returns {Array} Returns `array`.
*/
function copyArray(source, array) {
var index = -1,
length = source.length;
array || (array = Array(length));
while (++index < length) {
array[index] = source[index];
}
return array;
}
/**
* Copies properties of `source` to `object`.
*
* @private
* @param {Object} source The object to copy properties from.
* @param {Array} props The property identifiers to copy.
* @param {Object} [object={}] The object to copy properties to.
* @param {Function} [customizer] The function to customize copied values.
* @returns {Object} Returns `object`.
*/
function copyObject(source, props, object, customizer) {
var isNew = !object;
object || (object = {});
var index = -1,
length = props.length;
while (++index < length) {
var key = props[index];
var newValue = customizer
? customizer(object[key], source[key], key, object, source)
: undefined$1;
if (newValue === undefined$1) {
newValue = source[key];
}
if (isNew) {
baseAssignValue(object, key, newValue);
} else {
assignValue(object, key, newValue);
}
}
return object;
}
/**
* Copies own symbols of `source` to `object`.
*
* @private
* @param {Object} source The object to copy symbols from.
* @param {Object} [object={}] The object to copy symbols to.
* @returns {Object} Returns `object`.
*/
function copySymbols(source, object) {
return copyObject(source, getSymbols(source), object);
}
/**
* Copies own and inherited symbols of `source` to `object`.
*
* @private
* @param {Object} source The object to copy symbols from.
* @param {Object} [object={}] The object to copy symbols to.
* @returns {Object} Returns `object`.
*/
function copySymbolsIn(source, object) {
return copyObject(source, getSymbolsIn(source), object);
}
/**
* Creates a function like `_.groupBy`.
*
* @private
* @param {Function} setter The function to set accumulator values.
* @param {Function} [initializer] The accumulator object initializer.
* @returns {Function} Returns the new aggregator function.
*/
function createAggregator(setter, initializer) {
return function(collection, iteratee) {
var func = isArray(collection) ? arrayAggregator : baseAggregator,
accumulator = initializer ? initializer() : {};
return func(collection, setter, getIteratee(iteratee, 2), accumulator);
};
}
/**
* Creates a function like `_.assign`.
*
* @private
* @param {Function} assigner The function to assign values.
* @returns {Function} Returns the new assigner function.
*/
function createAssigner(assigner) {
return baseRest(function(object, sources) {
var index = -1,
length = sources.length,
customizer = length > 1 ? sources[length - 1] : undefined$1,
guard = length > 2 ? sources[2] : undefined$1;
customizer = (assigner.length > 3 && typeof customizer == 'function')
? (length--, customizer)
: undefined$1;
if (guard && isIterateeCall(sources[0], sources[1], guard)) {
customizer = length < 3 ? undefined$1 : customizer;
length = 1;
}
object = Object(object);
while (++index < length) {
var source = sources[index];
if (source) {
assigner(object, source, index, customizer);
}
}
return object;
});
}
/**
* Creates a `baseEach` or `baseEachRight` function.
*
* @private
* @param {Function} eachFunc The function to iterate over a collection.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Function} Returns the new base function.
*/
function createBaseEach(eachFunc, fromRight) {
return function(collection, iteratee) {
if (collection == null) {
return collection;
}
if (!isArrayLike(collection)) {
return eachFunc(collection, iteratee);
}
var length = collection.length,
index = fromRight ? length : -1,
iterable = Object(collection);
while ((fromRight ? index-- : ++index < length)) {
if (iteratee(iterable[index], index, iterable) === false) {
break;
}
}
return collection;
};
}
/**
* Creates a base function for methods like `_.forIn` and `_.forOwn`.
*
* @private
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Function} Returns the new base function.
*/
function createBaseFor(fromRight) {
return function(object, iteratee, keysFunc) {
var index = -1,
iterable = Object(object),
props = keysFunc(object),
length = props.length;
while (length--) {
var key = props[fromRight ? length : ++index];
if (iteratee(iterable[key], key, iterable) === false) {
break;
}
}
return object;
};
}
/**
* Creates a function that wraps `func` to invoke it with the optional `this`
* binding of `thisArg`.
*
* @private
* @param {Function} func The function to wrap.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @param {*} [thisArg] The `this` binding of `func`.
* @returns {Function} Returns the new wrapped function.
*/
function createBind(func, bitmask, thisArg) {
var isBind = bitmask & WRAP_BIND_FLAG,
Ctor = createCtor(func);
function wrapper() {
var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
return fn.apply(isBind ? thisArg : this, arguments);
}
return wrapper;
}
/**
* Creates a function like `_.lowerFirst`.
*
* @private
* @param {string} methodName The name of the `String` case method to use.
* @returns {Function} Returns the new case function.
*/
function createCaseFirst(methodName) {
return function(string) {
string = toString(string);
var strSymbols = hasUnicode(string)
? stringToArray(string)
: undefined$1;
var chr = strSymbols
? strSymbols[0]
: string.charAt(0);
var trailing = strSymbols
? castSlice(strSymbols, 1).join('')
: string.slice(1);
return chr[methodName]() + trailing;
};
}
/**
* Creates a function like `_.camelCase`.
*
* @private
* @param {Function} callback The function to combine each word.
* @returns {Function} Returns the new compounder function.
*/
function createCompounder(callback) {
return function(string) {
return arrayReduce(words(deburr(string).replace(reApos, '')), callback, '');
};
}
/**
* Creates a function that produces an instance of `Ctor` regardless of
* whether it was invoked as part of a `new` expression or by `call` or `apply`.
*
* @private
* @param {Function} Ctor The constructor to wrap.
* @returns {Function} Returns the new wrapped function.
*/
function createCtor(Ctor) {
return function() {
// Use a `switch` statement to work with class constructors. See
// http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist
// for more details.
var args = arguments;
switch (args.length) {
case 0: return new Ctor;
case 1: return new Ctor(args[0]);
case 2: return new Ctor(args[0], args[1]);
case 3: return new Ctor(args[0], args[1], args[2]);
case 4: return new Ctor(args[0], args[1], args[2], args[3]);
case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]);
case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]);
case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
}
var thisBinding = baseCreate(Ctor.prototype),
result = Ctor.apply(thisBinding, args);
// Mimic the constructor's `return` behavior.
// See https://es5.github.io/#x13.2.2 for more details.
return isObject(result) ? result : thisBinding;
};
}
/**
* Creates a function that wraps `func` to enable currying.
*
* @private
* @param {Function} func The function to wrap.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @param {number} arity The arity of `func`.
* @returns {Function} Returns the new wrapped function.
*/
function createCurry(func, bitmask, arity) {
var Ctor = createCtor(func);
function wrapper() {
var length = arguments.length,
args = Array(length),
index = length,
placeholder = getHolder(wrapper);
while (index--) {
args[index] = arguments[index];
}
var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)
? []
: replaceHolders(args, placeholder);
length -= holders.length;
if (length < arity) {
return createRecurry(
func, bitmask, createHybrid, wrapper.placeholder, undefined$1,
args, holders, undefined$1, undefined$1, arity - length);
}
var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
return apply(fn, this, args);
}
return wrapper;
}
/**
* Creates a `_.find` or `_.findLast` function.
*
* @private
* @param {Function} findIndexFunc The function to find the collection index.
* @returns {Function} Returns the new find function.
*/
function createFind(findIndexFunc) {
return function(collection, predicate, fromIndex) {
var iterable = Object(collection);
if (!isArrayLike(collection)) {
var iteratee = getIteratee(predicate, 3);
collection = keys(collection);
predicate = function(key) { return iteratee(iterable[key], key, iterable); };
}
var index = findIndexFunc(collection, predicate, fromIndex);
return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined$1;
};
}
/**
* Creates a `_.flow` or `_.flowRight` function.
*
* @private
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Function} Returns the new flow function.
*/
function createFlow(fromRight) {
return flatRest(function(funcs) {
var length = funcs.length,
index = length,
prereq = LodashWrapper.prototype.thru;
if (fromRight) {
funcs.reverse();
}
while (index--) {
var func = funcs[index];
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (prereq && !wrapper && getFuncName(func) == 'wrapper') {
var wrapper = new LodashWrapper([], true);
}
}
index = wrapper ? index : length;
while (++index < length) {
func = funcs[index];
var funcName = getFuncName(func),
data = funcName == 'wrapper' ? getData(func) : undefined$1;
if (data && isLaziable(data[0]) &&
data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) &&
!data[4].length && data[9] == 1
) {
wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);
} else {
wrapper = (func.length == 1 && isLaziable(func))
? wrapper[funcName]()
: wrapper.thru(func);
}
}
return function() {
var args = arguments,
value = args[0];
if (wrapper && args.length == 1 && isArray(value)) {
return wrapper.plant(value).value();
}
var index = 0,
result = length ? funcs[index].apply(this, args) : value;
while (++index < length) {
result = funcs[index].call(this, result);
}
return result;
};
});
}
/**
* Creates a function that wraps `func` to invoke it with optional `this`
* binding of `thisArg`, partial application, and currying.
*
* @private
* @param {Function|string} func The function or method name to wrap.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @param {*} [thisArg] The `this` binding of `func`.
* @param {Array} [partials] The arguments to prepend to those provided to
* the new function.
* @param {Array} [holders] The `partials` placeholder indexes.
* @param {Array} [partialsRight] The arguments to append to those provided
* to the new function.
* @param {Array} [holdersRight] The `partialsRight` placeholder indexes.
* @param {Array} [argPos] The argument positions of the new function.
* @param {number} [ary] The arity cap of `func`.
* @param {number} [arity] The arity of `func`.
* @returns {Function} Returns the new wrapped function.
*/
function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
var isAry = bitmask & WRAP_ARY_FLAG,
isBind = bitmask & WRAP_BIND_FLAG,
isBindKey = bitmask & WRAP_BIND_KEY_FLAG,
isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG),
isFlip = bitmask & WRAP_FLIP_FLAG,
Ctor = isBindKey ? undefined$1 : createCtor(func);
function wrapper() {
var length = arguments.length,
args = Array(length),
index = length;
while (index--) {
args[index] = arguments[index];
}
if (isCurried) {
var placeholder = getHolder(wrapper),
holdersCount = countHolders(args, placeholder);
}
if (partials) {
args = composeArgs(args, partials, holders, isCurried);
}
if (partialsRight) {
args = composeArgsRight(args, partialsRight, holdersRight, isCurried);
}
length -= holdersCount;
if (isCurried && length < arity) {
var newHolders = replaceHolders(args, placeholder);
return createRecurry(
func, bitmask, createHybrid, wrapper.placeholder, thisArg,
args, newHolders, argPos, ary, arity - length
);
}
var thisBinding = isBind ? thisArg : this,
fn = isBindKey ? thisBinding[func] : func;
length = args.length;
if (argPos) {
args = reorder(args, argPos);
} else if (isFlip && length > 1) {
args.reverse();
}
if (isAry && ary < length) {
args.length = ary;
}
if (this && this !== root && this instanceof wrapper) {
fn = Ctor || createCtor(fn);
}
return fn.apply(thisBinding, args);
}
return wrapper;
}
/**
* Creates a function like `_.invertBy`.
*
* @private
* @param {Function} setter The function to set accumulator values.
* @param {Function} toIteratee The function to resolve iteratees.
* @returns {Function} Returns the new inverter function.
*/
function createInverter(setter, toIteratee) {
return function(object, iteratee) {
return baseInverter(object, setter, toIteratee(iteratee), {});
};
}
/**
* Creates a function that performs a mathematical operation on two values.
*
* @private
* @param {Function} operator The function to perform the operation.
* @param {number} [defaultValue] The value used for `undefined` arguments.
* @returns {Function} Returns the new mathematical operation function.
*/
function createMathOperation(operator, defaultValue) {
return function(value, other) {
var result;
if (value === undefined$1 && other === undefined$1) {
return defaultValue;
}
if (value !== undefined$1) {
result = value;
}
if (other !== undefined$1) {
if (result === undefined$1) {
return other;
}
if (typeof value == 'string' || typeof other == 'string') {
value = baseToString(value);
other = baseToString(other);
} else {
value = baseToNumber(value);
other = baseToNumber(other);
}
result = operator(value, other);
}
return result;
};
}
/**
* Creates a function like `_.over`.
*
* @private
* @param {Function} arrayFunc The function to iterate over iteratees.
* @returns {Function} Returns the new over function.
*/
function createOver(arrayFunc) {
return flatRest(function(iteratees) {
iteratees = arrayMap(iteratees, baseUnary(getIteratee()));
return baseRest(function(args) {
var thisArg = this;
return arrayFunc(iteratees, function(iteratee) {
return apply(iteratee, thisArg, args);
});
});
});
}
/**
* Creates the padding for `string` based on `length`. The `chars` string
* is truncated if the number of characters exceeds `length`.
*
* @private
* @param {number} length The padding length.
* @param {string} [chars=' '] The string used as padding.
* @returns {string} Returns the padding for `string`.
*/
function createPadding(length, chars) {
chars = chars === undefined$1 ? ' ' : baseToString(chars);
var charsLength = chars.length;
if (charsLength < 2) {
return charsLength ? baseRepeat(chars, length) : chars;
}
var result = baseRepeat(chars, nativeCeil(length / stringSize(chars)));
return hasUnicode(chars)
? castSlice(stringToArray(result), 0, length).join('')
: result.slice(0, length);
}
/**
* Creates a function that wraps `func` to invoke it with the `this` binding
* of `thisArg` and `partials` prepended to the arguments it receives.
*
* @private
* @param {Function} func The function to wrap.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @param {*} thisArg The `this` binding of `func`.
* @param {Array} partials The arguments to prepend to those provided to
* the new function.
* @returns {Function} Returns the new wrapped function.
*/
function createPartial(func, bitmask, thisArg, partials) {
var isBind = bitmask & WRAP_BIND_FLAG,
Ctor = createCtor(func);
function wrapper() {
var argsIndex = -1,
argsLength = arguments.length,
leftIndex = -1,
leftLength = partials.length,
args = Array(leftLength + argsLength),
fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
while (++leftIndex < leftLength) {
args[leftIndex] = partials[leftIndex];
}
while (argsLength--) {
args[leftIndex++] = arguments[++argsIndex];
}
return apply(fn, isBind ? thisArg : this, args);
}
return wrapper;
}
/**
* Creates a `_.range` or `_.rangeRight` function.
*
* @private
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Function} Returns the new range function.
*/
function createRange(fromRight) {
return function(start, end, step) {
if (step && typeof step != 'number' && isIterateeCall(start, end, step)) {
end = step = undefined$1;
}
// Ensure the sign of `-0` is preserved.
start = toFinite(start);
if (end === undefined$1) {
end = start;
start = 0;
} else {
end = toFinite(end);
}
step = step === undefined$1 ? (start < end ? 1 : -1) : toFinite(step);
return baseRange(start, end, step, fromRight);
};
}
/**
* Creates a function that performs a relational operation on two values.
*
* @private
* @param {Function} operator The function to perform the operation.
* @returns {Function} Returns the new relational operation function.
*/
function createRelationalOperation(operator) {
return function(value, other) {
if (!(typeof value == 'string' && typeof other == 'string')) {
value = toNumber(value);
other = toNumber(other);
}
return operator(value, other);
};
}
/**
* Creates a function that wraps `func` to continue currying.
*
* @private
* @param {Function} func The function to wrap.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @param {Function} wrapFunc The function to create the `func` wrapper.
* @param {*} placeholder The placeholder value.
* @param {*} [thisArg] The `this` binding of `func`.
* @param {Array} [partials] The arguments to prepend to those provided to
* the new function.
* @param {Array} [holders] The `partials` placeholder indexes.
* @param {Array} [argPos] The argument positions of the new function.
* @param {number} [ary] The arity cap of `func`.
* @param {number} [arity] The arity of `func`.
* @returns {Function} Returns the new wrapped function.
*/
function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
var isCurry = bitmask & WRAP_CURRY_FLAG,
newHolders = isCurry ? holders : undefined$1,
newHoldersRight = isCurry ? undefined$1 : holders,
newPartials = isCurry ? partials : undefined$1,
newPartialsRight = isCurry ? undefined$1 : partials;
bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG);
bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG);
if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) {
bitmask &= -4;
}
var newData = [
func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,
newHoldersRight, argPos, ary, arity
];
var result = wrapFunc.apply(undefined$1, newData);
if (isLaziable(func)) {
setData(result, newData);
}
result.placeholder = placeholder;
return setWrapToString(result, func, bitmask);
}
/**
* Creates a function like `_.round`.
*
* @private
* @param {string} methodName The name of the `Math` method to use when rounding.
* @returns {Function} Returns the new round function.
*/
function createRound(methodName) {
var func = Math[methodName];
return function(number, precision) {
number = toNumber(number);
precision = precision == null ? 0 : nativeMin(toInteger(precision), 292);
if (precision && nativeIsFinite(number)) {
// Shift with exponential notation to avoid floating-point issues.
// See [MDN](https://mdn.io/round#Examples) for more details.
var pair = (toString(number) + 'e').split('e'),
value = func(pair[0] + 'e' + (+pair[1] + precision));
pair = (toString(value) + 'e').split('e');
return +(pair[0] + 'e' + (+pair[1] - precision));
}
return func(number);
};
}
/**
* Creates a set object of `values`.
*
* @private
* @param {Array} values The values to add to the set.
* @returns {Object} Returns the new set.
*/
var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) {
return new Set(values);
};
/**
* Creates a `_.toPairs` or `_.toPairsIn` function.
*
* @private
* @param {Function} keysFunc The function to get the keys of a given object.
* @returns {Function} Returns the new pairs function.
*/
function createToPairs(keysFunc) {
return function(object) {
var tag = getTag(object);
if (tag == mapTag) {
return mapToArray(object);
}
if (tag == setTag) {
return setToPairs(object);
}
return baseToPairs(object, keysFunc(object));
};
}
/**
* Creates a function that either curries or invokes `func` with optional
* `this` binding and partially applied arguments.
*
* @private
* @param {Function|string} func The function or method name to wrap.
* @param {number} bitmask The bitmask flags.
* 1 - `_.bind`
* 2 - `_.bindKey`
* 4 - `_.curry` or `_.curryRight` of a bound function
* 8 - `_.curry`
* 16 - `_.curryRight`
* 32 - `_.partial`
* 64 - `_.partialRight`
* 128 - `_.rearg`
* 256 - `_.ary`
* 512 - `_.flip`
* @param {*} [thisArg] The `this` binding of `func`.
* @param {Array} [partials] The arguments to be partially applied.
* @param {Array} [holders] The `partials` placeholder indexes.
* @param {Array} [argPos] The argument positions of the new function.
* @param {number} [ary] The arity cap of `func`.
* @param {number} [arity] The arity of `func`.
* @returns {Function} Returns the new wrapped function.
*/
function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;
if (!isBindKey && typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
var length = partials ? partials.length : 0;
if (!length) {
bitmask &= -97;
partials = holders = undefined$1;
}
ary = ary === undefined$1 ? ary : nativeMax(toInteger(ary), 0);
arity = arity === undefined$1 ? arity : toInteger(arity);
length -= holders ? holders.length : 0;
if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) {
var partialsRight = partials,
holdersRight = holders;
partials = holders = undefined$1;
}
var data = isBindKey ? undefined$1 : getData(func);
var newData = [
func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
argPos, ary, arity
];
if (data) {
mergeData(newData, data);
}
func = newData[0];
bitmask = newData[1];
thisArg = newData[2];
partials = newData[3];
holders = newData[4];
arity = newData[9] = newData[9] === undefined$1
? (isBindKey ? 0 : func.length)
: nativeMax(newData[9] - length, 0);
if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) {
bitmask &= -25;
}
if (!bitmask || bitmask == WRAP_BIND_FLAG) {
var result = createBind(func, bitmask, thisArg);
} else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) {
result = createCurry(func, bitmask, arity);
} else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) {
result = createPartial(func, bitmask, thisArg, partials);
} else {
result = createHybrid.apply(undefined$1, newData);
}
var setter = data ? baseSetData : setData;
return setWrapToString(setter(result, newData), func, bitmask);
}
/**
* Used by `_.defaults` to customize its `_.assignIn` use to assign properties
* of source objects to the destination object for all destination properties
* that resolve to `undefined`.
*
* @private
* @param {*} objValue The destination value.
* @param {*} srcValue The source value.
* @param {string} key The key of the property to assign.
* @param {Object} object The parent object of `objValue`.
* @returns {*} Returns the value to assign.
*/
function customDefaultsAssignIn(objValue, srcValue, key, object) {
if (objValue === undefined$1 ||
(eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) {
return srcValue;
}
return objValue;
}
/**
* Used by `_.defaultsDeep` to customize its `_.merge` use to merge source
* objects into destination objects that are passed thru.
*
* @private
* @param {*} objValue The destination value.
* @param {*} srcValue The source value.
* @param {string} key The key of the property to merge.
* @param {Object} object The parent object of `objValue`.
* @param {Object} source The parent object of `srcValue`.
* @param {Object} [stack] Tracks traversed source values and their merged
* counterparts.
* @returns {*} Returns the value to assign.
*/
function customDefaultsMerge(objValue, srcValue, key, object, source, stack) {
if (isObject(objValue) && isObject(srcValue)) {
// Recursively merge objects and arrays (susceptible to call stack limits).
stack.set(srcValue, objValue);
baseMerge(objValue, srcValue, undefined$1, customDefaultsMerge, stack);
stack['delete'](srcValue);
}
return objValue;
}
/**
* Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain
* objects.
*
* @private
* @param {*} value The value to inspect.
* @param {string} key The key of the property to inspect.
* @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`.
*/
function customOmitClone(value) {
return isPlainObject(value) ? undefined$1 : value;
}
/**
* A specialized version of `baseIsEqualDeep` for arrays with support for
* partial deep comparisons.
*
* @private
* @param {Array} array The array to compare.
* @param {Array} other The other array to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} stack Tracks traversed `array` and `other` objects.
* @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
*/
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
arrLength = array.length,
othLength = other.length;
if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
return false;
}
// Check that cyclic values are equal.
var arrStacked = stack.get(array);
var othStacked = stack.get(other);
if (arrStacked && othStacked) {
return arrStacked == other && othStacked == array;
}
var index = -1,
result = true,
seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined$1;
stack.set(array, other);
stack.set(other, array);
// Ignore non-index properties.
while (++index < arrLength) {
var arrValue = array[index],
othValue = other[index];
if (customizer) {
var compared = isPartial
? customizer(othValue, arrValue, index, other, array, stack)
: customizer(arrValue, othValue, index, array, other, stack);
}
if (compared !== undefined$1) {
if (compared) {
continue;
}
result = false;
break;
}
// Recursively compare arrays (susceptible to call stack limits).
if (seen) {
if (!arraySome(other, function(othValue, othIndex) {
if (!cacheHas(seen, othIndex) &&
(arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
return seen.push(othIndex);
}
})) {
result = false;
break;
}
} else if (!(
arrValue === othValue ||
equalFunc(arrValue, othValue, bitmask, customizer, stack)
)) {
result = false;
break;
}
}
stack['delete'](array);
stack['delete'](other);
return result;
}
/**
* A specialized version of `baseIsEqualDeep` for comparing objects of
* the same `toStringTag`.
*
* **Note:** This function only supports comparing values with tags of
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {string} tag The `toStringTag` of the objects to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} stack Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {
switch (tag) {
case dataViewTag:
if ((object.byteLength != other.byteLength) ||
(object.byteOffset != other.byteOffset)) {
return false;
}
object = object.buffer;
other = other.buffer;
case arrayBufferTag:
if ((object.byteLength != other.byteLength) ||
!equalFunc(new Uint8Array(object), new Uint8Array(other))) {
return false;
}
return true;
case boolTag:
case dateTag:
case numberTag:
// Coerce booleans to `1` or `0` and dates to milliseconds.
// Invalid dates are coerced to `NaN`.
return eq(+object, +other);
case errorTag:
return object.name == other.name && object.message == other.message;
case regexpTag:
case stringTag:
// Coerce regexes to strings and treat strings, primitives and objects,
// as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
// for more details.
return object == (other + '');
case mapTag:
var convert = mapToArray;
case setTag:
var isPartial = bitmask & COMPARE_PARTIAL_FLAG;
convert || (convert = setToArray);
if (object.size != other.size && !isPartial) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked) {
return stacked == other;
}
bitmask |= COMPARE_UNORDERED_FLAG;
// Recursively compare objects (susceptible to call stack limits).
stack.set(object, other);
var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);
stack['delete'](object);
return result;
case symbolTag:
if (symbolValueOf) {
return symbolValueOf.call(object) == symbolValueOf.call(other);
}
}
return false;
}
/**
* A specialized version of `baseIsEqualDeep` for objects with support for
* partial deep comparisons.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} stack Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
objProps = getAllKeys(object),
objLength = objProps.length,
othProps = getAllKeys(other),
othLength = othProps.length;
if (objLength != othLength && !isPartial) {
return false;
}
var index = objLength;
while (index--) {
var key = objProps[index];
if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
return false;
}
}
// Check that cyclic values are equal.
var objStacked = stack.get(object);
var othStacked = stack.get(other);
if (objStacked && othStacked) {
return objStacked == other && othStacked == object;
}
var result = true;
stack.set(object, other);
stack.set(other, object);
var skipCtor = isPartial;
while (++index < objLength) {
key = objProps[index];
var objValue = object[key],
othValue = other[key];
if (customizer) {
var compared = isPartial
? customizer(othValue, objValue, key, other, object, stack)
: customizer(objValue, othValue, key, object, other, stack);
}
// Recursively compare objects (susceptible to call stack limits).
if (!(compared === undefined$1
? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))
: compared
)) {
result = false;
break;
}
skipCtor || (skipCtor = key == 'constructor');
}
if (result && !skipCtor) {
var objCtor = object.constructor,
othCtor = other.constructor;
// Non `Object` object instances with different constructors are not equal.
if (objCtor != othCtor &&
('constructor' in object && 'constructor' in other) &&
!(typeof objCtor == 'function' && objCtor instanceof objCtor &&
typeof othCtor == 'function' && othCtor instanceof othCtor)) {
result = false;
}
}
stack['delete'](object);
stack['delete'](other);
return result;
}
/**
* A specialized version of `baseRest` which flattens the rest array.
*
* @private
* @param {Function} func The function to apply a rest parameter to.
* @returns {Function} Returns the new function.
*/
function flatRest(func) {
return setToString(overRest(func, undefined$1, flatten), func + '');
}
/**
* Creates an array of own enumerable property names and symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names and symbols.
*/
function getAllKeys(object) {
return baseGetAllKeys(object, keys, getSymbols);
}
/**
* Creates an array of own and inherited enumerable property names and
* symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names and symbols.
*/
function getAllKeysIn(object) {
return baseGetAllKeys(object, keysIn, getSymbolsIn);
}
/**
* Gets metadata for `func`.
*
* @private
* @param {Function} func The function to query.
* @returns {*} Returns the metadata for `func`.
*/
var getData = !metaMap ? noop : function(func) {
return metaMap.get(func);
};
/**
* Gets the name of `func`.
*
* @private
* @param {Function} func The function to query.
* @returns {string} Returns the function name.
*/
function getFuncName(func) {
var result = (func.name + ''),
array = realNames[result],
length = hasOwnProperty.call(realNames, result) ? array.length : 0;
while (length--) {
var data = array[length],
otherFunc = data.func;
if (otherFunc == null || otherFunc == func) {
return data.name;
}
}
return result;
}
/**
* Gets the argument placeholder value for `func`.
*
* @private
* @param {Function} func The function to inspect.
* @returns {*} Returns the placeholder value.
*/
function getHolder(func) {
var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func;
return object.placeholder;
}
/**
* Gets the appropriate "iteratee" function. If `_.iteratee` is customized,
* this function returns the custom method, otherwise it returns `baseIteratee`.
* If arguments are provided, the chosen function is invoked with them and
* its result is returned.
*
* @private
* @param {*} [value] The value to convert to an iteratee.
* @param {number} [arity] The arity of the created iteratee.
* @returns {Function} Returns the chosen function or its result.
*/
function getIteratee() {
var result = lodash.iteratee || iteratee;
result = result === iteratee ? baseIteratee : result;
return arguments.length ? result(arguments[0], arguments[1]) : result;
}
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}
/**
* Gets the property names, values, and compare flags of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the match data of `object`.
*/
function getMatchData(object) {
var result = keys(object),
length = result.length;
while (length--) {
var key = result[length],
value = object[key];
result[length] = [key, value, isStrictComparable(value)];
}
return result;
}
/**
* Gets the native function at `key` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else `undefined`.
*/
function getNative(object, key) {
var value = getValue(object, key);
return baseIsNative(value) ? value : undefined$1;
}
/**
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the raw `toStringTag`.
*/
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];
try {
value[symToStringTag] = undefined$1;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
/**
* Creates an array of the own enumerable symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of symbols.
*/
var getSymbols = !nativeGetSymbols ? stubArray : function(object) {
if (object == null) {
return [];
}
object = Object(object);
return arrayFilter(nativeGetSymbols(object), function(symbol) {
return propertyIsEnumerable.call(object, symbol);
});
};
/**
* Creates an array of the own and inherited enumerable symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of symbols.
*/
var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) {
var result = [];
while (object) {
arrayPush(result, getSymbols(object));
object = getPrototype(object);
}
return result;
};
/**
* Gets the `toStringTag` of `value`.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
var getTag = baseGetTag;
// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
(Map && getTag(new Map) != mapTag) ||
(Promise && getTag(Promise.resolve()) != promiseTag) ||
(Set && getTag(new Set) != setTag) ||
(WeakMap && getTag(new WeakMap) != weakMapTag)) {
getTag = function(value) {
var result = baseGetTag(value),
Ctor = result == objectTag ? value.constructor : undefined$1,
ctorString = Ctor ? toSource(Ctor) : '';
if (ctorString) {
switch (ctorString) {
case dataViewCtorString: return dataViewTag;
case mapCtorString: return mapTag;
case promiseCtorString: return promiseTag;
case setCtorString: return setTag;
case weakMapCtorString: return weakMapTag;
}
}
return result;
};
}
/**
* Gets the view, applying any `transforms` to the `start` and `end` positions.
*
* @private
* @param {number} start The start of the view.
* @param {number} end The end of the view.
* @param {Array} transforms The transformations to apply to the view.
* @returns {Object} Returns an object containing the `start` and `end`
* positions of the view.
*/
function getView(start, end, transforms) {
var index = -1,
length = transforms.length;
while (++index < length) {
var data = transforms[index],
size = data.size;
switch (data.type) {
case 'drop': start += size; break;
case 'dropRight': end -= size; break;
case 'take': end = nativeMin(end, start + size); break;
case 'takeRight': start = nativeMax(start, end - size); break;
}
}
return { 'start': start, 'end': end };
}
/**
* Extracts wrapper details from the `source` body comment.
*
* @private
* @param {string} source The source to inspect.
* @returns {Array} Returns the wrapper details.
*/
function getWrapDetails(source) {
var match = source.match(reWrapDetails);
return match ? match[1].split(reSplitDetails) : [];
}
/**
* Checks if `path` exists on `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @param {Function} hasFunc The function to check properties.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
*/
function hasPath(object, path, hasFunc) {
path = castPath(path, object);
var index = -1,
length = path.length,
result = false;
while (++index < length) {
var key = toKey(path[index]);
if (!(result = object != null && hasFunc(object, key))) {
break;
}
object = object[key];
}
if (result || ++index != length) {
return result;
}
length = object == null ? 0 : object.length;
return !!length && isLength(length) && isIndex(key, length) &&
(isArray(object) || isArguments(object));
}
/**
* Initializes an array clone.
*
* @private
* @param {Array} array The array to clone.
* @returns {Array} Returns the initialized clone.
*/
function initCloneArray(array) {
var length = array.length,
result = new array.constructor(length);
// Add properties assigned by `RegExp#exec`.
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index;
result.input = array.input;
}
return result;
}
/**
* Initializes an object clone.
*
* @private
* @param {Object} object The object to clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneObject(object) {
return (typeof object.constructor == 'function' && !isPrototype(object))
? baseCreate(getPrototype(object))
: {};
}
/**
* Initializes an object clone based on its `toStringTag`.
*
* **Note:** This function only supports cloning values with tags of
* `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.
*
* @private
* @param {Object} object The object to clone.
* @param {string} tag The `toStringTag` of the object to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneByTag(object, tag, isDeep) {
var Ctor = object.constructor;
switch (tag) {
case arrayBufferTag:
return cloneArrayBuffer(object);
case boolTag:
case dateTag:
return new Ctor(+object);
case dataViewTag:
return cloneDataView(object, isDeep);
case float32Tag: case float64Tag:
case int8Tag: case int16Tag: case int32Tag:
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
return cloneTypedArray(object, isDeep);
case mapTag:
return new Ctor;
case numberTag:
case stringTag:
return new Ctor(object);
case regexpTag:
return cloneRegExp(object);
case setTag:
return new Ctor;
case symbolTag:
return cloneSymbol(object);
}
}
/**
* Inserts wrapper `details` in a comment at the top of the `source` body.
*
* @private
* @param {string} source The source to modify.
* @returns {Array} details The details to insert.
* @returns {string} Returns the modified source.
*/
function insertWrapDetails(source, details) {
var length = details.length;
if (!length) {
return source;
}
var lastIndex = length - 1;
details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex];
details = details.join(length > 2 ? ', ' : ' ');
return source.replace(reWrapComment, '{\n/* [wrapped with ' + details + '] */\n');
}
/**
* Checks if `value` is a flattenable `arguments` object or array.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is flattenable, else `false`.
*/
function isFlattenable(value) {
return isArray(value) || isArguments(value) ||
!!(spreadableSymbol && value && value[spreadableSymbol]);
}
/**
* Checks if `value` is a valid array-like index.
*
* @private
* @param {*} value The value to check.
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
var type = typeof value;
length = length == null ? MAX_SAFE_INTEGER : length;
return !!length &&
(type == 'number' ||
(type != 'symbol' && reIsUint.test(value))) &&
(value > -1 && value % 1 == 0 && value < length);
}
/**
* Checks if the given arguments are from an iteratee call.
*
* @private
* @param {*} value The potential iteratee value argument.
* @param {*} index The potential iteratee index or key argument.
* @param {*} object The potential iteratee object argument.
* @returns {boolean} Returns `true` if the arguments are from an iteratee call,
* else `false`.
*/
function isIterateeCall(value, index, object) {
if (!isObject(object)) {
return false;
}
var type = typeof index;
if (type == 'number'
? (isArrayLike(object) && isIndex(index, object.length))
: (type == 'string' && index in object)
) {
return eq(object[index], value);
}
return false;
}
/**
* Checks if `value` is a property name and not a property path.
*
* @private
* @param {*} value The value to check.
* @param {Object} [object] The object to query keys on.
* @returns {boolean} Returns `true` if `value` is a property name, else `false`.
*/
function isKey(value, object) {
if (isArray(value)) {
return false;
}
var type = typeof value;
if (type == 'number' || type == 'symbol' || type == 'boolean' ||
value == null || isSymbol(value)) {
return true;
}
return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
(object != null && value in Object(object));
}
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
function isKeyable(value) {
var type = typeof value;
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
? (value !== '__proto__')
: (value === null);
}
/**
* Checks if `func` has a lazy counterpart.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns `true` if `func` has a lazy counterpart,
* else `false`.
*/
function isLaziable(func) {
var funcName = getFuncName(func),
other = lodash[funcName];
if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) {
return false;
}
if (func === other) {
return true;
}
var data = getData(other);
return !!data && func === data[0];
}
/**
* Checks if `func` has its source masked.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns `true` if `func` is masked, else `false`.
*/
function isMasked(func) {
return !!maskSrcKey && (maskSrcKey in func);
}
/**
* Checks if `func` is capable of being masked.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `func` is maskable, else `false`.
*/
var isMaskable = coreJsData ? isFunction : stubFalse;
/**
* Checks if `value` is likely a prototype object.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
*/
function isPrototype(value) {
var Ctor = value && value.constructor,
proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
return value === proto;
}
/**
* Checks if `value` is suitable for strict equality comparisons, i.e. `===`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` if suitable for strict
* equality comparisons, else `false`.
*/
function isStrictComparable(value) {
return value === value && !isObject(value);
}
/**
* A specialized version of `matchesProperty` for source values suitable
* for strict equality comparisons, i.e. `===`.
*
* @private
* @param {string} key The key of the property to get.
* @param {*} srcValue The value to match.
* @returns {Function} Returns the new spec function.
*/
function matchesStrictComparable(key, srcValue) {
return function(object) {
if (object == null) {
return false;
}
return object[key] === srcValue &&
(srcValue !== undefined$1 || (key in Object(object)));
};
}
/**
* A specialized version of `_.memoize` which clears the memoized function's
* cache when it exceeds `MAX_MEMOIZE_SIZE`.
*
* @private
* @param {Function} func The function to have its output memoized.
* @returns {Function} Returns the new memoized function.
*/
function memoizeCapped(func) {
var result = memoize(func, function(key) {
if (cache.size === MAX_MEMOIZE_SIZE) {
cache.clear();
}
return key;
});
var cache = result.cache;
return result;
}
/**
* Merges the function metadata of `source` into `data`.
*
* Merging metadata reduces the number of wrappers used to invoke a function.
* This is possible because methods like `_.bind`, `_.curry`, and `_.partial`
* may be applied regardless of execution order. Methods like `_.ary` and
* `_.rearg` modify function arguments, making the order in which they are
* executed important, preventing the merging of metadata. However, we make
* an exception for a safe combined case where curried functions have `_.ary`
* and or `_.rearg` applied.
*
* @private
* @param {Array} data The destination metadata.
* @param {Array} source The source metadata.
* @returns {Array} Returns `data`.
*/
function mergeData(data, source) {
var bitmask = data[1],
srcBitmask = source[1],
newBitmask = bitmask | srcBitmask,
isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG);
var isCombo =
((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) ||
((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) ||
((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG));
// Exit early if metadata can't be merged.
if (!(isCommon || isCombo)) {
return data;
}
// Use source `thisArg` if available.
if (srcBitmask & WRAP_BIND_FLAG) {
data[2] = source[2];
// Set when currying a bound function.
newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG;
}
// Compose partial arguments.
var value = source[3];
if (value) {
var partials = data[3];
data[3] = partials ? composeArgs(partials, value, source[4]) : value;
data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4];
}
// Compose partial right arguments.
value = source[5];
if (value) {
partials = data[5];
data[5] = partials ? composeArgsRight(partials, value, source[6]) : value;
data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6];
}
// Use source `argPos` if available.
value = source[7];
if (value) {
data[7] = value;
}
// Use source `ary` if it's smaller.
if (srcBitmask & WRAP_ARY_FLAG) {
data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]);
}
// Use source `arity` if one is not provided.
if (data[9] == null) {
data[9] = source[9];
}
// Use source `func` and merge bitmasks.
data[0] = source[0];
data[1] = newBitmask;
return data;
}
/**
* This function is like
* [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* except that it includes inherited enumerable properties.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function nativeKeysIn(object) {
var result = [];
if (object != null) {
for (var key in Object(object)) {
result.push(key);
}
}
return result;
}
/**
* Converts `value` to a string using `Object.prototype.toString`.
*
* @private
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
*/
function objectToString(value) {
return nativeObjectToString.call(value);
}
/**
* A specialized version of `baseRest` which transforms the rest array.
*
* @private
* @param {Function} func The function to apply a rest parameter to.
* @param {number} [start=func.length-1] The start position of the rest parameter.
* @param {Function} transform The rest array transform.
* @returns {Function} Returns the new function.
*/
function overRest(func, start, transform) {
start = nativeMax(start === undefined$1 ? (func.length - 1) : start, 0);
return function() {
var args = arguments,
index = -1,
length = nativeMax(args.length - start, 0),
array = Array(length);
while (++index < length) {
array[index] = args[start + index];
}
index = -1;
var otherArgs = Array(start + 1);
while (++index < start) {
otherArgs[index] = args[index];
}
otherArgs[start] = transform(array);
return apply(func, this, otherArgs);
};
}
/**
* Gets the parent value at `path` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Array} path The path to get the parent value of.
* @returns {*} Returns the parent value.
*/
function parent(object, path) {
return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1));
}
/**
* Reorder `array` according to the specified indexes where the element at
* the first index is assigned as the first element, the element at
* the second index is assigned as the second element, and so on.
*
* @private
* @param {Array} array The array to reorder.
* @param {Array} indexes The arranged array indexes.
* @returns {Array} Returns `array`.
*/
function reorder(array, indexes) {
var arrLength = array.length,
length = nativeMin(indexes.length, arrLength),
oldArray = copyArray(array);
while (length--) {
var index = indexes[length];
array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined$1;
}
return array;
}
/**
* Gets the value at `key`, unless `key` is "__proto__" or "constructor".
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function safeGet(object, key) {
if (key === 'constructor' && typeof object[key] === 'function') {
return;
}
if (key == '__proto__') {
return;
}
return object[key];
}
/**
* Sets metadata for `func`.
*
* **Note:** If this function becomes hot, i.e. is invoked a lot in a short
* period of time, it will trip its breaker and transition to an identity
* function to avoid garbage collection pauses in V8. See
* [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070)
* for more details.
*
* @private
* @param {Function} func The function to associate metadata with.
* @param {*} data The metadata.
* @returns {Function} Returns `func`.
*/
var setData = shortOut(baseSetData);
/**
* A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout).
*
* @private
* @param {Function} func The function to delay.
* @param {number} wait The number of milliseconds to delay invocation.
* @returns {number|Object} Returns the timer id or timeout object.
*/
var setTimeout = ctxSetTimeout || function(func, wait) {
return root.setTimeout(func, wait);
};
/**
* Sets the `toString` method of `func` to return `string`.
*
* @private
* @param {Function} func The function to modify.
* @param {Function} string The `toString` result.
* @returns {Function} Returns `func`.
*/
var setToString = shortOut(baseSetToString);
/**
* Sets the `toString` method of `wrapper` to mimic the source of `reference`
* with wrapper details in a comment at the top of the source body.
*
* @private
* @param {Function} wrapper The function to modify.
* @param {Function} reference The reference function.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @returns {Function} Returns `wrapper`.
*/
function setWrapToString(wrapper, reference, bitmask) {
var source = (reference + '');
return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask)));
}
/**
* Creates a function that'll short out and invoke `identity` instead
* of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`
* milliseconds.
*
* @private
* @param {Function} func The function to restrict.
* @returns {Function} Returns the new shortable function.
*/
function shortOut(func) {
var count = 0,
lastCalled = 0;
return function() {
var stamp = nativeNow(),
remaining = HOT_SPAN - (stamp - lastCalled);
lastCalled = stamp;
if (remaining > 0) {
if (++count >= HOT_COUNT) {
return arguments[0];
}
} else {
count = 0;
}
return func.apply(undefined$1, arguments);
};
}
/**
* A specialized version of `_.shuffle` which mutates and sets the size of `array`.
*
* @private
* @param {Array} array The array to shuffle.
* @param {number} [size=array.length] The size of `array`.
* @returns {Array} Returns `array`.
*/
function shuffleSelf(array, size) {
var index = -1,
length = array.length,
lastIndex = length - 1;
size = size === undefined$1 ? length : size;
while (++index < size) {
var rand = baseRandom(index, lastIndex),
value = array[rand];
array[rand] = array[index];
array[index] = value;
}
array.length = size;
return array;
}
/**
* Converts `string` to a property path array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the property path array.
*/
var stringToPath = memoizeCapped(function(string) {
var result = [];
if (string.charCodeAt(0) === 46 /* . */) {
result.push('');
}
string.replace(rePropName, function(match, number, quote, subString) {
result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));
});
return result;
});
/**
* Converts `value` to a string key if it's not a string or symbol.
*
* @private
* @param {*} value The value to inspect.
* @returns {string|symbol} Returns the key.
*/
function toKey(value) {
if (typeof value == 'string' || isSymbol(value)) {
return value;
}
var result = (value + '');
return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
}
/**
* Converts `func` to its source code.
*
* @private
* @param {Function} func The function to convert.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return funcToString.call(func);
} catch (e) {}
try {
return (func + '');
} catch (e) {}
}
return '';
}
/**
* Updates wrapper `details` based on `bitmask` flags.
*
* @private
* @returns {Array} details The details to modify.
* @param {number} bitmask The bitmask flags. See `createWrap` for more details.
* @returns {Array} Returns `details`.
*/
function updateWrapDetails(details, bitmask) {
arrayEach(wrapFlags, function(pair) {
var value = '_.' + pair[0];
if ((bitmask & pair[1]) && !arrayIncludes(details, value)) {
details.push(value);
}
});
return details.sort();
}
/**
* Creates a clone of `wrapper`.
*
* @private
* @param {Object} wrapper The wrapper to clone.
* @returns {Object} Returns the cloned wrapper.
*/
function wrapperClone(wrapper) {
if (wrapper instanceof LazyWrapper) {
return wrapper.clone();
}
var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__);
result.__actions__ = copyArray(wrapper.__actions__);
result.__index__ = wrapper.__index__;
result.__values__ = wrapper.__values__;
return result;
}
/*------------------------------------------------------------------------*/
/**
* Creates an array of elements split into groups the length of `size`.
* If `array` can't be split evenly, the final chunk will be the remaining
* elements.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to process.
* @param {number} [size=1] The length of each chunk
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the new array of chunks.
* @example
*
* _.chunk(['a', 'b', 'c', 'd'], 2);
* // => [['a', 'b'], ['c', 'd']]
*
* _.chunk(['a', 'b', 'c', 'd'], 3);
* // => [['a', 'b', 'c'], ['d']]
*/
function chunk(array, size, guard) {
if ((guard ? isIterateeCall(array, size, guard) : size === undefined$1)) {
size = 1;
} else {
size = nativeMax(toInteger(size), 0);
}
var length = array == null ? 0 : array.length;
if (!length || size < 1) {
return [];
}
var index = 0,
resIndex = 0,
result = Array(nativeCeil(length / size));
while (index < length) {
result[resIndex++] = baseSlice(array, index, (index += size));
}
return result;
}
/**
* Creates an array with all falsey values removed. The values `false`, `null`,
* `0`, `""`, `undefined`, and `NaN` are falsey.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to compact.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.compact([0, 1, false, 2, '', 3]);
* // => [1, 2, 3]
*/
function compact(array) {
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (value) {
result[resIndex++] = value;
}
}
return result;
}
/**
* Creates a new array concatenating `array` with any additional arrays
* and/or values.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to concatenate.
* @param {...*} [values] The values to concatenate.
* @returns {Array} Returns the new concatenated array.
* @example
*
* var array = [1];
* var other = _.concat(array, 2, [3], [[4]]);
*
* console.log(other);
* // => [1, 2, 3, [4]]
*
* console.log(array);
* // => [1]
*/
function concat() {
var length = arguments.length;
if (!length) {
return [];
}
var args = Array(length - 1),
array = arguments[0],
index = length;
while (index--) {
args[index - 1] = arguments[index];
}
return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));
}
/**
* Creates an array of `array` values not included in the other given arrays
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons. The order and references of result values are
* determined by the first array.
*
* **Note:** Unlike `_.pullAll`, this method returns a new array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @returns {Array} Returns the new array of filtered values.
* @see _.without, _.xor
* @example
*
* _.difference([2, 1], [2, 3]);
* // => [1]
*/
var difference = baseRest(function(array, values) {
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))
: [];
});
/**
* This method is like `_.difference` except that it accepts `iteratee` which
* is invoked for each element of `array` and `values` to generate the criterion
* by which they're compared. The order and references of result values are
* determined by the first array. The iteratee is invoked with one argument:
* (value).
*
* **Note:** Unlike `_.pullAllBy`, this method returns a new array.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
* // => [1.2]
*
* // The `_.property` iteratee shorthand.
* _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
* // => [{ 'x': 2 }]
*/
var differenceBy = baseRest(function(array, values) {
var iteratee = last(values);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined$1;
}
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2))
: [];
});
/**
* This method is like `_.difference` except that it accepts `comparator`
* which is invoked to compare elements of `array` to `values`. The order and
* references of result values are determined by the first array. The comparator
* is invoked with two arguments: (arrVal, othVal).
*
* **Note:** Unlike `_.pullAllWith`, this method returns a new array.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
*
* _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);
* // => [{ 'x': 2, 'y': 1 }]
*/
var differenceWith = baseRest(function(array, values) {
var comparator = last(values);
if (isArrayLikeObject(comparator)) {
comparator = undefined$1;
}
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined$1, comparator)
: [];
});
/**
* Creates a slice of `array` with `n` elements dropped from the beginning.
*
* @static
* @memberOf _
* @since 0.5.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=1] The number of elements to drop.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.drop([1, 2, 3]);
* // => [2, 3]
*
* _.drop([1, 2, 3], 2);
* // => [3]
*
* _.drop([1, 2, 3], 5);
* // => []
*
* _.drop([1, 2, 3], 0);
* // => [1, 2, 3]
*/
function drop(array, n, guard) {
var length = array == null ? 0 : array.length;
if (!length) {
return [];
}
n = (guard || n === undefined$1) ? 1 : toInteger(n);
return baseSlice(array, n < 0 ? 0 : n, length);
}
/**
* Creates a slice of `array` with `n` elements dropped from the end.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=1] The number of elements to drop.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.dropRight([1, 2, 3]);
* // => [1, 2]
*
* _.dropRight([1, 2, 3], 2);
* // => [1]
*
* _.dropRight([1, 2, 3], 5);
* // => []
*
* _.dropRight([1, 2, 3], 0);
* // => [1, 2, 3]
*/
function dropRight(array, n, guard) {
var length = array == null ? 0 : array.length;
if (!length) {
return [];
}
n = (guard || n === undefined$1) ? 1 : toInteger(n);
n = length - n;
return baseSlice(array, 0, n < 0 ? 0 : n);
}
/**
* Creates a slice of `array` excluding elements dropped from the end.
* Elements are dropped until `predicate` returns falsey. The predicate is
* invoked with three arguments: (value, index, array).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the slice of `array`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': false }
* ];
*
* _.dropRightWhile(users, function(o) { return !o.active; });
* // => objects for ['barney']
*
* // The `_.matches` iteratee shorthand.
* _.dropRightWhile(users, { 'user': 'pebbles', 'active': false });
* // => objects for ['barney', 'fred']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.dropRightWhile(users, ['active', false]);
* // => objects for ['barney']
*
* // The `_.property` iteratee shorthand.
* _.dropRightWhile(users, 'active');
* // => objects for ['barney', 'fred', 'pebbles']
*/
function dropRightWhile(array, predicate) {
return (array && array.length)
? baseWhile(array, getIteratee(predicate, 3), true, true)
: [];
}
/**
* Creates a slice of `array` excluding elements dropped from the beginning.
* Elements are dropped until `predicate` returns falsey. The predicate is
* invoked with three arguments: (value, index, array).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the slice of `array`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': false },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': true }
* ];
*
* _.dropWhile(users, function(o) { return !o.active; });
* // => objects for ['pebbles']
*
* // The `_.matches` iteratee shorthand.
* _.dropWhile(users, { 'user': 'barney', 'active': false });
* // => objects for ['fred', 'pebbles']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.dropWhile(users, ['active', false]);
* // => objects for ['pebbles']
*
* // The `_.property` iteratee shorthand.
* _.dropWhile(users, 'active');
* // => objects for ['barney', 'fred', 'pebbles']
*/
function dropWhile(array, predicate) {
return (array && array.length)
? baseWhile(array, getIteratee(predicate, 3), true)
: [];
}
/**
* Fills elements of `array` with `value` from `start` up to, but not
* including, `end`.
*
* **Note:** This method mutates `array`.
*
* @static
* @memberOf _
* @since 3.2.0
* @category Array
* @param {Array} array The array to fill.
* @param {*} value The value to fill `array` with.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns `array`.
* @example
*
* var array = [1, 2, 3];
*
* _.fill(array, 'a');
* console.log(array);
* // => ['a', 'a', 'a']
*
* _.fill(Array(3), 2);
* // => [2, 2, 2]
*
* _.fill([4, 6, 8, 10], '*', 1, 3);
* // => [4, '*', '*', 10]
*/
function fill(array, value, start, end) {
var length = array == null ? 0 : array.length;
if (!length) {
return [];
}
if (start && typeof start != 'number' && isIterateeCall(array, value, start)) {
start = 0;
end = length;
}
return baseFill(array, value, start, end);
}
/**
* This method is like `_.find` except that it returns the index of the first
* element `predicate` returns truthy for instead of the element itself.
*
* @static
* @memberOf _
* @since 1.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @param {number} [fromIndex=0] The index to search from.
* @returns {number} Returns the index of the found element, else `-1`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': false },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': true }
* ];
*
* _.findIndex(users, function(o) { return o.user == 'barney'; });
* // => 0
*
* // The `_.matches` iteratee shorthand.
* _.findIndex(users, { 'user': 'fred', 'active': false });
* // => 1
*
* // The `_.matchesProperty` iteratee shorthand.
* _.findIndex(users, ['active', false]);
* // => 0
*
* // The `_.property` iteratee shorthand.
* _.findIndex(users, 'active');
* // => 2
*/
function findIndex(array, predicate, fromIndex) {
var length = array == null ? 0 : array.length;
if (!length) {
return -1;
}
var index = fromIndex == null ? 0 : toInteger(fromIndex);
if (index < 0) {
index = nativeMax(length + index, 0);
}
return baseFindIndex(array, getIteratee(predicate, 3), index);
}
/**
* This method is like `_.findIndex` except that it iterates over elements
* of `collection` from right to left.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @param {number} [fromIndex=array.length-1] The index to search from.
* @returns {number} Returns the index of the found element, else `-1`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': false }
* ];
*
* _.findLastIndex(users, function(o) { return o.user == 'pebbles'; });
* // => 2
*
* // The `_.matches` iteratee shorthand.
* _.findLastIndex(users, { 'user': 'barney', 'active': true });
* // => 0
*
* // The `_.matchesProperty` iteratee shorthand.
* _.findLastIndex(users, ['active', false]);
* // => 2
*
* // The `_.property` iteratee shorthand.
* _.findLastIndex(users, 'active');
* // => 0
*/
function findLastIndex(array, predicate, fromIndex) {
var length = array == null ? 0 : array.length;
if (!length) {
return -1;
}
var index = length - 1;
if (fromIndex !== undefined$1) {
index = toInteger(fromIndex);
index = fromIndex < 0
? nativeMax(length + index, 0)
: nativeMin(index, length - 1);
}
return baseFindIndex(array, getIteratee(predicate, 3), index, true);
}
/**
* Flattens `array` a single level deep.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to flatten.
* @returns {Array} Returns the new flattened array.
* @example
*
* _.flatten([1, [2, [3, [4]], 5]]);
* // => [1, 2, [3, [4]], 5]
*/
function flatten(array) {
var length = array == null ? 0 : array.length;
return length ? baseFlatten(array, 1) : [];
}
/**
* Recursively flattens `array`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to flatten.
* @returns {Array} Returns the new flattened array.
* @example
*
* _.flattenDeep([1, [2, [3, [4]], 5]]);
* // => [1, 2, 3, 4, 5]
*/
function flattenDeep(array) {
var length = array == null ? 0 : array.length;
return length ? baseFlatten(array, INFINITY) : [];
}
/**
* Recursively flatten `array` up to `depth` times.
*
* @static
* @memberOf _
* @since 4.4.0
* @category Array
* @param {Array} array The array to flatten.
* @param {number} [depth=1] The maximum recursion depth.
* @returns {Array} Returns the new flattened array.
* @example
*
* var array = [1, [2, [3, [4]], 5]];
*
* _.flattenDepth(array, 1);
* // => [1, 2, [3, [4]], 5]
*
* _.flattenDepth(array, 2);
* // => [1, 2, 3, [4], 5]
*/
function flattenDepth(array, depth) {
var length = array == null ? 0 : array.length;
if (!length) {
return [];
}
depth = depth === undefined$1 ? 1 : toInteger(depth);
return baseFlatten(array, depth);
}
/**
* The inverse of `_.toPairs`; this method returns an object composed
* from key-value `pairs`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} pairs The key-value pairs.
* @returns {Object} Returns the new object.
* @example
*
* _.fromPairs([['a', 1], ['b', 2]]);
* // => { 'a': 1, 'b': 2 }
*/
function fromPairs(pairs) {
var index = -1,
length = pairs == null ? 0 : pairs.length,
result = {};
while (++index < length) {
var pair = pairs[index];
result[pair[0]] = pair[1];
}
return result;
}
/**
* Gets the first element of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @alias first
* @category Array
* @param {Array} array The array to query.
* @returns {*} Returns the first element of `array`.
* @example
*
* _.head([1, 2, 3]);
* // => 1
*
* _.head([]);
* // => undefined
*/
function head(array) {
return (array && array.length) ? array[0] : undefined$1;
}
/**
* Gets the index at which the first occurrence of `value` is found in `array`
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons. If `fromIndex` is negative, it's used as the
* offset from the end of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} [fromIndex=0] The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.indexOf([1, 2, 1, 2], 2);
* // => 1
*
* // Search from the `fromIndex`.
* _.indexOf([1, 2, 1, 2], 2, 2);
* // => 3
*/
function indexOf(array, value, fromIndex) {
var length = array == null ? 0 : array.length;
if (!length) {
return -1;
}
var index = fromIndex == null ? 0 : toInteger(fromIndex);
if (index < 0) {
index = nativeMax(length + index, 0);
}
return baseIndexOf(array, value, index);
}
/**
* Gets all but the last element of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to query.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.initial([1, 2, 3]);
* // => [1, 2]
*/
function initial(array) {
var length = array == null ? 0 : array.length;
return length ? baseSlice(array, 0, -1) : [];
}
/**
* Creates an array of unique values that are included in all given arrays
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons. The order and references of result values are
* determined by the first array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @returns {Array} Returns the new array of intersecting values.
* @example
*
* _.intersection([2, 1], [2, 3]);
* // => [2]
*/
var intersection = baseRest(function(arrays) {
var mapped = arrayMap(arrays, castArrayLikeObject);
return (mapped.length && mapped[0] === arrays[0])
? baseIntersection(mapped)
: [];
});
/**
* This method is like `_.intersection` except that it accepts `iteratee`
* which is invoked for each element of each `arrays` to generate the criterion
* by which they're compared. The order and references of result values are
* determined by the first array. The iteratee is invoked with one argument:
* (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns the new array of intersecting values.
* @example
*
* _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);
* // => [2.1]
*
* // The `_.property` iteratee shorthand.
* _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 1 }]
*/
var intersectionBy = baseRest(function(arrays) {
var iteratee = last(arrays),
mapped = arrayMap(arrays, castArrayLikeObject);
if (iteratee === last(mapped)) {
iteratee = undefined$1;
} else {
mapped.pop();
}
return (mapped.length && mapped[0] === arrays[0])
? baseIntersection(mapped, getIteratee(iteratee, 2))
: [];
});
/**
* This method is like `_.intersection` except that it accepts `comparator`
* which is invoked to compare elements of `arrays`. The order and references
* of result values are determined by the first array. The comparator is
* invoked with two arguments: (arrVal, othVal).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of intersecting values.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
* var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
*
* _.intersectionWith(objects, others, _.isEqual);
* // => [{ 'x': 1, 'y': 2 }]
*/
var intersectionWith = baseRest(function(arrays) {
var comparator = last(arrays),
mapped = arrayMap(arrays, castArrayLikeObject);
comparator = typeof comparator == 'function' ? comparator : undefined$1;
if (comparator) {
mapped.pop();
}
return (mapped.length && mapped[0] === arrays[0])
? baseIntersection(mapped, undefined$1, comparator)
: [];
});
/**
* Converts all elements in `array` into a string separated by `separator`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to convert.
* @param {string} [separator=','] The element separator.
* @returns {string} Returns the joined string.
* @example
*
* _.join(['a', 'b', 'c'], '~');
* // => 'a~b~c'
*/
function join(array, separator) {
return array == null ? '' : nativeJoin.call(array, separator);
}
/**
* Gets the last element of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to query.
* @returns {*} Returns the last element of `array`.
* @example
*
* _.last([1, 2, 3]);
* // => 3
*/
function last(array) {
var length = array == null ? 0 : array.length;
return length ? array[length - 1] : undefined$1;
}
/**
* This method is like `_.indexOf` except that it iterates over elements of
* `array` from right to left.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} [fromIndex=array.length-1] The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.lastIndexOf([1, 2, 1, 2], 2);
* // => 3
*
* // Search from the `fromIndex`.
* _.lastIndexOf([1, 2, 1, 2], 2, 2);
* // => 1
*/
function lastIndexOf(array, value, fromIndex) {
var length = array == null ? 0 : array.length;
if (!length) {
return -1;
}
var index = length;
if (fromIndex !== undefined$1) {
index = toInteger(fromIndex);
index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1);
}
return value === value
? strictLastIndexOf(array, value, index)
: baseFindIndex(array, baseIsNaN, index, true);
}
/**
* Gets the element at index `n` of `array`. If `n` is negative, the nth
* element from the end is returned.
*
* @static
* @memberOf _
* @since 4.11.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=0] The index of the element to return.
* @returns {*} Returns the nth element of `array`.
* @example
*
* var array = ['a', 'b', 'c', 'd'];
*
* _.nth(array, 1);
* // => 'b'
*
* _.nth(array, -2);
* // => 'c';
*/
function nth(array, n) {
return (array && array.length) ? baseNth(array, toInteger(n)) : undefined$1;
}
/**
* Removes all given values from `array` using
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons.
*
* **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove`
* to remove elements from an array by predicate.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {...*} [values] The values to remove.
* @returns {Array} Returns `array`.
* @example
*
* var array = ['a', 'b', 'c', 'a', 'b', 'c'];
*
* _.pull(array, 'a', 'c');
* console.log(array);
* // => ['b', 'b']
*/
var pull = baseRest(pullAll);
/**
* This method is like `_.pull` except that it accepts an array of values to remove.
*
* **Note:** Unlike `_.difference`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @returns {Array} Returns `array`.
* @example
*
* var array = ['a', 'b', 'c', 'a', 'b', 'c'];
*
* _.pullAll(array, ['a', 'c']);
* console.log(array);
* // => ['b', 'b']
*/
function pullAll(array, values) {
return (array && array.length && values && values.length)
? basePullAll(array, values)
: array;
}
/**
* This method is like `_.pullAll` except that it accepts `iteratee` which is
* invoked for each element of `array` and `values` to generate the criterion
* by which they're compared. The iteratee is invoked with one argument: (value).
*
* **Note:** Unlike `_.differenceBy`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns `array`.
* @example
*
* var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];
*
* _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');
* console.log(array);
* // => [{ 'x': 2 }]
*/
function pullAllBy(array, values, iteratee) {
return (array && array.length && values && values.length)
? basePullAll(array, values, getIteratee(iteratee, 2))
: array;
}
/**
* This method is like `_.pullAll` except that it accepts `comparator` which
* is invoked to compare elements of `array` to `values`. The comparator is
* invoked with two arguments: (arrVal, othVal).
*
* **Note:** Unlike `_.differenceWith`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 4.6.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns `array`.
* @example
*
* var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }];
*
* _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual);
* console.log(array);
* // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]
*/
function pullAllWith(array, values, comparator) {
return (array && array.length && values && values.length)
? basePullAll(array, values, undefined$1, comparator)
: array;
}
/**
* Removes elements from `array` corresponding to `indexes` and returns an
* array of removed elements.
*
* **Note:** Unlike `_.at`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {...(number|number[])} [indexes] The indexes of elements to remove.
* @returns {Array} Returns the new array of removed elements.
* @example
*
* var array = ['a', 'b', 'c', 'd'];
* var pulled = _.pullAt(array, [1, 3]);
*
* console.log(array);
* // => ['a', 'c']
*
* console.log(pulled);
* // => ['b', 'd']
*/
var pullAt = flatRest(function(array, indexes) {
var length = array == null ? 0 : array.length,
result = baseAt(array, indexes);
basePullAt(array, arrayMap(indexes, function(index) {
return isIndex(index, length) ? +index : index;
}).sort(compareAscending));
return result;
});
/**
* Removes all elements from `array` that `predicate` returns truthy for
* and returns an array of the removed elements. The predicate is invoked
* with three arguments: (value, index, array).
*
* **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull`
* to pull elements from an array by value.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the new array of removed elements.
* @example
*
* var array = [1, 2, 3, 4];
* var evens = _.remove(array, function(n) {
* return n % 2 == 0;
* });
*
* console.log(array);
* // => [1, 3]
*
* console.log(evens);
* // => [2, 4]
*/
function remove(array, predicate) {
var result = [];
if (!(array && array.length)) {
return result;
}
var index = -1,
indexes = [],
length = array.length;
predicate = getIteratee(predicate, 3);
while (++index < length) {
var value = array[index];
if (predicate(value, index, array)) {
result.push(value);
indexes.push(index);
}
}
basePullAt(array, indexes);
return result;
}
/**
* Reverses `array` so that the first element becomes the last, the second
* element becomes the second to last, and so on.
*
* **Note:** This method mutates `array` and is based on
* [`Array#reverse`](https://mdn.io/Array/reverse).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to modify.
* @returns {Array} Returns `array`.
* @example
*
* var array = [1, 2, 3];
*
* _.reverse(array);
* // => [3, 2, 1]
*
* console.log(array);
* // => [3, 2, 1]
*/
function reverse(array) {
return array == null ? array : nativeReverse.call(array);
}
/**
* Creates a slice of `array` from `start` up to, but not including, `end`.
*
* **Note:** This method is used instead of
* [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are
* returned.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to slice.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the slice of `array`.
*/
function slice(array, start, end) {
var length = array == null ? 0 : array.length;
if (!length) {
return [];
}
if (end && typeof end != 'number' && isIterateeCall(array, start, end)) {
start = 0;
end = length;
}
else {
start = start == null ? 0 : toInteger(start);
end = end === undefined$1 ? length : toInteger(end);
}
return baseSlice(array, start, end);
}
/**
* Uses a binary search to determine the lowest index at which `value`
* should be inserted into `array` in order to maintain its sort order.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* _.sortedIndex([30, 50], 40);
* // => 1
*/
function sortedIndex(array, value) {
return baseSortedIndex(array, value);
}
/**
* This method is like `_.sortedIndex` except that it accepts `iteratee`
* which is invoked for `value` and each element of `array` to compute their
* sort ranking. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* var objects = [{ 'x': 4 }, { 'x': 5 }];
*
* _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });
* // => 0
*
* // The `_.property` iteratee shorthand.
* _.sortedIndexBy(objects, { 'x': 4 }, 'x');
* // => 0
*/
function sortedIndexBy(array, value, iteratee) {
return baseSortedIndexBy(array, value, getIteratee(iteratee, 2));
}
/**
* This method is like `_.indexOf` except that it performs a binary
* search on a sorted `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.sortedIndexOf([4, 5, 5, 5, 6], 5);
* // => 1
*/
function sortedIndexOf(array, value) {
var length = array == null ? 0 : array.length;
if (length) {
var index = baseSortedIndex(array, value);
if (index < length && eq(array[index], value)) {
return index;
}
}
return -1;
}
/**
* This method is like `_.sortedIndex` except that it returns the highest
* index at which `value` should be inserted into `array` in order to
* maintain its sort order.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* _.sortedLastIndex([4, 5, 5, 5, 6], 5);
* // => 4
*/
function sortedLastIndex(array, value) {
return baseSortedIndex(array, value, true);
}
/**
* This method is like `_.sortedLastIndex` except that it accepts `iteratee`
* which is invoked for `value` and each element of `array` to compute their
* sort ranking. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* var objects = [{ 'x': 4 }, { 'x': 5 }];
*
* _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });
* // => 1
*
* // The `_.property` iteratee shorthand.
* _.sortedLastIndexBy(objects, { 'x': 4 }, 'x');
* // => 1
*/
function sortedLastIndexBy(array, value, iteratee) {
return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true);
}
/**
* This method is like `_.lastIndexOf` except that it performs a binary
* search on a sorted `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.sortedLastIndexOf([4, 5, 5, 5, 6], 5);
* // => 3
*/
function sortedLastIndexOf(array, value) {
var length = array == null ? 0 : array.length;
if (length) {
var index = baseSortedIndex(array, value, true) - 1;
if (eq(array[index], value)) {
return index;
}
}
return -1;
}
/**
* This method is like `_.uniq` except that it's designed and optimized
* for sorted arrays.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @returns {Array} Returns the new duplicate free array.
* @example
*
* _.sortedUniq([1, 1, 2]);
* // => [1, 2]
*/
function sortedUniq(array) {
return (array && array.length)
? baseSortedUniq(array)
: [];
}
/**
* This method is like `_.uniqBy` except that it's designed and optimized
* for sorted arrays.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @returns {Array} Returns the new duplicate free array.
* @example
*
* _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);
* // => [1.1, 2.3]
*/
function sortedUniqBy(array, iteratee) {
return (array && array.length)
? baseSortedUniq(array, getIteratee(iteratee, 2))
: [];
}
/**
* Gets all but the first element of `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to query.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.tail([1, 2, 3]);
* // => [2, 3]
*/
function tail(array) {
var length = array == null ? 0 : array.length;
return length ? baseSlice(array, 1, length) : [];
}
/**
* Creates a slice of `array` with `n` elements taken from the beginning.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=1] The number of elements to take.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.take([1, 2, 3]);
* // => [1]
*
* _.take([1, 2, 3], 2);
* // => [1, 2]
*
* _.take([1, 2, 3], 5);
* // => [1, 2, 3]
*
* _.take([1, 2, 3], 0);
* // => []
*/
function take(array, n, guard) {
if (!(array && array.length)) {
return [];
}
n = (guard || n === undefined$1) ? 1 : toInteger(n);
return baseSlice(array, 0, n < 0 ? 0 : n);
}
/**
* Creates a slice of `array` with `n` elements taken from the end.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=1] The number of elements to take.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.takeRight([1, 2, 3]);
* // => [3]
*
* _.takeRight([1, 2, 3], 2);
* // => [2, 3]
*
* _.takeRight([1, 2, 3], 5);
* // => [1, 2, 3]
*
* _.takeRight([1, 2, 3], 0);
* // => []
*/
function takeRight(array, n, guard) {
var length = array == null ? 0 : array.length;
if (!length) {
return [];
}
n = (guard || n === undefined$1) ? 1 : toInteger(n);
n = length - n;
return baseSlice(array, n < 0 ? 0 : n, length);
}
/**
* Creates a slice of `array` with elements taken from the end. Elements are
* taken until `predicate` returns falsey. The predicate is invoked with
* three arguments: (value, index, array).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the slice of `array`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': false }
* ];
*
* _.takeRightWhile(users, function(o) { return !o.active; });
* // => objects for ['fred', 'pebbles']
*
* // The `_.matches` iteratee shorthand.
* _.takeRightWhile(users, { 'user': 'pebbles', 'active': false });
* // => objects for ['pebbles']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.takeRightWhile(users, ['active', false]);
* // => objects for ['fred', 'pebbles']
*
* // The `_.property` iteratee shorthand.
* _.takeRightWhile(users, 'active');
* // => []
*/
function takeRightWhile(array, predicate) {
return (array && array.length)
? baseWhile(array, getIteratee(predicate, 3), false, true)
: [];
}
/**
* Creates a slice of `array` with elements taken from the beginning. Elements
* are taken until `predicate` returns falsey. The predicate is invoked with
* three arguments: (value, index, array).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the slice of `array`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': false },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': true }
* ];
*
* _.takeWhile(users, function(o) { return !o.active; });
* // => objects for ['barney', 'fred']
*
* // The `_.matches` iteratee shorthand.
* _.takeWhile(users, { 'user': 'barney', 'active': false });
* // => objects for ['barney']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.takeWhile(users, ['active', false]);
* // => objects for ['barney', 'fred']
*
* // The `_.property` iteratee shorthand.
* _.takeWhile(users, 'active');
* // => []
*/
function takeWhile(array, predicate) {
return (array && array.length)
? baseWhile(array, getIteratee(predicate, 3))
: [];
}
/**
* Creates an array of unique values, in order, from all given arrays using
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @returns {Array} Returns the new array of combined values.
* @example
*
* _.union([2], [1, 2]);
* // => [2, 1]
*/
var union = baseRest(function(arrays) {
return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true));
});
/**
* This method is like `_.union` except that it accepts `iteratee` which is
* invoked for each element of each `arrays` to generate the criterion by
* which uniqueness is computed. Result values are chosen from the first
* array in which the value occurs. The iteratee is invoked with one argument:
* (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns the new array of combined values.
* @example
*
* _.unionBy([2.1], [1.2, 2.3], Math.floor);
* // => [2.1, 1.2]
*
* // The `_.property` iteratee shorthand.
* _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 1 }, { 'x': 2 }]
*/
var unionBy = baseRest(function(arrays) {
var iteratee = last(arrays);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined$1;
}
return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2));
});
/**
* This method is like `_.union` except that it accepts `comparator` which
* is invoked to compare elements of `arrays`. Result values are chosen from
* the first array in which the value occurs. The comparator is invoked
* with two arguments: (arrVal, othVal).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of combined values.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
* var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
*
* _.unionWith(objects, others, _.isEqual);
* // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
*/
var unionWith = baseRest(function(arrays) {
var comparator = last(arrays);
comparator = typeof comparator == 'function' ? comparator : undefined$1;
return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined$1, comparator);
});
/**
* Creates a duplicate-free version of an array, using
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons, in which only the first occurrence of each element
* is kept. The order of result values is determined by the order they occur
* in the array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @returns {Array} Returns the new duplicate free array.
* @example
*
* _.uniq([2, 1, 2]);
* // => [2, 1]
*/
function uniq(array) {
return (array && array.length) ? baseUniq(array) : [];
}
/**
* This method is like `_.uniq` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the criterion by which
* uniqueness is computed. The order of result values is determined by the
* order they occur in the array. The iteratee is invoked with one argument:
* (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns the new duplicate free array.
* @example
*
* _.uniqBy([2.1, 1.2, 2.3], Math.floor);
* // => [2.1, 1.2]
*
* // The `_.property` iteratee shorthand.
* _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 1 }, { 'x': 2 }]
*/
function uniqBy(array, iteratee) {
return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : [];
}
/**
* This method is like `_.uniq` except that it accepts `comparator` which
* is invoked to compare elements of `array`. The order of result values is
* determined by the order they occur in the array.The comparator is invoked
* with two arguments: (arrVal, othVal).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new duplicate free array.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
*
* _.uniqWith(objects, _.isEqual);
* // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
*/
function uniqWith(array, comparator) {
comparator = typeof comparator == 'function' ? comparator : undefined$1;
return (array && array.length) ? baseUniq(array, undefined$1, comparator) : [];
}
/**
* This method is like `_.zip` except that it accepts an array of grouped
* elements and creates an array regrouping the elements to their pre-zip
* configuration.
*
* @static
* @memberOf _
* @since 1.2.0
* @category Array
* @param {Array} array The array of grouped elements to process.
* @returns {Array} Returns the new array of regrouped elements.
* @example
*
* var zipped = _.zip(['a', 'b'], [1, 2], [true, false]);
* // => [['a', 1, true], ['b', 2, false]]
*
* _.unzip(zipped);
* // => [['a', 'b'], [1, 2], [true, false]]
*/
function unzip(array) {
if (!(array && array.length)) {
return [];
}
var length = 0;
array = arrayFilter(array, function(group) {
if (isArrayLikeObject(group)) {
length = nativeMax(group.length, length);
return true;
}
});
return baseTimes(length, function(index) {
return arrayMap(array, baseProperty(index));
});
}
/**
* This method is like `_.unzip` except that it accepts `iteratee` to specify
* how regrouped values should be combined. The iteratee is invoked with the
* elements of each group: (...group).
*
* @static
* @memberOf _
* @since 3.8.0
* @category Array
* @param {Array} array The array of grouped elements to process.
* @param {Function} [iteratee=_.identity] The function to combine
* regrouped values.
* @returns {Array} Returns the new array of regrouped elements.
* @example
*
* var zipped = _.zip([1, 2], [10, 20], [100, 200]);
* // => [[1, 10, 100], [2, 20, 200]]
*
* _.unzipWith(zipped, _.add);
* // => [3, 30, 300]
*/
function unzipWith(array, iteratee) {
if (!(array && array.length)) {
return [];
}
var result = unzip(array);
if (iteratee == null) {
return result;
}
return arrayMap(result, function(group) {
return apply(iteratee, undefined$1, group);
});
}
/**
* Creates an array excluding all given values using
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons.
*
* **Note:** Unlike `_.pull`, this method returns a new array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...*} [values] The values to exclude.
* @returns {Array} Returns the new array of filtered values.
* @see _.difference, _.xor
* @example
*
* _.without([2, 1, 2, 3], 1, 2);
* // => [3]
*/
var without = baseRest(function(array, values) {
return isArrayLikeObject(array)
? baseDifference(array, values)
: [];
});
/**
* Creates an array of unique values that is the
* [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
* of the given arrays. The order of result values is determined by the order
* they occur in the arrays.
*
* @static
* @memberOf _
* @since 2.4.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @returns {Array} Returns the new array of filtered values.
* @see _.difference, _.without
* @example
*
* _.xor([2, 1], [2, 3]);
* // => [1, 3]
*/
var xor = baseRest(function(arrays) {
return baseXor(arrayFilter(arrays, isArrayLikeObject));
});
/**
* This method is like `_.xor` except that it accepts `iteratee` which is
* invoked for each element of each `arrays` to generate the criterion by
* which by which they're compared. The order of result values is determined
* by the order they occur in the arrays. The iteratee is invoked with one
* argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor);
* // => [1.2, 3.4]
*
* // The `_.property` iteratee shorthand.
* _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 2 }]
*/
var xorBy = baseRest(function(arrays) {
var iteratee = last(arrays);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined$1;
}
return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2));
});
/**
* This method is like `_.xor` except that it accepts `comparator` which is
* invoked to compare elements of `arrays`. The order of result values is
* determined by the order they occur in the arrays. The comparator is invoked
* with two arguments: (arrVal, othVal).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
* var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
*
* _.xorWith(objects, others, _.isEqual);
* // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
*/
var xorWith = baseRest(function(arrays) {
var comparator = last(arrays);
comparator = typeof comparator == 'function' ? comparator : undefined$1;
return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined$1, comparator);
});
/**
* Creates an array of grouped elements, the first of which contains the
* first elements of the given arrays, the second of which contains the
* second elements of the given arrays, and so on.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {...Array} [arrays] The arrays to process.
* @returns {Array} Returns the new array of grouped elements.
* @example
*
* _.zip(['a', 'b'], [1, 2], [true, false]);
* // => [['a', 1, true], ['b', 2, false]]
*/
var zip = baseRest(unzip);
/**
* This method is like `_.fromPairs` except that it accepts two arrays,
* one of property identifiers and one of corresponding values.
*
* @static
* @memberOf _
* @since 0.4.0
* @category Array
* @param {Array} [props=[]] The property identifiers.
* @param {Array} [values=[]] The property values.
* @returns {Object} Returns the new object.
* @example
*
* _.zipObject(['a', 'b'], [1, 2]);
* // => { 'a': 1, 'b': 2 }
*/
function zipObject(props, values) {
return baseZipObject(props || [], values || [], assignValue);
}
/**
* This method is like `_.zipObject` except that it supports property paths.
*
* @static
* @memberOf _
* @since 4.1.0
* @category Array
* @param {Array} [props=[]] The property identifiers.
* @param {Array} [values=[]] The property values.
* @returns {Object} Returns the new object.
* @example
*
* _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]);
* // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } }
*/
function zipObjectDeep(props, values) {
return baseZipObject(props || [], values || [], baseSet);
}
/**
* This method is like `_.zip` except that it accepts `iteratee` to specify
* how grouped values should be combined. The iteratee is invoked with the
* elements of each group: (...group).
*
* @static
* @memberOf _
* @since 3.8.0
* @category Array
* @param {...Array} [arrays] The arrays to process.
* @param {Function} [iteratee=_.identity] The function to combine
* grouped values.
* @returns {Array} Returns the new array of grouped elements.
* @example
*
* _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) {
* return a + b + c;
* });
* // => [111, 222]
*/
var zipWith = baseRest(function(arrays) {
var length = arrays.length,
iteratee = length > 1 ? arrays[length - 1] : undefined$1;
iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined$1;
return unzipWith(arrays, iteratee);
});
/*------------------------------------------------------------------------*/
/**
* Creates a `lodash` wrapper instance that wraps `value` with explicit method
* chain sequences enabled. The result of such sequences must be unwrapped
* with `_#value`.
*
* @static
* @memberOf _
* @since 1.3.0
* @category Seq
* @param {*} value The value to wrap.
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36 },
* { 'user': 'fred', 'age': 40 },
* { 'user': 'pebbles', 'age': 1 }
* ];
*
* var youngest = _
* .chain(users)
* .sortBy('age')
* .map(function(o) {
* return o.user + ' is ' + o.age;
* })
* .head()
* .value();
* // => 'pebbles is 1'
*/
function chain(value) {
var result = lodash(value);
result.__chain__ = true;
return result;
}
/**
* This method invokes `interceptor` and returns `value`. The interceptor
* is invoked with one argument; (value). The purpose of this method is to
* "tap into" a method chain sequence in order to modify intermediate results.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Seq
* @param {*} value The value to provide to `interceptor`.
* @param {Function} interceptor The function to invoke.
* @returns {*} Returns `value`.
* @example
*
* _([1, 2, 3])
* .tap(function(array) {
* // Mutate input array.
* array.pop();
* })
* .reverse()
* .value();
* // => [2, 1]
*/
function tap(value, interceptor) {
interceptor(value);
return value;
}
/**
* This method is like `_.tap` except that it returns the result of `interceptor`.
* The purpose of this method is to "pass thru" values replacing intermediate
* results in a method chain sequence.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Seq
* @param {*} value The value to provide to `interceptor`.
* @param {Function} interceptor The function to invoke.
* @returns {*} Returns the result of `interceptor`.
* @example
*
* _(' abc ')
* .chain()
* .trim()
* .thru(function(value) {
* return [value];
* })
* .value();
* // => ['abc']
*/
function thru(value, interceptor) {
return interceptor(value);
}
/**
* This method is the wrapper version of `_.at`.
*
* @name at
* @memberOf _
* @since 1.0.0
* @category Seq
* @param {...(string|string[])} [paths] The property paths to pick.
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
*
* _(object).at(['a[0].b.c', 'a[1]']).value();
* // => [3, 4]
*/
var wrapperAt = flatRest(function(paths) {
var length = paths.length,
start = length ? paths[0] : 0,
value = this.__wrapped__,
interceptor = function(object) { return baseAt(object, paths); };
if (length > 1 || this.__actions__.length ||
!(value instanceof LazyWrapper) || !isIndex(start)) {
return this.thru(interceptor);
}
value = value.slice(start, +start + (length ? 1 : 0));
value.__actions__.push({
'func': thru,
'args': [interceptor],
'thisArg': undefined$1
});
return new LodashWrapper(value, this.__chain__).thru(function(array) {
if (length && !array.length) {
array.push(undefined$1);
}
return array;
});
});
/**
* Creates a `lodash` wrapper instance with explicit method chain sequences enabled.
*
* @name chain
* @memberOf _
* @since 0.1.0
* @category Seq
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36 },
* { 'user': 'fred', 'age': 40 }
* ];
*
* // A sequence without explicit chaining.
* _(users).head();
* // => { 'user': 'barney', 'age': 36 }
*
* // A sequence with explicit chaining.
* _(users)
* .chain()
* .head()
* .pick('user')
* .value();
* // => { 'user': 'barney' }
*/
function wrapperChain() {
return chain(this);
}
/**
* Executes the chain sequence and returns the wrapped result.
*
* @name commit
* @memberOf _
* @since 3.2.0
* @category Seq
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* var array = [1, 2];
* var wrapped = _(array).push(3);
*
* console.log(array);
* // => [1, 2]
*
* wrapped = wrapped.commit();
* console.log(array);
* // => [1, 2, 3]
*
* wrapped.last();
* // => 3
*
* console.log(array);
* // => [1, 2, 3]
*/
function wrapperCommit() {
return new LodashWrapper(this.value(), this.__chain__);
}
/**
* Gets the next value on a wrapped object following the
* [iterator protocol](https://mdn.io/iteration_protocols#iterator).
*
* @name next
* @memberOf _
* @since 4.0.0
* @category Seq
* @returns {Object} Returns the next iterator value.
* @example
*
* var wrapped = _([1, 2]);
*
* wrapped.next();
* // => { 'done': false, 'value': 1 }
*
* wrapped.next();
* // => { 'done': false, 'value': 2 }
*
* wrapped.next();
* // => { 'done': true, 'value': undefined }
*/
function wrapperNext() {
if (this.__values__ === undefined$1) {
this.__values__ = toArray(this.value());
}
var done = this.__index__ >= this.__values__.length,
value = done ? undefined$1 : this.__values__[this.__index__++];
return { 'done': done, 'value': value };
}
/**
* Enables the wrapper to be iterable.
*
* @name Symbol.iterator
* @memberOf _
* @since 4.0.0
* @category Seq
* @returns {Object} Returns the wrapper object.
* @example
*
* var wrapped = _([1, 2]);
*
* wrapped[Symbol.iterator]() === wrapped;
* // => true
*
* Array.from(wrapped);
* // => [1, 2]
*/
function wrapperToIterator() {
return this;
}
/**
* Creates a clone of the chain sequence planting `value` as the wrapped value.
*
* @name plant
* @memberOf _
* @since 3.2.0
* @category Seq
* @param {*} value The value to plant.
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* function square(n) {
* return n * n;
* }
*
* var wrapped = _([1, 2]).map(square);
* var other = wrapped.plant([3, 4]);
*
* other.value();
* // => [9, 16]
*
* wrapped.value();
* // => [1, 4]
*/
function wrapperPlant(value) {
var result,
parent = this;
while (parent instanceof baseLodash) {
var clone = wrapperClone(parent);
clone.__index__ = 0;
clone.__values__ = undefined$1;
if (result) {
previous.__wrapped__ = clone;
} else {
result = clone;
}
var previous = clone;
parent = parent.__wrapped__;
}
previous.__wrapped__ = value;
return result;
}
/**
* This method is the wrapper version of `_.reverse`.
*
* **Note:** This method mutates the wrapped array.
*
* @name reverse
* @memberOf _
* @since 0.1.0
* @category Seq
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* var array = [1, 2, 3];
*
* _(array).reverse().value()
* // => [3, 2, 1]
*
* console.log(array);
* // => [3, 2, 1]
*/
function wrapperReverse() {
var value = this.__wrapped__;
if (value instanceof LazyWrapper) {
var wrapped = value;
if (this.__actions__.length) {
wrapped = new LazyWrapper(this);
}
wrapped = wrapped.reverse();
wrapped.__actions__.push({
'func': thru,
'args': [reverse],
'thisArg': undefined$1
});
return new LodashWrapper(wrapped, this.__chain__);
}
return this.thru(reverse);
}
/**
* Executes the chain sequence to resolve the unwrapped value.
*
* @name value
* @memberOf _
* @since 0.1.0
* @alias toJSON, valueOf
* @category Seq
* @returns {*} Returns the resolved unwrapped value.
* @example
*
* _([1, 2, 3]).value();
* // => [1, 2, 3]
*/
function wrapperValue() {
return baseWrapperValue(this.__wrapped__, this.__actions__);
}
/*------------------------------------------------------------------------*/
/**
* Creates an object composed of keys generated from the results of running
* each element of `collection` thru `iteratee`. The corresponding value of
* each key is the number of times the key was returned by `iteratee`. The
* iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 0.5.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee to transform keys.
* @returns {Object} Returns the composed aggregate object.
* @example
*
* _.countBy([6.1, 4.2, 6.3], Math.floor);
* // => { '4': 1, '6': 2 }
*
* // The `_.property` iteratee shorthand.
* _.countBy(['one', 'two', 'three'], 'length');
* // => { '3': 2, '5': 1 }
*/
var countBy = createAggregator(function(result, value, key) {
if (hasOwnProperty.call(result, key)) {
++result[key];
} else {
baseAssignValue(result, key, 1);
}
});
/**
* Checks if `predicate` returns truthy for **all** elements of `collection`.
* Iteration is stopped once `predicate` returns falsey. The predicate is
* invoked with three arguments: (value, index|key, collection).
*
* **Note:** This method returns `true` for
* [empty collections](https://en.wikipedia.org/wiki/Empty_set) because
* [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of
* elements of empty collections.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {boolean} Returns `true` if all elements pass the predicate check,
* else `false`.
* @example
*
* _.every([true, 1, null, 'yes'], Boolean);
* // => false
*
* var users = [
* { 'user': 'barney', 'age': 36, 'active': false },
* { 'user': 'fred', 'age': 40, 'active': false }
* ];
*
* // The `_.matches` iteratee shorthand.
* _.every(users, { 'user': 'barney', 'active': false });
* // => false
*
* // The `_.matchesProperty` iteratee shorthand.
* _.every(users, ['active', false]);
* // => true
*
* // The `_.property` iteratee shorthand.
* _.every(users, 'active');
* // => false
*/
function every(collection, predicate, guard) {
var func = isArray(collection) ? arrayEvery : baseEvery;
if (guard && isIterateeCall(collection, predicate, guard)) {
predicate = undefined$1;
}
return func(collection, getIteratee(predicate, 3));
}
/**
* Iterates over elements of `collection`, returning an array of all elements
* `predicate` returns truthy for. The predicate is invoked with three
* arguments: (value, index|key, collection).
*
* **Note:** Unlike `_.remove`, this method returns a new array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the new filtered array.
* @see _.reject
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36, 'active': true },
* { 'user': 'fred', 'age': 40, 'active': false }
* ];
*
* _.filter(users, function(o) { return !o.active; });
* // => objects for ['fred']
*
* // The `_.matches` iteratee shorthand.
* _.filter(users, { 'age': 36, 'active': true });
* // => objects for ['barney']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.filter(users, ['active', false]);
* // => objects for ['fred']
*
* // The `_.property` iteratee shorthand.
* _.filter(users, 'active');
* // => objects for ['barney']
*
* // Combining several predicates using `_.overEvery` or `_.overSome`.
* _.filter(users, _.overSome([{ 'age': 36 }, ['age', 40]]));
* // => objects for ['fred', 'barney']
*/
function filter(collection, predicate) {
var func = isArray(collection) ? arrayFilter : baseFilter;
return func(collection, getIteratee(predicate, 3));
}
/**
* Iterates over elements of `collection`, returning the first element
* `predicate` returns truthy for. The predicate is invoked with three
* arguments: (value, index|key, collection).
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to inspect.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @param {number} [fromIndex=0] The index to search from.
* @returns {*} Returns the matched element, else `undefined`.
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36, 'active': true },
* { 'user': 'fred', 'age': 40, 'active': false },
* { 'user': 'pebbles', 'age': 1, 'active': true }
* ];
*
* _.find(users, function(o) { return o.age < 40; });
* // => object for 'barney'
*
* // The `_.matches` iteratee shorthand.
* _.find(users, { 'age': 1, 'active': true });
* // => object for 'pebbles'
*
* // The `_.matchesProperty` iteratee shorthand.
* _.find(users, ['active', false]);
* // => object for 'fred'
*
* // The `_.property` iteratee shorthand.
* _.find(users, 'active');
* // => object for 'barney'
*/
var find = createFind(findIndex);
/**
* This method is like `_.find` except that it iterates over elements of
* `collection` from right to left.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Collection
* @param {Array|Object} collection The collection to inspect.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @param {number} [fromIndex=collection.length-1] The index to search from.
* @returns {*} Returns the matched element, else `undefined`.
* @example
*
* _.findLast([1, 2, 3, 4], function(n) {
* return n % 2 == 1;
* });
* // => 3
*/
var findLast = createFind(findLastIndex);
/**
* Creates a flattened array of values by running each element in `collection`
* thru `iteratee` and flattening the mapped results. The iteratee is invoked
* with three arguments: (value, index|key, collection).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array} Returns the new flattened array.
* @example
*
* function duplicate(n) {
* return [n, n];
* }
*
* _.flatMap([1, 2], duplicate);
* // => [1, 1, 2, 2]
*/
function flatMap(collection, iteratee) {
return baseFlatten(map(collection, iteratee), 1);
}
/**
* This method is like `_.flatMap` except that it recursively flattens the
* mapped results.
*
* @static
* @memberOf _
* @since 4.7.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array} Returns the new flattened array.
* @example
*
* function duplicate(n) {
* return [[[n, n]]];
* }
*
* _.flatMapDeep([1, 2], duplicate);
* // => [1, 1, 2, 2]
*/
function flatMapDeep(collection, iteratee) {
return baseFlatten(map(collection, iteratee), INFINITY);
}
/**
* This method is like `_.flatMap` except that it recursively flattens the
* mapped results up to `depth` times.
*
* @static
* @memberOf _
* @since 4.7.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @param {number} [depth=1] The maximum recursion depth.
* @returns {Array} Returns the new flattened array.
* @example
*
* function duplicate(n) {
* return [[[n, n]]];
* }
*
* _.flatMapDepth([1, 2], duplicate, 2);
* // => [[1, 1], [2, 2]]
*/
function flatMapDepth(collection, iteratee, depth) {
depth = depth === undefined$1 ? 1 : toInteger(depth);
return baseFlatten(map(collection, iteratee), depth);
}
/**
* Iterates over elements of `collection` and invokes `iteratee` for each element.
* The iteratee is invoked with three arguments: (value, index|key, collection).
* Iteratee functions may exit iteration early by explicitly returning `false`.
*
* **Note:** As with other "Collections" methods, objects with a "length"
* property are iterated like arrays. To avoid this behavior use `_.forIn`
* or `_.forOwn` for object iteration.
*
* @static
* @memberOf _
* @since 0.1.0
* @alias each
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array|Object} Returns `collection`.
* @see _.forEachRight
* @example
*
* _.forEach([1, 2], function(value) {
* console.log(value);
* });
* // => Logs `1` then `2`.
*
* _.forEach({ 'a': 1, 'b': 2 }, function(value, key) {
* console.log(key);
* });
* // => Logs 'a' then 'b' (iteration order is not guaranteed).
*/
function forEach(collection, iteratee) {
var func = isArray(collection) ? arrayEach : baseEach;
return func(collection, getIteratee(iteratee, 3));
}
/**
* This method is like `_.forEach` except that it iterates over elements of
* `collection` from right to left.
*
* @static
* @memberOf _
* @since 2.0.0
* @alias eachRight
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array|Object} Returns `collection`.
* @see _.forEach
* @example
*
* _.forEachRight([1, 2], function(value) {
* console.log(value);
* });
* // => Logs `2` then `1`.
*/
function forEachRight(collection, iteratee) {
var func = isArray(collection) ? arrayEachRight : baseEachRight;
return func(collection, getIteratee(iteratee, 3));
}
/**
* Creates an object composed of keys generated from the results of running
* each element of `collection` thru `iteratee`. The order of grouped values
* is determined by the order they occur in `collection`. The corresponding
* value of each key is an array of elements responsible for generating the
* key. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee to transform keys.
* @returns {Object} Returns the composed aggregate object.
* @example
*
* _.groupBy([6.1, 4.2, 6.3], Math.floor);
* // => { '4': [4.2], '6': [6.1, 6.3] }
*
* // The `_.property` iteratee shorthand.
* _.groupBy(['one', 'two', 'three'], 'length');
* // => { '3': ['one', 'two'], '5': ['three'] }
*/
var groupBy = createAggregator(function(result, value, key) {
if (hasOwnProperty.call(result, key)) {
result[key].push(value);
} else {
baseAssignValue(result, key, [value]);
}
});
/**
* Checks if `value` is in `collection`. If `collection` is a string, it's
* checked for a substring of `value`, otherwise
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* is used for equality comparisons. If `fromIndex` is negative, it's used as
* the offset from the end of `collection`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object|string} collection The collection to inspect.
* @param {*} value The value to search for.
* @param {number} [fromIndex=0] The index to search from.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.
* @returns {boolean} Returns `true` if `value` is found, else `false`.
* @example
*
* _.includes([1, 2, 3], 1);
* // => true
*
* _.includes([1, 2, 3], 1, 2);
* // => false
*
* _.includes({ 'a': 1, 'b': 2 }, 1);
* // => true
*
* _.includes('abcd', 'bc');
* // => true
*/
function includes(collection, value, fromIndex, guard) {
collection = isArrayLike(collection) ? collection : values(collection);
fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0;
var length = collection.length;
if (fromIndex < 0) {
fromIndex = nativeMax(length + fromIndex, 0);
}
return isString(collection)
? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1)
: (!!length && baseIndexOf(collection, value, fromIndex) > -1);
}
/**
* Invokes the method at `path` of each element in `collection`, returning
* an array of the results of each invoked method. Any additional arguments
* are provided to each invoked method. If `path` is a function, it's invoked
* for, and `this` bound to, each element in `collection`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Array|Function|string} path The path of the method to invoke or
* the function invoked per iteration.
* @param {...*} [args] The arguments to invoke each method with.
* @returns {Array} Returns the array of results.
* @example
*
* _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort');
* // => [[1, 5, 7], [1, 2, 3]]
*
* _.invokeMap([123, 456], String.prototype.split, '');
* // => [['1', '2', '3'], ['4', '5', '6']]
*/
var invokeMap = baseRest(function(collection, path, args) {
var index = -1,
isFunc = typeof path == 'function',
result = isArrayLike(collection) ? Array(collection.length) : [];
baseEach(collection, function(value) {
result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args);
});
return result;
});
/**
* Creates an object composed of keys generated from the results of running
* each element of `collection` thru `iteratee`. The corresponding value of
* each key is the last element responsible for generating the key. The
* iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee to transform keys.
* @returns {Object} Returns the composed aggregate object.
* @example
*
* var array = [
* { 'dir': 'left', 'code': 97 },
* { 'dir': 'right', 'code': 100 }
* ];
*
* _.keyBy(array, function(o) {
* return String.fromCharCode(o.code);
* });
* // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }
*
* _.keyBy(array, 'dir');
* // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }
*/
var keyBy = createAggregator(function(result, value, key) {
baseAssignValue(result, key, value);
});
/**
* Creates an array of values by running each element in `collection` thru
* `iteratee`. The iteratee is invoked with three arguments:
* (value, index|key, collection).
*
* Many lodash methods are guarded to work as iteratees for methods like
* `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`.
*
* The guarded methods are:
* `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`,
* `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`,
* `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`,
* `template`, `trim`, `trimEnd`, `trimStart`, and `words`
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array} Returns the new mapped array.
* @example
*
* function square(n) {
* return n * n;
* }
*
* _.map([4, 8], square);
* // => [16, 64]
*
* _.map({ 'a': 4, 'b': 8 }, square);
* // => [16, 64] (iteration order is not guaranteed)
*
* var users = [
* { 'user': 'barney' },
* { 'user': 'fred' }
* ];
*
* // The `_.property` iteratee shorthand.
* _.map(users, 'user');
* // => ['barney', 'fred']
*/
function map(collection, iteratee) {
var func = isArray(collection) ? arrayMap : baseMap;
return func(collection, getIteratee(iteratee, 3));
}
/**
* This method is like `_.sortBy` except that it allows specifying the sort
* orders of the iteratees to sort by. If `orders` is unspecified, all values
* are sorted in ascending order. Otherwise, specify an order of "desc" for
* descending or "asc" for ascending sort order of corresponding values.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]]
* The iteratees to sort by.
* @param {string[]} [orders] The sort orders of `iteratees`.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.
* @returns {Array} Returns the new sorted array.
* @example
*
* var users = [
* { 'user': 'fred', 'age': 48 },
* { 'user': 'barney', 'age': 34 },
* { 'user': 'fred', 'age': 40 },
* { 'user': 'barney', 'age': 36 }
* ];
*
* // Sort by `user` in ascending order and by `age` in descending order.
* _.orderBy(users, ['user', 'age'], ['asc', 'desc']);
* // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
*/
function orderBy(collection, iteratees, orders, guard) {
if (collection == null) {
return [];
}
if (!isArray(iteratees)) {
iteratees = iteratees == null ? [] : [iteratees];
}
orders = guard ? undefined$1 : orders;
if (!isArray(orders)) {
orders = orders == null ? [] : [orders];
}
return baseOrderBy(collection, iteratees, orders);
}
/**
* Creates an array of elements split into two groups, the first of which
* contains elements `predicate` returns truthy for, the second of which
* contains elements `predicate` returns falsey for. The predicate is
* invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the array of grouped elements.
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36, 'active': false },
* { 'user': 'fred', 'age': 40, 'active': true },
* { 'user': 'pebbles', 'age': 1, 'active': false }
* ];
*
* _.partition(users, function(o) { return o.active; });
* // => objects for [['fred'], ['barney', 'pebbles']]
*
* // The `_.matches` iteratee shorthand.
* _.partition(users, { 'age': 1, 'active': false });
* // => objects for [['pebbles'], ['barney', 'fred']]
*
* // The `_.matchesProperty` iteratee shorthand.
* _.partition(users, ['active', false]);
* // => objects for [['barney', 'pebbles'], ['fred']]
*
* // The `_.property` iteratee shorthand.
* _.partition(users, 'active');
* // => objects for [['fred'], ['barney', 'pebbles']]
*/
var partition = createAggregator(function(result, value, key) {
result[key ? 0 : 1].push(value);
}, function() { return [[], []]; });
/**
* Reduces `collection` to a value which is the accumulated result of running
* each element in `collection` thru `iteratee`, where each successive
* invocation is supplied the return value of the previous. If `accumulator`
* is not given, the first element of `collection` is used as the initial
* value. The iteratee is invoked with four arguments:
* (accumulator, value, index|key, collection).
*
* Many lodash methods are guarded to work as iteratees for methods like
* `_.reduce`, `_.reduceRight`, and `_.transform`.
*
* The guarded methods are:
* `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`,
* and `sortBy`
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @param {*} [accumulator] The initial value.
* @returns {*} Returns the accumulated value.
* @see _.reduceRight
* @example
*
* _.reduce([1, 2], function(sum, n) {
* return sum + n;
* }, 0);
* // => 3
*
* _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {
* (result[value] || (result[value] = [])).push(key);
* return result;
* }, {});
* // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed)
*/
function reduce(collection, iteratee, accumulator) {
var func = isArray(collection) ? arrayReduce : baseReduce,
initAccum = arguments.length < 3;
return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach);
}
/**
* This method is like `_.reduce` except that it iterates over elements of
* `collection` from right to left.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @param {*} [accumulator] The initial value.
* @returns {*} Returns the accumulated value.
* @see _.reduce
* @example
*
* var array = [[0, 1], [2, 3], [4, 5]];
*
* _.reduceRight(array, function(flattened, other) {
* return flattened.concat(other);
* }, []);
* // => [4, 5, 2, 3, 0, 1]
*/
function reduceRight(collection, iteratee, accumulator) {
var func = isArray(collection) ? arrayReduceRight : baseReduce,
initAccum = arguments.length < 3;
return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight);
}
/**
* The opposite of `_.filter`; this method returns the elements of `collection`
* that `predicate` does **not** return truthy for.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {Array} Returns the new filtered array.
* @see _.filter
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36, 'active': false },
* { 'user': 'fred', 'age': 40, 'active': true }
* ];
*
* _.reject(users, function(o) { return !o.active; });
* // => objects for ['fred']
*
* // The `_.matches` iteratee shorthand.
* _.reject(users, { 'age': 40, 'active': true });
* // => objects for ['barney']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.reject(users, ['active', false]);
* // => objects for ['fred']
*
* // The `_.property` iteratee shorthand.
* _.reject(users, 'active');
* // => objects for ['barney']
*/
function reject(collection, predicate) {
var func = isArray(collection) ? arrayFilter : baseFilter;
return func(collection, negate(getIteratee(predicate, 3)));
}
/**
* Gets a random element from `collection`.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Collection
* @param {Array|Object} collection The collection to sample.
* @returns {*} Returns the random element.
* @example
*
* _.sample([1, 2, 3, 4]);
* // => 2
*/
function sample(collection) {
var func = isArray(collection) ? arraySample : baseSample;
return func(collection);
}
/**
* Gets `n` random elements at unique keys from `collection` up to the
* size of `collection`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Collection
* @param {Array|Object} collection The collection to sample.
* @param {number} [n=1] The number of elements to sample.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the random elements.
* @example
*
* _.sampleSize([1, 2, 3], 2);
* // => [3, 1]
*
* _.sampleSize([1, 2, 3], 4);
* // => [2, 3, 1]
*/
function sampleSize(collection, n, guard) {
if ((guard ? isIterateeCall(collection, n, guard) : n === undefined$1)) {
n = 1;
} else {
n = toInteger(n);
}
var func = isArray(collection) ? arraySampleSize : baseSampleSize;
return func(collection, n);
}
/**
* Creates an array of shuffled values, using a version of the
* [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to shuffle.
* @returns {Array} Returns the new shuffled array.
* @example
*
* _.shuffle([1, 2, 3, 4]);
* // => [4, 1, 3, 2]
*/
function shuffle(collection) {
var func = isArray(collection) ? arrayShuffle : baseShuffle;
return func(collection);
}
/**
* Gets the size of `collection` by returning its length for array-like
* values or the number of own enumerable string keyed properties for objects.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object|string} collection The collection to inspect.
* @returns {number} Returns the collection size.
* @example
*
* _.size([1, 2, 3]);
* // => 3
*
* _.size({ 'a': 1, 'b': 2 });
* // => 2
*
* _.size('pebbles');
* // => 7
*/
function size(collection) {
if (collection == null) {
return 0;
}
if (isArrayLike(collection)) {
return isString(collection) ? stringSize(collection) : collection.length;
}
var tag = getTag(collection);
if (tag == mapTag || tag == setTag) {
return collection.size;
}
return baseKeys(collection).length;
}
/**
* Checks if `predicate` returns truthy for **any** element of `collection`.
* Iteration is stopped once `predicate` returns truthy. The predicate is
* invoked with three arguments: (value, index|key, collection).
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {boolean} Returns `true` if any element passes the predicate check,
* else `false`.
* @example
*
* _.some([null, 0, 'yes', false], Boolean);
* // => true
*
* var users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false }
* ];
*
* // The `_.matches` iteratee shorthand.
* _.some(users, { 'user': 'barney', 'active': false });
* // => false
*
* // The `_.matchesProperty` iteratee shorthand.
* _.some(users, ['active', false]);
* // => true
*
* // The `_.property` iteratee shorthand.
* _.some(users, 'active');
* // => true
*/
function some(collection, predicate, guard) {
var func = isArray(collection) ? arraySome : baseSome;
if (guard && isIterateeCall(collection, predicate, guard)) {
predicate = undefined$1;
}
return func(collection, getIteratee(predicate, 3));
}
/**
* Creates an array of elements, sorted in ascending order by the results of
* running each element in a collection thru each iteratee. This method
* performs a stable sort, that is, it preserves the original sort order of
* equal elements. The iteratees are invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {...(Function|Function[])} [iteratees=[_.identity]]
* The iteratees to sort by.
* @returns {Array} Returns the new sorted array.
* @example
*
* var users = [
* { 'user': 'fred', 'age': 48 },
* { 'user': 'barney', 'age': 36 },
* { 'user': 'fred', 'age': 30 },
* { 'user': 'barney', 'age': 34 }
* ];
*
* _.sortBy(users, [function(o) { return o.user; }]);
* // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 30]]
*
* _.sortBy(users, ['user', 'age']);
* // => objects for [['barney', 34], ['barney', 36], ['fred', 30], ['fred', 48]]
*/
var sortBy = baseRest(function(collection, iteratees) {
if (collection == null) {
return [];
}
var length = iteratees.length;
if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {
iteratees = [];
} else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {
iteratees = [iteratees[0]];
}
return baseOrderBy(collection, baseFlatten(iteratees, 1), []);
});
/*------------------------------------------------------------------------*/
/**
* Gets the timestamp of the number of milliseconds that have elapsed since
* the Unix epoch (1 January 1970 00:00:00 UTC).
*
* @static
* @memberOf _
* @since 2.4.0
* @category Date
* @returns {number} Returns the timestamp.
* @example
*
* _.defer(function(stamp) {
* console.log(_.now() - stamp);
* }, _.now());
* // => Logs the number of milliseconds it took for the deferred invocation.
*/
var now = ctxNow || function() {
return root.Date.now();
};
/*------------------------------------------------------------------------*/
/**
* The opposite of `_.before`; this method creates a function that invokes
* `func` once it's called `n` or more times.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {number} n The number of calls before `func` is invoked.
* @param {Function} func The function to restrict.
* @returns {Function} Returns the new restricted function.
* @example
*
* var saves = ['profile', 'settings'];
*
* var done = _.after(saves.length, function() {
* console.log('done saving!');
* });
*
* _.forEach(saves, function(type) {
* asyncSave({ 'type': type, 'complete': done });
* });
* // => Logs 'done saving!' after the two async saves have completed.
*/
function after(n, func) {
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
n = toInteger(n);
return function() {
if (--n < 1) {
return func.apply(this, arguments);
}
};
}
/**
* Creates a function that invokes `func`, with up to `n` arguments,
* ignoring any additional arguments.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Function
* @param {Function} func The function to cap arguments for.
* @param {number} [n=func.length] The arity cap.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Function} Returns the new capped function.
* @example
*
* _.map(['6', '8', '10'], _.ary(parseInt, 1));
* // => [6, 8, 10]
*/
function ary(func, n, guard) {
n = guard ? undefined$1 : n;
n = (func && n == null) ? func.length : n;
return createWrap(func, WRAP_ARY_FLAG, undefined$1, undefined$1, undefined$1, undefined$1, n);
}
/**
* Creates a function that invokes `func`, with the `this` binding and arguments
* of the created function, while it's called less than `n` times. Subsequent
* calls to the created function return the result of the last `func` invocation.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Function
* @param {number} n The number of calls at which `func` is no longer invoked.
* @param {Function} func The function to restrict.
* @returns {Function} Returns the new restricted function.
* @example
*
* jQuery(element).on('click', _.before(5, addContactToList));
* // => Allows adding up to 4 contacts to the list.
*/
function before(n, func) {
var result;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
n = toInteger(n);
return function() {
if (--n > 0) {
result = func.apply(this, arguments);
}
if (n <= 1) {
func = undefined$1;
}
return result;
};
}
/**
* Creates a function that invokes `func` with the `this` binding of `thisArg`
* and `partials` prepended to the arguments it receives.
*
* The `_.bind.placeholder` value, which defaults to `_` in monolithic builds,
* may be used as a placeholder for partially applied arguments.
*
* **Note:** Unlike native `Function#bind`, this method doesn't set the "length"
* property of bound functions.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to bind.
* @param {*} thisArg The `this` binding of `func`.
* @param {...*} [partials] The arguments to be partially applied.
* @returns {Function} Returns the new bound function.
* @example
*
* function greet(greeting, punctuation) {
* return greeting + ' ' + this.user + punctuation;
* }
*
* var object = { 'user': 'fred' };
*
* var bound = _.bind(greet, object, 'hi');
* bound('!');
* // => 'hi fred!'
*
* // Bound with placeholders.
* var bound = _.bind(greet, object, _, '!');
* bound('hi');
* // => 'hi fred!'
*/
var bind = baseRest(function(func, thisArg, partials) {
var bitmask = WRAP_BIND_FLAG;
if (partials.length) {
var holders = replaceHolders(partials, getHolder(bind));
bitmask |= WRAP_PARTIAL_FLAG;
}
return createWrap(func, bitmask, thisArg, partials, holders);
});
/**
* Creates a function that invokes the method at `object[key]` with `partials`
* prepended to the arguments it receives.
*
* This method differs from `_.bind` by allowing bound functions to reference
* methods that may be redefined or don't yet exist. See
* [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern)
* for more details.
*
* The `_.bindKey.placeholder` value, which defaults to `_` in monolithic
* builds, may be used as a placeholder for partially applied arguments.
*
* @static
* @memberOf _
* @since 0.10.0
* @category Function
* @param {Object} object The object to invoke the method on.
* @param {string} key The key of the method.
* @param {...*} [partials] The arguments to be partially applied.
* @returns {Function} Returns the new bound function.
* @example
*
* var object = {
* 'user': 'fred',
* 'greet': function(greeting, punctuation) {
* return greeting + ' ' + this.user + punctuation;
* }
* };
*
* var bound = _.bindKey(object, 'greet', 'hi');
* bound('!');
* // => 'hi fred!'
*
* object.greet = function(greeting, punctuation) {
* return greeting + 'ya ' + this.user + punctuation;
* };
*
* bound('!');
* // => 'hiya fred!'
*
* // Bound with placeholders.
* var bound = _.bindKey(object, 'greet', _, '!');
* bound('hi');
* // => 'hiya fred!'
*/
var bindKey = baseRest(function(object, key, partials) {
var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG;
if (partials.length) {
var holders = replaceHolders(partials, getHolder(bindKey));
bitmask |= WRAP_PARTIAL_FLAG;
}
return createWrap(key, bitmask, object, partials, holders);
});
/**
* Creates a function that accepts arguments of `func` and either invokes
* `func` returning its result, if at least `arity` number of arguments have
* been provided, or returns a function that accepts the remaining `func`
* arguments, and so on. The arity of `func` may be specified if `func.length`
* is not sufficient.
*
* The `_.curry.placeholder` value, which defaults to `_` in monolithic builds,
* may be used as a placeholder for provided arguments.
*
* **Note:** This method doesn't set the "length" property of curried functions.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Function
* @param {Function} func The function to curry.
* @param {number} [arity=func.length] The arity of `func`.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Function} Returns the new curried function.
* @example
*
* var abc = function(a, b, c) {
* return [a, b, c];
* };
*
* var curried = _.curry(abc);
*
* curried(1)(2)(3);
* // => [1, 2, 3]
*
* curried(1, 2)(3);
* // => [1, 2, 3]
*
* curried(1, 2, 3);
* // => [1, 2, 3]
*
* // Curried with placeholders.
* curried(1)(_, 3)(2);
* // => [1, 2, 3]
*/
function curry(func, arity, guard) {
arity = guard ? undefined$1 : arity;
var result = createWrap(func, WRAP_CURRY_FLAG, undefined$1, undefined$1, undefined$1, undefined$1, undefined$1, arity);
result.placeholder = curry.placeholder;
return result;
}
/**
* This method is like `_.curry` except that arguments are applied to `func`
* in the manner of `_.partialRight` instead of `_.partial`.
*
* The `_.curryRight.placeholder` value, which defaults to `_` in monolithic
* builds, may be used as a placeholder for provided arguments.
*
* **Note:** This method doesn't set the "length" property of curried functions.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Function
* @param {Function} func The function to curry.
* @param {number} [arity=func.length] The arity of `func`.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Function} Returns the new curried function.
* @example
*
* var abc = function(a, b, c) {
* return [a, b, c];
* };
*
* var curried = _.curryRight(abc);
*
* curried(3)(2)(1);
* // => [1, 2, 3]
*
* curried(2, 3)(1);
* // => [1, 2, 3]
*
* curried(1, 2, 3);
* // => [1, 2, 3]
*
* // Curried with placeholders.
* curried(3)(1, _)(2);
* // => [1, 2, 3]
*/
function curryRight(func, arity, guard) {
arity = guard ? undefined$1 : arity;
var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined$1, undefined$1, undefined$1, undefined$1, undefined$1, arity);
result.placeholder = curryRight.placeholder;
return result;
}
/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked. The debounced function comes with a `cancel` method to cancel
* delayed `func` invocations and a `flush` method to immediately invoke them.
* Provide `options` to indicate whether `func` should be invoked on the
* leading and/or trailing edge of the `wait` timeout. The `func` is invoked
* with the last arguments provided to the debounced function. Subsequent
* calls to the debounced function return the result of the last `func`
* invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the debounced function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `_.debounce` and `_.throttle`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to debounce.
* @param {number} [wait=0] The number of milliseconds to delay.
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=false]
* Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new debounced function.
* @example
*
* // Avoid costly calculations while the window size is in flux.
* jQuery(window).on('resize', _.debounce(calculateLayout, 150));
*
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
* jQuery(element).on('click', _.debounce(sendMail, 300, {
* 'leading': true,
* 'trailing': false
* }));
*
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
* var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
* var source = new EventSource('/stream');
* jQuery(source).on('message', debounced);
*
* // Cancel the trailing debounced invocation.
* jQuery(window).on('popstate', debounced.cancel);
*/
function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined$1;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;
return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined$1 || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined$1;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined$1;
return result;
}
function cancel() {
if (timerId !== undefined$1) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined$1;
}
function flush() {
return timerId === undefined$1 ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined$1) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined$1) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
/**
* Defers invoking the `func` until the current call stack has cleared. Any
* additional arguments are provided to `func` when it's invoked.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to defer.
* @param {...*} [args] The arguments to invoke `func` with.
* @returns {number} Returns the timer id.
* @example
*
* _.defer(function(text) {
* console.log(text);
* }, 'deferred');
* // => Logs 'deferred' after one millisecond.
*/
var defer = baseRest(function(func, args) {
return baseDelay(func, 1, args);
});
/**
* Invokes `func` after `wait` milliseconds. Any additional arguments are
* provided to `func` when it's invoked.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to delay.
* @param {number} wait The number of milliseconds to delay invocation.
* @param {...*} [args] The arguments to invoke `func` with.
* @returns {number} Returns the timer id.
* @example
*
* _.delay(function(text) {
* console.log(text);
* }, 1000, 'later');
* // => Logs 'later' after one second.
*/
var delay = baseRest(function(func, wait, args) {
return baseDelay(func, toNumber(wait) || 0, args);
});
/**
* Creates a function that invokes `func` with arguments reversed.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Function
* @param {Function} func The function to flip arguments for.
* @returns {Function} Returns the new flipped function.
* @example
*
* var flipped = _.flip(function() {
* return _.toArray(arguments);
* });
*
* flipped('a', 'b', 'c', 'd');
* // => ['d', 'c', 'b', 'a']
*/
function flip(func) {
return createWrap(func, WRAP_FLIP_FLAG);
}
/**
* Creates a function that memoizes the result of `func`. If `resolver` is
* provided, it determines the cache key for storing the result based on the
* arguments provided to the memoized function. By default, the first argument
* provided to the memoized function is used as the map cache key. The `func`
* is invoked with the `this` binding of the memoized function.
*
* **Note:** The cache is exposed as the `cache` property on the memoized
* function. Its creation may be customized by replacing the `_.memoize.Cache`
* constructor with one whose instances implement the
* [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
* method interface of `clear`, `delete`, `get`, `has`, and `set`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to have its output memoized.
* @param {Function} [resolver] The function to resolve the cache key.
* @returns {Function} Returns the new memoized function.
* @example
*
* var object = { 'a': 1, 'b': 2 };
* var other = { 'c': 3, 'd': 4 };
*
* var values = _.memoize(_.values);
* values(object);
* // => [1, 2]
*
* values(other);
* // => [3, 4]
*
* object.a = 2;
* values(object);
* // => [1, 2]
*
* // Modify the result cache.
* values.cache.set(object, ['a', 'b']);
* values(object);
* // => ['a', 'b']
*
* // Replace `_.memoize.Cache`.
* _.memoize.Cache = WeakMap;
*/
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function() {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache);
return memoized;
}
// Expose `MapCache`.
memoize.Cache = MapCache;
/**
* Creates a function that negates the result of the predicate `func`. The
* `func` predicate is invoked with the `this` binding and arguments of the
* created function.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Function
* @param {Function} predicate The predicate to negate.
* @returns {Function} Returns the new negated function.
* @example
*
* function isEven(n) {
* return n % 2 == 0;
* }
*
* _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven));
* // => [1, 3, 5]
*/
function negate(predicate) {
if (typeof predicate != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
return function() {
var args = arguments;
switch (args.length) {
case 0: return !predicate.call(this);
case 1: return !predicate.call(this, args[0]);
case 2: return !predicate.call(this, args[0], args[1]);
case 3: return !predicate.call(this, args[0], args[1], args[2]);
}
return !predicate.apply(this, args);
};
}
/**
* Creates a function that is restricted to invoking `func` once. Repeat calls
* to the function return the value of the first invocation. The `func` is
* invoked with the `this` binding and arguments of the created function.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to restrict.
* @returns {Function} Returns the new restricted function.
* @example
*
* var initialize = _.once(createApplication);
* initialize();
* initialize();
* // => `createApplication` is invoked once
*/
function once(func) {
return before(2, func);
}
/**
* Creates a function that invokes `func` with its arguments transformed.
*
* @static
* @since 4.0.0
* @memberOf _
* @category Function
* @param {Function} func The function to wrap.
* @param {...(Function|Function[])} [transforms=[_.identity]]
* The argument transforms.
* @returns {Function} Returns the new function.
* @example
*
* function doubled(n) {
* return n * 2;
* }
*
* function square(n) {
* return n * n;
* }
*
* var func = _.overArgs(function(x, y) {
* return [x, y];
* }, [square, doubled]);
*
* func(9, 3);
* // => [81, 6]
*
* func(10, 5);
* // => [100, 10]
*/
var overArgs = castRest(function(func, transforms) {
transforms = (transforms.length == 1 && isArray(transforms[0]))
? arrayMap(transforms[0], baseUnary(getIteratee()))
: arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee()));
var funcsLength = transforms.length;
return baseRest(function(args) {
var index = -1,
length = nativeMin(args.length, funcsLength);
while (++index < length) {
args[index] = transforms[index].call(this, args[index]);
}
return apply(func, this, args);
});
});
/**
* Creates a function that invokes `func` with `partials` prepended to the
* arguments it receives. This method is like `_.bind` except it does **not**
* alter the `this` binding.
*
* The `_.partial.placeholder` value, which defaults to `_` in monolithic
* builds, may be used as a placeholder for partially applied arguments.
*
* **Note:** This method doesn't set the "length" property of partially
* applied functions.
*
* @static
* @memberOf _
* @since 0.2.0
* @category Function
* @param {Function} func The function to partially apply arguments to.
* @param {...*} [partials] The arguments to be partially applied.
* @returns {Function} Returns the new partially applied function.
* @example
*
* function greet(greeting, name) {
* return greeting + ' ' + name;
* }
*
* var sayHelloTo = _.partial(greet, 'hello');
* sayHelloTo('fred');
* // => 'hello fred'
*
* // Partially applied with placeholders.
* var greetFred = _.partial(greet, _, 'fred');
* greetFred('hi');
* // => 'hi fred'
*/
var partial = baseRest(function(func, partials) {
var holders = replaceHolders(partials, getHolder(partial));
return createWrap(func, WRAP_PARTIAL_FLAG, undefined$1, partials, holders);
});
/**
* This method is like `_.partial` except that partially applied arguments
* are appended to the arguments it receives.
*
* The `_.partialRight.placeholder` value, which defaults to `_` in monolithic
* builds, may be used as a placeholder for partially applied arguments.
*
* **Note:** This method doesn't set the "length" property of partially
* applied functions.
*
* @static
* @memberOf _
* @since 1.0.0
* @category Function
* @param {Function} func The function to partially apply arguments to.
* @param {...*} [partials] The arguments to be partially applied.
* @returns {Function} Returns the new partially applied function.
* @example
*
* function greet(greeting, name) {
* return greeting + ' ' + name;
* }
*
* var greetFred = _.partialRight(greet, 'fred');
* greetFred('hi');
* // => 'hi fred'
*
* // Partially applied with placeholders.
* var sayHelloTo = _.partialRight(greet, 'hello', _);
* sayHelloTo('fred');
* // => 'hello fred'
*/
var partialRight = baseRest(function(func, partials) {
var holders = replaceHolders(partials, getHolder(partialRight));
return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined$1, partials, holders);
});
/**
* Creates a function that invokes `func` with arguments arranged according
* to the specified `indexes` where the argument value at the first index is
* provided as the first argument, the argument value at the second index is
* provided as the second argument, and so on.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Function
* @param {Function} func The function to rearrange arguments for.
* @param {...(number|number[])} indexes The arranged argument indexes.
* @returns {Function} Returns the new function.
* @example
*
* var rearged = _.rearg(function(a, b, c) {
* return [a, b, c];
* }, [2, 0, 1]);
*
* rearged('b', 'c', 'a')
* // => ['a', 'b', 'c']
*/
var rearg = flatRest(function(func, indexes) {
return createWrap(func, WRAP_REARG_FLAG, undefined$1, undefined$1, undefined$1, indexes);
});
/**
* Creates a function that invokes `func` with the `this` binding of the
* created function and arguments from `start` and beyond provided as
* an array.
*
* **Note:** This method is based on the
* [rest parameter](https://mdn.io/rest_parameters).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Function
* @param {Function} func The function to apply a rest parameter to.
* @param {number} [start=func.length-1] The start position of the rest parameter.
* @returns {Function} Returns the new function.
* @example
*
* var say = _.rest(function(what, names) {
* return what + ' ' + _.initial(names).join(', ') +
* (_.size(names) > 1 ? ', & ' : '') + _.last(names);
* });
*
* say('hello', 'fred', 'barney', 'pebbles');
* // => 'hello fred, barney, & pebbles'
*/
function rest(func, start) {
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
start = start === undefined$1 ? start : toInteger(start);
return baseRest(func, start);
}
/**
* Creates a function that invokes `func` with the `this` binding of the
* create function and an array of arguments much like
* [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply).
*
* **Note:** This method is based on the
* [spread operator](https://mdn.io/spread_operator).
*
* @static
* @memberOf _
* @since 3.2.0
* @category Function
* @param {Function} func The function to spread arguments over.
* @param {number} [start=0] The start position of the spread.
* @returns {Function} Returns the new function.
* @example
*
* var say = _.spread(function(who, what) {
* return who + ' says ' + what;
* });
*
* say(['fred', 'hello']);
* // => 'fred says hello'
*
* var numbers = Promise.all([
* Promise.resolve(40),
* Promise.resolve(36)
* ]);
*
* numbers.then(_.spread(function(x, y) {
* return x + y;
* }));
* // => a Promise of 76
*/
function spread(func, start) {
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
start = start == null ? 0 : nativeMax(toInteger(start), 0);
return baseRest(function(args) {
var array = args[start],
otherArgs = castSlice(args, 0, start);
if (array) {
arrayPush(otherArgs, array);
}
return apply(func, this, otherArgs);
});
}
/**
* Creates a throttled function that only invokes `func` at most once per
* every `wait` milliseconds. The throttled function comes with a `cancel`
* method to cancel delayed `func` invocations and a `flush` method to
* immediately invoke them. Provide `options` to indicate whether `func`
* should be invoked on the leading and/or trailing edge of the `wait`
* timeout. The `func` is invoked with the last arguments provided to the
* throttled function. Subsequent calls to the throttled function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the throttled function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `_.throttle` and `_.debounce`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to throttle.
* @param {number} [wait=0] The number of milliseconds to throttle invocations to.
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=true]
* Specify invoking on the leading edge of the timeout.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new throttled function.
* @example
*
* // Avoid excessively updating the position while scrolling.
* jQuery(window).on('scroll', _.throttle(updatePosition, 100));
*
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
* var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
* jQuery(element).on('click', throttled);
*
* // Cancel the trailing throttled invocation.
* jQuery(window).on('popstate', throttled.cancel);
*/
function throttle(func, wait, options) {
var leading = true,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
}
/**
* Creates a function that accepts up to one argument, ignoring any
* additional arguments.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Function
* @param {Function} func The function to cap arguments for.
* @returns {Function} Returns the new capped function.
* @example
*
* _.map(['6', '8', '10'], _.unary(parseInt));
* // => [6, 8, 10]
*/
function unary(func) {
return ary(func, 1);
}
/**
* Creates a function that provides `value` to `wrapper` as its first
* argument. Any additional arguments provided to the function are appended
* to those provided to the `wrapper`. The wrapper is invoked with the `this`
* binding of the created function.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {*} value The value to wrap.
* @param {Function} [wrapper=identity] The wrapper function.
* @returns {Function} Returns the new function.
* @example
*
* var p = _.wrap(_.escape, function(func, text) {
* return '<p>' + func(text) + '</p>';
* });
*
* p('fred, barney, & pebbles');
* // => '<p>fred, barney, &amp; pebbles</p>'
*/
function wrap(value, wrapper) {
return partial(castFunction(wrapper), value);
}
/*------------------------------------------------------------------------*/
/**
* Casts `value` as an array if it's not one.
*
* @static
* @memberOf _
* @since 4.4.0
* @category Lang
* @param {*} value The value to inspect.
* @returns {Array} Returns the cast array.
* @example
*
* _.castArray(1);
* // => [1]
*
* _.castArray({ 'a': 1 });
* // => [{ 'a': 1 }]
*
* _.castArray('abc');
* // => ['abc']
*
* _.castArray(null);
* // => [null]
*
* _.castArray(undefined);
* // => [undefined]
*
* _.castArray();
* // => []
*
* var array = [1, 2, 3];
* console.log(_.castArray(array) === array);
* // => true
*/
function castArray() {
if (!arguments.length) {
return [];
}
var value = arguments[0];
return isArray(value) ? value : [value];
}
/**
* Creates a shallow clone of `value`.
*
* **Note:** This method is loosely based on the
* [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)
* and supports cloning arrays, array buffers, booleans, date objects, maps,
* numbers, `Object` objects, regexes, sets, strings, symbols, and typed
* arrays. The own enumerable properties of `arguments` objects are cloned
* as plain objects. An empty object is returned for uncloneable values such
* as error objects, functions, DOM nodes, and WeakMaps.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to clone.
* @returns {*} Returns the cloned value.
* @see _.cloneDeep
* @example
*
* var objects = [{ 'a': 1 }, { 'b': 2 }];
*
* var shallow = _.clone(objects);
* console.log(shallow[0] === objects[0]);
* // => true
*/
function clone(value) {
return baseClone(value, CLONE_SYMBOLS_FLAG);
}
/**
* This method is like `_.clone` except that it accepts `customizer` which
* is invoked to produce the cloned value. If `customizer` returns `undefined`,
* cloning is handled by the method instead. The `customizer` is invoked with
* up to four arguments; (value [, index|key, object, stack]).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to clone.
* @param {Function} [customizer] The function to customize cloning.
* @returns {*} Returns the cloned value.
* @see _.cloneDeepWith
* @example
*
* function customizer(value) {
* if (_.isElement(value)) {
* return value.cloneNode(false);
* }
* }
*
* var el = _.cloneWith(document.body, customizer);
*
* console.log(el === document.body);
* // => false
* console.log(el.nodeName);
* // => 'BODY'
* console.log(el.childNodes.length);
* // => 0
*/
function cloneWith(value, customizer) {
customizer = typeof customizer == 'function' ? customizer : undefined$1;
return baseClone(value, CLONE_SYMBOLS_FLAG, customizer);
}
/**
* This method is like `_.clone` except that it recursively clones `value`.
*
* @static
* @memberOf _
* @since 1.0.0
* @category Lang
* @param {*} value The value to recursively clone.
* @returns {*} Returns the deep cloned value.
* @see _.clone
* @example
*
* var objects = [{ 'a': 1 }, { 'b': 2 }];
*
* var deep = _.cloneDeep(objects);
* console.log(deep[0] === objects[0]);
* // => false
*/
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}
/**
* This method is like `_.cloneWith` except that it recursively clones `value`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to recursively clone.
* @param {Function} [customizer] The function to customize cloning.
* @returns {*} Returns the deep cloned value.
* @see _.cloneWith
* @example
*
* function customizer(value) {
* if (_.isElement(value)) {
* return value.cloneNode(true);
* }
* }
*
* var el = _.cloneDeepWith(document.body, customizer);
*
* console.log(el === document.body);
* // => false
* console.log(el.nodeName);
* // => 'BODY'
* console.log(el.childNodes.length);
* // => 20
*/
function cloneDeepWith(value, customizer) {
customizer = typeof customizer == 'function' ? customizer : undefined$1;
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer);
}
/**
* Checks if `object` conforms to `source` by invoking the predicate
* properties of `source` with the corresponding property values of `object`.
*
* **Note:** This method is equivalent to `_.conforms` when `source` is
* partially applied.
*
* @static
* @memberOf _
* @since 4.14.0
* @category Lang
* @param {Object} object The object to inspect.
* @param {Object} source The object of property predicates to conform to.
* @returns {boolean} Returns `true` if `object` conforms, else `false`.
* @example
*
* var object = { 'a': 1, 'b': 2 };
*
* _.conformsTo(object, { 'b': function(n) { return n > 1; } });
* // => true
*
* _.conformsTo(object, { 'b': function(n) { return n > 2; } });
* // => false
*/
function conformsTo(object, source) {
return source == null || baseConformsTo(object, source, keys(source));
}
/**
* Performs a
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
/**
* Checks if `value` is greater than `other`.
*
* @static
* @memberOf _
* @since 3.9.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if `value` is greater than `other`,
* else `false`.
* @see _.lt
* @example
*
* _.gt(3, 1);
* // => true
*
* _.gt(3, 3);
* // => false
*
* _.gt(1, 3);
* // => false
*/
var gt = createRelationalOperation(baseGt);
/**
* Checks if `value` is greater than or equal to `other`.
*
* @static
* @memberOf _
* @since 3.9.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if `value` is greater than or equal to
* `other`, else `false`.
* @see _.lte
* @example
*
* _.gte(3, 1);
* // => true
*
* _.gte(3, 3);
* // => true
*
* _.gte(1, 3);
* // => false
*/
var gte = createRelationalOperation(function(value, other) {
return value >= other;
});
/**
* Checks if `value` is likely an `arguments` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
* else `false`.
* @example
*
* _.isArguments(function() { return arguments; }());
* // => true
*
* _.isArguments([1, 2, 3]);
* // => false
*/
var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
!propertyIsEnumerable.call(value, 'callee');
};
/**
* Checks if `value` is classified as an `Array` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array, else `false`.
* @example
*
* _.isArray([1, 2, 3]);
* // => true
*
* _.isArray(document.body.children);
* // => false
*
* _.isArray('abc');
* // => false
*
* _.isArray(_.noop);
* // => false
*/
var isArray = Array.isArray;
/**
* Checks if `value` is classified as an `ArrayBuffer` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.
* @example
*
* _.isArrayBuffer(new ArrayBuffer(2));
* // => true
*
* _.isArrayBuffer(new Array(2));
* // => false
*/
var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer;
/**
* Checks if `value` is array-like. A value is considered array-like if it's
* not a function and has a `value.length` that's an integer greater than or
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is array-like, else `false`.
* @example
*
* _.isArrayLike([1, 2, 3]);
* // => true
*
* _.isArrayLike(document.body.children);
* // => true
*
* _.isArrayLike('abc');
* // => true
*
* _.isArrayLike(_.noop);
* // => false
*/
function isArrayLike(value) {
return value != null && isLength(value.length) && !isFunction(value);
}
/**
* This method is like `_.isArrayLike` except that it also checks if `value`
* is an object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array-like object,
* else `false`.
* @example
*
* _.isArrayLikeObject([1, 2, 3]);
* // => true
*
* _.isArrayLikeObject(document.body.children);
* // => true
*
* _.isArrayLikeObject('abc');
* // => false
*
* _.isArrayLikeObject(_.noop);
* // => false
*/
function isArrayLikeObject(value) {
return isObjectLike(value) && isArrayLike(value);
}
/**
* Checks if `value` is classified as a boolean primitive or object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a boolean, else `false`.
* @example
*
* _.isBoolean(false);
* // => true
*
* _.isBoolean(null);
* // => false
*/
function isBoolean(value) {
return value === true || value === false ||
(isObjectLike(value) && baseGetTag(value) == boolTag);
}
/**
* Checks if `value` is a buffer.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
* @example
*
* _.isBuffer(new Buffer(2));
* // => true
*
* _.isBuffer(new Uint8Array(2));
* // => false
*/
var isBuffer = nativeIsBuffer || stubFalse;
/**
* Checks if `value` is classified as a `Date` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a date object, else `false`.
* @example
*
* _.isDate(new Date);
* // => true
*
* _.isDate('Mon April 23 2012');
* // => false
*/
var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate;
/**
* Checks if `value` is likely a DOM element.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.
* @example
*
* _.isElement(document.body);
* // => true
*
* _.isElement('<body>');
* // => false
*/
function isElement(value) {
return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value);
}
/**
* Checks if `value` is an empty object, collection, map, or set.
*
* Objects are considered empty if they have no own enumerable string keyed
* properties.
*
* Array-like values such as `arguments` objects, arrays, buffers, strings, or
* jQuery-like collections are considered empty if they have a `length` of `0`.
* Similarly, maps and sets are considered empty if they have a `size` of `0`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is empty, else `false`.
* @example
*
* _.isEmpty(null);
* // => true
*
* _.isEmpty(true);
* // => true
*
* _.isEmpty(1);
* // => true
*
* _.isEmpty([1, 2, 3]);
* // => false
*
* _.isEmpty({ 'a': 1 });
* // => false
*/
function isEmpty(value) {
if (value == null) {
return true;
}
if (isArrayLike(value) &&
(isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||
isBuffer(value) || isTypedArray(value) || isArguments(value))) {
return !value.length;
}
var tag = getTag(value);
if (tag == mapTag || tag == setTag) {
return !value.size;
}
if (isPrototype(value)) {
return !baseKeys(value).length;
}
for (var key in value) {
if (hasOwnProperty.call(value, key)) {
return false;
}
}
return true;
}
/**
* Performs a deep comparison between two values to determine if they are
* equivalent.
*
* **Note:** This method supports comparing arrays, array buffers, booleans,
* date objects, error objects, maps, numbers, `Object` objects, regexes,
* sets, strings, symbols, and typed arrays. `Object` objects are compared
* by their own, not inherited, enumerable properties. Functions and DOM
* nodes are compared by strict equality, i.e. `===`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.isEqual(object, other);
* // => true
*
* object === other;
* // => false
*/
function isEqual(value, other) {
return baseIsEqual(value, other);
}
/**
* This method is like `_.isEqual` except that it accepts `customizer` which
* is invoked to compare values. If `customizer` returns `undefined`, comparisons
* are handled by the method instead. The `customizer` is invoked with up to
* six arguments: (objValue, othValue [, index|key, object, other, stack]).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @param {Function} [customizer] The function to customize comparisons.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* function isGreeting(value) {
* return /^h(?:i|ello)$/.test(value);
* }
*
* function customizer(objValue, othValue) {
* if (isGreeting(objValue) && isGreeting(othValue)) {
* return true;
* }
* }
*
* var array = ['hello', 'goodbye'];
* var other = ['hi', 'goodbye'];
*
* _.isEqualWith(array, other, customizer);
* // => true
*/
function isEqualWith(value, other, customizer) {
customizer = typeof customizer == 'function' ? customizer : undefined$1;
var result = customizer ? customizer(value, other) : undefined$1;
return result === undefined$1 ? baseIsEqual(value, other, undefined$1, customizer) : !!result;
}
/**
* Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`,
* `SyntaxError`, `TypeError`, or `URIError` object.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an error object, else `false`.
* @example
*
* _.isError(new Error);
* // => true
*
* _.isError(Error);
* // => false
*/
function isError(value) {
if (!isObjectLike(value)) {
return false;
}
var tag = baseGetTag(value);
return tag == errorTag || tag == domExcTag ||
(typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value));
}
/**
* Checks if `value` is a finite primitive number.
*
* **Note:** This method is based on
* [`Number.isFinite`](https://mdn.io/Number/isFinite).
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a finite number, else `false`.
* @example
*
* _.isFinite(3);
* // => true
*
* _.isFinite(Number.MIN_VALUE);
* // => true
*
* _.isFinite(Infinity);
* // => false
*
* _.isFinite('3');
* // => false
*/
function isFinite(value) {
return typeof value == 'number' && nativeIsFinite(value);
}
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
if (!isObject(value)) {
return false;
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
/**
* Checks if `value` is an integer.
*
* **Note:** This method is based on
* [`Number.isInteger`](https://mdn.io/Number/isInteger).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an integer, else `false`.
* @example
*
* _.isInteger(3);
* // => true
*
* _.isInteger(Number.MIN_VALUE);
* // => false
*
* _.isInteger(Infinity);
* // => false
*
* _.isInteger('3');
* // => false
*/
function isInteger(value) {
return typeof value == 'number' && value == toInteger(value);
}
/**
* Checks if `value` is a valid array-like length.
*
* **Note:** This method is loosely based on
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
* @example
*
* _.isLength(3);
* // => true
*
* _.isLength(Number.MIN_VALUE);
* // => false
*
* _.isLength(Infinity);
* // => false
*
* _.isLength('3');
* // => false
*/
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value) {
return value != null && typeof value == 'object';
}
/**
* Checks if `value` is classified as a `Map` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a map, else `false`.
* @example
*
* _.isMap(new Map);
* // => true
*
* _.isMap(new WeakMap);
* // => false
*/
var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;
/**
* Performs a partial deep comparison between `object` and `source` to
* determine if `object` contains equivalent property values.
*
* **Note:** This method is equivalent to `_.matches` when `source` is
* partially applied.
*
* Partial comparisons will match empty array and empty object `source`
* values against any array or object value, respectively. See `_.isEqual`
* for a list of supported value comparisons.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {Object} object The object to inspect.
* @param {Object} source The object of property values to match.
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
* @example
*
* var object = { 'a': 1, 'b': 2 };
*
* _.isMatch(object, { 'b': 2 });
* // => true
*
* _.isMatch(object, { 'b': 1 });
* // => false
*/
function isMatch(object, source) {
return object === source || baseIsMatch(object, source, getMatchData(source));
}
/**
* This method is like `_.isMatch` except that it accepts `customizer` which
* is invoked to compare values. If `customizer` returns `undefined`, comparisons
* are handled by the method instead. The `customizer` is invoked with five
* arguments: (objValue, srcValue, index|key, object, source).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {Object} object The object to inspect.
* @param {Object} source The object of property values to match.
* @param {Function} [customizer] The function to customize comparisons.
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
* @example
*
* function isGreeting(value) {
* return /^h(?:i|ello)$/.test(value);
* }
*
* function customizer(objValue, srcValue) {
* if (isGreeting(objValue) && isGreeting(srcValue)) {
* return true;
* }
* }
*
* var object = { 'greeting': 'hello' };
* var source = { 'greeting': 'hi' };
*
* _.isMatchWith(object, source, customizer);
* // => true
*/
function isMatchWith(object, source, customizer) {
customizer = typeof customizer == 'function' ? customizer : undefined$1;
return baseIsMatch(object, source, getMatchData(source), customizer);
}
/**
* Checks if `value` is `NaN`.
*
* **Note:** This method is based on
* [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as
* global [`isNaN`](https://mdn.io/isNaN) which returns `true` for
* `undefined` and other non-number values.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
* @example
*
* _.isNaN(NaN);
* // => true
*
* _.isNaN(new Number(NaN));
* // => true
*
* isNaN(undefined);
* // => true
*
* _.isNaN(undefined);
* // => false
*/
function isNaN(value) {
// An `NaN` primitive is the only value that is not equal to itself.
// Perform the `toStringTag` check first to avoid errors with some
// ActiveX objects in IE.
return isNumber(value) && value != +value;
}
/**
* Checks if `value` is a pristine native function.
*
* **Note:** This method can't reliably detect native functions in the presence
* of the core-js package because core-js circumvents this kind of detection.
* Despite multiple requests, the core-js maintainer has made it clear: any
* attempt to fix the detection will be obstructed. As a result, we're left
* with little choice but to throw an error. Unfortunately, this also affects
* packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill),
* which rely on core-js.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
* @example
*
* _.isNative(Array.prototype.push);
* // => true
*
* _.isNative(_);
* // => false
*/
function isNative(value) {
if (isMaskable(value)) {
throw new Error(CORE_ERROR_TEXT);
}
return baseIsNative(value);
}
/**
* Checks if `value` is `null`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `null`, else `false`.
* @example
*
* _.isNull(null);
* // => true
*
* _.isNull(void 0);
* // => false
*/
function isNull(value) {
return value === null;
}
/**
* Checks if `value` is `null` or `undefined`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is nullish, else `false`.
* @example
*
* _.isNil(null);
* // => true
*
* _.isNil(void 0);
* // => true
*
* _.isNil(NaN);
* // => false
*/
function isNil(value) {
return value == null;
}
/**
* Checks if `value` is classified as a `Number` primitive or object.
*
* **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are
* classified as numbers, use the `_.isFinite` method.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a number, else `false`.
* @example
*
* _.isNumber(3);
* // => true
*
* _.isNumber(Number.MIN_VALUE);
* // => true
*
* _.isNumber(Infinity);
* // => true
*
* _.isNumber('3');
* // => false
*/
function isNumber(value) {
return typeof value == 'number' ||
(isObjectLike(value) && baseGetTag(value) == numberTag);
}
/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @static
* @memberOf _
* @since 0.8.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
* @example
*
* function Foo() {
* this.a = 1;
* }
*
* _.isPlainObject(new Foo);
* // => false
*
* _.isPlainObject([1, 2, 3]);
* // => false
*
* _.isPlainObject({ 'x': 0, 'y': 0 });
* // => true
*
* _.isPlainObject(Object.create(null));
* // => true
*/
function isPlainObject(value) {
if (!isObjectLike(value) || baseGetTag(value) != objectTag) {
return false;
}
var proto = getPrototype(value);
if (proto === null) {
return true;
}
var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor == 'function' && Ctor instanceof Ctor &&
funcToString.call(Ctor) == objectCtorString;
}
/**
* Checks if `value` is classified as a `RegExp` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a regexp, else `false`.
* @example
*
* _.isRegExp(/abc/);
* // => true
*
* _.isRegExp('/abc/');
* // => false
*/
var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp;
/**
* Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754
* double precision number which isn't the result of a rounded unsafe integer.
*
* **Note:** This method is based on
* [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.
* @example
*
* _.isSafeInteger(3);
* // => true
*
* _.isSafeInteger(Number.MIN_VALUE);
* // => false
*
* _.isSafeInteger(Infinity);
* // => false
*
* _.isSafeInteger('3');
* // => false
*/
function isSafeInteger(value) {
return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER;
}
/**
* Checks if `value` is classified as a `Set` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a set, else `false`.
* @example
*
* _.isSet(new Set);
* // => true
*
* _.isSet(new WeakSet);
* // => false
*/
var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;
/**
* Checks if `value` is classified as a `String` primitive or object.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a string, else `false`.
* @example
*
* _.isString('abc');
* // => true
*
* _.isString(1);
* // => false
*/
function isString(value) {
return typeof value == 'string' ||
(!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag);
}
/**
* Checks if `value` is classified as a `Symbol` primitive or object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
* @example
*
* _.isSymbol(Symbol.iterator);
* // => true
*
* _.isSymbol('abc');
* // => false
*/
function isSymbol(value) {
return typeof value == 'symbol' ||
(isObjectLike(value) && baseGetTag(value) == symbolTag);
}
/**
* Checks if `value` is classified as a typed array.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
* @example
*
* _.isTypedArray(new Uint8Array);
* // => true
*
* _.isTypedArray([]);
* // => false
*/
var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
/**
* Checks if `value` is `undefined`.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.
* @example
*
* _.isUndefined(void 0);
* // => true
*
* _.isUndefined(null);
* // => false
*/
function isUndefined(value) {
return value === undefined$1;
}
/**
* Checks if `value` is classified as a `WeakMap` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a weak map, else `false`.
* @example
*
* _.isWeakMap(new WeakMap);
* // => true
*
* _.isWeakMap(new Map);
* // => false
*/
function isWeakMap(value) {
return isObjectLike(value) && getTag(value) == weakMapTag;
}
/**
* Checks if `value` is classified as a `WeakSet` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a weak set, else `false`.
* @example
*
* _.isWeakSet(new WeakSet);
* // => true
*
* _.isWeakSet(new Set);
* // => false
*/
function isWeakSet(value) {
return isObjectLike(value) && baseGetTag(value) == weakSetTag;
}
/**
* Checks if `value` is less than `other`.
*
* @static
* @memberOf _
* @since 3.9.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if `value` is less than `other`,
* else `false`.
* @see _.gt
* @example
*
* _.lt(1, 3);
* // => true
*
* _.lt(3, 3);
* // => false
*
* _.lt(3, 1);
* // => false
*/
var lt = createRelationalOperation(baseLt);
/**
* Checks if `value` is less than or equal to `other`.
*
* @static
* @memberOf _
* @since 3.9.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if `value` is less than or equal to
* `other`, else `false`.
* @see _.gte
* @example
*
* _.lte(1, 3);
* // => true
*
* _.lte(3, 3);
* // => true
*
* _.lte(3, 1);
* // => false
*/
var lte = createRelationalOperation(function(value, other) {
return value <= other;
});
/**
* Converts `value` to an array.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Lang
* @param {*} value The value to convert.
* @returns {Array} Returns the converted array.
* @example
*
* _.toArray({ 'a': 1, 'b': 2 });
* // => [1, 2]
*
* _.toArray('abc');
* // => ['a', 'b', 'c']
*
* _.toArray(1);
* // => []
*
* _.toArray(null);
* // => []
*/
function toArray(value) {
if (!value) {
return [];
}
if (isArrayLike(value)) {
return isString(value) ? stringToArray(value) : copyArray(value);
}
if (symIterator && value[symIterator]) {
return iteratorToArray(value[symIterator]());
}
var tag = getTag(value),
func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values);
return func(value);
}
/**
* Converts `value` to a finite number.
*
* @static
* @memberOf _
* @since 4.12.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted number.
* @example
*
* _.toFinite(3.2);
* // => 3.2
*
* _.toFinite(Number.MIN_VALUE);
* // => 5e-324
*
* _.toFinite(Infinity);
* // => 1.7976931348623157e+308
*
* _.toFinite('3.2');
* // => 3.2
*/
function toFinite(value) {
if (!value) {
return value === 0 ? value : 0;
}
value = toNumber(value);
if (value === INFINITY || value === -INFINITY) {
var sign = (value < 0 ? -1 : 1);
return sign * MAX_INTEGER;
}
return value === value ? value : 0;
}
/**
* Converts `value` to an integer.
*
* **Note:** This method is loosely based on
* [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted integer.
* @example
*
* _.toInteger(3.2);
* // => 3
*
* _.toInteger(Number.MIN_VALUE);
* // => 0
*
* _.toInteger(Infinity);
* // => 1.7976931348623157e+308
*
* _.toInteger('3.2');
* // => 3
*/
function toInteger(value) {
var result = toFinite(value),
remainder = result % 1;
return result === result ? (remainder ? result - remainder : result) : 0;
}
/**
* Converts `value` to an integer suitable for use as the length of an
* array-like object.
*
* **Note:** This method is based on
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted integer.
* @example
*
* _.toLength(3.2);
* // => 3
*
* _.toLength(Number.MIN_VALUE);
* // => 0
*
* _.toLength(Infinity);
* // => 4294967295
*
* _.toLength('3.2');
* // => 3
*/
function toLength(value) {
return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0;
}
/**
* Converts `value` to a number.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to process.
* @returns {number} Returns the number.
* @example
*
* _.toNumber(3.2);
* // => 3.2
*
* _.toNumber(Number.MIN_VALUE);
* // => 5e-324
*
* _.toNumber(Infinity);
* // => Infinity
*
* _.toNumber('3.2');
* // => 3.2
*/
function toNumber(value) {
if (typeof value == 'number') {
return value;
}
if (isSymbol(value)) {
return NAN;
}
if (isObject(value)) {
var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
value = isObject(other) ? (other + '') : other;
}
if (typeof value != 'string') {
return value === 0 ? value : +value;
}
value = baseTrim(value);
var isBinary = reIsBinary.test(value);
return (isBinary || reIsOctal.test(value))
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: (reIsBadHex.test(value) ? NAN : +value);
}
/**
* Converts `value` to a plain object flattening inherited enumerable string
* keyed properties of `value` to own properties of the plain object.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {Object} Returns the converted plain object.
* @example
*
* function Foo() {
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.assign({ 'a': 1 }, new Foo);
* // => { 'a': 1, 'b': 2 }
*
* _.assign({ 'a': 1 }, _.toPlainObject(new Foo));
* // => { 'a': 1, 'b': 2, 'c': 3 }
*/
function toPlainObject(value) {
return copyObject(value, keysIn(value));
}
/**
* Converts `value` to a safe integer. A safe integer can be compared and
* represented correctly.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted integer.
* @example
*
* _.toSafeInteger(3.2);
* // => 3
*
* _.toSafeInteger(Number.MIN_VALUE);
* // => 0
*
* _.toSafeInteger(Infinity);
* // => 9007199254740991
*
* _.toSafeInteger('3.2');
* // => 3
*/
function toSafeInteger(value) {
return value
? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER)
: (value === 0 ? value : 0);
}
/**
* Converts `value` to a string. An empty string is returned for `null`
* and `undefined` values. The sign of `-0` is preserved.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
* @example
*
* _.toString(null);
* // => ''
*
* _.toString(-0);
* // => '-0'
*
* _.toString([1, 2, 3]);
* // => '1,2,3'
*/
function toString(value) {
return value == null ? '' : baseToString(value);
}
/*------------------------------------------------------------------------*/
/**
* Assigns own enumerable string keyed properties of source objects to the
* destination object. Source objects are applied from left to right.
* Subsequent sources overwrite property assignments of previous sources.
*
* **Note:** This method mutates `object` and is loosely based on
* [`Object.assign`](https://mdn.io/Object/assign).
*
* @static
* @memberOf _
* @since 0.10.0
* @category Object
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @see _.assignIn
* @example
*
* function Foo() {
* this.a = 1;
* }
*
* function Bar() {
* this.c = 3;
* }
*
* Foo.prototype.b = 2;
* Bar.prototype.d = 4;
*
* _.assign({ 'a': 0 }, new Foo, new Bar);
* // => { 'a': 1, 'c': 3 }
*/
var assign = createAssigner(function(object, source) {
if (isPrototype(source) || isArrayLike(source)) {
copyObject(source, keys(source), object);
return;
}
for (var key in source) {
if (hasOwnProperty.call(source, key)) {
assignValue(object, key, source[key]);
}
}
});
/**
* This method is like `_.assign` except that it iterates over own and
* inherited source properties.
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @alias extend
* @category Object
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @see _.assign
* @example
*
* function Foo() {
* this.a = 1;
* }
*
* function Bar() {
* this.c = 3;
* }
*
* Foo.prototype.b = 2;
* Bar.prototype.d = 4;
*
* _.assignIn({ 'a': 0 }, new Foo, new Bar);
* // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }
*/
var assignIn = createAssigner(function(object, source) {
copyObject(source, keysIn(source), object);
});
/**
* This method is like `_.assignIn` except that it accepts `customizer`
* which is invoked to produce the assigned values. If `customizer` returns
* `undefined`, assignment is handled by the method instead. The `customizer`
* is invoked with five arguments: (objValue, srcValue, key, object, source).
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @alias extendWith
* @category Object
* @param {Object} object The destination object.
* @param {...Object} sources The source objects.
* @param {Function} [customizer] The function to customize assigned values.
* @returns {Object} Returns `object`.
* @see _.assignWith
* @example
*
* function customizer(objValue, srcValue) {
* return _.isUndefined(objValue) ? srcValue : objValue;
* }
*
* var defaults = _.partialRight(_.assignInWith, customizer);
*
* defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
* // => { 'a': 1, 'b': 2 }
*/
var assignInWith = createAssigner(function(object, source, srcIndex, customizer) {
copyObject(source, keysIn(source), object, customizer);
});
/**
* This method is like `_.assign` except that it accepts `customizer`
* which is invoked to produce the assigned values. If `customizer` returns
* `undefined`, assignment is handled by the method instead. The `customizer`
* is invoked with five arguments: (objValue, srcValue, key, object, source).
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The destination object.
* @param {...Object} sources The source objects.
* @param {Function} [customizer] The function to customize assigned values.
* @returns {Object} Returns `object`.
* @see _.assignInWith
* @example
*
* function customizer(objValue, srcValue) {
* return _.isUndefined(objValue) ? srcValue : objValue;
* }
*
* var defaults = _.partialRight(_.assignWith, customizer);
*
* defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
* // => { 'a': 1, 'b': 2 }
*/
var assignWith = createAssigner(function(object, source, srcIndex, customizer) {
copyObject(source, keys(source), object, customizer);
});
/**
* Creates an array of values corresponding to `paths` of `object`.
*
* @static
* @memberOf _
* @since 1.0.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {...(string|string[])} [paths] The property paths to pick.
* @returns {Array} Returns the picked values.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
*
* _.at(object, ['a[0].b.c', 'a[1]']);
* // => [3, 4]
*/
var at = flatRest(baseAt);
/**
* Creates an object that inherits from the `prototype` object. If a
* `properties` object is given, its own enumerable string keyed properties
* are assigned to the created object.
*
* @static
* @memberOf _
* @since 2.3.0
* @category Object
* @param {Object} prototype The object to inherit from.
* @param {Object} [properties] The properties to assign to the object.
* @returns {Object} Returns the new object.
* @example
*
* function Shape() {
* this.x = 0;
* this.y = 0;
* }
*
* function Circle() {
* Shape.call(this);
* }
*
* Circle.prototype = _.create(Shape.prototype, {
* 'constructor': Circle
* });
*
* var circle = new Circle;
* circle instanceof Circle;
* // => true
*
* circle instanceof Shape;
* // => true
*/
function create(prototype, properties) {
var result = baseCreate(prototype);
return properties == null ? result : baseAssign(result, properties);
}
/**
* Assigns own and inherited enumerable string keyed properties of source
* objects to the destination object for all destination properties that
* resolve to `undefined`. Source objects are applied from left to right.
* Once a property is set, additional values of the same property are ignored.
*
* **Note:** This method mutates `object`.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @see _.defaultsDeep
* @example
*
* _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
* // => { 'a': 1, 'b': 2 }
*/
var defaults = baseRest(function(object, sources) {
object = Object(object);
var index = -1;
var length = sources.length;
var guard = length > 2 ? sources[2] : undefined$1;
if (guard && isIterateeCall(sources[0], sources[1], guard)) {
length = 1;
}
while (++index < length) {
var source = sources[index];
var props = keysIn(source);
var propsIndex = -1;
var propsLength = props.length;
while (++propsIndex < propsLength) {
var key = props[propsIndex];
var value = object[key];
if (value === undefined$1 ||
(eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) {
object[key] = source[key];
}
}
}
return object;
});
/**
* This method is like `_.defaults` except that it recursively assigns
* default properties.
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 3.10.0
* @category Object
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @see _.defaults
* @example
*
* _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });
* // => { 'a': { 'b': 2, 'c': 3 } }
*/
var defaultsDeep = baseRest(function(args) {
args.push(undefined$1, customDefaultsMerge);
return apply(mergeWith, undefined$1, args);
});
/**
* This method is like `_.find` except that it returns the key of the first
* element `predicate` returns truthy for instead of the element itself.
*
* @static
* @memberOf _
* @since 1.1.0
* @category Object
* @param {Object} object The object to inspect.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {string|undefined} Returns the key of the matched element,
* else `undefined`.
* @example
*
* var users = {
* 'barney': { 'age': 36, 'active': true },
* 'fred': { 'age': 40, 'active': false },
* 'pebbles': { 'age': 1, 'active': true }
* };
*
* _.findKey(users, function(o) { return o.age < 40; });
* // => 'barney' (iteration order is not guaranteed)
*
* // The `_.matches` iteratee shorthand.
* _.findKey(users, { 'age': 1, 'active': true });
* // => 'pebbles'
*
* // The `_.matchesProperty` iteratee shorthand.
* _.findKey(users, ['active', false]);
* // => 'fred'
*
* // The `_.property` iteratee shorthand.
* _.findKey(users, 'active');
* // => 'barney'
*/
function findKey(object, predicate) {
return baseFindKey(object, getIteratee(predicate, 3), baseForOwn);
}
/**
* This method is like `_.findKey` except that it iterates over elements of
* a collection in the opposite order.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Object
* @param {Object} object The object to inspect.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {string|undefined} Returns the key of the matched element,
* else `undefined`.
* @example
*
* var users = {
* 'barney': { 'age': 36, 'active': true },
* 'fred': { 'age': 40, 'active': false },
* 'pebbles': { 'age': 1, 'active': true }
* };
*
* _.findLastKey(users, function(o) { return o.age < 40; });
* // => returns 'pebbles' assuming `_.findKey` returns 'barney'
*
* // The `_.matches` iteratee shorthand.
* _.findLastKey(users, { 'age': 36, 'active': true });
* // => 'barney'
*
* // The `_.matchesProperty` iteratee shorthand.
* _.findLastKey(users, ['active', false]);
* // => 'fred'
*
* // The `_.property` iteratee shorthand.
* _.findLastKey(users, 'active');
* // => 'pebbles'
*/
function findLastKey(object, predicate) {
return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight);
}
/**
* Iterates over own and inherited enumerable string keyed properties of an
* object and invokes `iteratee` for each property. The iteratee is invoked
* with three arguments: (value, key, object). Iteratee functions may exit
* iteration early by explicitly returning `false`.
*
* @static
* @memberOf _
* @since 0.3.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Object} Returns `object`.
* @see _.forInRight
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.forIn(new Foo, function(value, key) {
* console.log(key);
* });
* // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed).
*/
function forIn(object, iteratee) {
return object == null
? object
: baseFor(object, getIteratee(iteratee, 3), keysIn);
}
/**
* This method is like `_.forIn` except that it iterates over properties of
* `object` in the opposite order.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Object} Returns `object`.
* @see _.forIn
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.forInRight(new Foo, function(value, key) {
* console.log(key);
* });
* // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'.
*/
function forInRight(object, iteratee) {
return object == null
? object
: baseForRight(object, getIteratee(iteratee, 3), keysIn);
}
/**
* Iterates over own enumerable string keyed properties of an object and
* invokes `iteratee` for each property. The iteratee is invoked with three
* arguments: (value, key, object). Iteratee functions may exit iteration
* early by explicitly returning `false`.
*
* @static
* @memberOf _
* @since 0.3.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Object} Returns `object`.
* @see _.forOwnRight
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.forOwn(new Foo, function(value, key) {
* console.log(key);
* });
* // => Logs 'a' then 'b' (iteration order is not guaranteed).
*/
function forOwn(object, iteratee) {
return object && baseForOwn(object, getIteratee(iteratee, 3));
}
/**
* This method is like `_.forOwn` except that it iterates over properties of
* `object` in the opposite order.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Object} Returns `object`.
* @see _.forOwn
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.forOwnRight(new Foo, function(value, key) {
* console.log(key);
* });
* // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'.
*/
function forOwnRight(object, iteratee) {
return object && baseForOwnRight(object, getIteratee(iteratee, 3));
}
/**
* Creates an array of function property names from own enumerable properties
* of `object`.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to inspect.
* @returns {Array} Returns the function names.
* @see _.functionsIn
* @example
*
* function Foo() {
* this.a = _.constant('a');
* this.b = _.constant('b');
* }
*
* Foo.prototype.c = _.constant('c');
*
* _.functions(new Foo);
* // => ['a', 'b']
*/
function functions(object) {
return object == null ? [] : baseFunctions(object, keys(object));
}
/**
* Creates an array of function property names from own and inherited
* enumerable properties of `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to inspect.
* @returns {Array} Returns the function names.
* @see _.functions
* @example
*
* function Foo() {
* this.a = _.constant('a');
* this.b = _.constant('b');
* }
*
* Foo.prototype.c = _.constant('c');
*
* _.functionsIn(new Foo);
* // => ['a', 'b', 'c']
*/
function functionsIn(object) {
return object == null ? [] : baseFunctions(object, keysIn(object));
}
/**
* Gets the value at `path` of `object`. If the resolved value is
* `undefined`, the `defaultValue` is returned in its place.
*
* @static
* @memberOf _
* @since 3.7.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @param {*} [defaultValue] The value returned for `undefined` resolved values.
* @returns {*} Returns the resolved value.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 3 } }] };
*
* _.get(object, 'a[0].b.c');
* // => 3
*
* _.get(object, ['a', '0', 'b', 'c']);
* // => 3
*
* _.get(object, 'a.b.c', 'default');
* // => 'default'
*/
function get(object, path, defaultValue) {
var result = object == null ? undefined$1 : baseGet(object, path);
return result === undefined$1 ? defaultValue : result;
}
/**
* Checks if `path` is a direct property of `object`.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
* @example
*
* var object = { 'a': { 'b': 2 } };
* var other = _.create({ 'a': _.create({ 'b': 2 }) });
*
* _.has(object, 'a');
* // => true
*
* _.has(object, 'a.b');
* // => true
*
* _.has(object, ['a', 'b']);
* // => true
*
* _.has(other, 'a');
* // => false
*/
function has(object, path) {
return object != null && hasPath(object, path, baseHas);
}
/**
* Checks if `path` is a direct or inherited property of `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
* @example
*
* var object = _.create({ 'a': _.create({ 'b': 2 }) });
*
* _.hasIn(object, 'a');
* // => true
*
* _.hasIn(object, 'a.b');
* // => true
*
* _.hasIn(object, ['a', 'b']);
* // => true
*
* _.hasIn(object, 'b');
* // => false
*/
function hasIn(object, path) {
return object != null && hasPath(object, path, baseHasIn);
}
/**
* Creates an object composed of the inverted keys and values of `object`.
* If `object` contains duplicate values, subsequent values overwrite
* property assignments of previous values.
*
* @static
* @memberOf _
* @since 0.7.0
* @category Object
* @param {Object} object The object to invert.
* @returns {Object} Returns the new inverted object.
* @example
*
* var object = { 'a': 1, 'b': 2, 'c': 1 };
*
* _.invert(object);
* // => { '1': 'c', '2': 'b' }
*/
var invert = createInverter(function(result, value, key) {
if (value != null &&
typeof value.toString != 'function') {
value = nativeObjectToString.call(value);
}
result[value] = key;
}, constant(identity));
/**
* This method is like `_.invert` except that the inverted object is generated
* from the results of running each element of `object` thru `iteratee`. The
* corresponding inverted value of each inverted key is an array of keys
* responsible for generating the inverted value. The iteratee is invoked
* with one argument: (value).
*
* @static
* @memberOf _
* @since 4.1.0
* @category Object
* @param {Object} object The object to invert.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Object} Returns the new inverted object.
* @example
*
* var object = { 'a': 1, 'b': 2, 'c': 1 };
*
* _.invertBy(object);
* // => { '1': ['a', 'c'], '2': ['b'] }
*
* _.invertBy(object, function(value) {
* return 'group' + value;
* });
* // => { 'group1': ['a', 'c'], 'group2': ['b'] }
*/
var invertBy = createInverter(function(result, value, key) {
if (value != null &&
typeof value.toString != 'function') {
value = nativeObjectToString.call(value);
}
if (hasOwnProperty.call(result, value)) {
result[value].push(key);
} else {
result[value] = [key];
}
}, getIteratee);
/**
* Invokes the method at `path` of `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path of the method to invoke.
* @param {...*} [args] The arguments to invoke the method with.
* @returns {*} Returns the result of the invoked method.
* @example
*
* var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] };
*
* _.invoke(object, 'a[0].b.c.slice', 1, 3);
* // => [2, 3]
*/
var invoke = baseRest(baseInvoke);
/**
* Creates an array of the own enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects. See the
* [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* for more details.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keys(new Foo);
* // => ['a', 'b'] (iteration order is not guaranteed)
*
* _.keys('hi');
* // => ['0', '1']
*/
function keys(object) {
return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}
/**
* Creates an array of the own and inherited enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keysIn(new Foo);
* // => ['a', 'b', 'c'] (iteration order is not guaranteed)
*/
function keysIn(object) {
return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
}
/**
* The opposite of `_.mapValues`; this method creates an object with the
* same values as `object` and keys generated by running each own enumerable
* string keyed property of `object` thru `iteratee`. The iteratee is invoked
* with three arguments: (value, key, object).
*
* @static
* @memberOf _
* @since 3.8.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Object} Returns the new mapped object.
* @see _.mapValues
* @example
*
* _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) {
* return key + value;
* });
* // => { 'a1': 1, 'b2': 2 }
*/
function mapKeys(object, iteratee) {
var result = {};
iteratee = getIteratee(iteratee, 3);
baseForOwn(object, function(value, key, object) {
baseAssignValue(result, iteratee(value, key, object), value);
});
return result;
}
/**
* Creates an object with the same keys as `object` and values generated
* by running each own enumerable string keyed property of `object` thru
* `iteratee`. The iteratee is invoked with three arguments:
* (value, key, object).
*
* @static
* @memberOf _
* @since 2.4.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Object} Returns the new mapped object.
* @see _.mapKeys
* @example
*
* var users = {
* 'fred': { 'user': 'fred', 'age': 40 },
* 'pebbles': { 'user': 'pebbles', 'age': 1 }
* };
*
* _.mapValues(users, function(o) { return o.age; });
* // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)
*
* // The `_.property` iteratee shorthand.
* _.mapValues(users, 'age');
* // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)
*/
function mapValues(object, iteratee) {
var result = {};
iteratee = getIteratee(iteratee, 3);
baseForOwn(object, function(value, key, object) {
baseAssignValue(result, key, iteratee(value, key, object));
});
return result;
}
/**
* This method is like `_.assign` except that it recursively merges own and
* inherited enumerable string keyed properties of source objects into the
* destination object. Source properties that resolve to `undefined` are
* skipped if a destination value exists. Array and plain object properties
* are merged recursively. Other objects and value types are overridden by
* assignment. Source objects are applied from left to right. Subsequent
* sources overwrite property assignments of previous sources.
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 0.5.0
* @category Object
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @example
*
* var object = {
* 'a': [{ 'b': 2 }, { 'd': 4 }]
* };
*
* var other = {
* 'a': [{ 'c': 3 }, { 'e': 5 }]
* };
*
* _.merge(object, other);
* // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
*/
var merge = createAssigner(function(object, source, srcIndex) {
baseMerge(object, source, srcIndex);
});
/**
* This method is like `_.merge` except that it accepts `customizer` which
* is invoked to produce the merged values of the destination and source
* properties. If `customizer` returns `undefined`, merging is handled by the
* method instead. The `customizer` is invoked with six arguments:
* (objValue, srcValue, key, object, source, stack).
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The destination object.
* @param {...Object} sources The source objects.
* @param {Function} customizer The function to customize assigned values.
* @returns {Object} Returns `object`.
* @example
*
* function customizer(objValue, srcValue) {
* if (_.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* }
*
* var object = { 'a': [1], 'b': [2] };
* var other = { 'a': [3], 'b': [4] };
*
* _.mergeWith(object, other, customizer);
* // => { 'a': [1, 3], 'b': [2, 4] }
*/
var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {
baseMerge(object, source, srcIndex, customizer);
});
/**
* The opposite of `_.pick`; this method creates an object composed of the
* own and inherited enumerable property paths of `object` that are not omitted.
*
* **Note:** This method is considerably slower than `_.pick`.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The source object.
* @param {...(string|string[])} [paths] The property paths to omit.
* @returns {Object} Returns the new object.
* @example
*
* var object = { 'a': 1, 'b': '2', 'c': 3 };
*
* _.omit(object, ['a', 'c']);
* // => { 'b': '2' }
*/
var omit = flatRest(function(object, paths) {
var result = {};
if (object == null) {
return result;
}
var isDeep = false;
paths = arrayMap(paths, function(path) {
path = castPath(path, object);
isDeep || (isDeep = path.length > 1);
return path;
});
copyObject(object, getAllKeysIn(object), result);
if (isDeep) {
result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone);
}
var length = paths.length;
while (length--) {
baseUnset(result, paths[length]);
}
return result;
});
/**
* The opposite of `_.pickBy`; this method creates an object composed of
* the own and inherited enumerable string keyed properties of `object` that
* `predicate` doesn't return truthy for. The predicate is invoked with two
* arguments: (value, key).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The source object.
* @param {Function} [predicate=_.identity] The function invoked per property.
* @returns {Object} Returns the new object.
* @example
*
* var object = { 'a': 1, 'b': '2', 'c': 3 };
*
* _.omitBy(object, _.isNumber);
* // => { 'b': '2' }
*/
function omitBy(object, predicate) {
return pickBy(object, negate(getIteratee(predicate)));
}
/**
* Creates an object composed of the picked `object` properties.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The source object.
* @param {...(string|string[])} [paths] The property paths to pick.
* @returns {Object} Returns the new object.
* @example
*
* var object = { 'a': 1, 'b': '2', 'c': 3 };
*
* _.pick(object, ['a', 'c']);
* // => { 'a': 1, 'c': 3 }
*/
var pick = flatRest(function(object, paths) {
return object == null ? {} : basePick(object, paths);
});
/**
* Creates an object composed of the `object` properties `predicate` returns
* truthy for. The predicate is invoked with two arguments: (value, key).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The source object.
* @param {Function} [predicate=_.identity] The function invoked per property.
* @returns {Object} Returns the new object.
* @example
*
* var object = { 'a': 1, 'b': '2', 'c': 3 };
*
* _.pickBy(object, _.isNumber);
* // => { 'a': 1, 'c': 3 }
*/
function pickBy(object, predicate) {
if (object == null) {
return {};
}
var props = arrayMap(getAllKeysIn(object), function(prop) {
return [prop];
});
predicate = getIteratee(predicate);
return basePickBy(object, props, function(value, path) {
return predicate(value, path[0]);
});
}
/**
* This method is like `_.get` except that if the resolved value is a
* function it's invoked with the `this` binding of its parent object and
* its result is returned.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to resolve.
* @param {*} [defaultValue] The value returned for `undefined` resolved values.
* @returns {*} Returns the resolved value.
* @example
*
* var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] };
*
* _.result(object, 'a[0].b.c1');
* // => 3
*
* _.result(object, 'a[0].b.c2');
* // => 4
*
* _.result(object, 'a[0].b.c3', 'default');
* // => 'default'
*
* _.result(object, 'a[0].b.c3', _.constant('default'));
* // => 'default'
*/
function result(object, path, defaultValue) {
path = castPath(path, object);
var index = -1,
length = path.length;
// Ensure the loop is entered when path is empty.
if (!length) {
length = 1;
object = undefined$1;
}
while (++index < length) {
var value = object == null ? undefined$1 : object[toKey(path[index])];
if (value === undefined$1) {
index = length;
value = defaultValue;
}
object = isFunction(value) ? value.call(object) : value;
}
return object;
}
/**
* Sets the value at `path` of `object`. If a portion of `path` doesn't exist,
* it's created. Arrays are created for missing index properties while objects
* are created for all other missing properties. Use `_.setWith` to customize
* `path` creation.
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 3.7.0
* @category Object
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to set.
* @param {*} value The value to set.
* @returns {Object} Returns `object`.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 3 } }] };
*
* _.set(object, 'a[0].b.c', 4);
* console.log(object.a[0].b.c);
* // => 4
*
* _.set(object, ['x', '0', 'y', 'z'], 5);
* console.log(object.x[0].y.z);
* // => 5
*/
function set(object, path, value) {
return object == null ? object : baseSet(object, path, value);
}
/**
* This method is like `_.set` except that it accepts `customizer` which is
* invoked to produce the objects of `path`. If `customizer` returns `undefined`
* path creation is handled by the method instead. The `customizer` is invoked
* with three arguments: (nsValue, key, nsObject).
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to set.
* @param {*} value The value to set.
* @param {Function} [customizer] The function to customize assigned values.
* @returns {Object} Returns `object`.
* @example
*
* var object = {};
*
* _.setWith(object, '[0][1]', 'a', Object);
* // => { '0': { '1': 'a' } }
*/
function setWith(object, path, value, customizer) {
customizer = typeof customizer == 'function' ? customizer : undefined$1;
return object == null ? object : baseSet(object, path, value, customizer);
}
/**
* Creates an array of own enumerable string keyed-value pairs for `object`
* which can be consumed by `_.fromPairs`. If `object` is a map or set, its
* entries are returned.
*
* @static
* @memberOf _
* @since 4.0.0
* @alias entries
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the key-value pairs.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.toPairs(new Foo);
* // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)
*/
var toPairs = createToPairs(keys);
/**
* Creates an array of own and inherited enumerable string keyed-value pairs
* for `object` which can be consumed by `_.fromPairs`. If `object` is a map
* or set, its entries are returned.
*
* @static
* @memberOf _
* @since 4.0.0
* @alias entriesIn
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the key-value pairs.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.toPairsIn(new Foo);
* // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed)
*/
var toPairsIn = createToPairs(keysIn);
/**
* An alternative to `_.reduce`; this method transforms `object` to a new
* `accumulator` object which is the result of running each of its own
* enumerable string keyed properties thru `iteratee`, with each invocation
* potentially mutating the `accumulator` object. If `accumulator` is not
* provided, a new object with the same `[[Prototype]]` will be used. The
* iteratee is invoked with four arguments: (accumulator, value, key, object).
* Iteratee functions may exit iteration early by explicitly returning `false`.
*
* @static
* @memberOf _
* @since 1.3.0
* @category Object
* @param {Object} object The object to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @param {*} [accumulator] The custom accumulator value.
* @returns {*} Returns the accumulated value.
* @example
*
* _.transform([2, 3, 4], function(result, n) {
* result.push(n *= n);
* return n % 2 == 0;
* }, []);
* // => [4, 9]
*
* _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {
* (result[value] || (result[value] = [])).push(key);
* }, {});
* // => { '1': ['a', 'c'], '2': ['b'] }
*/
function transform(object, iteratee, accumulator) {
var isArr = isArray(object),
isArrLike = isArr || isBuffer(object) || isTypedArray(object);
iteratee = getIteratee(iteratee, 4);
if (accumulator == null) {
var Ctor = object && object.constructor;
if (isArrLike) {
accumulator = isArr ? new Ctor : [];
}
else if (isObject(object)) {
accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {};
}
else {
accumulator = {};
}
}
(isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) {
return iteratee(accumulator, value, index, object);
});
return accumulator;
}
/**
* Removes the property at `path` of `object`.
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to unset.
* @returns {boolean} Returns `true` if the property is deleted, else `false`.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 7 } }] };
* _.unset(object, 'a[0].b.c');
* // => true
*
* console.log(object);
* // => { 'a': [{ 'b': {} }] };
*
* _.unset(object, ['a', '0', 'b', 'c']);
* // => true
*
* console.log(object);
* // => { 'a': [{ 'b': {} }] };
*/
function unset(object, path) {
return object == null ? true : baseUnset(object, path);
}
/**
* This method is like `_.set` except that accepts `updater` to produce the
* value to set. Use `_.updateWith` to customize `path` creation. The `updater`
* is invoked with one argument: (value).
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.6.0
* @category Object
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to set.
* @param {Function} updater The function to produce the updated value.
* @returns {Object} Returns `object`.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 3 } }] };
*
* _.update(object, 'a[0].b.c', function(n) { return n * n; });
* console.log(object.a[0].b.c);
* // => 9
*
* _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; });
* console.log(object.x[0].y.z);
* // => 0
*/
function update(object, path, updater) {
return object == null ? object : baseUpdate(object, path, castFunction(updater));
}
/**
* This method is like `_.update` except that it accepts `customizer` which is
* invoked to produce the objects of `path`. If `customizer` returns `undefined`
* path creation is handled by the method instead. The `customizer` is invoked
* with three arguments: (nsValue, key, nsObject).
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.6.0
* @category Object
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to set.
* @param {Function} updater The function to produce the updated value.
* @param {Function} [customizer] The function to customize assigned values.
* @returns {Object} Returns `object`.
* @example
*
* var object = {};
*
* _.updateWith(object, '[0][1]', _.constant('a'), Object);
* // => { '0': { '1': 'a' } }
*/
function updateWith(object, path, updater, customizer) {
customizer = typeof customizer == 'function' ? customizer : undefined$1;
return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer);
}
/**
* Creates an array of the own enumerable string keyed property values of `object`.
*
* **Note:** Non-object values are coerced to objects.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property values.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.values(new Foo);
* // => [1, 2] (iteration order is not guaranteed)
*
* _.values('hi');
* // => ['h', 'i']
*/
function values(object) {
return object == null ? [] : baseValues(object, keys(object));
}
/**
* Creates an array of the own and inherited enumerable string keyed property
* values of `object`.
*
* **Note:** Non-object values are coerced to objects.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property values.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.valuesIn(new Foo);
* // => [1, 2, 3] (iteration order is not guaranteed)
*/
function valuesIn(object) {
return object == null ? [] : baseValues(object, keysIn(object));
}
/*------------------------------------------------------------------------*/
/**
* Clamps `number` within the inclusive `lower` and `upper` bounds.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Number
* @param {number} number The number to clamp.
* @param {number} [lower] The lower bound.
* @param {number} upper The upper bound.
* @returns {number} Returns the clamped number.
* @example
*
* _.clamp(-10, -5, 5);
* // => -5
*
* _.clamp(10, -5, 5);
* // => 5
*/
function clamp(number, lower, upper) {
if (upper === undefined$1) {
upper = lower;
lower = undefined$1;
}
if (upper !== undefined$1) {
upper = toNumber(upper);
upper = upper === upper ? upper : 0;
}
if (lower !== undefined$1) {
lower = toNumber(lower);
lower = lower === lower ? lower : 0;
}
return baseClamp(toNumber(number), lower, upper);
}
/**
* Checks if `n` is between `start` and up to, but not including, `end`. If
* `end` is not specified, it's set to `start` with `start` then set to `0`.
* If `start` is greater than `end` the params are swapped to support
* negative ranges.
*
* @static
* @memberOf _
* @since 3.3.0
* @category Number
* @param {number} number The number to check.
* @param {number} [start=0] The start of the range.
* @param {number} end The end of the range.
* @returns {boolean} Returns `true` if `number` is in the range, else `false`.
* @see _.range, _.rangeRight
* @example
*
* _.inRange(3, 2, 4);
* // => true
*
* _.inRange(4, 8);
* // => true
*
* _.inRange(4, 2);
* // => false
*
* _.inRange(2, 2);
* // => false
*
* _.inRange(1.2, 2);
* // => true
*
* _.inRange(5.2, 4);
* // => false
*
* _.inRange(-3, -2, -6);
* // => true
*/
function inRange(number, start, end) {
start = toFinite(start);
if (end === undefined$1) {
end = start;
start = 0;
} else {
end = toFinite(end);
}
number = toNumber(number);
return baseInRange(number, start, end);
}
/**
* Produces a random number between the inclusive `lower` and `upper` bounds.
* If only one argument is provided a number between `0` and the given number
* is returned. If `floating` is `true`, or either `lower` or `upper` are
* floats, a floating-point number is returned instead of an integer.
*
* **Note:** JavaScript follows the IEEE-754 standard for resolving
* floating-point values which can produce unexpected results.
*
* @static
* @memberOf _
* @since 0.7.0
* @category Number
* @param {number} [lower=0] The lower bound.
* @param {number} [upper=1] The upper bound.
* @param {boolean} [floating] Specify returning a floating-point number.
* @returns {number} Returns the random number.
* @example
*
* _.random(0, 5);
* // => an integer between 0 and 5
*
* _.random(5);
* // => also an integer between 0 and 5
*
* _.random(5, true);
* // => a floating-point number between 0 and 5
*
* _.random(1.2, 5.2);
* // => a floating-point number between 1.2 and 5.2
*/
function random(lower, upper, floating) {
if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) {
upper = floating = undefined$1;
}
if (floating === undefined$1) {
if (typeof upper == 'boolean') {
floating = upper;
upper = undefined$1;
}
else if (typeof lower == 'boolean') {
floating = lower;
lower = undefined$1;
}
}
if (lower === undefined$1 && upper === undefined$1) {
lower = 0;
upper = 1;
}
else {
lower = toFinite(lower);
if (upper === undefined$1) {
upper = lower;
lower = 0;
} else {
upper = toFinite(upper);
}
}
if (lower > upper) {
var temp = lower;
lower = upper;
upper = temp;
}
if (floating || lower % 1 || upper % 1) {
var rand = nativeRandom();
return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper);
}
return baseRandom(lower, upper);
}
/*------------------------------------------------------------------------*/
/**
* Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase).
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the camel cased string.
* @example
*
* _.camelCase('Foo Bar');
* // => 'fooBar'
*
* _.camelCase('--foo-bar--');
* // => 'fooBar'
*
* _.camelCase('__FOO_BAR__');
* // => 'fooBar'
*/
var camelCase = createCompounder(function(result, word, index) {
word = word.toLowerCase();
return result + (index ? capitalize(word) : word);
});
/**
* Converts the first character of `string` to upper case and the remaining
* to lower case.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to capitalize.
* @returns {string} Returns the capitalized string.
* @example
*
* _.capitalize('FRED');
* // => 'Fred'
*/
function capitalize(string) {
return upperFirst(toString(string).toLowerCase());
}
/**
* Deburrs `string` by converting
* [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table)
* and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A)
* letters to basic Latin letters and removing
* [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks).
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to deburr.
* @returns {string} Returns the deburred string.
* @example
*
* _.deburr('déjà vu');
* // => 'deja vu'
*/
function deburr(string) {
string = toString(string);
return string && string.replace(reLatin, deburrLetter).replace(reComboMark, '');
}
/**
* Checks if `string` ends with the given target string.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to inspect.
* @param {string} [target] The string to search for.
* @param {number} [position=string.length] The position to search up to.
* @returns {boolean} Returns `true` if `string` ends with `target`,
* else `false`.
* @example
*
* _.endsWith('abc', 'c');
* // => true
*
* _.endsWith('abc', 'b');
* // => false
*
* _.endsWith('abc', 'b', 2);
* // => true
*/
function endsWith(string, target, position) {
string = toString(string);
target = baseToString(target);
var length = string.length;
position = position === undefined$1
? length
: baseClamp(toInteger(position), 0, length);
var end = position;
position -= target.length;
return position >= 0 && string.slice(position, end) == target;
}
/**
* Converts the characters "&", "<", ">", '"', and "'" in `string` to their
* corresponding HTML entities.
*
* **Note:** No other characters are escaped. To escape additional
* characters use a third-party library like [_he_](https://mths.be/he).
*
* Though the ">" character is escaped for symmetry, characters like
* ">" and "/" don't need escaping in HTML and have no special meaning
* unless they're part of a tag or unquoted attribute value. See
* [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands)
* (under "semi-related fun fact") for more details.
*
* When working with HTML you should always
* [quote attribute values](http://wonko.com/post/html-escaping) to reduce
* XSS vectors.
*
* @static
* @since 0.1.0
* @memberOf _
* @category String
* @param {string} [string=''] The string to escape.
* @returns {string} Returns the escaped string.
* @example
*
* _.escape('fred, barney, & pebbles');
* // => 'fred, barney, &amp; pebbles'
*/
function escape(string) {
string = toString(string);
return (string && reHasUnescapedHtml.test(string))
? string.replace(reUnescapedHtml, escapeHtmlChar)
: string;
}
/**
* Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
* "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to escape.
* @returns {string} Returns the escaped string.
* @example
*
* _.escapeRegExp('[lodash](https://lodash.com/)');
* // => '\[lodash\]\(https://lodash\.com/\)'
*/
function escapeRegExp(string) {
string = toString(string);
return (string && reHasRegExpChar.test(string))
? string.replace(reRegExpChar, '\\$&')
: string;
}
/**
* Converts `string` to
* [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles).
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the kebab cased string.
* @example
*
* _.kebabCase('Foo Bar');
* // => 'foo-bar'
*
* _.kebabCase('fooBar');
* // => 'foo-bar'
*
* _.kebabCase('__FOO_BAR__');
* // => 'foo-bar'
*/
var kebabCase = createCompounder(function(result, word, index) {
return result + (index ? '-' : '') + word.toLowerCase();
});
/**
* Converts `string`, as space separated words, to lower case.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the lower cased string.
* @example
*
* _.lowerCase('--Foo-Bar--');
* // => 'foo bar'
*
* _.lowerCase('fooBar');
* // => 'foo bar'
*
* _.lowerCase('__FOO_BAR__');
* // => 'foo bar'
*/
var lowerCase = createCompounder(function(result, word, index) {
return result + (index ? ' ' : '') + word.toLowerCase();
});
/**
* Converts the first character of `string` to lower case.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the converted string.
* @example
*
* _.lowerFirst('Fred');
* // => 'fred'
*
* _.lowerFirst('FRED');
* // => 'fRED'
*/
var lowerFirst = createCaseFirst('toLowerCase');
/**
* Pads `string` on the left and right sides if it's shorter than `length`.
* Padding characters are truncated if they can't be evenly divided by `length`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to pad.
* @param {number} [length=0] The padding length.
* @param {string} [chars=' '] The string used as padding.
* @returns {string} Returns the padded string.
* @example
*
* _.pad('abc', 8);
* // => ' abc '
*
* _.pad('abc', 8, '_-');
* // => '_-abc_-_'
*
* _.pad('abc', 3);
* // => 'abc'
*/
function pad(string, length, chars) {
string = toString(string);
length = toInteger(length);
var strLength = length ? stringSize(string) : 0;
if (!length || strLength >= length) {
return string;
}
var mid = (length - strLength) / 2;
return (
createPadding(nativeFloor(mid), chars) +
string +
createPadding(nativeCeil(mid), chars)
);
}
/**
* Pads `string` on the right side if it's shorter than `length`. Padding
* characters are truncated if they exceed `length`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to pad.
* @param {number} [length=0] The padding length.
* @param {string} [chars=' '] The string used as padding.
* @returns {string} Returns the padded string.
* @example
*
* _.padEnd('abc', 6);
* // => 'abc '
*
* _.padEnd('abc', 6, '_-');
* // => 'abc_-_'
*
* _.padEnd('abc', 3);
* // => 'abc'
*/
function padEnd(string, length, chars) {
string = toString(string);
length = toInteger(length);
var strLength = length ? stringSize(string) : 0;
return (length && strLength < length)
? (string + createPadding(length - strLength, chars))
: string;
}
/**
* Pads `string` on the left side if it's shorter than `length`. Padding
* characters are truncated if they exceed `length`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to pad.
* @param {number} [length=0] The padding length.
* @param {string} [chars=' '] The string used as padding.
* @returns {string} Returns the padded string.
* @example
*
* _.padStart('abc', 6);
* // => ' abc'
*
* _.padStart('abc', 6, '_-');
* // => '_-_abc'
*
* _.padStart('abc', 3);
* // => 'abc'
*/
function padStart(string, length, chars) {
string = toString(string);
length = toInteger(length);
var strLength = length ? stringSize(string) : 0;
return (length && strLength < length)
? (createPadding(length - strLength, chars) + string)
: string;
}
/**
* Converts `string` to an integer of the specified radix. If `radix` is
* `undefined` or `0`, a `radix` of `10` is used unless `value` is a
* hexadecimal, in which case a `radix` of `16` is used.
*
* **Note:** This method aligns with the
* [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`.
*
* @static
* @memberOf _
* @since 1.1.0
* @category String
* @param {string} string The string to convert.
* @param {number} [radix=10] The radix to interpret `value` by.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {number} Returns the converted integer.
* @example
*
* _.parseInt('08');
* // => 8
*
* _.map(['6', '08', '10'], _.parseInt);
* // => [6, 8, 10]
*/
function parseInt(string, radix, guard) {
if (guard || radix == null) {
radix = 0;
} else if (radix) {
radix = +radix;
}
return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0);
}
/**
* Repeats the given string `n` times.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to repeat.
* @param {number} [n=1] The number of times to repeat the string.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {string} Returns the repeated string.
* @example
*
* _.repeat('*', 3);
* // => '***'
*
* _.repeat('abc', 2);
* // => 'abcabc'
*
* _.repeat('abc', 0);
* // => ''
*/
function repeat(string, n, guard) {
if ((guard ? isIterateeCall(string, n, guard) : n === undefined$1)) {
n = 1;
} else {
n = toInteger(n);
}
return baseRepeat(toString(string), n);
}
/**
* Replaces matches for `pattern` in `string` with `replacement`.
*
* **Note:** This method is based on
* [`String#replace`](https://mdn.io/String/replace).
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to modify.
* @param {RegExp|string} pattern The pattern to replace.
* @param {Function|string} replacement The match replacement.
* @returns {string} Returns the modified string.
* @example
*
* _.replace('Hi Fred', 'Fred', 'Barney');
* // => 'Hi Barney'
*/
function replace() {
var args = arguments,
string = toString(args[0]);
return args.length < 3 ? string : string.replace(args[1], args[2]);
}
/**
* Converts `string` to
* [snake case](https://en.wikipedia.org/wiki/Snake_case).
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the snake cased string.
* @example
*
* _.snakeCase('Foo Bar');
* // => 'foo_bar'
*
* _.snakeCase('fooBar');
* // => 'foo_bar'
*
* _.snakeCase('--FOO-BAR--');
* // => 'foo_bar'
*/
var snakeCase = createCompounder(function(result, word, index) {
return result + (index ? '_' : '') + word.toLowerCase();
});
/**
* Splits `string` by `separator`.
*
* **Note:** This method is based on
* [`String#split`](https://mdn.io/String/split).
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to split.
* @param {RegExp|string} separator The separator pattern to split by.
* @param {number} [limit] The length to truncate results to.
* @returns {Array} Returns the string segments.
* @example
*
* _.split('a-b-c', '-', 2);
* // => ['a', 'b']
*/
function split(string, separator, limit) {
if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) {
separator = limit = undefined$1;
}
limit = limit === undefined$1 ? MAX_ARRAY_LENGTH : limit >>> 0;
if (!limit) {
return [];
}
string = toString(string);
if (string && (
typeof separator == 'string' ||
(separator != null && !isRegExp(separator))
)) {
separator = baseToString(separator);
if (!separator && hasUnicode(string)) {
return castSlice(stringToArray(string), 0, limit);
}
}
return string.split(separator, limit);
}
/**
* Converts `string` to
* [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).
*
* @static
* @memberOf _
* @since 3.1.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the start cased string.
* @example
*
* _.startCase('--foo-bar--');
* // => 'Foo Bar'
*
* _.startCase('fooBar');
* // => 'Foo Bar'
*
* _.startCase('__FOO_BAR__');
* // => 'FOO BAR'
*/
var startCase = createCompounder(function(result, word, index) {
return result + (index ? ' ' : '') + upperFirst(word);
});
/**
* Checks if `string` starts with the given target string.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to inspect.
* @param {string} [target] The string to search for.
* @param {number} [position=0] The position to search from.
* @returns {boolean} Returns `true` if `string` starts with `target`,
* else `false`.
* @example
*
* _.startsWith('abc', 'a');
* // => true
*
* _.startsWith('abc', 'b');
* // => false
*
* _.startsWith('abc', 'b', 1);
* // => true
*/
function startsWith(string, target, position) {
string = toString(string);
position = position == null
? 0
: baseClamp(toInteger(position), 0, string.length);
target = baseToString(target);
return string.slice(position, position + target.length) == target;
}
/**
* Creates a compiled template function that can interpolate data properties
* in "interpolate" delimiters, HTML-escape interpolated data properties in
* "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data
* properties may be accessed as free variables in the template. If a setting
* object is given, it takes precedence over `_.templateSettings` values.
*
* **Note:** In the development build `_.template` utilizes
* [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl)
* for easier debugging.
*
* For more information on precompiling templates see
* [lodash's custom builds documentation](https://lodash.com/custom-builds).
*
* For more information on Chrome extension sandboxes see
* [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).
*
* @static
* @since 0.1.0
* @memberOf _
* @category String
* @param {string} [string=''] The template string.
* @param {Object} [options={}] The options object.
* @param {RegExp} [options.escape=_.templateSettings.escape]
* The HTML "escape" delimiter.
* @param {RegExp} [options.evaluate=_.templateSettings.evaluate]
* The "evaluate" delimiter.
* @param {Object} [options.imports=_.templateSettings.imports]
* An object to import into the template as free variables.
* @param {RegExp} [options.interpolate=_.templateSettings.interpolate]
* The "interpolate" delimiter.
* @param {string} [options.sourceURL='lodash.templateSources[n]']
* The sourceURL of the compiled template.
* @param {string} [options.variable='obj']
* The data object variable name.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Function} Returns the compiled template function.
* @example
*
* // Use the "interpolate" delimiter to create a compiled template.
* var compiled = _.template('hello <%= user %>!');
* compiled({ 'user': 'fred' });
* // => 'hello fred!'
*
* // Use the HTML "escape" delimiter to escape data property values.
* var compiled = _.template('<b><%- value %></b>');
* compiled({ 'value': '<script>' });
* // => '<b>&lt;script&gt;</b>'
*
* // Use the "evaluate" delimiter to execute JavaScript and generate HTML.
* var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>');
* compiled({ 'users': ['fred', 'barney'] });
* // => '<li>fred</li><li>barney</li>'
*
* // Use the internal `print` function in "evaluate" delimiters.
* var compiled = _.template('<% print("hello " + user); %>!');
* compiled({ 'user': 'barney' });
* // => 'hello barney!'
*
* // Use the ES template literal delimiter as an "interpolate" delimiter.
* // Disable support by replacing the "interpolate" delimiter.
* var compiled = _.template('hello ${ user }!');
* compiled({ 'user': 'pebbles' });
* // => 'hello pebbles!'
*
* // Use backslashes to treat delimiters as plain text.
* var compiled = _.template('<%= "\\<%- value %\\>" %>');
* compiled({ 'value': 'ignored' });
* // => '<%- value %>'
*
* // Use the `imports` option to import `jQuery` as `jq`.
* var text = '<% jq.each(users, function(user) { %><li><%- user %></li><% }); %>';
* var compiled = _.template(text, { 'imports': { 'jq': jQuery } });
* compiled({ 'users': ['fred', 'barney'] });
* // => '<li>fred</li><li>barney</li>'
*
* // Use the `sourceURL` option to specify a custom sourceURL for the template.
* var compiled = _.template('hello <%= user %>!', { 'sourceURL': '/basic/greeting.jst' });
* compiled(data);
* // => Find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector.
*
* // Use the `variable` option to ensure a with-statement isn't used in the compiled template.
* var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' });
* compiled.source;
* // => function(data) {
* // var __t, __p = '';
* // __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!';
* // return __p;
* // }
*
* // Use custom template delimiters.
* _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
* var compiled = _.template('hello {{ user }}!');
* compiled({ 'user': 'mustache' });
* // => 'hello mustache!'
*
* // Use the `source` property to inline compiled templates for meaningful
* // line numbers in error messages and stack traces.
* fs.writeFileSync(path.join(process.cwd(), 'jst.js'), '\
* var JST = {\
* "main": ' + _.template(mainText).source + '\
* };\
* ');
*/
function template(string, options, guard) {
// Based on John Resig's `tmpl` implementation
// (http://ejohn.org/blog/javascript-micro-templating/)
// and Laura Doktorova's doT.js (https://github.com/olado/doT).
var settings = lodash.templateSettings;
if (guard && isIterateeCall(string, options, guard)) {
options = undefined$1;
}
string = toString(string);
options = assignInWith({}, options, settings, customDefaultsAssignIn);
var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn),
importsKeys = keys(imports),
importsValues = baseValues(imports, importsKeys);
var isEscaping,
isEvaluating,
index = 0,
interpolate = options.interpolate || reNoMatch,
source = "__p += '";
// Compile the regexp to match each delimiter.
var reDelimiters = RegExp(
(options.escape || reNoMatch).source + '|' +
interpolate.source + '|' +
(interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' +
(options.evaluate || reNoMatch).source + '|$'
, 'g');
// Use a sourceURL for easier debugging.
// The sourceURL gets injected into the source that's eval-ed, so be careful
// to normalize all kinds of whitespace, so e.g. newlines (and unicode versions of it) can't sneak in
// and escape the comment, thus injecting code that gets evaled.
var sourceURL = '//# sourceURL=' +
(hasOwnProperty.call(options, 'sourceURL')
? (options.sourceURL + '').replace(/\s/g, ' ')
: ('lodash.templateSources[' + (++templateCounter) + ']')
) + '\n';
string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) {
interpolateValue || (interpolateValue = esTemplateValue);
// Escape characters that can't be included in string literals.
source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar);
// Replace delimiters with snippets.
if (escapeValue) {
isEscaping = true;
source += "' +\n__e(" + escapeValue + ") +\n'";
}
if (evaluateValue) {
isEvaluating = true;
source += "';\n" + evaluateValue + ";\n__p += '";
}
if (interpolateValue) {
source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'";
}
index = offset + match.length;
// The JS engine embedded in Adobe products needs `match` returned in
// order to produce the correct `offset` value.
return match;
});
source += "';\n";
// If `variable` is not specified wrap a with-statement around the generated
// code to add the data object to the top of the scope chain.
var variable = hasOwnProperty.call(options, 'variable') && options.variable;
if (!variable) {
source = 'with (obj) {\n' + source + '\n}\n';
}
// Throw an error if a forbidden character was found in `variable`, to prevent
// potential command injection attacks.
else if (reForbiddenIdentifierChars.test(variable)) {
throw new Error(INVALID_TEMPL_VAR_ERROR_TEXT);
}
// Cleanup code by stripping empty strings.
source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)
.replace(reEmptyStringMiddle, '$1')
.replace(reEmptyStringTrailing, '$1;');
// Frame code as the function body.
source = 'function(' + (variable || 'obj') + ') {\n' +
(variable
? ''
: 'obj || (obj = {});\n'
) +
"var __t, __p = ''" +
(isEscaping
? ', __e = _.escape'
: ''
) +
(isEvaluating
? ', __j = Array.prototype.join;\n' +
"function print() { __p += __j.call(arguments, '') }\n"
: ';\n'
) +
source +
'return __p\n}';
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined$1, importsValues);
});
// Provide the compiled function's source by its `toString` method or
// the `source` property as a convenience for inlining compiled templates.
result.source = source;
if (isError(result)) {
throw result;
}
return result;
}
/**
* Converts `string`, as a whole, to lower case just like
* [String#toLowerCase](https://mdn.io/toLowerCase).
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the lower cased string.
* @example
*
* _.toLower('--Foo-Bar--');
* // => '--foo-bar--'
*
* _.toLower('fooBar');
* // => 'foobar'
*
* _.toLower('__FOO_BAR__');
* // => '__foo_bar__'
*/
function toLower(value) {
return toString(value).toLowerCase();
}
/**
* Converts `string`, as a whole, to upper case just like
* [String#toUpperCase](https://mdn.io/toUpperCase).
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the upper cased string.
* @example
*
* _.toUpper('--foo-bar--');
* // => '--FOO-BAR--'
*
* _.toUpper('fooBar');
* // => 'FOOBAR'
*
* _.toUpper('__foo_bar__');
* // => '__FOO_BAR__'
*/
function toUpper(value) {
return toString(value).toUpperCase();
}
/**
* Removes leading and trailing whitespace or specified characters from `string`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to trim.
* @param {string} [chars=whitespace] The characters to trim.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {string} Returns the trimmed string.
* @example
*
* _.trim(' abc ');
* // => 'abc'
*
* _.trim('-_-abc-_-', '_-');
* // => 'abc'
*
* _.map([' foo ', ' bar '], _.trim);
* // => ['foo', 'bar']
*/
function trim(string, chars, guard) {
string = toString(string);
if (string && (guard || chars === undefined$1)) {
return baseTrim(string);
}
if (!string || !(chars = baseToString(chars))) {
return string;
}
var strSymbols = stringToArray(string),
chrSymbols = stringToArray(chars),
start = charsStartIndex(strSymbols, chrSymbols),
end = charsEndIndex(strSymbols, chrSymbols) + 1;
return castSlice(strSymbols, start, end).join('');
}
/**
* Removes trailing whitespace or specified characters from `string`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to trim.
* @param {string} [chars=whitespace] The characters to trim.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {string} Returns the trimmed string.
* @example
*
* _.trimEnd(' abc ');
* // => ' abc'
*
* _.trimEnd('-_-abc-_-', '_-');
* // => '-_-abc'
*/
function trimEnd(string, chars, guard) {
string = toString(string);
if (string && (guard || chars === undefined$1)) {
return string.slice(0, trimmedEndIndex(string) + 1);
}
if (!string || !(chars = baseToString(chars))) {
return string;
}
var strSymbols = stringToArray(string),
end = charsEndIndex(strSymbols, stringToArray(chars)) + 1;
return castSlice(strSymbols, 0, end).join('');
}
/**
* Removes leading whitespace or specified characters from `string`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to trim.
* @param {string} [chars=whitespace] The characters to trim.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {string} Returns the trimmed string.
* @example
*
* _.trimStart(' abc ');
* // => 'abc '
*
* _.trimStart('-_-abc-_-', '_-');
* // => 'abc-_-'
*/
function trimStart(string, chars, guard) {
string = toString(string);
if (string && (guard || chars === undefined$1)) {
return string.replace(reTrimStart, '');
}
if (!string || !(chars = baseToString(chars))) {
return string;
}
var strSymbols = stringToArray(string),
start = charsStartIndex(strSymbols, stringToArray(chars));
return castSlice(strSymbols, start).join('');
}
/**
* Truncates `string` if it's longer than the given maximum string length.
* The last characters of the truncated string are replaced with the omission
* string which defaults to "...".
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to truncate.
* @param {Object} [options={}] The options object.
* @param {number} [options.length=30] The maximum string length.
* @param {string} [options.omission='...'] The string to indicate text is omitted.
* @param {RegExp|string} [options.separator] The separator pattern to truncate to.
* @returns {string} Returns the truncated string.
* @example
*
* _.truncate('hi-diddly-ho there, neighborino');
* // => 'hi-diddly-ho there, neighbo...'
*
* _.truncate('hi-diddly-ho there, neighborino', {
* 'length': 24,
* 'separator': ' '
* });
* // => 'hi-diddly-ho there,...'
*
* _.truncate('hi-diddly-ho there, neighborino', {
* 'length': 24,
* 'separator': /,? +/
* });
* // => 'hi-diddly-ho there...'
*
* _.truncate('hi-diddly-ho there, neighborino', {
* 'omission': ' [...]'
* });
* // => 'hi-diddly-ho there, neig [...]'
*/
function truncate(string, options) {
var length = DEFAULT_TRUNC_LENGTH,
omission = DEFAULT_TRUNC_OMISSION;
if (isObject(options)) {
var separator = 'separator' in options ? options.separator : separator;
length = 'length' in options ? toInteger(options.length) : length;
omission = 'omission' in options ? baseToString(options.omission) : omission;
}
string = toString(string);
var strLength = string.length;
if (hasUnicode(string)) {
var strSymbols = stringToArray(string);
strLength = strSymbols.length;
}
if (length >= strLength) {
return string;
}
var end = length - stringSize(omission);
if (end < 1) {
return omission;
}
var result = strSymbols
? castSlice(strSymbols, 0, end).join('')
: string.slice(0, end);
if (separator === undefined$1) {
return result + omission;
}
if (strSymbols) {
end += (result.length - end);
}
if (isRegExp(separator)) {
if (string.slice(end).search(separator)) {
var match,
substring = result;
if (!separator.global) {
separator = RegExp(separator.source, toString(reFlags.exec(separator)) + 'g');
}
separator.lastIndex = 0;
while ((match = separator.exec(substring))) {
var newEnd = match.index;
}
result = result.slice(0, newEnd === undefined$1 ? end : newEnd);
}
} else if (string.indexOf(baseToString(separator), end) != end) {
var index = result.lastIndexOf(separator);
if (index > -1) {
result = result.slice(0, index);
}
}
return result + omission;
}
/**
* The inverse of `_.escape`; this method converts the HTML entities
* `&amp;`, `&lt;`, `&gt;`, `&quot;`, and `&#39;` in `string` to
* their corresponding characters.
*
* **Note:** No other HTML entities are unescaped. To unescape additional
* HTML entities use a third-party library like [_he_](https://mths.be/he).
*
* @static
* @memberOf _
* @since 0.6.0
* @category String
* @param {string} [string=''] The string to unescape.
* @returns {string} Returns the unescaped string.
* @example
*
* _.unescape('fred, barney, &amp; pebbles');
* // => 'fred, barney, & pebbles'
*/
function unescape(string) {
string = toString(string);
return (string && reHasEscapedHtml.test(string))
? string.replace(reEscapedHtml, unescapeHtmlChar)
: string;
}
/**
* Converts `string`, as space separated words, to upper case.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the upper cased string.
* @example
*
* _.upperCase('--foo-bar');
* // => 'FOO BAR'
*
* _.upperCase('fooBar');
* // => 'FOO BAR'
*
* _.upperCase('__foo_bar__');
* // => 'FOO BAR'
*/
var upperCase = createCompounder(function(result, word, index) {
return result + (index ? ' ' : '') + word.toUpperCase();
});
/**
* Converts the first character of `string` to upper case.
*
* @static
* @memberOf _
* @since 4.0.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the converted string.
* @example
*
* _.upperFirst('fred');
* // => 'Fred'
*
* _.upperFirst('FRED');
* // => 'FRED'
*/
var upperFirst = createCaseFirst('toUpperCase');
/**
* Splits `string` into an array of its words.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to inspect.
* @param {RegExp|string} [pattern] The pattern to match words.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the words of `string`.
* @example
*
* _.words('fred, barney, & pebbles');
* // => ['fred', 'barney', 'pebbles']
*
* _.words('fred, barney, & pebbles', /[^, ]+/g);
* // => ['fred', 'barney', '&', 'pebbles']
*/
function words(string, pattern, guard) {
string = toString(string);
pattern = guard ? undefined$1 : pattern;
if (pattern === undefined$1) {
return hasUnicodeWord(string) ? unicodeWords(string) : asciiWords(string);
}
return string.match(pattern) || [];
}
/*------------------------------------------------------------------------*/
/**
* Attempts to invoke `func`, returning either the result or the caught error
* object. Any additional arguments are provided to `func` when it's invoked.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Util
* @param {Function} func The function to attempt.
* @param {...*} [args] The arguments to invoke `func` with.
* @returns {*} Returns the `func` result or error object.
* @example
*
* // Avoid throwing errors for invalid selectors.
* var elements = _.attempt(function(selector) {
* return document.querySelectorAll(selector);
* }, '>_>');
*
* if (_.isError(elements)) {
* elements = [];
* }
*/
var attempt = baseRest(function(func, args) {
try {
return apply(func, undefined$1, args);
} catch (e) {
return isError(e) ? e : new Error(e);
}
});
/**
* Binds methods of an object to the object itself, overwriting the existing
* method.
*
* **Note:** This method doesn't set the "length" property of bound functions.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {Object} object The object to bind and assign the bound methods to.
* @param {...(string|string[])} methodNames The object method names to bind.
* @returns {Object} Returns `object`.
* @example
*
* var view = {
* 'label': 'docs',
* 'click': function() {
* console.log('clicked ' + this.label);
* }
* };
*
* _.bindAll(view, ['click']);
* jQuery(element).on('click', view.click);
* // => Logs 'clicked docs' when clicked.
*/
var bindAll = flatRest(function(object, methodNames) {
arrayEach(methodNames, function(key) {
key = toKey(key);
baseAssignValue(object, key, bind(object[key], object));
});
return object;
});
/**
* Creates a function that iterates over `pairs` and invokes the corresponding
* function of the first predicate to return truthy. The predicate-function
* pairs are invoked with the `this` binding and arguments of the created
* function.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {Array} pairs The predicate-function pairs.
* @returns {Function} Returns the new composite function.
* @example
*
* var func = _.cond([
* [_.matches({ 'a': 1 }), _.constant('matches A')],
* [_.conforms({ 'b': _.isNumber }), _.constant('matches B')],
* [_.stubTrue, _.constant('no match')]
* ]);
*
* func({ 'a': 1, 'b': 2 });
* // => 'matches A'
*
* func({ 'a': 0, 'b': 1 });
* // => 'matches B'
*
* func({ 'a': '1', 'b': '2' });
* // => 'no match'
*/
function cond(pairs) {
var length = pairs == null ? 0 : pairs.length,
toIteratee = getIteratee();
pairs = !length ? [] : arrayMap(pairs, function(pair) {
if (typeof pair[1] != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
return [toIteratee(pair[0]), pair[1]];
});
return baseRest(function(args) {
var index = -1;
while (++index < length) {
var pair = pairs[index];
if (apply(pair[0], this, args)) {
return apply(pair[1], this, args);
}
}
});
}
/**
* Creates a function that invokes the predicate properties of `source` with
* the corresponding property values of a given object, returning `true` if
* all predicates return truthy, else `false`.
*
* **Note:** The created function is equivalent to `_.conformsTo` with
* `source` partially applied.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {Object} source The object of property predicates to conform to.
* @returns {Function} Returns the new spec function.
* @example
*
* var objects = [
* { 'a': 2, 'b': 1 },
* { 'a': 1, 'b': 2 }
* ];
*
* _.filter(objects, _.conforms({ 'b': function(n) { return n > 1; } }));
* // => [{ 'a': 1, 'b': 2 }]
*/
function conforms(source) {
return baseConforms(baseClone(source, CLONE_DEEP_FLAG));
}
/**
* Creates a function that returns `value`.
*
* @static
* @memberOf _
* @since 2.4.0
* @category Util
* @param {*} value The value to return from the new function.
* @returns {Function} Returns the new constant function.
* @example
*
* var objects = _.times(2, _.constant({ 'a': 1 }));
*
* console.log(objects);
* // => [{ 'a': 1 }, { 'a': 1 }]
*
* console.log(objects[0] === objects[1]);
* // => true
*/
function constant(value) {
return function() {
return value;
};
}
/**
* Checks `value` to determine whether a default value should be returned in
* its place. The `defaultValue` is returned if `value` is `NaN`, `null`,
* or `undefined`.
*
* @static
* @memberOf _
* @since 4.14.0
* @category Util
* @param {*} value The value to check.
* @param {*} defaultValue The default value.
* @returns {*} Returns the resolved value.
* @example
*
* _.defaultTo(1, 10);
* // => 1
*
* _.defaultTo(undefined, 10);
* // => 10
*/
function defaultTo(value, defaultValue) {
return (value == null || value !== value) ? defaultValue : value;
}
/**
* Creates a function that returns the result of invoking the given functions
* with the `this` binding of the created function, where each successive
* invocation is supplied the return value of the previous.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Util
* @param {...(Function|Function[])} [funcs] The functions to invoke.
* @returns {Function} Returns the new composite function.
* @see _.flowRight
* @example
*
* function square(n) {
* return n * n;
* }
*
* var addSquare = _.flow([_.add, square]);
* addSquare(1, 2);
* // => 9
*/
var flow = createFlow();
/**
* This method is like `_.flow` except that it creates a function that
* invokes the given functions from right to left.
*
* @static
* @since 3.0.0
* @memberOf _
* @category Util
* @param {...(Function|Function[])} [funcs] The functions to invoke.
* @returns {Function} Returns the new composite function.
* @see _.flow
* @example
*
* function square(n) {
* return n * n;
* }
*
* var addSquare = _.flowRight([square, _.add]);
* addSquare(1, 2);
* // => 9
*/
var flowRight = createFlow(true);
/**
* This method returns the first argument it receives.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {*} value Any value.
* @returns {*} Returns `value`.
* @example
*
* var object = { 'a': 1 };
*
* console.log(_.identity(object) === object);
* // => true
*/
function identity(value) {
return value;
}
/**
* Creates a function that invokes `func` with the arguments of the created
* function. If `func` is a property name, the created function returns the
* property value for a given element. If `func` is an array or object, the
* created function returns `true` for elements that contain the equivalent
* source properties, otherwise it returns `false`.
*
* @static
* @since 4.0.0
* @memberOf _
* @category Util
* @param {*} [func=_.identity] The value to convert to a callback.
* @returns {Function} Returns the callback.
* @example
*
* var users = [
* { 'user': 'barney', 'age': 36, 'active': true },
* { 'user': 'fred', 'age': 40, 'active': false }
* ];
*
* // The `_.matches` iteratee shorthand.
* _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
* // => [{ 'user': 'barney', 'age': 36, 'active': true }]
*
* // The `_.matchesProperty` iteratee shorthand.
* _.filter(users, _.iteratee(['user', 'fred']));
* // => [{ 'user': 'fred', 'age': 40 }]
*
* // The `_.property` iteratee shorthand.
* _.map(users, _.iteratee('user'));
* // => ['barney', 'fred']
*
* // Create custom iteratee shorthands.
* _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
* return !_.isRegExp(func) ? iteratee(func) : function(string) {
* return func.test(string);
* };
* });
*
* _.filter(['abc', 'def'], /ef/);
* // => ['def']
*/
function iteratee(func) {
return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG));
}
/**
* Creates a function that performs a partial deep comparison between a given
* object and `source`, returning `true` if the given object has equivalent
* property values, else `false`.
*
* **Note:** The created function is equivalent to `_.isMatch` with `source`
* partially applied.
*
* Partial comparisons will match empty array and empty object `source`
* values against any array or object value, respectively. See `_.isEqual`
* for a list of supported value comparisons.
*
* **Note:** Multiple values can be checked by combining several matchers
* using `_.overSome`
*
* @static
* @memberOf _
* @since 3.0.0
* @category Util
* @param {Object} source The object of property values to match.
* @returns {Function} Returns the new spec function.
* @example
*
* var objects = [
* { 'a': 1, 'b': 2, 'c': 3 },
* { 'a': 4, 'b': 5, 'c': 6 }
* ];
*
* _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
* // => [{ 'a': 4, 'b': 5, 'c': 6 }]
*
* // Checking for several possible values
* _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
* // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
*/
function matches(source) {
return baseMatches(baseClone(source, CLONE_DEEP_FLAG));
}
/**
* Creates a function that performs a partial deep comparison between the
* value at `path` of a given object to `srcValue`, returning `true` if the
* object value is equivalent, else `false`.
*
* **Note:** Partial comparisons will match empty array and empty object
* `srcValue` values against any array or object value, respectively. See
* `_.isEqual` for a list of supported value comparisons.
*
* **Note:** Multiple values can be checked by combining several matchers
* using `_.overSome`
*
* @static
* @memberOf _
* @since 3.2.0
* @category Util
* @param {Array|string} path The path of the property to get.
* @param {*} srcValue The value to match.
* @returns {Function} Returns the new spec function.
* @example
*
* var objects = [
* { 'a': 1, 'b': 2, 'c': 3 },
* { 'a': 4, 'b': 5, 'c': 6 }
* ];
*
* _.find(objects, _.matchesProperty('a', 4));
* // => { 'a': 4, 'b': 5, 'c': 6 }
*
* // Checking for several possible values
* _.filter(objects, _.overSome([_.matchesProperty('a', 1), _.matchesProperty('a', 4)]));
* // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
*/
function matchesProperty(path, srcValue) {
return baseMatchesProperty(path, baseClone(srcValue, CLONE_DEEP_FLAG));
}
/**
* Creates a function that invokes the method at `path` of a given object.
* Any additional arguments are provided to the invoked method.
*
* @static
* @memberOf _
* @since 3.7.0
* @category Util
* @param {Array|string} path The path of the method to invoke.
* @param {...*} [args] The arguments to invoke the method with.
* @returns {Function} Returns the new invoker function.
* @example
*
* var objects = [
* { 'a': { 'b': _.constant(2) } },
* { 'a': { 'b': _.constant(1) } }
* ];
*
* _.map(objects, _.method('a.b'));
* // => [2, 1]
*
* _.map(objects, _.method(['a', 'b']));
* // => [2, 1]
*/
var method = baseRest(function(path, args) {
return function(object) {
return baseInvoke(object, path, args);
};
});
/**
* The opposite of `_.method`; this method creates a function that invokes
* the method at a given path of `object`. Any additional arguments are
* provided to the invoked method.
*
* @static
* @memberOf _
* @since 3.7.0
* @category Util
* @param {Object} object The object to query.
* @param {...*} [args] The arguments to invoke the method with.
* @returns {Function} Returns the new invoker function.
* @example
*
* var array = _.times(3, _.constant),
* object = { 'a': array, 'b': array, 'c': array };
*
* _.map(['a[2]', 'c[0]'], _.methodOf(object));
* // => [2, 0]
*
* _.map([['a', '2'], ['c', '0']], _.methodOf(object));
* // => [2, 0]
*/
var methodOf = baseRest(function(object, args) {
return function(path) {
return baseInvoke(object, path, args);
};
});
/**
* Adds all own enumerable string keyed function properties of a source
* object to the destination object. If `object` is a function, then methods
* are added to its prototype as well.
*
* **Note:** Use `_.runInContext` to create a pristine `lodash` function to
* avoid conflicts caused by modifying the original.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {Function|Object} [object=lodash] The destination object.
* @param {Object} source The object of functions to add.
* @param {Object} [options={}] The options object.
* @param {boolean} [options.chain=true] Specify whether mixins are chainable.
* @returns {Function|Object} Returns `object`.
* @example
*
* function vowels(string) {
* return _.filter(string, function(v) {
* return /[aeiou]/i.test(v);
* });
* }
*
* _.mixin({ 'vowels': vowels });
* _.vowels('fred');
* // => ['e']
*
* _('fred').vowels().value();
* // => ['e']
*
* _.mixin({ 'vowels': vowels }, { 'chain': false });
* _('fred').vowels();
* // => ['e']
*/
function mixin(object, source, options) {
var props = keys(source),
methodNames = baseFunctions(source, props);
if (options == null &&
!(isObject(source) && (methodNames.length || !props.length))) {
options = source;
source = object;
object = this;
methodNames = baseFunctions(source, keys(source));
}
var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
isFunc = isFunction(object);
arrayEach(methodNames, function(methodName) {
var func = source[methodName];
object[methodName] = func;
if (isFunc) {
object.prototype[methodName] = function() {
var chainAll = this.__chain__;
if (chain || chainAll) {
var result = object(this.__wrapped__),
actions = result.__actions__ = copyArray(this.__actions__);
actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
result.__chain__ = chainAll;
return result;
}
return func.apply(object, arrayPush([this.value()], arguments));
};
}
});
return object;
}
/**
* Reverts the `_` variable to its previous value and returns a reference to
* the `lodash` function.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @returns {Function} Returns the `lodash` function.
* @example
*
* var lodash = _.noConflict();
*/
function noConflict() {
if (root._ === this) {
root._ = oldDash;
}
return this;
}
/**
* This method returns `undefined`.
*
* @static
* @memberOf _
* @since 2.3.0
* @category Util
* @example
*
* _.times(2, _.noop);
* // => [undefined, undefined]
*/
function noop() {
// No operation performed.
}
/**
* Creates a function that gets the argument at index `n`. If `n` is negative,
* the nth argument from the end is returned.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {number} [n=0] The index of the argument to return.
* @returns {Function} Returns the new pass-thru function.
* @example
*
* var func = _.nthArg(1);
* func('a', 'b', 'c', 'd');
* // => 'b'
*
* var func = _.nthArg(-2);
* func('a', 'b', 'c', 'd');
* // => 'c'
*/
function nthArg(n) {
n = toInteger(n);
return baseRest(function(args) {
return baseNth(args, n);
});
}
/**
* Creates a function that invokes `iteratees` with the arguments it receives
* and returns their results.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {...(Function|Function[])} [iteratees=[_.identity]]
* The iteratees to invoke.
* @returns {Function} Returns the new function.
* @example
*
* var func = _.over([Math.max, Math.min]);
*
* func(1, 2, 3, 4);
* // => [4, 1]
*/
var over = createOver(arrayMap);
/**
* Creates a function that checks if **all** of the `predicates` return
* truthy when invoked with the arguments it receives.
*
* Following shorthands are possible for providing predicates.
* Pass an `Object` and it will be used as an parameter for `_.matches` to create the predicate.
* Pass an `Array` of parameters for `_.matchesProperty` and the predicate will be created using them.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {...(Function|Function[])} [predicates=[_.identity]]
* The predicates to check.
* @returns {Function} Returns the new function.
* @example
*
* var func = _.overEvery([Boolean, isFinite]);
*
* func('1');
* // => true
*
* func(null);
* // => false
*
* func(NaN);
* // => false
*/
var overEvery = createOver(arrayEvery);
/**
* Creates a function that checks if **any** of the `predicates` return
* truthy when invoked with the arguments it receives.
*
* Following shorthands are possible for providing predicates.
* Pass an `Object` and it will be used as an parameter for `_.matches` to create the predicate.
* Pass an `Array` of parameters for `_.matchesProperty` and the predicate will be created using them.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {...(Function|Function[])} [predicates=[_.identity]]
* The predicates to check.
* @returns {Function} Returns the new function.
* @example
*
* var func = _.overSome([Boolean, isFinite]);
*
* func('1');
* // => true
*
* func(null);
* // => true
*
* func(NaN);
* // => false
*
* var matchesFunc = _.overSome([{ 'a': 1 }, { 'a': 2 }])
* var matchesPropertyFunc = _.overSome([['a', 1], ['a', 2]])
*/
var overSome = createOver(arraySome);
/**
* Creates a function that returns the value at `path` of a given object.
*
* @static
* @memberOf _
* @since 2.4.0
* @category Util
* @param {Array|string} path The path of the property to get.
* @returns {Function} Returns the new accessor function.
* @example
*
* var objects = [
* { 'a': { 'b': 2 } },
* { 'a': { 'b': 1 } }
* ];
*
* _.map(objects, _.property('a.b'));
* // => [2, 1]
*
* _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b');
* // => [1, 2]
*/
function property(path) {
return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
}
/**
* The opposite of `_.property`; this method creates a function that returns
* the value at a given path of `object`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Util
* @param {Object} object The object to query.
* @returns {Function} Returns the new accessor function.
* @example
*
* var array = [0, 1, 2],
* object = { 'a': array, 'b': array, 'c': array };
*
* _.map(['a[2]', 'c[0]'], _.propertyOf(object));
* // => [2, 0]
*
* _.map([['a', '2'], ['c', '0']], _.propertyOf(object));
* // => [2, 0]
*/
function propertyOf(object) {
return function(path) {
return object == null ? undefined$1 : baseGet(object, path);
};
}
/**
* Creates an array of numbers (positive and/or negative) progressing from
* `start` up to, but not including, `end`. A step of `-1` is used if a negative
* `start` is specified without an `end` or `step`. If `end` is not specified,
* it's set to `start` with `start` then set to `0`.
*
* **Note:** JavaScript follows the IEEE-754 standard for resolving
* floating-point values which can produce unexpected results.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {number} [start=0] The start of the range.
* @param {number} end The end of the range.
* @param {number} [step=1] The value to increment or decrement by.
* @returns {Array} Returns the range of numbers.
* @see _.inRange, _.rangeRight
* @example
*
* _.range(4);
* // => [0, 1, 2, 3]
*
* _.range(-4);
* // => [0, -1, -2, -3]
*
* _.range(1, 5);
* // => [1, 2, 3, 4]
*
* _.range(0, 20, 5);
* // => [0, 5, 10, 15]
*
* _.range(0, -4, -1);
* // => [0, -1, -2, -3]
*
* _.range(1, 4, 0);
* // => [1, 1, 1]
*
* _.range(0);
* // => []
*/
var range = createRange();
/**
* This method is like `_.range` except that it populates values in
* descending order.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {number} [start=0] The start of the range.
* @param {number} end The end of the range.
* @param {number} [step=1] The value to increment or decrement by.
* @returns {Array} Returns the range of numbers.
* @see _.inRange, _.range
* @example
*
* _.rangeRight(4);
* // => [3, 2, 1, 0]
*
* _.rangeRight(-4);
* // => [-3, -2, -1, 0]
*
* _.rangeRight(1, 5);
* // => [4, 3, 2, 1]
*
* _.rangeRight(0, 20, 5);
* // => [15, 10, 5, 0]
*
* _.rangeRight(0, -4, -1);
* // => [-3, -2, -1, 0]
*
* _.rangeRight(1, 4, 0);
* // => [1, 1, 1]
*
* _.rangeRight(0);
* // => []
*/
var rangeRight = createRange(true);
/**
* This method returns a new empty array.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {Array} Returns the new empty array.
* @example
*
* var arrays = _.times(2, _.stubArray);
*
* console.log(arrays);
* // => [[], []]
*
* console.log(arrays[0] === arrays[1]);
* // => false
*/
function stubArray() {
return [];
}
/**
* This method returns `false`.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {boolean} Returns `false`.
* @example
*
* _.times(2, _.stubFalse);
* // => [false, false]
*/
function stubFalse() {
return false;
}
/**
* This method returns a new empty object.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {Object} Returns the new empty object.
* @example
*
* var objects = _.times(2, _.stubObject);
*
* console.log(objects);
* // => [{}, {}]
*
* console.log(objects[0] === objects[1]);
* // => false
*/
function stubObject() {
return {};
}
/**
* This method returns an empty string.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {string} Returns the empty string.
* @example
*
* _.times(2, _.stubString);
* // => ['', '']
*/
function stubString() {
return '';
}
/**
* This method returns `true`.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {boolean} Returns `true`.
* @example
*
* _.times(2, _.stubTrue);
* // => [true, true]
*/
function stubTrue() {
return true;
}
/**
* Invokes the iteratee `n` times, returning an array of the results of
* each invocation. The iteratee is invoked with one argument; (index).
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {number} n The number of times to invoke `iteratee`.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array} Returns the array of results.
* @example
*
* _.times(3, String);
* // => ['0', '1', '2']
*
* _.times(4, _.constant(0));
* // => [0, 0, 0, 0]
*/
function times(n, iteratee) {
n = toInteger(n);
if (n < 1 || n > MAX_SAFE_INTEGER) {
return [];
}
var index = MAX_ARRAY_LENGTH,
length = nativeMin(n, MAX_ARRAY_LENGTH);
iteratee = getIteratee(iteratee);
n -= MAX_ARRAY_LENGTH;
var result = baseTimes(length, iteratee);
while (++index < n) {
iteratee(index);
}
return result;
}
/**
* Converts `value` to a property path array.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Util
* @param {*} value The value to convert.
* @returns {Array} Returns the new property path array.
* @example
*
* _.toPath('a.b.c');
* // => ['a', 'b', 'c']
*
* _.toPath('a[0].b.c');
* // => ['a', '0', 'b', 'c']
*/
function toPath(value) {
if (isArray(value)) {
return arrayMap(value, toKey);
}
return isSymbol(value) ? [value] : copyArray(stringToPath(toString(value)));
}
/**
* Generates a unique ID. If `prefix` is given, the ID is appended to it.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {string} [prefix=''] The value to prefix the ID with.
* @returns {string} Returns the unique ID.
* @example
*
* _.uniqueId('contact_');
* // => 'contact_104'
*
* _.uniqueId();
* // => '105'
*/
function uniqueId(prefix) {
var id = ++idCounter;
return toString(prefix) + id;
}
/*------------------------------------------------------------------------*/
/**
* Adds two numbers.
*
* @static
* @memberOf _
* @since 3.4.0
* @category Math
* @param {number} augend The first number in an addition.
* @param {number} addend The second number in an addition.
* @returns {number} Returns the total.
* @example
*
* _.add(6, 4);
* // => 10
*/
var add = createMathOperation(function(augend, addend) {
return augend + addend;
}, 0);
/**
* Computes `number` rounded up to `precision`.
*
* @static
* @memberOf _
* @since 3.10.0
* @category Math
* @param {number} number The number to round up.
* @param {number} [precision=0] The precision to round up to.
* @returns {number} Returns the rounded up number.
* @example
*
* _.ceil(4.006);
* // => 5
*
* _.ceil(6.004, 2);
* // => 6.01
*
* _.ceil(6040, -2);
* // => 6100
*/
var ceil = createRound('ceil');
/**
* Divide two numbers.
*
* @static
* @memberOf _
* @since 4.7.0
* @category Math
* @param {number} dividend The first number in a division.
* @param {number} divisor The second number in a division.
* @returns {number} Returns the quotient.
* @example
*
* _.divide(6, 4);
* // => 1.5
*/
var divide = createMathOperation(function(dividend, divisor) {
return dividend / divisor;
}, 1);
/**
* Computes `number` rounded down to `precision`.
*
* @static
* @memberOf _
* @since 3.10.0
* @category Math
* @param {number} number The number to round down.
* @param {number} [precision=0] The precision to round down to.
* @returns {number} Returns the rounded down number.
* @example
*
* _.floor(4.006);
* // => 4
*
* _.floor(0.046, 2);
* // => 0.04
*
* _.floor(4060, -2);
* // => 4000
*/
var floor = createRound('floor');
/**
* Computes the maximum value of `array`. If `array` is empty or falsey,
* `undefined` is returned.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Math
* @param {Array} array The array to iterate over.
* @returns {*} Returns the maximum value.
* @example
*
* _.max([4, 2, 8, 6]);
* // => 8
*
* _.max([]);
* // => undefined
*/
function max(array) {
return (array && array.length)
? baseExtremum(array, identity, baseGt)
: undefined$1;
}
/**
* This method is like `_.max` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the criterion by which
* the value is ranked. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Math
* @param {Array} array The array to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {*} Returns the maximum value.
* @example
*
* var objects = [{ 'n': 1 }, { 'n': 2 }];
*
* _.maxBy(objects, function(o) { return o.n; });
* // => { 'n': 2 }
*
* // The `_.property` iteratee shorthand.
* _.maxBy(objects, 'n');
* // => { 'n': 2 }
*/
function maxBy(array, iteratee) {
return (array && array.length)
? baseExtremum(array, getIteratee(iteratee, 2), baseGt)
: undefined$1;
}
/**
* Computes the mean of the values in `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Math
* @param {Array} array The array to iterate over.
* @returns {number} Returns the mean.
* @example
*
* _.mean([4, 2, 8, 6]);
* // => 5
*/
function mean(array) {
return baseMean(array, identity);
}
/**
* This method is like `_.mean` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the value to be averaged.
* The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.7.0
* @category Math
* @param {Array} array The array to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {number} Returns the mean.
* @example
*
* var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];
*
* _.meanBy(objects, function(o) { return o.n; });
* // => 5
*
* // The `_.property` iteratee shorthand.
* _.meanBy(objects, 'n');
* // => 5
*/
function meanBy(array, iteratee) {
return baseMean(array, getIteratee(iteratee, 2));
}
/**
* Computes the minimum value of `array`. If `array` is empty or falsey,
* `undefined` is returned.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Math
* @param {Array} array The array to iterate over.
* @returns {*} Returns the minimum value.
* @example
*
* _.min([4, 2, 8, 6]);
* // => 2
*
* _.min([]);
* // => undefined
*/
function min(array) {
return (array && array.length)
? baseExtremum(array, identity, baseLt)
: undefined$1;
}
/**
* This method is like `_.min` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the criterion by which
* the value is ranked. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Math
* @param {Array} array The array to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {*} Returns the minimum value.
* @example
*
* var objects = [{ 'n': 1 }, { 'n': 2 }];
*
* _.minBy(objects, function(o) { return o.n; });
* // => { 'n': 1 }
*
* // The `_.property` iteratee shorthand.
* _.minBy(objects, 'n');
* // => { 'n': 1 }
*/
function minBy(array, iteratee) {
return (array && array.length)
? baseExtremum(array, getIteratee(iteratee, 2), baseLt)
: undefined$1;
}
/**
* Multiply two numbers.
*
* @static
* @memberOf _
* @since 4.7.0
* @category Math
* @param {number} multiplier The first number in a multiplication.
* @param {number} multiplicand The second number in a multiplication.
* @returns {number} Returns the product.
* @example
*
* _.multiply(6, 4);
* // => 24
*/
var multiply = createMathOperation(function(multiplier, multiplicand) {
return multiplier * multiplicand;
}, 1);
/**
* Computes `number` rounded to `precision`.
*
* @static
* @memberOf _
* @since 3.10.0
* @category Math
* @param {number} number The number to round.
* @param {number} [precision=0] The precision to round to.
* @returns {number} Returns the rounded number.
* @example
*
* _.round(4.006);
* // => 4
*
* _.round(4.006, 2);
* // => 4.01
*
* _.round(4060, -2);
* // => 4100
*/
var round = createRound('round');
/**
* Subtract two numbers.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Math
* @param {number} minuend The first number in a subtraction.
* @param {number} subtrahend The second number in a subtraction.
* @returns {number} Returns the difference.
* @example
*
* _.subtract(6, 4);
* // => 2
*/
var subtract = createMathOperation(function(minuend, subtrahend) {
return minuend - subtrahend;
}, 0);
/**
* Computes the sum of the values in `array`.
*
* @static
* @memberOf _
* @since 3.4.0
* @category Math
* @param {Array} array The array to iterate over.
* @returns {number} Returns the sum.
* @example
*
* _.sum([4, 2, 8, 6]);
* // => 20
*/
function sum(array) {
return (array && array.length)
? baseSum(array, identity)
: 0;
}
/**
* This method is like `_.sum` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the value to be summed.
* The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Math
* @param {Array} array The array to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {number} Returns the sum.
* @example
*
* var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];
*
* _.sumBy(objects, function(o) { return o.n; });
* // => 20
*
* // The `_.property` iteratee shorthand.
* _.sumBy(objects, 'n');
* // => 20
*/
function sumBy(array, iteratee) {
return (array && array.length)
? baseSum(array, getIteratee(iteratee, 2))
: 0;
}
/*------------------------------------------------------------------------*/
// Add methods that return wrapped values in chain sequences.
lodash.after = after;
lodash.ary = ary;
lodash.assign = assign;
lodash.assignIn = assignIn;
lodash.assignInWith = assignInWith;
lodash.assignWith = assignWith;
lodash.at = at;
lodash.before = before;
lodash.bind = bind;
lodash.bindAll = bindAll;
lodash.bindKey = bindKey;
lodash.castArray = castArray;
lodash.chain = chain;
lodash.chunk = chunk;
lodash.compact = compact;
lodash.concat = concat;
lodash.cond = cond;
lodash.conforms = conforms;
lodash.constant = constant;
lodash.countBy = countBy;
lodash.create = create;
lodash.curry = curry;
lodash.curryRight = curryRight;
lodash.debounce = debounce;
lodash.defaults = defaults;
lodash.defaultsDeep = defaultsDeep;
lodash.defer = defer;
lodash.delay = delay;
lodash.difference = difference;
lodash.differenceBy = differenceBy;
lodash.differenceWith = differenceWith;
lodash.drop = drop;
lodash.dropRight = dropRight;
lodash.dropRightWhile = dropRightWhile;
lodash.dropWhile = dropWhile;
lodash.fill = fill;
lodash.filter = filter;
lodash.flatMap = flatMap;
lodash.flatMapDeep = flatMapDeep;
lodash.flatMapDepth = flatMapDepth;
lodash.flatten = flatten;
lodash.flattenDeep = flattenDeep;
lodash.flattenDepth = flattenDepth;
lodash.flip = flip;
lodash.flow = flow;
lodash.flowRight = flowRight;
lodash.fromPairs = fromPairs;
lodash.functions = functions;
lodash.functionsIn = functionsIn;
lodash.groupBy = groupBy;
lodash.initial = initial;
lodash.intersection = intersection;
lodash.intersectionBy = intersectionBy;
lodash.intersectionWith = intersectionWith;
lodash.invert = invert;
lodash.invertBy = invertBy;
lodash.invokeMap = invokeMap;
lodash.iteratee = iteratee;
lodash.keyBy = keyBy;
lodash.keys = keys;
lodash.keysIn = keysIn;
lodash.map = map;
lodash.mapKeys = mapKeys;
lodash.mapValues = mapValues;
lodash.matches = matches;
lodash.matchesProperty = matchesProperty;
lodash.memoize = memoize;
lodash.merge = merge;
lodash.mergeWith = mergeWith;
lodash.method = method;
lodash.methodOf = methodOf;
lodash.mixin = mixin;
lodash.negate = negate;
lodash.nthArg = nthArg;
lodash.omit = omit;
lodash.omitBy = omitBy;
lodash.once = once;
lodash.orderBy = orderBy;
lodash.over = over;
lodash.overArgs = overArgs;
lodash.overEvery = overEvery;
lodash.overSome = overSome;
lodash.partial = partial;
lodash.partialRight = partialRight;
lodash.partition = partition;
lodash.pick = pick;
lodash.pickBy = pickBy;
lodash.property = property;
lodash.propertyOf = propertyOf;
lodash.pull = pull;
lodash.pullAll = pullAll;
lodash.pullAllBy = pullAllBy;
lodash.pullAllWith = pullAllWith;
lodash.pullAt = pullAt;
lodash.range = range;
lodash.rangeRight = rangeRight;
lodash.rearg = rearg;
lodash.reject = reject;
lodash.remove = remove;
lodash.rest = rest;
lodash.reverse = reverse;
lodash.sampleSize = sampleSize;
lodash.set = set;
lodash.setWith = setWith;
lodash.shuffle = shuffle;
lodash.slice = slice;
lodash.sortBy = sortBy;
lodash.sortedUniq = sortedUniq;
lodash.sortedUniqBy = sortedUniqBy;
lodash.split = split;
lodash.spread = spread;
lodash.tail = tail;
lodash.take = take;
lodash.takeRight = takeRight;
lodash.takeRightWhile = takeRightWhile;
lodash.takeWhile = takeWhile;
lodash.tap = tap;
lodash.throttle = throttle;
lodash.thru = thru;
lodash.toArray = toArray;
lodash.toPairs = toPairs;
lodash.toPairsIn = toPairsIn;
lodash.toPath = toPath;
lodash.toPlainObject = toPlainObject;
lodash.transform = transform;
lodash.unary = unary;
lodash.union = union;
lodash.unionBy = unionBy;
lodash.unionWith = unionWith;
lodash.uniq = uniq;
lodash.uniqBy = uniqBy;
lodash.uniqWith = uniqWith;
lodash.unset = unset;
lodash.unzip = unzip;
lodash.unzipWith = unzipWith;
lodash.update = update;
lodash.updateWith = updateWith;
lodash.values = values;
lodash.valuesIn = valuesIn;
lodash.without = without;
lodash.words = words;
lodash.wrap = wrap;
lodash.xor = xor;
lodash.xorBy = xorBy;
lodash.xorWith = xorWith;
lodash.zip = zip;
lodash.zipObject = zipObject;
lodash.zipObjectDeep = zipObjectDeep;
lodash.zipWith = zipWith;
// Add aliases.
lodash.entries = toPairs;
lodash.entriesIn = toPairsIn;
lodash.extend = assignIn;
lodash.extendWith = assignInWith;
// Add methods to `lodash.prototype`.
mixin(lodash, lodash);
/*------------------------------------------------------------------------*/
// Add methods that return unwrapped values in chain sequences.
lodash.add = add;
lodash.attempt = attempt;
lodash.camelCase = camelCase;
lodash.capitalize = capitalize;
lodash.ceil = ceil;
lodash.clamp = clamp;
lodash.clone = clone;
lodash.cloneDeep = cloneDeep;
lodash.cloneDeepWith = cloneDeepWith;
lodash.cloneWith = cloneWith;
lodash.conformsTo = conformsTo;
lodash.deburr = deburr;
lodash.defaultTo = defaultTo;
lodash.divide = divide;
lodash.endsWith = endsWith;
lodash.eq = eq;
lodash.escape = escape;
lodash.escapeRegExp = escapeRegExp;
lodash.every = every;
lodash.find = find;
lodash.findIndex = findIndex;
lodash.findKey = findKey;
lodash.findLast = findLast;
lodash.findLastIndex = findLastIndex;
lodash.findLastKey = findLastKey;
lodash.floor = floor;
lodash.forEach = forEach;
lodash.forEachRight = forEachRight;
lodash.forIn = forIn;
lodash.forInRight = forInRight;
lodash.forOwn = forOwn;
lodash.forOwnRight = forOwnRight;
lodash.get = get;
lodash.gt = gt;
lodash.gte = gte;
lodash.has = has;
lodash.hasIn = hasIn;
lodash.head = head;
lodash.identity = identity;
lodash.includes = includes;
lodash.indexOf = indexOf;
lodash.inRange = inRange;
lodash.invoke = invoke;
lodash.isArguments = isArguments;
lodash.isArray = isArray;
lodash.isArrayBuffer = isArrayBuffer;
lodash.isArrayLike = isArrayLike;
lodash.isArrayLikeObject = isArrayLikeObject;
lodash.isBoolean = isBoolean;
lodash.isBuffer = isBuffer;
lodash.isDate = isDate;
lodash.isElement = isElement;
lodash.isEmpty = isEmpty;
lodash.isEqual = isEqual;
lodash.isEqualWith = isEqualWith;
lodash.isError = isError;
lodash.isFinite = isFinite;
lodash.isFunction = isFunction;
lodash.isInteger = isInteger;
lodash.isLength = isLength;
lodash.isMap = isMap;
lodash.isMatch = isMatch;
lodash.isMatchWith = isMatchWith;
lodash.isNaN = isNaN;
lodash.isNative = isNative;
lodash.isNil = isNil;
lodash.isNull = isNull;
lodash.isNumber = isNumber;
lodash.isObject = isObject;
lodash.isObjectLike = isObjectLike;
lodash.isPlainObject = isPlainObject;
lodash.isRegExp = isRegExp;
lodash.isSafeInteger = isSafeInteger;
lodash.isSet = isSet;
lodash.isString = isString;
lodash.isSymbol = isSymbol;
lodash.isTypedArray = isTypedArray;
lodash.isUndefined = isUndefined;
lodash.isWeakMap = isWeakMap;
lodash.isWeakSet = isWeakSet;
lodash.join = join;
lodash.kebabCase = kebabCase;
lodash.last = last;
lodash.lastIndexOf = lastIndexOf;
lodash.lowerCase = lowerCase;
lodash.lowerFirst = lowerFirst;
lodash.lt = lt;
lodash.lte = lte;
lodash.max = max;
lodash.maxBy = maxBy;
lodash.mean = mean;
lodash.meanBy = meanBy;
lodash.min = min;
lodash.minBy = minBy;
lodash.stubArray = stubArray;
lodash.stubFalse = stubFalse;
lodash.stubObject = stubObject;
lodash.stubString = stubString;
lodash.stubTrue = stubTrue;
lodash.multiply = multiply;
lodash.nth = nth;
lodash.noConflict = noConflict;
lodash.noop = noop;
lodash.now = now;
lodash.pad = pad;
lodash.padEnd = padEnd;
lodash.padStart = padStart;
lodash.parseInt = parseInt;
lodash.random = random;
lodash.reduce = reduce;
lodash.reduceRight = reduceRight;
lodash.repeat = repeat;
lodash.replace = replace;
lodash.result = result;
lodash.round = round;
lodash.runInContext = runInContext;
lodash.sample = sample;
lodash.size = size;
lodash.snakeCase = snakeCase;
lodash.some = some;
lodash.sortedIndex = sortedIndex;
lodash.sortedIndexBy = sortedIndexBy;
lodash.sortedIndexOf = sortedIndexOf;
lodash.sortedLastIndex = sortedLastIndex;
lodash.sortedLastIndexBy = sortedLastIndexBy;
lodash.sortedLastIndexOf = sortedLastIndexOf;
lodash.startCase = startCase;
lodash.startsWith = startsWith;
lodash.subtract = subtract;
lodash.sum = sum;
lodash.sumBy = sumBy;
lodash.template = template;
lodash.times = times;
lodash.toFinite = toFinite;
lodash.toInteger = toInteger;
lodash.toLength = toLength;
lodash.toLower = toLower;
lodash.toNumber = toNumber;
lodash.toSafeInteger = toSafeInteger;
lodash.toString = toString;
lodash.toUpper = toUpper;
lodash.trim = trim;
lodash.trimEnd = trimEnd;
lodash.trimStart = trimStart;
lodash.truncate = truncate;
lodash.unescape = unescape;
lodash.uniqueId = uniqueId;
lodash.upperCase = upperCase;
lodash.upperFirst = upperFirst;
// Add aliases.
lodash.each = forEach;
lodash.eachRight = forEachRight;
lodash.first = head;
mixin(lodash, (function() {
var source = {};
baseForOwn(lodash, function(func, methodName) {
if (!hasOwnProperty.call(lodash.prototype, methodName)) {
source[methodName] = func;
}
});
return source;
}()), { 'chain': false });
/*------------------------------------------------------------------------*/
/**
* The semantic version number.
*
* @static
* @memberOf _
* @type {string}
*/
lodash.VERSION = VERSION;
// Assign default placeholders.
arrayEach(['bind', 'bindKey', 'curry', 'curryRight', 'partial', 'partialRight'], function(methodName) {
lodash[methodName].placeholder = lodash;
});
// Add `LazyWrapper` methods for `_.drop` and `_.take` variants.
arrayEach(['drop', 'take'], function(methodName, index) {
LazyWrapper.prototype[methodName] = function(n) {
n = n === undefined$1 ? 1 : nativeMax(toInteger(n), 0);
var result = (this.__filtered__ && !index)
? new LazyWrapper(this)
: this.clone();
if (result.__filtered__) {
result.__takeCount__ = nativeMin(n, result.__takeCount__);
} else {
result.__views__.push({
'size': nativeMin(n, MAX_ARRAY_LENGTH),
'type': methodName + (result.__dir__ < 0 ? 'Right' : '')
});
}
return result;
};
LazyWrapper.prototype[methodName + 'Right'] = function(n) {
return this.reverse()[methodName](n).reverse();
};
});
// Add `LazyWrapper` methods that accept an `iteratee` value.
arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) {
var type = index + 1,
isFilter = type == LAZY_FILTER_FLAG || type == LAZY_WHILE_FLAG;
LazyWrapper.prototype[methodName] = function(iteratee) {
var result = this.clone();
result.__iteratees__.push({
'iteratee': getIteratee(iteratee, 3),
'type': type
});
result.__filtered__ = result.__filtered__ || isFilter;
return result;
};
});
// Add `LazyWrapper` methods for `_.head` and `_.last`.
arrayEach(['head', 'last'], function(methodName, index) {
var takeName = 'take' + (index ? 'Right' : '');
LazyWrapper.prototype[methodName] = function() {
return this[takeName](1).value()[0];
};
});
// Add `LazyWrapper` methods for `_.initial` and `_.tail`.
arrayEach(['initial', 'tail'], function(methodName, index) {
var dropName = 'drop' + (index ? '' : 'Right');
LazyWrapper.prototype[methodName] = function() {
return this.__filtered__ ? new LazyWrapper(this) : this[dropName](1);
};
});
LazyWrapper.prototype.compact = function() {
return this.filter(identity);
};
LazyWrapper.prototype.find = function(predicate) {
return this.filter(predicate).head();
};
LazyWrapper.prototype.findLast = function(predicate) {
return this.reverse().find(predicate);
};
LazyWrapper.prototype.invokeMap = baseRest(function(path, args) {
if (typeof path == 'function') {
return new LazyWrapper(this);
}
return this.map(function(value) {
return baseInvoke(value, path, args);
});
});
LazyWrapper.prototype.reject = function(predicate) {
return this.filter(negate(getIteratee(predicate)));
};
LazyWrapper.prototype.slice = function(start, end) {
start = toInteger(start);
var result = this;
if (result.__filtered__ && (start > 0 || end < 0)) {
return new LazyWrapper(result);
}
if (start < 0) {
result = result.takeRight(-start);
} else if (start) {
result = result.drop(start);
}
if (end !== undefined$1) {
end = toInteger(end);
result = end < 0 ? result.dropRight(-end) : result.take(end - start);
}
return result;
};
LazyWrapper.prototype.takeRightWhile = function(predicate) {
return this.reverse().takeWhile(predicate).reverse();
};
LazyWrapper.prototype.toArray = function() {
return this.take(MAX_ARRAY_LENGTH);
};
// Add `LazyWrapper` methods to `lodash.prototype`.
baseForOwn(LazyWrapper.prototype, function(func, methodName) {
var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
isTaker = /^(?:head|last)$/.test(methodName),
lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
retUnwrapped = isTaker || /^find/.test(methodName);
if (!lodashFunc) {
return;
}
lodash.prototype[methodName] = function() {
var value = this.__wrapped__,
args = isTaker ? [1] : arguments,
isLazy = value instanceof LazyWrapper,
iteratee = args[0],
useLazy = isLazy || isArray(value);
var interceptor = function(value) {
var result = lodashFunc.apply(lodash, arrayPush([value], args));
return (isTaker && chainAll) ? result[0] : result;
};
if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
// Avoid lazy use if the iteratee has a "length" value other than `1`.
isLazy = useLazy = false;
}
var chainAll = this.__chain__,
isHybrid = !!this.__actions__.length,
isUnwrapped = retUnwrapped && !chainAll,
onlyLazy = isLazy && !isHybrid;
if (!retUnwrapped && useLazy) {
value = onlyLazy ? value : new LazyWrapper(this);
var result = func.apply(value, args);
result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined$1 });
return new LodashWrapper(result, chainAll);
}
if (isUnwrapped && onlyLazy) {
return func.apply(this, args);
}
result = this.thru(interceptor);
return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
};
});
// Add `Array` methods to `lodash.prototype`.
arrayEach(['pop', 'push', 'shift', 'sort', 'splice', 'unshift'], function(methodName) {
var func = arrayProto[methodName],
chainName = /^(?:push|sort|unshift)$/.test(methodName) ? 'tap' : 'thru',
retUnwrapped = /^(?:pop|shift)$/.test(methodName);
lodash.prototype[methodName] = function() {
var args = arguments;
if (retUnwrapped && !this.__chain__) {
var value = this.value();
return func.apply(isArray(value) ? value : [], args);
}
return this[chainName](function(value) {
return func.apply(isArray(value) ? value : [], args);
});
};
});
// Map minified method names to their real names.
baseForOwn(LazyWrapper.prototype, function(func, methodName) {
var lodashFunc = lodash[methodName];
if (lodashFunc) {
var key = lodashFunc.name + '';
if (!hasOwnProperty.call(realNames, key)) {
realNames[key] = [];
}
realNames[key].push({ 'name': methodName, 'func': lodashFunc });
}
});
realNames[createHybrid(undefined$1, WRAP_BIND_KEY_FLAG).name] = [{
'name': 'wrapper',
'func': undefined$1
}];
// Add methods to `LazyWrapper`.
LazyWrapper.prototype.clone = lazyClone;
LazyWrapper.prototype.reverse = lazyReverse;
LazyWrapper.prototype.value = lazyValue;
// Add chain sequence methods to the `lodash` wrapper.
lodash.prototype.at = wrapperAt;
lodash.prototype.chain = wrapperChain;
lodash.prototype.commit = wrapperCommit;
lodash.prototype.next = wrapperNext;
lodash.prototype.plant = wrapperPlant;
lodash.prototype.reverse = wrapperReverse;
lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;
// Add lazy aliases.
lodash.prototype.first = lodash.prototype.head;
if (symIterator) {
lodash.prototype[symIterator] = wrapperToIterator;
}
return lodash;
});
/*--------------------------------------------------------------------------*/
// Export lodash.
var _ = runInContext();
// Some AMD build optimizers, like r.js, check for condition patterns like:
if (freeModule) {
// Export for Node.js.
(freeModule.exports = _)._ = _;
// Export for CommonJS support.
freeExports._ = _;
}
else {
// Export to the global object.
root._ = _;
}
}.call(lodash));
} (lodash$1, lodash$1.exports));
return lodash$1.exports;
}
var lodashExports = requireLodash();
const clusterConfig = {
/** Duration in seconds that the leader lease is valid before it expires */
LEADER_LEASE_DURATION: math(process.env.LEADER_LEASE_DURATION, 25),
/** Interval in seconds at which the leader renews its lease */
LEADER_RENEW_INTERVAL: math(process.env.LEADER_RENEW_INTERVAL, 10),
/** Maximum number of retry attempts when renewing the lease fails */
LEADER_RENEW_ATTEMPTS: math(process.env.LEADER_RENEW_ATTEMPTS, 3),
/** Delay in seconds between retry attempts when renewing the lease */
LEADER_RENEW_RETRY_DELAY: math(process.env.LEADER_RENEW_RETRY_DELAY, 0.5),
};
/**
* Distributed leader election implementation using Redis for coordination across multiple server instances.
*
* Leadership election:
* - During bootup, every server attempts to become the leader by calling isLeader()
* - Uses atomic Redis SET NX (set if not exists) to ensure only ONE server can claim leadership
* - The first server to successfully set the key becomes the leader; others become followers
* - Works with any number of servers (1 to infinite) - single server always becomes leader
*
* Leadership maintenance:
* - Leader holds a key in Redis with a 25-second lease duration
* - Leader renews this lease every 10 seconds to maintain leadership
* - If leader crashes, the lease eventually expires, and the key disappears
* - On shutdown, leader deletes its key to allow immediate re-election
* - Followers check for leadership and attempt to claim it when the key is empty
*/
class LeaderElection {
// DO NOT create new instances of this class directly.
// Use the exported isLeader() function which uses a singleton instance.
constructor() {
this.UUID = crypto.randomUUID();
this.refreshTimer = undefined;
if (LeaderElection._instance)
return LeaderElection._instance;
process.on('SIGTERM', () => this.resign());
process.on('SIGINT', () => this.resign());
LeaderElection._instance = this;
}
/**
* Checks if this instance is the current leader.
* If no leader exists, waits upto 2 seconds (randomized to avoid thundering herd) then attempts self-election.
* Always returns true in non-Redis mode (single-instance deployment).
*/
isLeader() {
return __awaiter(this, void 0, void 0, function* () {
if (!cacheConfig.USE_REDIS)
return true;
try {
const currentLeader = yield LeaderElection.getLeaderUUID();
// If we own the leadership lock, return true.
// However, in case the leadership refresh retries have been exhausted, something has gone wrong.
// This server is not considered the leader anymore, similar to a crash, to avoid split-brain scenario.
if (currentLeader === this.UUID)
return this.refreshTimer != null;
if (currentLeader != null)
return false; // someone holds leadership lock
const delay = Math.random() * 2000;
yield new Promise((resolve) => setTimeout(resolve, delay));
return yield this.electSelf();
}
catch (error) {
dataSchemas.logger.error('Failed to check leadership status:', error);
return false;
}
});
}
/**
* Steps down from leadership by stopping the refresh timer and releasing the leader key.
* Atomically deletes the leader key (only if we still own it) so another server can become leader immediately.
*/
resign() {
return __awaiter(this, void 0, void 0, function* () {
if (!cacheConfig.USE_REDIS)
return;
try {
this.clearRefreshTimer();
// Lua script for atomic check-and-delete (only delete if we still own it)
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
redis.call("del", KEYS[1])
end
`;
yield exports.keyvRedisClient.eval(script, {
keys: [LeaderElection.LEADER_KEY],
arguments: [this.UUID],
});
}
catch (error) {
dataSchemas.logger.error('Failed to release leadership lock:', error);
}
});
}
/**
* Gets the UUID of the current leader from Redis.
* Returns null if no leader exists or in non-Redis mode.
* Useful for testing and observability.
*/
static getLeaderUUID() {
return __awaiter(this, void 0, void 0, function* () {
if (!cacheConfig.USE_REDIS)
return null;
return yield exports.keyvRedisClient.get(LeaderElection.LEADER_KEY);
});
}
/**
* Clears the refresh timer to stop leadership maintenance.
* Called when resigning or failing to refresh leadership.
* Calling this directly to simulate a crash in testing.
*/
clearRefreshTimer() {
clearInterval(this.refreshTimer);
this.refreshTimer = undefined;
}
/**
* Attempts to claim leadership using atomic Redis SET NX (set if not exists).
* If successful, starts a refresh timer to maintain leadership by extending the lease duration.
* The NX flag ensures only one server can become leader even if multiple attempt simultaneously.
*/
electSelf() {
return __awaiter(this, void 0, void 0, function* () {
try {
const result = yield exports.keyvRedisClient.set(LeaderElection.LEADER_KEY, this.UUID, {
NX: true,
EX: clusterConfig.LEADER_LEASE_DURATION,
});
if (result !== 'OK')
return false;
this.clearRefreshTimer();
this.refreshTimer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
yield this.renewLeadership();
}), clusterConfig.LEADER_RENEW_INTERVAL * 1000);
this.refreshTimer.unref();
return true;
}
catch (error) {
dataSchemas.logger.error('Leader election failed:', error);
return false;
}
});
}
/**
* Renews leadership by extending the lease duration on the leader key.
* Uses Lua script to atomically verify we still own the key before renewing (prevents race conditions).
* If we've lost leadership (key was taken by another server), stops the refresh timer.
* This is called every 10 seconds by the refresh timer.
*/
renewLeadership() {
return __awaiter(this, arguments, void 0, function* (attempts = 1) {
try {
// Lua script for atomic check-and-renew
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
`;
const result = yield exports.keyvRedisClient.eval(script, {
keys: [LeaderElection.LEADER_KEY],
arguments: [this.UUID, clusterConfig.LEADER_LEASE_DURATION.toString()],
});
if (result === 0) {
dataSchemas.logger.warn('Lost leadership, clearing refresh timer');
this.clearRefreshTimer();
}
}
catch (error) {
dataSchemas.logger.error(`Failed to renew leadership (attempts No.${attempts}):`, error);
if (attempts <= clusterConfig.LEADER_RENEW_ATTEMPTS) {
yield new Promise((resolve) => setTimeout(resolve, clusterConfig.LEADER_RENEW_RETRY_DELAY * 1000));
yield this.renewLeadership(attempts + 1);
}
else {
dataSchemas.logger.error('Exceeded maximum attempts to renew leadership.');
this.clearRefreshTimer();
}
}
});
}
}
// We can't use Keyv namespace here because we need direct Redis access for atomic operations
LeaderElection.LEADER_KEY = `${cacheConfig.REDIS_KEY_PREFIX}${cacheConfig.GLOBAL_PREFIX_SEPARATOR}LeadingServerUUID`;
LeaderElection._instance = new LeaderElection();
const defaultElection = new LeaderElection();
const isLeader = () => defaultElection.isLeader();
/**
* Base class for MCP registry caches that require distributed leader coordination.
* Provides helper methods for leader-only operations and success validation.
* All concrete implementations must provide their own Keyv cache instance.
*/
class BaseRegistryCache {
constructor(leaderOnly) {
this.PREFIX = 'MCP::ServersRegistry';
this.leaderOnly = leaderOnly !== null && leaderOnly !== void 0 ? leaderOnly : false;
}
leaderCheck(action) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield isLeader()))
throw new Error(`Only leader can ${action}.`);
});
}
successCheck(action, success) {
if (!success)
throw new Error(`Failed to ${action} in cache.`);
return true;
}
reset() {
return __awaiter(this, void 0, void 0, function* () {
if (this.leaderOnly) {
yield this.leaderCheck(`reset ${this.cache.namespace} cache`);
}
yield this.cache.clear();
});
}
}
/**
* Redis-backed implementation of MCP server configurations cache for distributed deployments.
* Stores server configs in Redis with namespace isolation.
* Enables data sharing across multiple server instances in a cluster environment.
* Supports optional leader-only write operations to prevent race conditions during initialization.
* Data persists across server restarts and is accessible from any instance in the cluster.
*/
const BATCH_SIZE = 100;
class ServerConfigsCacheRedis extends BaseRegistryCache {
constructor(namespace, leaderOnly) {
super(leaderOnly);
this.namespace = namespace;
this.cache = standardCache(`${this.PREFIX}::Servers::${namespace}`);
}
add(serverName, config) {
return __awaiter(this, void 0, void 0, function* () {
if (this.leaderOnly)
yield this.leaderCheck(`add ${this.namespace} MCP servers`);
const exists = yield this.cache.has(serverName);
if (exists)
throw new Error(`Server "${serverName}" already exists in cache. Use update() to modify existing configs.`);
const storedConfig = Object.assign(Object.assign({}, config), { updatedAt: Date.now() });
const success = yield this.cache.set(serverName, storedConfig);
this.successCheck(`add ${this.namespace} server "${serverName}"`, success);
return { serverName, config: storedConfig };
});
}
update(serverName, config) {
return __awaiter(this, void 0, void 0, function* () {
if (this.leaderOnly)
yield this.leaderCheck(`update ${this.namespace} MCP servers`);
const exists = yield this.cache.has(serverName);
if (!exists)
throw new Error(`Server "${serverName}" does not exist in cache. Use add() to create new configs.`);
const success = yield this.cache.set(serverName, Object.assign(Object.assign({}, config), { updatedAt: Date.now() }));
this.successCheck(`update ${this.namespace} server "${serverName}"`, success);
});
}
remove(serverName) {
return __awaiter(this, void 0, void 0, function* () {
if (this.leaderOnly)
yield this.leaderCheck(`remove ${this.namespace} MCP servers`);
const success = yield this.cache.delete(serverName);
this.successCheck(`remove ${this.namespace} server "${serverName}"`, success);
});
}
get(serverName) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.get(serverName);
});
}
getAll() {
return __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
if (!exports.keyvRedisClient || !('scanIterator' in exports.keyvRedisClient)) {
throw new Error('Redis client with scanIterator not available.');
}
const startTime = Date.now();
const pattern = `*${this.cache.namespace}:*`;
const keys = [];
try {
for (var _d = true, _e = __asyncValues(exports.keyvRedisClient.scanIterator({ MATCH: pattern })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
_c = _f.value;
_d = false;
const key = _c;
keys.push(key);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
if (keys.length === 0) {
dataSchemas.logger.debug(`[ServerConfigsCacheRedis] getAll(${this.namespace}): no keys found`);
return {};
}
/** Extract keyName from full Redis key format: "prefix::namespace:keyName" */
const keyNames = keys.map((key) => key.substring(key.lastIndexOf(':') + 1));
const entries = [];
for (let i = 0; i < keyNames.length; i += BATCH_SIZE) {
const batchEnd = Math.min(i + BATCH_SIZE, keyNames.length);
const promises = [];
for (let j = i; j < batchEnd; j++) {
promises.push(this.cache.get(keyNames[j]));
}
const configs = yield Promise.all(promises);
for (let j = 0; j < configs.length; j++) {
if (configs[j]) {
entries.push([keyNames[i + j], configs[j]]);
}
}
}
const elapsed = Date.now() - startTime;
dataSchemas.logger.debug(`[ServerConfigsCacheRedis] getAll(${this.namespace}): fetched ${entries.length} configs in ${elapsed}ms`);
return lodashExports.fromPairs(entries);
});
}
}
/**
* Factory for creating the appropriate ServerConfigsCache implementation based on deployment mode.
* Automatically selects between in-memory and Redis-backed storage depending on USE_REDIS config.
* In single-instance mode (USE_REDIS=false), returns lightweight in-memory cache.
* In cluster mode (USE_REDIS=true), returns Redis-backed cache with distributed coordination.
* Provides a unified interface regardless of the underlying storage mechanism.
*/
class ServerConfigsCacheFactory {
/**
* Create a ServerConfigsCache instance.
* Returns Redis implementation if Redis is configured, otherwise in-memory implementation.
*
* @param namespace - The namespace for the cache (e.g., 'App') - only used for Redis namespacing
* @param leaderOnly - Whether operations should only be performed by the leader (only applies to Redis)
* @returns ServerConfigsCache instance
*/
static create(namespace, leaderOnly) {
if (cacheConfig.USE_REDIS) {
return new ServerConfigsCacheRedis(namespace, leaderOnly);
}
// In-memory mode uses a simple Map - doesn't need namespace
return new ServerConfigsCacheInMemory();
}
}
const mcpToolPattern$1 = new RegExp(`^.+${librechatDataProvider.Constants.mcp_delimiter}.+$`);
/**
* Normalizes a server name to match the pattern ^[a-zA-Z0-9_.-]+$
* This is required for Azure OpenAI models with Tool Calling
*/
function normalizeServerName(serverName) {
// Check if the server name already matches the pattern
if (/^[a-zA-Z0-9_.-]+$/.test(serverName)) {
return serverName;
}
/** Replace non-matching characters with underscores.
This preserves the general structure while ensuring compatibility.
Trims leading/trailing underscores
*/
const normalized = serverName.replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/^_+|_+$/g, '');
// If the result is empty (e.g., all characters were non-ASCII and got trimmed),
// generate a fallback name to ensure we always have a valid function name
if (!normalized) {
/** Hash of the original name to ensure uniqueness */
let hash = 0;
for (let i = 0; i < serverName.length; i++) {
hash = (hash << 5) - hash + serverName.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return `server_${Math.abs(hash)}`;
}
return normalized;
}
/**
* Sanitizes a URL by removing query parameters to prevent credential leakage in logs.
* @param url - The URL to sanitize (string or URL object)
* @returns The sanitized URL string without query parameters
*/
function sanitizeUrlForLogging(url) {
try {
const urlObj = typeof url === 'string' ? new URL(url) : url;
return `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
}
catch (_a) {
return '[invalid URL]';
}
}
/**
* Escapes special regex characters in a string so they are treated literally.
* @param str - The string to escape
* @returns The escaped string safe for use in a regex pattern
*/
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Generates a URL-friendly server name from a title.
* Converts to lowercase, replaces spaces with hyphens, removes special characters.
* @param title - The display title to convert
* @returns A slug suitable for use as serverName (e.g., "GitHub MCP Tool" → "github-mcp-tool")
*/
function generateServerNameFromTitle(title) {
const slug = title
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars except spaces and hyphens
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove consecutive hyphens
.replace(/^-|-$/g, ''); // Trim leading/trailing hyphens
return slug || 'mcp-server'; // Fallback if empty
}
class MCPOAuthHandler {
static getForcedTokenEndpointAuthMethod(tokenExchangeMethod) {
if (tokenExchangeMethod === librechatDataProvider.TokenExchangeMethodEnum.DefaultPost) {
return 'client_secret_post';
}
if (tokenExchangeMethod === librechatDataProvider.TokenExchangeMethodEnum.BasicAuthHeader) {
return 'client_secret_basic';
}
return undefined;
}
static resolveTokenEndpointAuthMethod(options) {
const forcedMethod = this.getForcedTokenEndpointAuthMethod(options.tokenExchangeMethod);
const preferredMethod = forcedMethod !== null && forcedMethod !== void 0 ? forcedMethod : options.preferredMethod;
if (preferredMethod === 'client_secret_basic' || preferredMethod === 'client_secret_post') {
return preferredMethod;
}
if (options.tokenAuthMethods.includes('client_secret_basic')) {
return 'client_secret_basic';
}
if (options.tokenAuthMethods.includes('client_secret_post')) {
return 'client_secret_post';
}
return undefined;
}
/**
* Creates a fetch function with custom headers injected
*/
static createOAuthFetch(headers, clientInfo) {
return (url, init) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const newHeaders = new Headers((_a = init === null || init === void 0 ? void 0 : init.headers) !== null && _a !== void 0 ? _a : {});
for (const [key, value] of Object.entries(headers)) {
newHeaders.set(key, value);
}
const method = ((_b = init === null || init === void 0 ? void 0 : init.method) !== null && _b !== void 0 ? _b : 'GET').toUpperCase();
const initBody = init === null || init === void 0 ? void 0 : init.body;
let params;
if (initBody instanceof URLSearchParams) {
params = initBody;
}
else if (typeof initBody === 'string') {
const parsed = new URLSearchParams(initBody);
if (parsed.has('grant_type')) {
params = parsed;
}
}
/**
* FastMCP 2.14+/MCP SDK 1.24+ token endpoints can be strict about:
* - Content-Type (must be application/x-www-form-urlencoded)
* - where client_id/client_secret are supplied (default_post vs basic header)
*/
if (method === 'POST' && (params === null || params === void 0 ? void 0 : params.has('grant_type'))) {
newHeaders.set('Content-Type', 'application/x-www-form-urlencoded');
if (clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.client_id) {
let authMethod = clientInfo.token_endpoint_auth_method;
if (!authMethod) {
if (newHeaders.has('Authorization')) {
authMethod = 'client_secret_basic';
}
else if (params.has('client_id') || params.has('client_secret')) {
authMethod = 'client_secret_post';
}
else if (clientInfo.client_secret) {
authMethod = 'client_secret_post';
}
else {
authMethod = 'none';
}
}
if (!clientInfo.client_secret || authMethod === 'none') {
newHeaders.delete('Authorization');
if (!params.has('client_id')) {
params.set('client_id', clientInfo.client_id);
}
}
else if (authMethod === 'client_secret_post') {
newHeaders.delete('Authorization');
if (!params.has('client_id')) {
params.set('client_id', clientInfo.client_id);
}
if (!params.has('client_secret')) {
params.set('client_secret', clientInfo.client_secret);
}
}
else if (authMethod === 'client_secret_basic') {
if (!newHeaders.has('Authorization')) {
const clientAuth = Buffer.from(`${clientInfo.client_id}:${clientInfo.client_secret}`).toString('base64');
newHeaders.set('Authorization', `Basic ${clientAuth}`);
}
}
}
return fetch(url, Object.assign(Object.assign({}, init), { body: params.toString(), headers: newHeaders }));
}
return fetch(url, Object.assign(Object.assign({}, init), { headers: newHeaders }));
});
}
/**
* Discovers OAuth metadata from the server
*/
static discoverMetadata(serverUrl, oauthHeaders) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
dataSchemas.logger.debug(`[MCPOAuth] discoverMetadata called with serverUrl: ${sanitizeUrlForLogging(serverUrl)}`);
let authServerUrl = new URL(serverUrl);
let resourceMetadata;
const fetchFn = this.createOAuthFetch(oauthHeaders);
try {
// Try to discover resource metadata first
dataSchemas.logger.debug(`[MCPOAuth] Attempting to discover protected resource metadata from ${serverUrl}`);
resourceMetadata = yield auth_js.discoverOAuthProtectedResourceMetadata(serverUrl, {}, fetchFn);
if ((_a = resourceMetadata === null || resourceMetadata === void 0 ? void 0 : resourceMetadata.authorization_servers) === null || _a === void 0 ? void 0 : _a.length) {
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
dataSchemas.logger.debug(`[MCPOAuth] Found authorization server from resource metadata: ${authServerUrl}`);
}
else {
dataSchemas.logger.debug(`[MCPOAuth] No authorization servers found in resource metadata`);
}
}
catch (error) {
dataSchemas.logger.debug('[MCPOAuth] Resource metadata discovery failed, continuing with server URL', {
error,
});
}
// Discover OAuth metadata
dataSchemas.logger.debug(`[MCPOAuth] Discovering OAuth metadata from ${sanitizeUrlForLogging(authServerUrl)}`);
let rawMetadata = yield auth_js.discoverAuthorizationServerMetadata(authServerUrl, {
fetchFn,
});
// If discovery failed and we're using a path-based URL, try the base URL
if (!rawMetadata && authServerUrl.pathname !== '/') {
const baseUrl = new URL(authServerUrl.origin);
dataSchemas.logger.debug(`[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`);
rawMetadata = yield auth_js.discoverAuthorizationServerMetadata(baseUrl, {
fetchFn,
});
}
if (!rawMetadata) {
/**
* No metadata discovered - create fallback metadata using default OAuth endpoint paths.
* This mirrors the MCP SDK's behavior where it falls back to /authorize, /token, /register
* when metadata discovery fails (e.g., servers without .well-known endpoints).
* See: https://github.com/modelcontextprotocol/sdk/blob/main/src/client/auth.ts
*/
dataSchemas.logger.warn(`[MCPOAuth] No OAuth metadata discovered from ${sanitizeUrlForLogging(authServerUrl)}, using legacy fallback endpoints`);
const fallbackMetadata = {
issuer: authServerUrl.toString(),
authorization_endpoint: new URL('/authorize', authServerUrl).toString(),
token_endpoint: new URL('/token', authServerUrl).toString(),
registration_endpoint: new URL('/register', authServerUrl).toString(),
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
code_challenge_methods_supported: ['S256', 'plain'],
token_endpoint_auth_methods_supported: [
'client_secret_basic',
'client_secret_post',
'none',
],
};
dataSchemas.logger.debug(`[MCPOAuth] Using fallback metadata:`, fallbackMetadata);
return {
metadata: fallbackMetadata,
resourceMetadata,
authServerUrl,
};
}
dataSchemas.logger.debug(`[MCPOAuth] OAuth metadata discovered successfully`);
const metadata = yield auth_js$1.OAuthMetadataSchema.parseAsync(rawMetadata);
dataSchemas.logger.debug(`[MCPOAuth] OAuth metadata parsed successfully`);
return {
metadata: metadata,
resourceMetadata,
authServerUrl,
};
});
}
/**
* Registers an OAuth client dynamically
*/
static registerOAuthClient(serverUrl, metadata, oauthHeaders, resourceMetadata, redirectUri, tokenExchangeMethod) {
return __awaiter(this, void 0, void 0, function* () {
dataSchemas.logger.debug(`[MCPOAuth] Starting client registration for ${sanitizeUrlForLogging(serverUrl)}, server metadata:`, {
grant_types_supported: metadata.grant_types_supported,
response_types_supported: metadata.response_types_supported,
token_endpoint_auth_methods_supported: metadata.token_endpoint_auth_methods_supported,
scopes_supported: metadata.scopes_supported,
});
/** Client metadata based on what the server supports */
const clientMetadata = {
client_name: 'Hanzo Chat MCP Client',
redirect_uris: [redirectUri || this.getDefaultRedirectUri()],
grant_types: ['authorization_code'],
response_types: ['code'],
token_endpoint_auth_method: 'client_secret_basic',
scope: undefined,
logo_uri: undefined,
tos_uri: undefined,
};
const supportedGrantTypes = metadata.grant_types_supported || ['authorization_code'];
const requestedGrantTypes = ['authorization_code'];
if (supportedGrantTypes.includes('refresh_token')) {
requestedGrantTypes.push('refresh_token');
dataSchemas.logger.debug(`[MCPOAuth] Server ${serverUrl} supports \`refresh_token\` grant type, adding to request`);
}
else {
dataSchemas.logger.debug(`[MCPOAuth] Server ${sanitizeUrlForLogging(serverUrl)} does not support \`refresh_token\` grant type`);
}
clientMetadata.grant_types = requestedGrantTypes;
clientMetadata.response_types = metadata.response_types_supported || ['code'];
const forcedAuthMethod = this.getForcedTokenEndpointAuthMethod(tokenExchangeMethod);
if (forcedAuthMethod) {
clientMetadata.token_endpoint_auth_method = forcedAuthMethod;
}
else if (metadata.token_endpoint_auth_methods_supported) {
// Prefer client_secret_basic if supported, otherwise use the first supported method
if (metadata.token_endpoint_auth_methods_supported.includes('client_secret_basic')) {
clientMetadata.token_endpoint_auth_method = 'client_secret_basic';
}
else if (metadata.token_endpoint_auth_methods_supported.includes('client_secret_post')) {
clientMetadata.token_endpoint_auth_method = 'client_secret_post';
}
else if (metadata.token_endpoint_auth_methods_supported.includes('none')) {
clientMetadata.token_endpoint_auth_method = 'none';
}
else {
clientMetadata.token_endpoint_auth_method =
metadata.token_endpoint_auth_methods_supported[0];
}
}
const availableScopes = (resourceMetadata === null || resourceMetadata === void 0 ? void 0 : resourceMetadata.scopes_supported) || metadata.scopes_supported;
if (availableScopes) {
clientMetadata.scope = availableScopes.join(' ');
}
dataSchemas.logger.debug(`[MCPOAuth] Registering client for ${sanitizeUrlForLogging(serverUrl)} with metadata:`, clientMetadata);
const clientInfo = yield auth_js.registerClient(serverUrl, {
metadata: metadata,
clientMetadata,
fetchFn: this.createOAuthFetch(oauthHeaders),
});
if (forcedAuthMethod) {
clientInfo.token_endpoint_auth_method = forcedAuthMethod;
}
else if (!clientInfo.token_endpoint_auth_method) {
clientInfo.token_endpoint_auth_method = clientMetadata.token_endpoint_auth_method;
}
dataSchemas.logger.debug(`[MCPOAuth] Client registered successfully for ${sanitizeUrlForLogging(serverUrl)}:`, {
client_id: clientInfo.client_id,
has_client_secret: !!clientInfo.client_secret,
grant_types: clientInfo.grant_types,
scope: clientInfo.scope,
});
return clientInfo;
});
}
/**
* Initiates the OAuth flow for an MCP server
*/
static initiateOAuthFlow(serverName, serverUrl, userId, oauthHeaders, config) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
dataSchemas.logger.debug(`[MCPOAuth] initiateOAuthFlow called for ${serverName} with URL: ${sanitizeUrlForLogging(serverUrl)}`);
const flowId = this.generateFlowId(userId, serverName);
const state = this.generateState();
dataSchemas.logger.debug(`[MCPOAuth] Generated flowId: ${flowId}, state: ${state}`);
try {
// Check if we have pre-configured OAuth settings
if ((config === null || config === void 0 ? void 0 : config.authorization_url) && (config === null || config === void 0 ? void 0 : config.token_url) && (config === null || config === void 0 ? void 0 : config.client_id)) {
dataSchemas.logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for ${serverName}`);
const skipCodeChallengeCheck = (config === null || config === void 0 ? void 0 : config.skip_code_challenge_check) === true ||
process.env.MCP_SKIP_CODE_CHALLENGE_CHECK === 'true';
let codeChallengeMethodsSupported;
if ((config === null || config === void 0 ? void 0 : config.code_challenge_methods_supported) !== undefined) {
codeChallengeMethodsSupported = config.code_challenge_methods_supported;
}
else if (skipCodeChallengeCheck) {
codeChallengeMethodsSupported = ['S256', 'plain'];
dataSchemas.logger.debug(`[MCPOAuth] Code challenge check skip enabled, forcing S256 support for ${serverName}`);
}
else {
codeChallengeMethodsSupported = ['S256', 'plain'];
}
/** Metadata based on pre-configured settings */
let tokenEndpointAuthMethod;
if (!config.client_secret) {
tokenEndpointAuthMethod = 'none';
}
else {
// When token_exchange_method is undefined or not DefaultPost, default to using
// client_secret_basic (Basic Auth header) for token endpoint authentication.
tokenEndpointAuthMethod =
(_a = this.getForcedTokenEndpointAuthMethod(config.token_exchange_method)) !== null && _a !== void 0 ? _a : 'client_secret_basic';
}
let defaultTokenAuthMethods;
if (tokenEndpointAuthMethod === 'none') {
defaultTokenAuthMethods = ['none'];
}
else if (tokenEndpointAuthMethod === 'client_secret_post') {
defaultTokenAuthMethods = ['client_secret_post', 'client_secret_basic'];
}
else {
defaultTokenAuthMethods = ['client_secret_basic', 'client_secret_post'];
}
const metadata = {
authorization_endpoint: config.authorization_url,
token_endpoint: config.token_url,
issuer: serverUrl,
scopes_supported: (_c = (_b = config.scope) === null || _b === void 0 ? void 0 : _b.split(' ')) !== null && _c !== void 0 ? _c : [],
grant_types_supported: (_d = config === null || config === void 0 ? void 0 : config.grant_types_supported) !== null && _d !== void 0 ? _d : [
'authorization_code',
'refresh_token',
],
token_endpoint_auth_methods_supported: (_e = config === null || config === void 0 ? void 0 : config.token_endpoint_auth_methods_supported) !== null && _e !== void 0 ? _e : defaultTokenAuthMethods,
response_types_supported: (_f = config === null || config === void 0 ? void 0 : config.response_types_supported) !== null && _f !== void 0 ? _f : ['code'],
code_challenge_methods_supported: codeChallengeMethodsSupported,
};
dataSchemas.logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`);
const clientInfo = {
client_id: config.client_id,
client_secret: config.client_secret,
redirect_uris: [config.redirect_uri || this.getDefaultRedirectUri(serverName)],
scope: config.scope,
token_endpoint_auth_method: tokenEndpointAuthMethod,
};
dataSchemas.logger.debug(`[MCPOAuth] Starting authorization with pre-configured settings`);
const { authorizationUrl, codeVerifier } = yield auth_js.startAuthorization(serverUrl, {
metadata: metadata,
clientInformation: clientInfo,
redirectUrl: ((_g = clientInfo.redirect_uris) === null || _g === void 0 ? void 0 : _g[0]) || this.getDefaultRedirectUri(serverName),
scope: config.scope,
});
/** Add state parameter with flowId to the authorization URL */
authorizationUrl.searchParams.set('state', flowId);
dataSchemas.logger.debug(`[MCPOAuth] Added state parameter to authorization URL`);
const flowMetadata = {
serverName,
userId,
serverUrl,
state,
codeVerifier,
clientInfo,
metadata,
};
dataSchemas.logger.debug(`[MCPOAuth] Authorization URL generated: ${sanitizeUrlForLogging(authorizationUrl.toString())}`);
return {
authorizationUrl: authorizationUrl.toString(),
flowId,
flowMetadata,
};
}
dataSchemas.logger.debug(`[MCPOAuth] Starting auto-discovery of OAuth metadata from ${sanitizeUrlForLogging(serverUrl)}`);
const { metadata, resourceMetadata, authServerUrl } = yield this.discoverMetadata(serverUrl, oauthHeaders);
dataSchemas.logger.debug(`[MCPOAuth] OAuth metadata discovered, auth server URL: ${sanitizeUrlForLogging(authServerUrl)}`);
/** Dynamic client registration based on the discovered metadata */
const redirectUri = (config === null || config === void 0 ? void 0 : config.redirect_uri) || this.getDefaultRedirectUri(serverName);
dataSchemas.logger.debug(`[MCPOAuth] Registering OAuth client with redirect URI: ${redirectUri}`);
const clientInfo = yield this.registerOAuthClient(authServerUrl.toString(), metadata, oauthHeaders, resourceMetadata, redirectUri, config === null || config === void 0 ? void 0 : config.token_exchange_method);
dataSchemas.logger.debug(`[MCPOAuth] Client registered with ID: ${clientInfo.client_id}`);
/** Authorization Scope */
const scope = (config === null || config === void 0 ? void 0 : config.scope) ||
((_h = resourceMetadata === null || resourceMetadata === void 0 ? void 0 : resourceMetadata.scopes_supported) === null || _h === void 0 ? void 0 : _h.join(' ')) ||
((_j = metadata.scopes_supported) === null || _j === void 0 ? void 0 : _j.join(' '));
dataSchemas.logger.debug(`[MCPOAuth] Starting authorization with scope: ${scope}`);
let authorizationUrl;
let codeVerifier;
try {
dataSchemas.logger.debug(`[MCPOAuth] Calling startAuthorization...`);
const authResult = yield auth_js.startAuthorization(serverUrl, {
metadata: metadata,
clientInformation: clientInfo,
redirectUrl: redirectUri,
scope,
});
authorizationUrl = authResult.authorizationUrl;
codeVerifier = authResult.codeVerifier;
dataSchemas.logger.debug(`[MCPOAuth] startAuthorization completed successfully`);
dataSchemas.logger.debug(`[MCPOAuth] Authorization URL: ${sanitizeUrlForLogging(authorizationUrl.toString())}`);
/** Add state parameter with flowId to the authorization URL */
authorizationUrl.searchParams.set('state', flowId);
dataSchemas.logger.debug(`[MCPOAuth] Added state parameter to authorization URL`);
if ((resourceMetadata === null || resourceMetadata === void 0 ? void 0 : resourceMetadata.resource) != null && resourceMetadata.resource) {
authorizationUrl.searchParams.set('resource', resourceMetadata.resource);
dataSchemas.logger.debug(`[MCPOAuth] Added resource parameter to authorization URL: ${resourceMetadata.resource}`);
}
else {
dataSchemas.logger.warn(`[MCPOAuth] Resource metadata missing 'resource' property for ${serverName}. ` +
'This can cause issues with some Authorization Servers who expect a "resource" parameter.');
}
}
catch (error) {
dataSchemas.logger.error(`[MCPOAuth] startAuthorization failed:`, error);
throw error;
}
const flowMetadata = {
serverName,
userId,
serverUrl,
state,
codeVerifier,
clientInfo,
metadata,
resourceMetadata,
};
dataSchemas.logger.debug(`[MCPOAuth] Authorization URL generated for ${serverName}: ${authorizationUrl.toString()}`);
const result = {
authorizationUrl: authorizationUrl.toString(),
flowId,
flowMetadata,
};
dataSchemas.logger.debug(`[MCPOAuth] Returning from initiateOAuthFlow with result ${flowId} for ${serverName}`, result);
return result;
}
catch (error) {
dataSchemas.logger.error('[MCPOAuth] Failed to initiate OAuth flow', { error, serverName, userId });
throw error;
}
});
}
/**
* Completes the OAuth flow by exchanging the authorization code for tokens
*/
static completeOAuthFlow(flowId, authorizationCode, flowManager, oauthHeaders) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
/** Flow state which contains our metadata */
const flowState = yield flowManager.getFlowState(flowId, this.FLOW_TYPE);
if (!flowState) {
throw new Error('OAuth flow not found');
}
const flowMetadata = flowState.metadata;
if (!flowMetadata) {
throw new Error('OAuth flow metadata not found');
}
const metadata = flowMetadata;
if (!metadata.metadata || !metadata.clientInfo || !metadata.codeVerifier) {
throw new Error('Invalid flow metadata');
}
let resource;
try {
if (((_a = metadata.resourceMetadata) === null || _a === void 0 ? void 0 : _a.resource) != null && metadata.resourceMetadata.resource) {
resource = new URL(metadata.resourceMetadata.resource);
dataSchemas.logger.debug(`[MCPOAuth] Resource URL for flow ${flowId}: ${resource.toString()}`);
}
}
catch (error) {
dataSchemas.logger.warn(`[MCPOAuth] Invalid resource URL format for flow ${flowId}: '${metadata.resourceMetadata.resource}'. ` +
`Error: ${error instanceof Error ? error.message : 'Unknown error'}. Proceeding without resource parameter.`);
resource = undefined;
}
const tokens = yield auth_js.exchangeAuthorization(metadata.serverUrl, {
redirectUri: ((_b = metadata.clientInfo.redirect_uris) === null || _b === void 0 ? void 0 : _b[0]) || this.getDefaultRedirectUri(),
metadata: metadata.metadata,
clientInformation: metadata.clientInfo,
codeVerifier: metadata.codeVerifier,
authorizationCode,
resource,
fetchFn: this.createOAuthFetch(oauthHeaders, metadata.clientInfo),
});
dataSchemas.logger.debug('[MCPOAuth] Token exchange successful', {
flowId,
has_access_token: !!tokens.access_token,
has_refresh_token: !!tokens.refresh_token,
expires_in: tokens.expires_in,
token_type: tokens.token_type,
scope: tokens.scope,
});
const mcpTokens = Object.assign(Object.assign({}, tokens), { obtained_at: Date.now(), expires_at: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined });
/** Now complete the flow with the tokens */
yield flowManager.completeFlow(flowId, this.FLOW_TYPE, mcpTokens);
return mcpTokens;
}
catch (error) {
dataSchemas.logger.error('[MCPOAuth] Failed to complete OAuth flow', { error, flowId });
yield flowManager.failFlow(flowId, this.FLOW_TYPE, error);
throw error;
}
});
}
/**
* Gets the OAuth flow metadata
*/
static getFlowState(flowId, flowManager) {
return __awaiter(this, void 0, void 0, function* () {
const flowState = yield flowManager.getFlowState(flowId, this.FLOW_TYPE);
if (!flowState) {
return null;
}
return flowState.metadata;
});
}
/**
* Generates a flow ID for the OAuth flow
* @returns Consistent ID so concurrent requests share the same flow
*/
static generateFlowId(userId, serverName) {
return `${userId}:${serverName}`;
}
/**
* Generates a secure state parameter
*/
static generateState() {
return crypto$2.randomBytes(32).toString('base64url');
}
/**
* Gets the default redirect URI for a server
*/
static getDefaultRedirectUri(serverName) {
const baseUrl = process.env.DOMAIN_SERVER || 'http://localhost:3080';
return serverName
? `${baseUrl}/v1/chat/mcp/${serverName}/oauth/callback`
: `${baseUrl}/v1/chat/mcp/oauth/callback`;
}
/**
* Processes and logs a token refresh response from an OAuth server.
* Normalizes the response to MCPOAuthTokens format and logs debug info about refresh token rotation.
*/
static processRefreshResponse(tokens, serverName, source) {
const hasNewRefreshToken = !!tokens.refresh_token;
dataSchemas.logger.debug(`[MCPOAuth] Token refresh response (${source})`, {
serverName,
has_new_access_token: !!tokens.access_token,
has_new_refresh_token: hasNewRefreshToken,
refresh_token_rotated: hasNewRefreshToken,
expires_in: tokens.expires_in,
});
if (!hasNewRefreshToken) {
dataSchemas.logger.debug(`[MCPOAuth] OAuth server did not return new refresh_token for ${serverName} - existing refresh token remains valid (normal for non-rotating providers)`);
}
return Object.assign(Object.assign({}, tokens), { obtained_at: Date.now(), expires_at: typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined });
}
/**
* Refreshes OAuth tokens using a refresh token
*/
static refreshOAuthTokens(refreshToken, metadata, oauthHeaders, config) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
dataSchemas.logger.debug(`[MCPOAuth] Refreshing tokens for ${metadata.serverName}`);
try {
/** If we have stored client information from the original flow, use that first */
if ((_a = metadata.clientInfo) === null || _a === void 0 ? void 0 : _a.client_id) {
dataSchemas.logger.debug(`[MCPOAuth] Using stored client information for token refresh for ${metadata.serverName}`);
dataSchemas.logger.debug(`[MCPOAuth] Client ID: ${metadata.clientInfo.client_id} for ${metadata.serverName}`);
dataSchemas.logger.debug(`[MCPOAuth] Has client secret: ${!!metadata.clientInfo.client_secret} for ${metadata.serverName}`);
dataSchemas.logger.debug(`[MCPOAuth] Stored client info for ${metadata.serverName}:`, {
client_id: metadata.clientInfo.client_id,
has_client_secret: !!metadata.clientInfo.client_secret,
grant_types: metadata.clientInfo.grant_types,
scope: metadata.clientInfo.scope,
});
/** Use the stored client information and metadata to determine the token URL */
let tokenUrl;
let authMethods;
if (config === null || config === void 0 ? void 0 : config.token_url) {
tokenUrl = config.token_url;
authMethods = config.token_endpoint_auth_methods_supported;
}
else if (!metadata.serverUrl) {
throw new Error('No token URL available for refresh');
}
else {
/** Auto-discover OAuth configuration for refresh */
const oauthMetadata = yield auth_js.discoverAuthorizationServerMetadata(metadata.serverUrl, {
fetchFn: this.createOAuthFetch(oauthHeaders),
});
if (!oauthMetadata) {
/**
* No metadata discovered - use fallback /token endpoint.
* This mirrors the MCP SDK's behavior for legacy servers without .well-known endpoints.
*/
dataSchemas.logger.warn(`[MCPOAuth] No OAuth metadata discovered for token refresh, using fallback /token endpoint`);
tokenUrl = new URL('/token', metadata.serverUrl).toString();
authMethods = ['client_secret_basic', 'client_secret_post', 'none'];
}
else if (!oauthMetadata.token_endpoint) {
throw new Error('No token endpoint found in OAuth metadata');
}
else {
tokenUrl = oauthMetadata.token_endpoint;
authMethods = oauthMetadata.token_endpoint_auth_methods_supported;
}
}
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
});
/** Add scope if available */
if (metadata.clientInfo.scope) {
body.append('scope', metadata.clientInfo.scope);
}
const headers = Object.assign({ Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, oauthHeaders);
/** Handle authentication based on server's advertised methods */
if (metadata.clientInfo.client_secret) {
/** Default to client_secret_basic if no methods specified (per RFC 8414) */
const tokenAuthMethods = authMethods !== null && authMethods !== void 0 ? authMethods : ['client_secret_basic'];
const authMethod = this.resolveTokenEndpointAuthMethod({
tokenExchangeMethod: config === null || config === void 0 ? void 0 : config.token_exchange_method,
tokenAuthMethods,
preferredMethod: metadata.clientInfo.token_endpoint_auth_method,
});
if (authMethod === 'client_secret_basic') {
/** Use Basic auth */
dataSchemas.logger.debug('[MCPOAuth] Using client_secret_basic authentication method');
const clientAuth = Buffer.from(`${metadata.clientInfo.client_id}:${metadata.clientInfo.client_secret}`).toString('base64');
headers['Authorization'] = `Basic ${clientAuth}`;
}
else if (authMethod === 'client_secret_post') {
/** Use client_secret_post */
dataSchemas.logger.debug('[MCPOAuth] Using client_secret_post authentication method');
body.append('client_id', metadata.clientInfo.client_id);
body.append('client_secret', metadata.clientInfo.client_secret);
}
else {
/** No recognized method, default to Basic auth per RFC */
dataSchemas.logger.debug('[MCPOAuth] No recognized auth method, defaulting to client_secret_basic');
const clientAuth = Buffer.from(`${metadata.clientInfo.client_id}:${metadata.clientInfo.client_secret}`).toString('base64');
headers['Authorization'] = `Basic ${clientAuth}`;
}
}
else {
/** For public clients, client_id must be in the body */
dataSchemas.logger.debug('[MCPOAuth] Using public client authentication (no secret)');
body.append('client_id', metadata.clientInfo.client_id);
}
dataSchemas.logger.debug(`[MCPOAuth] Refresh request to: ${sanitizeUrlForLogging(tokenUrl)}`, {
body: body.toString(),
headers,
});
const response = yield fetch(tokenUrl, {
method: 'POST',
headers,
body,
});
if (!response.ok) {
const errorText = yield response.text();
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const tokens = yield response.json();
return this.processRefreshResponse(tokens, metadata.serverName, 'stored client info');
}
// Fallback: If we have pre-configured OAuth settings, use them
if ((config === null || config === void 0 ? void 0 : config.token_url) && (config === null || config === void 0 ? void 0 : config.client_id)) {
dataSchemas.logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for token refresh`);
const tokenUrl = new URL(config.token_url);
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
});
if (config.scope) {
body.append('scope', config.scope);
}
const headers = Object.assign({ Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, oauthHeaders);
/** Handle authentication based on configured methods */
if (config.client_secret) {
/** Default to client_secret_basic if no methods specified (per RFC 8414) */
const tokenAuthMethods = (_b = config.token_endpoint_auth_methods_supported) !== null && _b !== void 0 ? _b : [
'client_secret_basic',
];
const authMethod = this.resolveTokenEndpointAuthMethod({
tokenExchangeMethod: config.token_exchange_method,
tokenAuthMethods,
});
if (authMethod === 'client_secret_basic') {
/** Use Basic auth */
dataSchemas.logger.debug('[MCPOAuth] Using client_secret_basic authentication method (pre-configured)');
const clientAuth = Buffer.from(`${config.client_id}:${config.client_secret}`).toString('base64');
headers['Authorization'] = `Basic ${clientAuth}`;
}
else if (authMethod === 'client_secret_post') {
/** Use client_secret_post */
dataSchemas.logger.debug('[MCPOAuth] Using client_secret_post authentication method (pre-configured)');
body.append('client_id', config.client_id);
body.append('client_secret', config.client_secret);
}
else {
/** No recognized method, default to Basic auth per RFC */
dataSchemas.logger.debug('[MCPOAuth] No recognized auth method, defaulting to client_secret_basic (pre-configured)');
const clientAuth = Buffer.from(`${config.client_id}:${config.client_secret}`).toString('base64');
headers['Authorization'] = `Basic ${clientAuth}`;
}
}
else {
/** For public clients, client_id must be in the body */
dataSchemas.logger.debug('[MCPOAuth] Using public client authentication (no secret, pre-configured)');
body.append('client_id', config.client_id);
}
const response = yield fetch(tokenUrl, {
method: 'POST',
headers,
body,
});
if (!response.ok) {
const errorText = yield response.text();
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const tokens = yield response.json();
return this.processRefreshResponse(tokens, metadata.serverName, 'pre-configured OAuth');
}
/** For auto-discovered OAuth, we need the server URL */
if (!metadata.serverUrl) {
throw new Error('Server URL required for auto-discovered OAuth token refresh');
}
/** Auto-discover OAuth configuration for refresh */
const oauthMetadata = yield auth_js.discoverAuthorizationServerMetadata(metadata.serverUrl, {
fetchFn: this.createOAuthFetch(oauthHeaders),
});
let tokenUrl;
if (!(oauthMetadata === null || oauthMetadata === void 0 ? void 0 : oauthMetadata.token_endpoint)) {
/**
* No metadata or token_endpoint discovered - use fallback /token endpoint.
* This mirrors the MCP SDK's behavior for legacy servers without .well-known endpoints.
*/
dataSchemas.logger.warn(`[MCPOAuth] No OAuth metadata or token endpoint found, using fallback /token endpoint`);
tokenUrl = new URL('/token', metadata.serverUrl);
}
else {
tokenUrl = new URL(oauthMetadata.token_endpoint);
}
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
});
const headers = Object.assign({ Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, oauthHeaders);
const response = yield fetch(tokenUrl, {
method: 'POST',
headers,
body,
});
if (!response.ok) {
const errorText = yield response.text();
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const tokens = yield response.json();
return this.processRefreshResponse(tokens, metadata.serverName, 'auto-discovered OAuth');
}
catch (error) {
dataSchemas.logger.error(`[MCPOAuth] Failed to refresh tokens for ${metadata.serverName}`, error);
throw error;
}
});
}
/**
* Revokes OAuth tokens at the authorization server (RFC 7009)
*/
static revokeOAuthToken(serverName_1, token_1, tokenType_1, metadata_1) {
return __awaiter(this, arguments, void 0, function* (serverName, token, tokenType, metadata, oauthHeaders = {}) {
var _a;
// build the revoke URL, falling back to the server URL + /revoke if no revocation endpoint is provided
const revokeUrl = metadata.revocationEndpoint != null
? new URL(metadata.revocationEndpoint)
: new URL('/revoke', metadata.serverUrl);
// detect auth method to use
const authMethods = (_a = metadata.revocationEndpointAuthMethodsSupported) !== null && _a !== void 0 ? _a : [
'client_secret_basic', // RFC 8414 (https://datatracker.ietf.org/doc/html/rfc8414)
];
const usesBasicAuth = authMethods.includes('client_secret_basic');
const usesClientSecretPost = authMethods.includes('client_secret_post');
// init the request headers
const headers = Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded' }, oauthHeaders);
// init the request body
const body = new URLSearchParams({ token });
body.set('token_type_hint', tokenType === 'refresh' ? 'refresh_token' : 'access_token');
// process auth method
if (usesBasicAuth) {
// encode the client id and secret and add to the headers
const credentials = Buffer.from(`${metadata.clientId}:${metadata.clientSecret}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
}
else if (usesClientSecretPost) {
// add the client id and secret to the body
body.set('client_secret', metadata.clientSecret);
body.set('client_id', metadata.clientId);
}
// perform the revoke request
dataSchemas.logger.info(`[MCPOAuth] Revoking tokens for ${serverName} via ${sanitizeUrlForLogging(revokeUrl.toString())}`);
const response = yield fetch(revokeUrl, {
method: 'POST',
body: body.toString(),
headers,
});
if (!response.ok) {
dataSchemas.logger.error(`[MCPOAuth] Token revocation failed for ${serverName}: HTTP ${response.status}`);
throw new Error(`Token revocation failed: HTTP ${response.status}`);
}
});
}
}
MCPOAuthHandler.FLOW_TYPE = 'mcp_oauth';
MCPOAuthHandler.FLOW_TTL = 10 * 60 * 1000; // 10 minutes
var CONSTANTS;
(function (CONSTANTS) {
/** System user ID for app-level OAuth tokens (all zeros ObjectId) */
CONSTANTS["SYSTEM_USER_ID"] = "000000000000000000000000";
})(CONSTANTS || (CONSTANTS = {}));
function isSystemUserId(userId) {
return userId === CONSTANTS.SYSTEM_USER_ID;
}
class MCPTokenStorage {
static getLogPrefix(userId, serverName) {
return isSystemUserId(userId)
? `[MCP][${serverName}]`
: `[MCP][User: ${userId}][${serverName}]`;
}
/**
* Stores OAuth tokens for an MCP server
*
* @param params.existingTokens - Optional: Pass existing token state to avoid duplicate DB calls.
* This is useful when refreshing tokens, as getTokens() already has the token state.
*/
static storeTokens(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, serverName, tokens, createToken, updateToken, findToken, clientInfo, existingTokens, metadata, }) {
const logPrefix = this.getLogPrefix(userId, serverName);
try {
const identifier = `mcp:${serverName}`;
// Encrypt and store access token
const encryptedAccessToken = yield dataSchemas.encryptV2(tokens.access_token);
dataSchemas.logger.debug(`${logPrefix} Token expires_in: ${'expires_in' in tokens ? tokens.expires_in : 'N/A'}, expires_at: ${'expires_at' in tokens ? tokens.expires_at : 'N/A'}`);
// Handle both expires_in and expires_at formats
let accessTokenExpiry;
if ('expires_at' in tokens && tokens.expires_at) {
/** MCPOAuthTokens format - already has calculated expiry */
dataSchemas.logger.debug(`${logPrefix} Using expires_at: ${tokens.expires_at}`);
accessTokenExpiry = new Date(tokens.expires_at);
}
else if (tokens.expires_in) {
/** Standard OAuthTokens format - calculate expiry */
dataSchemas.logger.debug(`${logPrefix} Using expires_in: ${tokens.expires_in}`);
accessTokenExpiry = new Date(Date.now() + tokens.expires_in * 1000);
}
else {
/** No expiry provided - default to 1 year */
dataSchemas.logger.debug(`${logPrefix} No expiry provided, using default`);
accessTokenExpiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000);
}
dataSchemas.logger.debug(`${logPrefix} Calculated expiry date: ${accessTokenExpiry.toISOString()}`);
dataSchemas.logger.debug(`${logPrefix} Date object: ${JSON.stringify({
time: accessTokenExpiry.getTime(),
valid: !isNaN(accessTokenExpiry.getTime()),
iso: accessTokenExpiry.toISOString(),
})}`);
// Ensure the date is valid before passing to createToken
if (isNaN(accessTokenExpiry.getTime())) {
dataSchemas.logger.error(`${logPrefix} Invalid expiry date calculated, using default`);
accessTokenExpiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000);
}
// Calculate expiresIn (seconds from now)
const expiresIn = Math.floor((accessTokenExpiry.getTime() - Date.now()) / 1000);
const accessTokenData = {
userId,
type: 'mcp_oauth',
identifier,
token: encryptedAccessToken,
expiresIn: expiresIn > 0 ? expiresIn : 365 * 24 * 60 * 60, // Default to 1 year if negative
};
// Check if token already exists and update if it does
if (findToken && updateToken) {
// Use provided existing token state if available, otherwise look it up
const existingToken = (existingTokens === null || existingTokens === void 0 ? void 0 : existingTokens.accessToken) !== undefined
? existingTokens.accessToken
: yield findToken({ userId, identifier });
if (existingToken) {
yield updateToken({ userId, identifier }, accessTokenData);
dataSchemas.logger.debug(`${logPrefix} Updated existing access token`);
}
else {
yield createToken(accessTokenData);
dataSchemas.logger.debug(`${logPrefix} Created new access token`);
}
}
else {
// Create new token if it's initial store or update methods not provided
yield createToken(accessTokenData);
dataSchemas.logger.debug(`${logPrefix} Created access token (no update methods available)`);
}
// Store refresh token if available
if (tokens.refresh_token) {
dataSchemas.logger.debug(`${logPrefix} New refresh token received from OAuth server, will store/update`);
const encryptedRefreshToken = yield dataSchemas.encryptV2(tokens.refresh_token);
const extendedTokens = tokens;
const refreshTokenExpiry = extendedTokens.refresh_token_expires_in
? new Date(Date.now() + extendedTokens.refresh_token_expires_in * 1000)
: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); // Default to 1 year
/** Calculated expiresIn for refresh token */
const refreshExpiresIn = Math.floor((refreshTokenExpiry.getTime() - Date.now()) / 1000);
const refreshTokenData = {
userId,
type: 'mcp_oauth_refresh',
identifier: `${identifier}:refresh`,
token: encryptedRefreshToken,
expiresIn: refreshExpiresIn > 0 ? refreshExpiresIn : 365 * 24 * 60 * 60,
};
// Check if refresh token already exists and update if it does
if (findToken && updateToken) {
// Use provided existing token state if available, otherwise look it up
const existingRefreshToken = (existingTokens === null || existingTokens === void 0 ? void 0 : existingTokens.refreshToken) !== undefined
? existingTokens.refreshToken
: yield findToken({
userId,
identifier: `${identifier}:refresh`,
});
if (existingRefreshToken) {
yield updateToken({ userId, identifier: `${identifier}:refresh` }, refreshTokenData);
dataSchemas.logger.debug(`${logPrefix} Updated existing refresh token`);
}
else {
yield createToken(refreshTokenData);
dataSchemas.logger.debug(`${logPrefix} Created new refresh token`);
}
}
else {
yield createToken(refreshTokenData);
dataSchemas.logger.debug(`${logPrefix} Created refresh token (no update methods available)`);
}
}
else {
dataSchemas.logger.debug(`${logPrefix} No refresh token in response - OAuth server did not rotate refresh token (this is normal for some providers)`);
}
/** Store client information if provided */
if (clientInfo) {
dataSchemas.logger.debug(`${logPrefix} Storing client info:`, {
client_id: clientInfo.client_id,
has_client_secret: !!clientInfo.client_secret,
});
const encryptedClientInfo = yield dataSchemas.encryptV2(JSON.stringify(clientInfo));
const clientInfoData = {
userId,
type: 'mcp_oauth_client',
identifier: `${identifier}:client`,
token: encryptedClientInfo,
expiresIn: 365 * 24 * 60 * 60,
metadata,
};
// Check if client info already exists and update if it does
if (findToken && updateToken) {
// Use provided existing token state if available, otherwise look it up
const existingClientInfo = (existingTokens === null || existingTokens === void 0 ? void 0 : existingTokens.clientInfoToken) !== undefined
? existingTokens.clientInfoToken
: yield findToken({
userId,
identifier: `${identifier}:client`,
});
if (existingClientInfo) {
yield updateToken({ userId, identifier: `${identifier}:client` }, clientInfoData);
dataSchemas.logger.debug(`${logPrefix} Updated existing client info`);
}
else {
yield createToken(clientInfoData);
dataSchemas.logger.debug(`${logPrefix} Created new client info`);
}
}
else {
yield createToken(clientInfoData);
dataSchemas.logger.debug(`${logPrefix} Created client info (no update methods available)`);
}
}
dataSchemas.logger.debug(`${logPrefix} Stored OAuth tokens`, {
client_id: clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.client_id,
has_refresh_token: !!tokens.refresh_token,
expires_at: 'expires_at' in tokens ? tokens.expires_at : 'N/A',
});
}
catch (error) {
const logPrefix = this.getLogPrefix(userId, serverName);
dataSchemas.logger.error(`${logPrefix} Failed to store tokens`, error);
throw error;
}
});
}
/**
* Retrieves OAuth tokens for an MCP server
*/
static getTokens(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, serverName, findToken, createToken, updateToken, refreshTokens, }) {
var _b;
const logPrefix = this.getLogPrefix(userId, serverName);
try {
const identifier = `mcp:${serverName}`;
// Get access token
const accessTokenData = yield findToken({
userId,
type: 'mcp_oauth',
identifier,
});
/** Check if access token is missing or expired */
const isMissing = !accessTokenData;
const isExpired = (accessTokenData === null || accessTokenData === void 0 ? void 0 : accessTokenData.expiresAt) && new Date() >= accessTokenData.expiresAt;
if (isMissing || isExpired) {
dataSchemas.logger.info(`${logPrefix} Access token ${isMissing ? 'missing' : 'expired'}`);
/** Refresh data if we have a refresh token and refresh function */
const refreshTokenData = yield findToken({
userId,
type: 'mcp_oauth_refresh',
identifier: `${identifier}:refresh`,
});
if (!refreshTokenData) {
dataSchemas.logger.info(`${logPrefix} Access token ${isMissing ? 'missing' : 'expired'} and no refresh token available`);
return null;
}
if (!refreshTokens) {
dataSchemas.logger.warn(`${logPrefix} Access token ${isMissing ? 'missing' : 'expired'}, refresh token available but no \`refreshTokens\` provided`);
return null;
}
if (!createToken) {
dataSchemas.logger.warn(`${logPrefix} Access token ${isMissing ? 'missing' : 'expired'}, refresh token available but no \`createToken\` function provided`);
return null;
}
try {
dataSchemas.logger.info(`${logPrefix} Attempting to refresh token`);
const decryptedRefreshToken = yield dataSchemas.decryptV2(refreshTokenData.token);
/** Client information if available */
let clientInfo;
let clientInfoData;
try {
clientInfoData = yield findToken({
userId,
type: 'mcp_oauth_client',
identifier: `${identifier}:client`,
});
if (clientInfoData) {
const decryptedClientInfo = yield dataSchemas.decryptV2(clientInfoData.token);
clientInfo = JSON.parse(decryptedClientInfo);
dataSchemas.logger.debug(`${logPrefix} Retrieved client info:`, {
client_id: clientInfo.client_id,
has_client_secret: !!clientInfo.client_secret,
});
}
}
catch (_c) {
dataSchemas.logger.debug(`${logPrefix} No client info found`);
}
const metadata = {
userId,
serverName,
identifier,
clientInfo,
};
const newTokens = yield refreshTokens(decryptedRefreshToken, metadata);
dataSchemas.logger.debug(`${logPrefix} Refresh completed`, {
has_new_access_token: !!newTokens.access_token,
has_new_refresh_token: !!newTokens.refresh_token,
refresh_token_will_be_rotated: !!newTokens.refresh_token,
expires_at: newTokens.expires_at,
});
// Store the refreshed tokens (handles both create and update)
// Pass existing token state to avoid duplicate DB calls
yield this.storeTokens({
userId,
serverName,
tokens: newTokens,
createToken,
updateToken,
findToken,
clientInfo,
existingTokens: {
accessToken: accessTokenData, // We know this is expired/missing
refreshToken: refreshTokenData, // We already have this
clientInfoToken: clientInfoData, // We already looked this up
},
});
dataSchemas.logger.info(`${logPrefix} Successfully refreshed and stored OAuth tokens`);
return newTokens;
}
catch (refreshError) {
dataSchemas.logger.error(`${logPrefix} Failed to refresh tokens`, refreshError);
// Check if it's an unauthorized_client error (refresh not supported)
const errorMessage = refreshError instanceof Error ? refreshError.message : String(refreshError);
if (errorMessage.includes('unauthorized_client')) {
dataSchemas.logger.info(`${logPrefix} Server does not support refresh tokens for this client. New authentication required.`);
}
return null;
}
}
// If we reach here, access token should exist and be valid
if (!accessTokenData) {
return null;
}
const decryptedAccessToken = yield dataSchemas.decryptV2(accessTokenData.token);
/** Get refresh token if available */
const refreshTokenData = yield findToken({
userId,
type: 'mcp_oauth_refresh',
identifier: `${identifier}:refresh`,
});
const tokens = {
access_token: decryptedAccessToken,
token_type: 'Bearer',
obtained_at: accessTokenData.createdAt.getTime(),
expires_at: (_b = accessTokenData.expiresAt) === null || _b === void 0 ? void 0 : _b.getTime(),
};
if (refreshTokenData) {
tokens.refresh_token = yield dataSchemas.decryptV2(refreshTokenData.token);
}
dataSchemas.logger.debug(`${logPrefix} Loaded existing OAuth tokens from storage`);
return tokens;
}
catch (error) {
dataSchemas.logger.error(`${logPrefix} Failed to retrieve tokens`, error);
return null;
}
});
}
static getClientInfoAndMetadata(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, serverName, findToken, }) {
var _b;
const identifier = `mcp:${serverName}`;
const clientInfoData = yield findToken({
userId,
type: 'mcp_oauth_client',
identifier: `${identifier}:client`,
});
if (clientInfoData == null) {
return null;
}
const tokenData = yield dataSchemas.decryptV2(clientInfoData.token);
const clientInfo = JSON.parse(tokenData);
// get metadata from the token as a plain object. While it's defined as a Map in the database type, it's a plain object at runtime.
function getMetadata(metadata) {
if (metadata == null) {
return {};
}
if (metadata instanceof Map) {
return Object.fromEntries(metadata);
}
return Object.assign({}, metadata);
}
const clientMetadata = getMetadata((_b = clientInfoData.metadata) !== null && _b !== void 0 ? _b : null);
return {
clientInfo,
clientMetadata,
};
});
}
/**
* Deletes all OAuth-related tokens for a specific user and server
*/
static deleteUserTokens(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, serverName, deleteToken, }) {
const identifier = `mcp:${serverName}`;
// delete client info token
yield deleteToken({
userId,
type: 'mcp_oauth_client',
identifier: `${identifier}:client`,
});
// delete access token
yield deleteToken({
userId,
type: 'mcp_oauth',
identifier,
});
// delete refresh token
yield deleteToken({
userId,
type: 'mcp_oauth_refresh',
identifier: `${identifier}:refresh`,
});
});
}
}
var _a, _b, _c, _d;
/**
* Centralized configuration for MCP-related environment variables.
* Provides typed access to MCP settings with default values.
*/
const mcpConfig = {
OAUTH_ON_AUTH_ERROR: isEnabled((_a = process.env.MCP_OAUTH_ON_AUTH_ERROR) !== null && _a !== void 0 ? _a : true),
OAUTH_DETECTION_TIMEOUT: math((_b = process.env.MCP_OAUTH_DETECTION_TIMEOUT) !== null && _b !== void 0 ? _b : 5000),
CONNECTION_CHECK_TTL: math((_c = process.env.MCP_CONNECTION_CHECK_TTL) !== null && _c !== void 0 ? _c : 60000),
/** Idle timeout (ms) after which user connections are disconnected. Default: 15 minutes */
USER_CONNECTION_IDLE_TIMEOUT: math((_d = process.env.MCP_USER_CONNECTION_IDLE_TIMEOUT) !== null && _d !== void 0 ? _d : 15 * 60 * 1000),
};
// ATTENTION: If you modify OAuth detection logic in this file, run the integration tests to verify:
// npx jest --testMatch="**/detectOAuth.integration.dev.ts" (from packages/api directory)
//
// These tests are excluded from CI because they make live HTTP requests to external services,
// which could cause flaky builds due to network issues or changes in third-party endpoints.
// Manual testing ensures the OAuth detection still works against real MCP servers.
/**
* Detects if an MCP server requires OAuth authentication using proactive discovery methods.
*
* This function implements a comprehensive OAuth detection strategy:
* 1. Standard Protected Resource Metadata (RFC 9728) - checks /.well-known/oauth-protected-resource
* 2. 401 Challenge Method - checks WWW-Authenticate header for resource_metadata URL
* 3. Optional fallback: treat any 401/403 response as OAuth requirement (if MCP_OAUTH_ON_AUTH_ERROR=true)
*
* @param serverUrl - The MCP server URL to check for OAuth requirements
* @returns Promise<OAuthDetectionResult> - OAuth requirement details
*/
function detectOAuthRequirement(serverUrl) {
return __awaiter(this, void 0, void 0, function* () {
const protectedResourceResult = yield checkProtectedResourceMetadata(serverUrl);
if (protectedResourceResult)
return protectedResourceResult;
const challengeResult = yield check401ChallengeMetadata(serverUrl);
if (challengeResult)
return challengeResult;
const fallbackResult = yield checkAuthErrorFallback(serverUrl);
if (fallbackResult)
return fallbackResult;
// No OAuth detected
return {
requiresOAuth: false,
method: 'no-metadata-found',
metadata: null,
};
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// ------------------------ Private helper functions for OAuth detection -------------------------//
////////////////////////////////////////////////////////////////////////////////////////////////////
// Checks for OAuth using standard protected resource metadata (RFC 9728)
function checkProtectedResourceMetadata(serverUrl) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const resourceMetadata = yield auth_js.discoverOAuthProtectedResourceMetadata(serverUrl);
if (!((_a = resourceMetadata === null || resourceMetadata === void 0 ? void 0 : resourceMetadata.authorization_servers) === null || _a === void 0 ? void 0 : _a.length))
return null;
return {
requiresOAuth: true,
method: 'protected-resource-metadata',
metadata: resourceMetadata,
};
}
catch (_b) {
return null;
}
});
}
/**
* Checks for OAuth using 401 challenge with resource metadata URL or Bearer token.
* Tries HEAD first, then falls back to POST if HEAD doesn't return 401.
* Some servers (like StackOverflow) only return 401 for POST requests.
*/
function check401ChallengeMetadata(serverUrl) {
return __awaiter(this, void 0, void 0, function* () {
// Try HEAD first (lighter weight)
const headResult = yield check401WithMethod(serverUrl, 'HEAD');
if (headResult)
return headResult;
// Fall back to POST if HEAD didn't return 401 (some servers don't support HEAD)
const postResult = yield check401WithMethod(serverUrl, 'POST');
if (postResult)
return postResult;
return null;
});
}
function check401WithMethod(serverUrl, method) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
const fetchOptions = {
method,
signal: AbortSignal.timeout(mcpConfig.OAUTH_DETECTION_TIMEOUT),
};
// POST requests need headers and body for MCP servers
if (method === 'POST') {
fetchOptions.headers = { 'Content-Type': 'application/json' };
fetchOptions.body = JSON.stringify({});
}
const response = yield fetch(serverUrl, fetchOptions);
if (response.status !== 401)
return null;
const wwwAuth = response.headers.get('www-authenticate');
const metadataUrl = (_a = wwwAuth === null || wwwAuth === void 0 ? void 0 : wwwAuth.match(/resource_metadata="([^"]+)"/)) === null || _a === void 0 ? void 0 : _a[1];
if (metadataUrl) {
try {
// Try to fetch resource metadata from the provided URL
const metadataResponse = yield fetch(metadataUrl, {
signal: AbortSignal.timeout(mcpConfig.OAUTH_DETECTION_TIMEOUT),
});
const metadata = yield metadataResponse.json();
if ((_b = metadata === null || metadata === void 0 ? void 0 : metadata.authorization_servers) === null || _b === void 0 ? void 0 : _b.length) {
return {
requiresOAuth: true,
method: '401-challenge-metadata',
metadata,
};
}
}
catch (_c) {
// Metadata fetch failed, continue to Bearer check below
}
}
/**
* If we got a 401 with WWW-Authenticate containing "Bearer" (case-insensitive),
* the server requires OAuth authentication even without discovery metadata.
* This handles "legacy" OAuth servers (like StackOverflow's MCP) that use standard
* OAuth endpoints (/authorize, /token, /register) without .well-known metadata.
*/
if (wwwAuth && /bearer/i.test(wwwAuth)) {
return {
requiresOAuth: true,
method: '401-challenge-metadata',
metadata: null,
};
}
return null;
}
catch (_d) {
return null;
}
});
}
// Fallback method: treats any auth error as OAuth requirement if configured
function checkAuthErrorFallback(serverUrl) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (!mcpConfig.OAUTH_ON_AUTH_ERROR)
return null;
const response = yield fetch(serverUrl, {
method: 'HEAD',
signal: AbortSignal.timeout(mcpConfig.OAUTH_DETECTION_TIMEOUT),
});
if (response.status !== 401 && response.status !== 403)
return null;
return {
requiresOAuth: true,
method: 'no-metadata-found',
metadata: null,
};
}
catch (_a) {
return null;
}
});
}
function isStdioOptions(options) {
return 'command' in options;
}
function isWebSocketOptions(options) {
if ('url' in options) {
const protocol = new URL(options.url).protocol;
return protocol === 'ws:' || protocol === 'wss:';
}
return false;
}
function isSSEOptions(options) {
if ('url' in options) {
const protocol = new URL(options.url).protocol;
return protocol !== 'ws:' && protocol !== 'wss:';
}
return false;
}
/**
* Checks if the provided options are for a Streamable HTTP transport.
*
* Streamable HTTP is an MCP transport that uses HTTP POST for sending messages
* and supports streaming responses. It provides better performance than
* SSE transport while maintaining compatibility with most network environments.
*
* @param options MCP connection options to check
* @returns True if options are for a streamable HTTP transport
*/
function isStreamableHTTPOptions(options) {
if ('url' in options && 'type' in options) {
const optionType = options.type;
if (optionType === 'streamable-http' || optionType === 'http') {
const protocol = new URL(options.url).protocol;
return protocol !== 'ws:' && protocol !== 'wss:';
}
}
return false;
}
const FIVE_MINUTES = 5 * 60 * 1000;
const DEFAULT_TIMEOUT = 60000;
/** SSE connections through proxies may need longer initial handshake time */
const SSE_CONNECT_TIMEOUT = 120000;
/**
* Headers for SSE connections.
*
* Headers we intentionally DO NOT include:
* - Accept: text/event-stream - Already set by eventsource library AND MCP SDK
* - X-Accel-Buffering: This is a RESPONSE header for Nginx, not a request header.
* The upstream MCP server must send this header for Nginx to respect it.
* - Connection: keep-alive: Forbidden in HTTP/2 (RFC 7540 §8.1.2.2).
* HTTP/2 manages connection persistence differently.
*/
const SSE_REQUEST_HEADERS = {
'Cache-Control': 'no-cache',
};
/**
* Extracts a meaningful error message from SSE transport errors.
* The MCP SDK's SSEClientTransport can produce "SSE error: undefined" when the
* underlying eventsource library encounters connection issues without a specific message.
*
* @returns Object containing:
* - message: Human-readable error description
* - code: HTTP status code if available
* - isProxyHint: Whether this error suggests proxy misconfiguration
* - isTransient: Whether this is likely a transient error that will auto-reconnect
*/
function extractSSEErrorMessage(error) {
var _a;
if (!error || typeof error !== 'object') {
return {
message: 'Unknown SSE transport error',
isProxyHint: true,
isTransient: true,
};
}
const errorObj = error;
const rawMessage = (_a = errorObj.message) !== null && _a !== void 0 ? _a : '';
const code = errorObj.code;
/**
* Handle the common "SSE error: undefined" case.
* This typically occurs when:
* 1. A reverse proxy buffers the SSE stream (proxy issue)
* 2. The server closes an idle connection (normal SSE behavior)
* 3. Network interruption without specific error details
*
* In all cases, the eventsource library will attempt to reconnect automatically.
*/
if (rawMessage === 'SSE error: undefined' || rawMessage === 'undefined' || !rawMessage) {
return {
message: 'SSE connection closed. This can occur due to: (1) idle connection timeout (normal), ' +
'(2) reverse proxy buffering (check proxy_buffering config), or (3) network interruption.',
code,
isProxyHint: true,
isTransient: true,
};
}
/**
* Check for timeout patterns. Use case-insensitive matching for common timeout error codes:
* - ETIMEDOUT: TCP connection timeout
* - ESOCKETTIMEDOUT: Socket timeout
* - "timed out" / "timeout": Generic timeout messages
*/
const lowerMessage = rawMessage.toLowerCase();
if (rawMessage.includes('ETIMEDOUT') ||
rawMessage.includes('ESOCKETTIMEDOUT') ||
lowerMessage.includes('timed out') ||
lowerMessage.includes('timeout after') ||
lowerMessage.includes('request timeout')) {
return {
message: `SSE connection timed out: ${rawMessage}. If behind a reverse proxy, increase proxy_read_timeout.`,
code,
isProxyHint: true,
isTransient: true,
};
}
// Connection reset is often transient (server restart, proxy reload)
if (rawMessage.includes('ECONNRESET')) {
return {
message: `SSE connection reset: ${rawMessage}. The server or proxy may have restarted.`,
code,
isProxyHint: false,
isTransient: true,
};
}
// Connection refused is more serious - server may be down
if (rawMessage.includes('ECONNREFUSED')) {
return {
message: `SSE connection refused: ${rawMessage}. Verify the MCP server is running and accessible.`,
code,
isProxyHint: false,
isTransient: false,
};
}
// DNS failure is usually a configuration issue, not transient
if (rawMessage.includes('ENOTFOUND') || rawMessage.includes('getaddrinfo')) {
return {
message: `SSE DNS resolution failed: ${rawMessage}. Check the server URL is correct.`,
code,
isProxyHint: false,
isTransient: false,
};
}
// Check for HTTP status codes in the message
const statusMatch = rawMessage.match(/\b(4\d{2}|5\d{2})\b/);
if (statusMatch) {
const statusCode = parseInt(statusMatch[1], 10);
// 5xx errors are often transient, 4xx are usually not
const isServerError = statusCode >= 500 && statusCode < 600;
return {
message: rawMessage,
code: statusCode,
isProxyHint: statusCode === 502 || statusCode === 503 || statusCode === 504,
isTransient: isServerError,
};
}
return {
message: rawMessage,
code,
isProxyHint: false,
isTransient: false,
};
}
class MCPConnection extends events.EventEmitter {
setRequestHeaders(headers) {
if (!headers) {
return;
}
const normalizedHeaders = {};
for (const [key, value] of Object.entries(headers)) {
normalizedHeaders[key.toLowerCase()] = value;
}
this.requestHeaders = normalizedHeaders;
}
getRequestHeaders() {
return this.requestHeaders;
}
constructor(params) {
super();
this.transport = null; // Make this nullable
this.connectionState = 'disconnected';
this.connectPromise = null;
this.MAX_RECONNECT_ATTEMPTS = 3;
this.shouldStopReconnecting = false;
this.isReconnecting = false;
this.isInitializing = false;
this.reconnectAttempts = 0;
this.lastConnectionCheckAt = 0;
this.oauthRequired = false;
this.options = params.serverConfig;
this.serverName = params.serverName;
this.userId = params.userId;
this.useSSRFProtection = params.useSSRFProtection === true;
this.iconPath = params.serverConfig.iconPath;
this.timeout = params.serverConfig.timeout;
this.lastPingTime = Date.now();
this.createdAt = Date.now(); // Record creation timestamp for staleness detection
if (params.oauthTokens) {
this.oauthTokens = params.oauthTokens;
}
this.client = new index_js.Client({
name: '@librechat/api-client',
version: '1.2.3',
}, {
capabilities: {},
});
this.setupEventListeners();
}
/** Helper to generate consistent log prefixes */
getLogPrefix() {
const userPart = this.userId ? `[User: ${this.userId}]` : '';
return `[MCP]${userPart}[${this.serverName}]`;
}
/**
* Factory function to create fetch functions without capturing the entire `this` context.
* This helps prevent memory leaks by only passing necessary dependencies.
*
* @param getHeaders Function to retrieve request headers
* @param timeout Timeout value for the agent (in milliseconds)
* @returns A fetch function that merges headers appropriately
*/
createFetchFunction(getHeaders, timeout) {
const ssrfConnect = this.useSSRFProtection ? createSSRFSafeUndiciConnect() : undefined;
return function customFetch(input, init) {
const requestHeaders = getHeaders();
const effectiveTimeout = timeout || DEFAULT_TIMEOUT;
const agent = new undici.Agent(Object.assign({ bodyTimeout: effectiveTimeout, headersTimeout: effectiveTimeout }, (ssrfConnect != null ? { connect: ssrfConnect } : {})));
if (!requestHeaders) {
return undici.fetch(input, Object.assign(Object.assign({}, init), { dispatcher: agent }));
}
let initHeaders = {};
if (init === null || init === void 0 ? void 0 : init.headers) {
if (init.headers instanceof Headers) {
initHeaders = Object.fromEntries(init.headers.entries());
}
else if (Array.isArray(init.headers)) {
initHeaders = Object.fromEntries(init.headers);
}
else {
initHeaders = init.headers;
}
}
return undici.fetch(input, Object.assign(Object.assign({}, init), { headers: Object.assign(Object.assign({}, initHeaders), requestHeaders), dispatcher: agent }));
};
}
emitError(error, errorContext) {
const errorMessage = error instanceof Error ? error.message : String(error);
dataSchemas.logger.error(`${this.getLogPrefix()} ${errorContext}: ${errorMessage}`);
}
constructTransport(options) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
try {
let type;
if (isStdioOptions(options)) {
type = 'stdio';
}
else if (isWebSocketOptions(options)) {
type = 'websocket';
}
else if (isStreamableHTTPOptions(options)) {
// Could be either 'streamable-http' or 'http', normalize to 'streamable-http'
type = 'streamable-http';
}
else if (isSSEOptions(options)) {
type = 'sse';
}
else {
throw new Error('Cannot infer transport type: options.type is not provided and cannot be inferred from other properties.');
}
switch (type) {
case 'stdio':
if (!isStdioOptions(options)) {
throw new Error('Invalid options for stdio transport.');
}
return new stdio_js.StdioClientTransport({
command: options.command,
args: options.args,
// workaround bug of mcp sdk that can't pass env:
// https://github.com/modelcontextprotocol/typescript-sdk/issues/216
env: Object.assign(Object.assign({}, stdio_js.getDefaultEnvironment()), ((_a = options.env) !== null && _a !== void 0 ? _a : {})),
});
case 'websocket':
if (!isWebSocketOptions(options)) {
throw new Error('Invalid options for websocket transport.');
}
this.url = options.url;
if (this.useSSRFProtection) {
const wsHostname = new URL(options.url).hostname;
const isSSRF = yield resolveHostnameSSRF(wsHostname);
if (isSSRF) {
throw new Error(`SSRF protection: WebSocket host "${wsHostname}" resolved to a private/reserved IP address`);
}
}
return new websocket_js.WebSocketClientTransport(new URL(options.url));
case 'sse': {
if (!isSSEOptions(options)) {
throw new Error('Invalid options for sse transport.');
}
this.url = options.url;
const url = new URL(options.url);
dataSchemas.logger.info(`${this.getLogPrefix()} Creating SSE transport: ${sanitizeUrlForLogging(url)}`);
const abortController = new AbortController();
/** Add OAuth token to headers if available */
const headers = Object.assign({}, options.headers);
if ((_b = this.oauthTokens) === null || _b === void 0 ? void 0 : _b.access_token) {
headers['Authorization'] = `Bearer ${this.oauthTokens.access_token}`;
}
/**
* SSE connections need longer timeouts for reliability.
* The connect timeout is extended because proxies may delay initial response.
*/
const sseTimeout = this.timeout || SSE_CONNECT_TIMEOUT;
const ssrfConnect = this.useSSRFProtection ? createSSRFSafeUndiciConnect() : undefined;
const transport = new sse_js.SSEClientTransport(url, {
requestInit: {
/** User/OAuth headers override SSE defaults */
headers: Object.assign(Object.assign({}, SSE_REQUEST_HEADERS), headers),
signal: abortController.signal,
},
eventSourceInit: {
fetch: (url, init) => {
/** Merge headers: SSE defaults < init headers < user headers (user wins) */
const fetchHeaders = new Headers(Object.assign({}, SSE_REQUEST_HEADERS, init === null || init === void 0 ? void 0 : init.headers, headers));
const agent = new undici.Agent(Object.assign({ bodyTimeout: sseTimeout, headersTimeout: sseTimeout,
/** Extended keep-alive for long-lived SSE connections */
keepAliveTimeout: sseTimeout, keepAliveMaxTimeout: sseTimeout * 2 }, (ssrfConnect != null ? { connect: ssrfConnect } : {})));
return undici.fetch(url, Object.assign(Object.assign({}, init), { dispatcher: agent, headers: fetchHeaders }));
},
},
fetch: this.createFetchFunction(this.getRequestHeaders.bind(this), sseTimeout),
});
transport.onclose = () => {
dataSchemas.logger.info(`${this.getLogPrefix()} SSE transport closed`);
this.emit('connectionChange', 'disconnected');
};
transport.onmessage = (message) => {
dataSchemas.logger.info(`${this.getLogPrefix()} Message received: ${JSON.stringify(message)}`);
};
this.setupTransportErrorHandlers(transport);
return transport;
}
case 'streamable-http': {
if (!isStreamableHTTPOptions(options)) {
throw new Error('Invalid options for streamable-http transport.');
}
this.url = options.url;
const url = new URL(options.url);
dataSchemas.logger.info(`${this.getLogPrefix()} Creating streamable-http transport: ${sanitizeUrlForLogging(url)}`);
const abortController = new AbortController();
/** Add OAuth token to headers if available */
const headers = Object.assign({}, options.headers);
if ((_c = this.oauthTokens) === null || _c === void 0 ? void 0 : _c.access_token) {
headers['Authorization'] = `Bearer ${this.oauthTokens.access_token}`;
}
const transport = new streamableHttp_js.StreamableHTTPClientTransport(url, {
requestInit: {
headers,
signal: abortController.signal,
},
fetch: this.createFetchFunction(this.getRequestHeaders.bind(this), this.timeout),
});
transport.onclose = () => {
dataSchemas.logger.info(`${this.getLogPrefix()} Streamable-http transport closed`);
this.emit('connectionChange', 'disconnected');
};
transport.onmessage = (message) => {
dataSchemas.logger.info(`${this.getLogPrefix()} Message received: ${JSON.stringify(message)}`);
};
this.setupTransportErrorHandlers(transport);
return transport;
}
default: {
throw new Error(`Unsupported transport type: ${type}`);
}
}
}
catch (error) {
this.emitError(error, 'Failed to construct transport');
throw error;
}
});
}
setupEventListeners() {
this.isInitializing = true;
this.on('connectionChange', (state) => {
this.connectionState = state;
if (state === 'connected') {
this.isReconnecting = false;
this.isInitializing = false;
this.shouldStopReconnecting = false;
this.reconnectAttempts = 0;
/**
* // FOR DEBUGGING
* // this.client.setRequestHandler(PingRequestSchema, async (request, extra) => {
* // logger.info(`[MCP][${this.serverName}] PingRequest: ${JSON.stringify(request)}`);
* // if (getEventListeners && extra.signal) {
* // const listenerCount = getEventListeners(extra.signal, 'abort').length;
* // logger.debug(`Signal has ${listenerCount} abort listeners`);
* // }
* // return {};
* // });
*/
}
else if (state === 'error' && !this.isReconnecting && !this.isInitializing) {
this.handleReconnection().catch((error) => {
dataSchemas.logger.error(`${this.getLogPrefix()} Reconnection handler failed:`, error);
});
}
});
this.subscribeToResources();
}
handleReconnection() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReconnecting ||
this.shouldStopReconnecting ||
this.isInitializing ||
this.oauthRequired) {
if (this.oauthRequired) {
dataSchemas.logger.info(`${this.getLogPrefix()} OAuth required, skipping reconnection attempts`);
}
return;
}
this.isReconnecting = true;
const backoffDelay = (attempt) => {
const base = Math.min(1000 * Math.pow(2, attempt), 30000);
const jitter = Math.floor(Math.random() * 1000); // up to 1s of random jitter
return base + jitter;
};
try {
while (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS &&
!this.shouldStopReconnecting) {
this.reconnectAttempts++;
const delay = backoffDelay(this.reconnectAttempts);
dataSchemas.logger.info(`${this.getLogPrefix()} Reconnecting ${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS} (delay: ${delay}ms)`);
yield new Promise((resolve) => setTimeout(resolve, delay));
try {
yield this.connect();
this.reconnectAttempts = 0;
return;
}
catch (error) {
dataSchemas.logger.error(`${this.getLogPrefix()} Reconnection attempt failed:`, error);
// Stop immediately if rate limited - retrying will only make it worse
if (this.isRateLimitError(error)) {
/**
* Rate limiting sets shouldStopReconnecting to prevent hammering the server.
* Silent return here (vs throw in connectClient) because we're already in
* error recovery mode - throwing would just add noise. The connection
* must be recreated to retry after rate limit lifts.
*/
dataSchemas.logger.warn(`${this.getLogPrefix()} Rate limited (429), stopping reconnection attempts`);
dataSchemas.logger.debug(`${this.getLogPrefix()} Rate limit block is permanent for this connection instance`);
this.shouldStopReconnecting = true;
return;
}
if (this.reconnectAttempts === this.MAX_RECONNECT_ATTEMPTS ||
this.shouldStopReconnecting) {
dataSchemas.logger.error(`${this.getLogPrefix()} Stopping reconnection attempts`);
return;
}
}
}
}
finally {
this.isReconnecting = false;
}
});
}
subscribeToResources() {
this.client.setNotificationHandler(types_js.ResourceListChangedNotificationSchema, () => __awaiter(this, void 0, void 0, function* () {
this.emit('resourcesChanged');
}));
}
connectClient() {
return __awaiter(this, void 0, void 0, function* () {
if (this.connectionState === 'connected') {
return;
}
if (this.connectPromise) {
return this.connectPromise;
}
if (this.shouldStopReconnecting) {
return;
}
this.emit('connectionChange', 'connecting');
this.connectPromise = (() => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
if (this.transport) {
try {
yield this.client.close();
this.transport = null;
}
catch (error) {
dataSchemas.logger.warn(`${this.getLogPrefix()} Error closing connection:`, error);
}
}
this.transport = yield this.constructTransport(this.options);
this.setupTransportDebugHandlers();
const connectTimeout = (_a = this.options.initTimeout) !== null && _a !== void 0 ? _a : 120000;
yield withTimeout(this.client.connect(this.transport), connectTimeout, `Connection timeout after ${connectTimeout}ms`);
this.connectionState = 'connected';
this.emit('connectionChange', 'connected');
this.reconnectAttempts = 0;
}
catch (error) {
// Check if it's a rate limit error - stop immediately to avoid making it worse
if (this.isRateLimitError(error)) {
/**
* Rate limiting sets shouldStopReconnecting to prevent hammering the server.
* This is a permanent block for this connection instance - the connection
* must be recreated (e.g., by user re-initiating) to retry after rate limit lifts.
*
* We throw here (unlike handleReconnection which returns silently) because:
* - connectClient() is a public API - callers expect async errors to throw
* - Other errors in this catch block also throw for consistency
* - handleReconnection is private/internal error recovery, different context
*/
dataSchemas.logger.warn(`${this.getLogPrefix()} Rate limited (429), stopping connection attempts`);
this.shouldStopReconnecting = true;
this.connectionState = 'error';
this.emit('connectionChange', 'error');
throw error;
}
// Check if it's an OAuth authentication error
if (this.isOAuthError(error)) {
dataSchemas.logger.warn(`${this.getLogPrefix()} OAuth authentication required`);
this.oauthRequired = true;
const serverUrl = this.url;
dataSchemas.logger.debug(`${this.getLogPrefix()} Server URL for OAuth: ${serverUrl ? sanitizeUrlForLogging(serverUrl) : 'undefined'}`);
const oauthTimeout = (_b = this.options.initTimeout) !== null && _b !== void 0 ? _b : 60000 * 2;
/** Promise that will resolve when OAuth is handled */
const oauthHandledPromise = new Promise((resolve, reject) => {
let timeoutId = null;
let oauthHandledListener = null;
let oauthFailedListener = null;
/** Cleanup function to remove listeners and clear timeout */
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (oauthHandledListener) {
this.off('oauthHandled', oauthHandledListener);
}
if (oauthFailedListener) {
this.off('oauthFailed', oauthFailedListener);
}
};
// Success handler
oauthHandledListener = () => {
cleanup();
resolve();
};
// Failure handler
oauthFailedListener = (error) => {
cleanup();
reject(error);
};
// Timeout handler
timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`OAuth handling timeout after ${oauthTimeout}ms`));
}, oauthTimeout);
// Listen for both success and failure events
this.once('oauthHandled', oauthHandledListener);
this.once('oauthFailed', oauthFailedListener);
});
// Emit the event
this.emit('oauthRequired', {
serverName: this.serverName,
error,
serverUrl,
userId: this.userId,
});
try {
// Wait for OAuth to be handled
yield oauthHandledPromise;
// Reset the oauthRequired flag
this.oauthRequired = false;
// Don't throw the error - just return so connection can be retried
dataSchemas.logger.info(`${this.getLogPrefix()} OAuth handled successfully, connection will be retried`);
return;
}
catch (oauthError) {
// OAuth failed or timed out
this.oauthRequired = false;
dataSchemas.logger.error(`${this.getLogPrefix()} OAuth handling failed:`, oauthError);
// Re-throw the original authentication error
throw error;
}
}
this.connectionState = 'error';
this.emit('connectionChange', 'error');
throw error;
}
finally {
this.connectPromise = null;
}
}))();
return this.connectPromise;
});
}
setupTransportDebugHandlers() {
if (!this.transport) {
return;
}
this.transport.onmessage = (msg) => {
dataSchemas.logger.debug(`${this.getLogPrefix()} Transport received: ${JSON.stringify(msg)}`);
};
const originalSend = this.transport.send.bind(this.transport);
this.transport.send = (msg) => __awaiter(this, void 0, void 0, function* () {
var _a;
if ('result' in msg && !('method' in msg) && Object.keys((_a = msg.result) !== null && _a !== void 0 ? _a : {}).length === 0) {
if (Date.now() - this.lastPingTime < FIVE_MINUTES) {
throw new Error('Empty result');
}
this.lastPingTime = Date.now();
}
dataSchemas.logger.debug(`${this.getLogPrefix()} Transport sending: ${JSON.stringify(msg)}`);
return originalSend(msg);
});
}
connect() {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.disconnect();
yield this.connectClient();
if (!(yield this.isConnected())) {
throw new Error('Connection not established');
}
}
catch (error) {
dataSchemas.logger.error(`${this.getLogPrefix()} Connection failed:`, error);
throw error;
}
});
}
setupTransportErrorHandlers(transport) {
transport.onerror = (error) => {
// Extract meaningful error information (handles "SSE error: undefined" cases)
const { message: errorMessage, code: errorCode, isProxyHint, isTransient, } = extractSSEErrorMessage(error);
// Ignore SSE 404 errors for servers that don't support SSE
if (errorCode === 404 && errorMessage.toLowerCase().includes('failed to open sse stream')) {
dataSchemas.logger.warn(`${this.getLogPrefix()} SSE stream not available (404). Ignoring.`);
return;
}
// Check if it's an OAuth authentication error
if (this.isOAuthError(error)) {
dataSchemas.logger.warn(`${this.getLogPrefix()} OAuth authentication error detected`);
this.emit('oauthError', error);
}
/**
* Log with enhanced context for debugging.
* All transport.onerror events are logged as errors to preserve stack traces.
* isTransient indicates whether auto-reconnection is expected to succeed.
*
* The MCP SDK's SseError extends Error and includes:
* - code: HTTP status code or eventsource error code
* - event: The original eventsource ErrorEvent
* - stack: Full stack trace
*/
const errorContext = {
code: errorCode,
isTransient,
};
if (isProxyHint) {
errorContext.hint = 'Check Nginx/proxy configuration for SSE endpoints';
}
// Extract additional debug info from SseError if available
if (error && typeof error === 'object') {
const sseError = error;
// Include the original eventsource event for debugging
if (sseError.event && typeof sseError.event === 'object') {
const event = sseError.event;
errorContext.eventDetails = {
type: event.type,
code: event.code,
message: event.message,
};
}
// Include stack trace if available
if (sseError.stack) {
errorContext.stack = sseError.stack;
}
}
const errorLabel = isTransient
? 'Transport error (transient, will reconnect)'
: 'Transport error (may require manual intervention)';
dataSchemas.logger.error(`${this.getLogPrefix()} ${errorLabel}: ${errorMessage}`, errorContext);
this.emit('connectionChange', 'error');
};
}
disconnect() {
return __awaiter(this, void 0, void 0, function* () {
try {
if (this.transport) {
yield this.client.close();
this.transport = null;
}
if (this.connectionState === 'disconnected') {
return;
}
this.connectionState = 'disconnected';
this.emit('connectionChange', 'disconnected');
}
finally {
this.connectPromise = null;
}
});
}
fetchResources() {
return __awaiter(this, void 0, void 0, function* () {
try {
const { resources } = yield this.client.listResources();
return resources;
}
catch (error) {
this.emitError(error, 'Failed to fetch resources');
return [];
}
});
}
fetchTools() {
return __awaiter(this, void 0, void 0, function* () {
try {
const { tools } = yield this.client.listTools();
return tools;
}
catch (error) {
this.emitError(error, 'Failed to fetch tools');
return [];
}
});
}
fetchPrompts() {
return __awaiter(this, void 0, void 0, function* () {
try {
const { prompts } = yield this.client.listPrompts();
return prompts;
}
catch (error) {
this.emitError(error, 'Failed to fetch prompts');
return [];
}
});
}
isConnected() {
return __awaiter(this, void 0, void 0, function* () {
// First check if we're in a connected state
if (this.connectionState !== 'connected') {
return false;
}
// If we recently checked, skip expensive verification
const now = Date.now();
if (now - this.lastConnectionCheckAt < mcpConfig.CONNECTION_CHECK_TTL) {
return true;
}
this.lastConnectionCheckAt = now;
try {
// Try ping first as it's the lightest check
yield this.client.ping();
return this.connectionState === 'connected';
}
catch (error) {
// Check if the error is because ping is not supported (method not found)
const pingUnsupported = error instanceof Error &&
((error === null || error === void 0 ? void 0 : error.message.includes('-32601')) ||
(error === null || error === void 0 ? void 0 : error.message.includes('-32602')) ||
(error === null || error === void 0 ? void 0 : error.message.includes('invalid method ping')) ||
(error === null || error === void 0 ? void 0 : error.message.includes('Unsupported method: ping')) ||
(error === null || error === void 0 ? void 0 : error.message.includes('method not found')));
if (!pingUnsupported) {
dataSchemas.logger.error(`${this.getLogPrefix()} Ping failed:`, error);
return false;
}
// Ping is not supported by this server, try an alternative verification
dataSchemas.logger.debug(`${this.getLogPrefix()} Server does not support ping method, verifying connection with capabilities`);
try {
// Get server capabilities to verify connection is truly active
const capabilities = this.client.getServerCapabilities();
// If we have capabilities, try calling a supported method to verify connection
if (capabilities === null || capabilities === void 0 ? void 0 : capabilities.tools) {
yield this.client.listTools();
return this.connectionState === 'connected';
}
else if (capabilities === null || capabilities === void 0 ? void 0 : capabilities.resources) {
yield this.client.listResources();
return this.connectionState === 'connected';
}
else if (capabilities === null || capabilities === void 0 ? void 0 : capabilities.prompts) {
yield this.client.listPrompts();
return this.connectionState === 'connected';
}
else {
// No capabilities to test, but we're in connected state and initialization succeeded
dataSchemas.logger.debug(`${this.getLogPrefix()} No capabilities to test, assuming connected based on state`);
return this.connectionState === 'connected';
}
}
catch (capabilityError) {
// If capability check fails, the connection is likely broken
dataSchemas.logger.error(`${this.getLogPrefix()} Connection verification failed:`, capabilityError);
return false;
}
}
});
}
setOAuthTokens(tokens) {
this.oauthTokens = tokens;
}
/**
* Check if this connection is stale compared to config update time.
* A connection is stale if it was created before the config was updated.
*
* @param configUpdatedAt - Unix timestamp (ms) when config was last updated
* @returns true if connection was created before config update, false otherwise
*/
isStale(configUpdatedAt) {
return this.createdAt < configUpdatedAt;
}
isOAuthError(error) {
if (!error || typeof error !== 'object') {
return false;
}
// Check for error code
if ('code' in error) {
const code = error.code;
if (code === 401 || code === 403) {
return true;
}
}
// Check message for various auth error indicators
if ('message' in error && typeof error.message === 'string') {
const message = error.message.toLowerCase();
// Check for 401 status
if (message.includes('401') || message.includes('non-200 status code (401)')) {
return true;
}
// Check for invalid_token (OAuth servers return this for expired/revoked tokens)
if (message.includes('invalid_token')) {
return true;
}
// Check for invalid_grant (OAuth servers return this for expired/revoked grants)
if (message.includes('invalid_grant')) {
return true;
}
// Check for authentication required
if (message.includes('authentication required') || message.includes('unauthorized')) {
return true;
}
}
return false;
}
/**
* Checks if an error indicates rate limiting (HTTP 429).
* Rate limited requests should stop reconnection attempts to avoid making the situation worse.
*/
isRateLimitError(error) {
if (!error || typeof error !== 'object') {
return false;
}
// Check for error code
if ('code' in error) {
const code = error.code;
if (code === 429) {
return true;
}
}
// Check message for rate limit indicators
if ('message' in error && typeof error.message === 'string') {
const message = error.message.toLowerCase();
if (message.includes('429') ||
message.includes('rate limit') ||
message.includes('too many requests')) {
return true;
}
}
return false;
}
}
/**
* Factory for creating MCP connections with optional OAuth authentication.
* Handles OAuth flows, token management, and connection retry logic.
* NOTE: Much of the OAuth logic was extracted from the old MCPManager class as is.
*/
class MCPConnectionFactory {
/** Creates a new MCP connection with optional OAuth support */
static create(basic, oauth) {
return __awaiter(this, void 0, void 0, function* () {
const factory = new this(basic, oauth);
return factory.createConnection();
});
}
/**
* Discovers tools from an MCP server, even when OAuth is required.
* Per MCP spec, tool listing should be possible without authentication.
* Returns tools if discoverable, plus OAuth status for tool execution.
*/
static discoverTools(basic, oauth) {
return __awaiter(this, void 0, void 0, function* () {
const factory = new this(basic, oauth ? Object.assign(Object.assign({}, oauth), { returnOnOAuth: true }) : undefined);
return factory.discoverToolsInternal();
});
}
discoverToolsInternal() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const oauthUrl = null;
let oauthRequired = false;
const oauthTokens = this.useOAuth ? yield this.getOAuthTokens() : null;
const connection = new MCPConnection({
serverName: this.serverName,
serverConfig: this.serverConfig,
userId: this.userId,
oauthTokens,
useSSRFProtection: this.useSSRFProtection,
});
const oauthHandler = () => __awaiter(this, void 0, void 0, function* () {
dataSchemas.logger.info(`${this.logPrefix} [Discovery] OAuth required; skipping URL generation in discovery mode`);
oauthRequired = true;
connection.emit('oauthFailed', new Error('OAuth required during tool discovery'));
});
if (this.useOAuth) {
connection.on('oauthRequired', oauthHandler);
}
try {
const connectTimeout = (_b = (_a = this.connectionTimeout) !== null && _a !== void 0 ? _a : this.serverConfig.initTimeout) !== null && _b !== void 0 ? _b : 30000;
yield withTimeout(connection.connect(), connectTimeout, `Connection timeout after ${connectTimeout}ms`);
if (yield connection.isConnected()) {
const tools = yield connection.fetchTools();
if (this.useOAuth) {
connection.removeListener('oauthRequired', oauthHandler);
}
return { tools, connection, oauthRequired: false, oauthUrl: null };
}
}
catch (_c) {
dataSchemas.logger.debug(`${this.logPrefix} [Discovery] Connection failed, attempting unauthenticated tool listing`);
}
try {
const tools = yield this.attemptUnauthenticatedToolListing();
if (this.useOAuth) {
connection.removeListener('oauthRequired', oauthHandler);
}
if (tools && tools.length > 0) {
dataSchemas.logger.info(`${this.logPrefix} [Discovery] Successfully discovered ${tools.length} tools without auth`);
try {
yield connection.disconnect();
}
catch (_d) {
// Ignore cleanup errors
}
return { tools, connection: null, oauthRequired, oauthUrl };
}
}
catch (listError) {
dataSchemas.logger.debug(`${this.logPrefix} [Discovery] Unauthenticated tool listing failed:`, listError);
}
if (this.useOAuth) {
connection.removeListener('oauthRequired', oauthHandler);
}
try {
yield connection.disconnect();
}
catch (_e) {
// Ignore cleanup errors
}
return { tools: null, connection: null, oauthRequired, oauthUrl };
});
}
attemptUnauthenticatedToolListing() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const unauthConnection = new MCPConnection({
serverName: this.serverName,
serverConfig: this.serverConfig,
userId: this.userId,
oauthTokens: null,
useSSRFProtection: this.useSSRFProtection,
});
unauthConnection.on('oauthRequired', () => {
dataSchemas.logger.debug(`${this.logPrefix} [Discovery] Unauthenticated connection requires OAuth, failing fast`);
unauthConnection.emit('oauthFailed', new Error('OAuth not supported in unauthenticated discovery'));
});
try {
const connectTimeout = (_b = (_a = this.connectionTimeout) !== null && _a !== void 0 ? _a : this.serverConfig.initTimeout) !== null && _b !== void 0 ? _b : 15000;
yield withTimeout(unauthConnection.connect(), connectTimeout, `Unauth connection timeout`);
if (yield unauthConnection.isConnected()) {
const tools = yield unauthConnection.fetchTools();
yield unauthConnection.disconnect();
return tools;
}
}
catch (_c) {
dataSchemas.logger.debug(`${this.logPrefix} [Discovery] Unauthenticated connection attempt failed`);
}
try {
yield unauthConnection.disconnect();
}
catch (_d) {
// Ignore cleanup errors
}
return null;
});
}
constructor(basic, oauth) {
var _a;
this.serverConfig = processMCPEnv({
options: basic.serverConfig,
user: oauth === null || oauth === void 0 ? void 0 : oauth.user,
customUserVars: oauth === null || oauth === void 0 ? void 0 : oauth.customUserVars,
body: oauth === null || oauth === void 0 ? void 0 : oauth.requestBody,
});
this.serverName = basic.serverName;
this.useOAuth = !!(oauth === null || oauth === void 0 ? void 0 : oauth.useOAuth);
this.useSSRFProtection = basic.useSSRFProtection === true;
this.connectionTimeout = oauth === null || oauth === void 0 ? void 0 : oauth.connectionTimeout;
this.logPrefix = (oauth === null || oauth === void 0 ? void 0 : oauth.user)
? `[MCP][${basic.serverName}][${oauth.user.id}]`
: `[MCP][${basic.serverName}]`;
if (oauth === null || oauth === void 0 ? void 0 : oauth.useOAuth) {
this.userId = (_a = oauth.user) === null || _a === void 0 ? void 0 : _a.id;
this.flowManager = oauth.flowManager;
this.tokenMethods = oauth.tokenMethods;
this.signal = oauth.signal;
this.oauthStart = oauth.oauthStart;
this.oauthEnd = oauth.oauthEnd;
this.returnOnOAuth = oauth.returnOnOAuth;
}
}
/** Creates the base MCP connection with OAuth tokens */
createConnection() {
return __awaiter(this, void 0, void 0, function* () {
const oauthTokens = this.useOAuth ? yield this.getOAuthTokens() : null;
const connection = new MCPConnection({
serverName: this.serverName,
serverConfig: this.serverConfig,
userId: this.userId,
oauthTokens,
useSSRFProtection: this.useSSRFProtection,
});
let cleanupOAuthHandlers = null;
if (this.useOAuth) {
cleanupOAuthHandlers = this.handleOAuthEvents(connection);
}
try {
yield this.attemptToConnect(connection);
if (cleanupOAuthHandlers) {
cleanupOAuthHandlers();
}
return connection;
}
catch (error) {
if (cleanupOAuthHandlers) {
cleanupOAuthHandlers();
}
throw error;
}
});
}
/** Retrieves existing OAuth tokens from storage or returns null */
getOAuthTokens() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (!((_a = this.tokenMethods) === null || _a === void 0 ? void 0 : _a.findToken))
return null;
try {
const flowId = MCPOAuthHandler.generateFlowId(this.userId, this.serverName);
const tokens = yield this.flowManager.createFlowWithHandler(flowId, 'mcp_get_tokens', () => __awaiter(this, void 0, void 0, function* () {
return yield MCPTokenStorage.getTokens({
userId: this.userId,
serverName: this.serverName,
findToken: this.tokenMethods.findToken,
createToken: this.tokenMethods.createToken,
updateToken: this.tokenMethods.updateToken,
refreshTokens: this.createRefreshTokensFunction(),
});
}), this.signal);
if (tokens)
dataSchemas.logger.info(`${this.logPrefix} Loaded OAuth tokens`);
return tokens;
}
catch (error) {
dataSchemas.logger.debug(`${this.logPrefix} No existing tokens found or error loading tokens`, error);
return null;
}
});
}
/** Creates a function to refresh OAuth tokens when they expire */
createRefreshTokensFunction() {
return (refreshToken, metadata) => __awaiter(this, void 0, void 0, function* () {
var _a;
return yield MCPOAuthHandler.refreshOAuthTokens(refreshToken, {
serverUrl: this.serverConfig.url,
serverName: metadata.serverName,
clientInfo: metadata.clientInfo,
}, (_a = this.serverConfig.oauth_headers) !== null && _a !== void 0 ? _a : {}, this.serverConfig.oauth);
});
}
/** Sets up OAuth event handlers for the connection */
handleOAuthEvents(connection) {
const oauthHandler = (data) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
dataSchemas.logger.info(`${this.logPrefix} oauthRequired event received`);
if (this.returnOnOAuth) {
try {
const config = this.serverConfig;
const flowId = MCPOAuthHandler.generateFlowId(this.userId, this.serverName);
const existingFlow = yield this.flowManager.getFlowState(flowId, 'mcp_oauth');
if ((existingFlow === null || existingFlow === void 0 ? void 0 : existingFlow.status) === 'PENDING') {
dataSchemas.logger.debug(`${this.logPrefix} PENDING OAuth flow already exists, skipping new initiation`);
connection.emit('oauthFailed', new Error('OAuth flow initiated - return early'));
return;
}
const { authorizationUrl, flowId: newFlowId, flowMetadata, } = yield MCPOAuthHandler.initiateOAuthFlow(this.serverName, data.serverUrl || '', this.userId, (_a = config === null || config === void 0 ? void 0 : config.oauth_headers) !== null && _a !== void 0 ? _a : {}, config === null || config === void 0 ? void 0 : config.oauth);
if (existingFlow) {
yield this.flowManager.deleteFlow(newFlowId, 'mcp_oauth');
}
this.flowManager.createFlow(newFlowId, 'mcp_oauth', flowMetadata, this.signal).catch(() => { });
if (this.oauthStart) {
dataSchemas.logger.info(`${this.logPrefix} OAuth flow started, issuing authorization URL`);
yield this.oauthStart(authorizationUrl);
}
connection.emit('oauthFailed', new Error('OAuth flow initiated - return early'));
return;
}
catch (error) {
dataSchemas.logger.error(`${this.logPrefix} Failed to initiate OAuth flow`, error);
connection.emit('oauthFailed', new Error('OAuth initiation failed'));
return;
}
}
// Normal OAuth handling - wait for completion
const result = yield this.handleOAuthRequired();
if ((result === null || result === void 0 ? void 0 : result.tokens) && ((_b = this.tokenMethods) === null || _b === void 0 ? void 0 : _b.createToken)) {
try {
connection.setOAuthTokens(result.tokens);
yield MCPTokenStorage.storeTokens({
userId: this.userId,
serverName: this.serverName,
tokens: result.tokens,
createToken: this.tokenMethods.createToken,
updateToken: this.tokenMethods.updateToken,
findToken: this.tokenMethods.findToken,
clientInfo: result.clientInfo,
metadata: result.metadata,
});
dataSchemas.logger.info(`${this.logPrefix} OAuth tokens saved to storage`);
}
catch (error) {
dataSchemas.logger.error(`${this.logPrefix} Failed to save OAuth tokens to storage`, error);
}
}
// Only emit oauthHandled if we actually got tokens (OAuth succeeded)
if (result === null || result === void 0 ? void 0 : result.tokens) {
connection.emit('oauthHandled');
}
else {
// OAuth failed, emit oauthFailed to properly reject the promise
dataSchemas.logger.warn(`${this.logPrefix} OAuth failed, emitting oauthFailed event`);
connection.emit('oauthFailed', new Error('OAuth authentication failed'));
}
});
connection.on('oauthRequired', oauthHandler);
return () => {
connection.removeListener('oauthRequired', oauthHandler);
};
}
/** Attempts to establish connection with timeout handling */
attemptToConnect(connection) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const connectTimeout = (_b = (_a = this.connectionTimeout) !== null && _a !== void 0 ? _a : this.serverConfig.initTimeout) !== null && _b !== void 0 ? _b : 30000;
yield withTimeout(this.connectTo(connection), connectTimeout, `Connection timeout after ${connectTimeout}ms`);
if (yield connection.isConnected())
return;
dataSchemas.logger.error(`${this.logPrefix} Failed to establish connection.`);
});
}
connectTo(connection) {
return __awaiter(this, void 0, void 0, function* () {
const maxAttempts = 3;
let attempts = 0;
while (attempts < maxAttempts) {
try {
yield connection.connect();
if (yield connection.isConnected()) {
return;
}
throw new Error('Connection attempt succeeded but status is not connected');
}
catch (error) {
attempts++;
if (this.useOAuth && this.isOAuthError(error)) {
dataSchemas.logger.info(`${this.logPrefix} OAuth required, stopping connection attempts`);
throw error;
}
if (attempts === maxAttempts) {
dataSchemas.logger.error(`${this.logPrefix} Failed to connect after ${maxAttempts} attempts`, error);
throw error;
}
yield new Promise((resolve) => setTimeout(resolve, 2000 * attempts));
}
}
});
}
// Determines if an error indicates OAuth authentication is required
isOAuthError(error) {
if (!error || typeof error !== 'object') {
return false;
}
// Check for error code
if ('code' in error) {
const code = error.code;
if (code === 401 || code === 403) {
return true;
}
}
// Check message for various auth error indicators
if ('message' in error && typeof error.message === 'string') {
const message = error.message.toLowerCase();
// Check for 401 status
if (message.includes('401') || message.includes('non-200 status code (401)')) {
return true;
}
// Check for invalid_token (OAuth servers return this for expired/revoked tokens)
if (message.includes('invalid_token')) {
return true;
}
// Check for authentication required
if (message.includes('authentication required') || message.includes('unauthorized')) {
return true;
}
}
return false;
}
/** Manages OAuth flow initiation and completion */
handleOAuthRequired() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const serverUrl = this.serverConfig.url;
dataSchemas.logger.debug(`${this.logPrefix} \`handleOAuthRequired\` called with serverUrl: ${serverUrl ? sanitizeUrlForLogging(serverUrl) : 'undefined'}`);
if (!this.flowManager || !serverUrl) {
dataSchemas.logger.error(`${this.logPrefix} OAuth required but flow manager not available or server URL missing for ${this.serverName}`);
dataSchemas.logger.warn(`${this.logPrefix} Please configure OAuth credentials for ${this.serverName}`);
return null;
}
try {
dataSchemas.logger.debug(`${this.logPrefix} Checking for existing OAuth flow for ${this.serverName}...`);
/** Flow ID to check if a flow already exists */
const flowId = MCPOAuthHandler.generateFlowId(this.userId, this.serverName);
/** Check if there's already an ongoing OAuth flow for this flowId */
const existingFlow = yield this.flowManager.getFlowState(flowId, 'mcp_oauth');
if (existingFlow) {
dataSchemas.logger.debug(`${this.logPrefix} Found existing OAuth flow (status: ${existingFlow.status}), cleaning up to start fresh`);
try {
yield this.flowManager.deleteFlow(flowId, 'mcp_oauth');
}
catch (error) {
dataSchemas.logger.warn(`${this.logPrefix} Failed to clean up existing OAuth flow`, error);
}
}
dataSchemas.logger.debug(`${this.logPrefix} Initiating new OAuth flow for ${this.serverName}...`);
const { authorizationUrl, flowId: newFlowId, flowMetadata, } = yield MCPOAuthHandler.initiateOAuthFlow(this.serverName, serverUrl, this.userId, (_a = this.serverConfig.oauth_headers) !== null && _a !== void 0 ? _a : {}, this.serverConfig.oauth);
if (typeof this.oauthStart === 'function') {
dataSchemas.logger.info(`${this.logPrefix} OAuth flow started, issued authorization URL to user`);
yield this.oauthStart(authorizationUrl);
}
else {
dataSchemas.logger.info(`${this.logPrefix} OAuth flow started, no \`oauthStart\` handler defined, relying on callback endpoint`);
}
/** Tokens from the new flow */
const tokens = yield this.flowManager.createFlow(newFlowId, 'mcp_oauth', flowMetadata, this.signal);
if (typeof this.oauthEnd === 'function') {
yield this.oauthEnd();
}
dataSchemas.logger.info(`${this.logPrefix} OAuth flow completed, tokens received for ${this.serverName}`);
/** Client information from the flow metadata */
const clientInfo = flowMetadata === null || flowMetadata === void 0 ? void 0 : flowMetadata.clientInfo;
const metadata = flowMetadata === null || flowMetadata === void 0 ? void 0 : flowMetadata.metadata;
return {
tokens,
clientInfo,
metadata,
};
}
catch (error) {
dataSchemas.logger.error(`${this.logPrefix} Failed to complete OAuth flow for ${this.serverName}`, error);
return null;
}
});
}
}
/**
* Inspects MCP servers to discover their metadata, capabilities, and tools.
* Connects to servers and populates configuration with OAuth requirements,
* server instructions, capabilities, and available tools.
*/
class MCPServerInspector {
constructor(serverName, config, connection, useSSRFProtection = false) {
this.serverName = serverName;
this.config = config;
this.connection = connection;
this.useSSRFProtection = useSSRFProtection;
}
/**
* Inspects a server and returns an enriched configuration with metadata.
* Detects OAuth requirements and fetches server capabilities.
* @param serverName - The name of the server (used for tool function naming)
* @param rawConfig - The raw server configuration
* @param connection - The MCP connection
* @param allowedDomains - Optional list of allowed domains for remote transports
* @returns A fully processed and enriched configuration with server metadata
*/
static inspect(serverName, rawConfig, connection, allowedDomains) {
return __awaiter(this, void 0, void 0, function* () {
// Validate domain against allowlist BEFORE attempting connection
const isDomainAllowed = yield isMCPDomainAllowed(rawConfig, allowedDomains);
if (!isDomainAllowed) {
const domain = extractMCPServerDomain(rawConfig);
throw new MCPDomainNotAllowedError(domain !== null && domain !== void 0 ? domain : 'unknown');
}
const useSSRFProtection = !Array.isArray(allowedDomains) || allowedDomains.length === 0;
const start = Date.now();
const inspector = new MCPServerInspector(serverName, rawConfig, connection, useSSRFProtection);
yield inspector.inspectServer();
inspector.config.initDuration = Date.now() - start;
return inspector.config;
});
}
inspectServer() {
return __awaiter(this, void 0, void 0, function* () {
yield this.detectOAuth();
if (this.config.startup !== false && !this.config.requiresOAuth) {
let tempConnection = false;
if (!this.connection) {
tempConnection = true;
this.connection = yield MCPConnectionFactory.create({
serverName: this.serverName,
serverConfig: this.config,
useSSRFProtection: this.useSSRFProtection,
});
}
yield Promise.allSettled([
this.fetchServerInstructions(),
this.fetchServerCapabilities(),
this.fetchToolFunctions(),
]);
if (tempConnection)
yield this.connection.disconnect();
}
});
}
detectOAuth() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.config.requiresOAuth != null)
return;
if (this.config.url == null || this.config.startup === false) {
this.config.requiresOAuth = false;
return;
}
// Admin-provided API key means no OAuth flow is needed
if (((_a = this.config.apiKey) === null || _a === void 0 ? void 0 : _a.source) === 'admin') {
this.config.requiresOAuth = false;
return;
}
const result = yield detectOAuthRequirement(this.config.url);
this.config.requiresOAuth = result.requiresOAuth;
this.config.oauthMetadata = result.metadata;
});
}
fetchServerInstructions() {
return __awaiter(this, void 0, void 0, function* () {
if (isEnabled(this.config.serverInstructions)) {
this.config.serverInstructions = this.connection.client.getInstructions();
}
});
}
fetchServerCapabilities() {
return __awaiter(this, void 0, void 0, function* () {
const capabilities = this.connection.client.getServerCapabilities();
this.config.capabilities = JSON.stringify(capabilities);
const tools = yield this.connection.client.listTools();
this.config.tools = tools.tools.map((tool) => tool.name).join(', ');
});
}
fetchToolFunctions() {
return __awaiter(this, void 0, void 0, function* () {
this.config.toolFunctions = yield MCPServerInspector.getToolFunctions(this.serverName, this.connection);
});
}
/**
* Converts server tools to LibreChat-compatible tool functions format.
* @param serverName - The name of the server
* @param connection - The MCP connection
* @returns Tool functions formatted for LibreChat
*/
static getToolFunctions(serverName, connection) {
return __awaiter(this, void 0, void 0, function* () {
const { tools } = yield connection.client.listTools();
const toolFunctions = {};
tools.forEach((tool) => {
const name = `${tool.name}${librechatDataProvider.Constants.mcp_delimiter}${serverName}`;
toolFunctions[name] = {
type: 'function',
['function']: {
name,
description: tool.description,
parameters: tool.inputSchema,
},
};
});
return toolFunctions;
});
}
}
class AccessControlService {
constructor(mongoose) {
this._dbMethods = dataSchemas.createMethods(mongoose);
this._aclModel = mongoose.models.AclEntry;
}
/**
* Grant a permission to a principal for a resource using a role
* @param {Object} params - Parameters for granting role-based permission
* @param {string} params.principalType - PrincipalType.USER, PrincipalType.GROUP, or PrincipalType.PUBLIC
* @param {string|mongoose.Types.ObjectId|null} params.principalId - The ID of the principal (null for PrincipalType.PUBLIC)
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
* @param {string} params.accessRoleId - The ID of the role (e.g., AccessRoleIds.AGENT_VIEWER, AccessRoleIds.AGENT_EDITOR)
* @param {Types.ObjectId} params.grantedBy - User ID granting the permission
* @param {ClientSession} [params.session] - Optional MongoDB session for transactions
* @returns {Promise<IAclEntry>} The created or updated ACL entry
*/
grantPermission(args) {
return __awaiter(this, void 0, void 0, function* () {
const { principalType, principalId, resourceType, resourceId, accessRoleId, grantedBy, session, } = args;
try {
if (!Object.values(librechatDataProvider.PrincipalType).includes(principalType)) {
throw new Error(`Invalid principal type: ${principalType}`);
}
if (principalType !== librechatDataProvider.PrincipalType.PUBLIC && !principalId) {
throw new Error('Principal ID is required for user, group, and role principals');
}
// Validate principalId based on type
if (principalId && principalType === librechatDataProvider.PrincipalType.ROLE) {
// Role IDs are strings (role names)
if (typeof principalId !== 'string' || principalId.trim().length === 0) {
throw new Error(`Invalid role ID: ${principalId}`);
}
}
else if (principalType &&
principalType !== librechatDataProvider.PrincipalType.PUBLIC &&
(!principalId || !mongoose.Types.ObjectId.isValid(principalId))) {
// User and Group IDs must be valid ObjectIds
throw new Error(`Invalid principal ID: ${principalId}`);
}
if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) {
throw new Error(`Invalid resource ID: ${resourceId}`);
}
this.validateResourceType(resourceType);
// Get the role to determine permission bits
const role = yield this._dbMethods.findRoleByIdentifier(accessRoleId);
if (!role) {
throw new Error(`Role ${accessRoleId} not found`);
}
// Ensure the role is for the correct resource type
if (role.resourceType !== resourceType) {
throw new Error(`Role ${accessRoleId} is for ${role.resourceType} resources, not ${resourceType}`);
}
return yield this._dbMethods.grantPermission(principalType, principalId, resourceType, resourceId, role.permBits, grantedBy, session, role._id);
}
catch (error) {
dataSchemas.logger.error(`[PermissionService.grantPermission] Error: ${error instanceof Error ? error.message : ''}`, error);
throw error;
}
});
}
/**
* Find all resources of a specific type that a user has access to with specific permission bits
* @param {Object} params - Parameters for finding accessible resources
* @param {string | Types.ObjectId} params.userId - The ID of the user
* @param {string} [params.role] - Optional user role (if not provided, will query from DB)
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
* @param {number} params.requiredPermissions - The minimum permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
* @returns {Promise<Array>} Array of resource IDs
*/
findAccessibleResources(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, role, resourceType, requiredPermissions, }) {
try {
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
throw new Error('requiredPermissions must be a positive number');
}
this.validateResourceType(resourceType);
// Get all principals for the user (user + groups + public)
const principalsList = yield this._dbMethods.getUserPrincipals({ userId, role });
if (principalsList.length === 0) {
return [];
}
return yield this._dbMethods.findAccessibleResources(principalsList, resourceType, requiredPermissions);
}
catch (error) {
if (error instanceof Error) {
dataSchemas.logger.error(`[PermissionService.findAccessibleResources] Error: ${error.message}`);
// Re-throw validation errors
if (error.message.includes('requiredPermissions must be')) {
throw error;
}
}
return [];
}
});
}
/**
* Find all publicly accessible resources of a specific type
* @param {Object} params - Parameters for finding publicly accessible resources
* @param {ResourceType} params.resourceType - Type of resource (e.g., 'agent')
* @param {number} params.requiredPermissions - The minimum permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
* @returns {Promise<Types.ObjectId[]>} Array of resource IDs
*/
findPubliclyAccessibleResources(_a) {
return __awaiter(this, arguments, void 0, function* ({ resourceType, requiredPermissions, }) {
try {
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
throw new Error('requiredPermissions must be a positive number');
}
this.validateResourceType(resourceType);
// Find all public ACL entries where the public principal has at least the required permission bits
const entries = yield this._aclModel
.find({
principalType: librechatDataProvider.PrincipalType.PUBLIC,
resourceType,
permBits: { $bitsAllSet: requiredPermissions },
})
.distinct('resourceId');
return entries;
}
catch (error) {
if (error instanceof Error) {
dataSchemas.logger.error(`[PermissionService.findPubliclyAccessibleResources] Error: ${error.message}`);
// Re-throw validation errors
if (error.message.includes('requiredPermissions must be')) {
throw error;
}
}
return [];
}
});
}
/**
* Get effective permissions for multiple resources in a batch operation
* Returns map of resourceId → effectivePermissionBits
*
* @param {Object} params - Parameters
* @param {string|mongoose.Types.ObjectId} params.userId - User ID
* @param {string} [params.role] - User role (for group membership)
* @param {string} params.resourceType - Resource type (must be valid ResourceType)
* @param {Array<mongoose.Types.ObjectId>} params.resourceIds - Array of resource IDs
* @returns {Promise<Map<string, number>>} Map of resourceId string → permission bits
* @throws {Error} If resourceType is invalid
*/
getResourcePermissionsMap(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, role, resourceType, resourceIds, }) {
// Validate resource type - throw on invalid type
this.validateResourceType(resourceType);
// Handle empty input
if (!Array.isArray(resourceIds) || resourceIds.length === 0) {
return new Map();
}
try {
// Get user principals (user + groups + public)
const principals = yield this._dbMethods.getUserPrincipals({ userId, role });
// Use batch method from aclEntry
const permissionsMap = yield this._dbMethods.getEffectivePermissionsForResources(principals, resourceType, resourceIds);
dataSchemas.logger.debug(`[PermissionService.getResourcePermissionsMap] Computed permissions for ${resourceIds.length} resources, ${permissionsMap.size} have permissions`);
return permissionsMap;
}
catch (error) {
if (error instanceof Error) {
dataSchemas.logger.error(`[PermissionService.getResourcePermissionsMap] Error: ${error.message}`, error);
}
throw error;
}
});
}
/**
* Remove all permissions for a resource (cleanup when resource is deleted)
* @param {Object} params - Parameters for removing all permissions
* @param {string} params.resourceType - Type of resource (e.g., 'agent', 'prompt')
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
* @returns {Promise<DeleteResult>} Result of the deletion operation
*/
removeAllPermissions(_a) {
return __awaiter(this, arguments, void 0, function* ({ resourceType, resourceId, }) {
try {
this.validateResourceType(resourceType);
if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) {
throw new Error(`Invalid resource ID: ${resourceId}`);
}
const result = yield this._aclModel.deleteMany({
resourceType,
resourceId,
});
return result;
}
catch (error) {
if (error instanceof Error) {
dataSchemas.logger.error(`[PermissionService.removeAllPermissions] Error: ${error.message}`);
}
throw error;
}
});
}
/**
* Check if a user has specific permission bits on a resource
* @param {Object} params - Parameters for checking permissions
* @param {string|mongoose.Types.ObjectId} params.userId - The ID of the user
* @param {string} [params.role] - Optional user role (if not provided, will query from DB)
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
* @param {number} params.requiredPermissions - The permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
* @returns {Promise<boolean>} Whether the user has the required permission bits
*/
checkPermission(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, role, resourceType, resourceId, requiredPermission, }) {
try {
if (typeof requiredPermission !== 'number' || requiredPermission < 1) {
throw new Error('requiredPermission must be a positive number');
}
this.validateResourceType(resourceType);
// Get all principals for the user (user + groups + public)
const principals = yield this._dbMethods.getUserPrincipals({ userId, role });
if (principals.length === 0) {
return false;
}
return yield this._dbMethods.hasPermission(principals, resourceType, resourceId, requiredPermission);
}
catch (error) {
if (error instanceof Error) {
dataSchemas.logger.error(`[PermissionService.checkPermission] Error: ${error.message}`);
// Re-throw validation errors
if (error.message.includes('requiredPermission must be')) {
throw error;
}
}
return false;
}
});
}
/**
* Validates that the resourceType is one of the supported enum values
* @param {string} resourceType - The resource type to validate
* @throws {Error} If resourceType is not valid
*/
validateResourceType(resourceType) {
const validTypes = Object.values(librechatDataProvider.ResourceType);
if (!validTypes.includes(resourceType)) {
throw new Error(`Invalid resourceType: ${resourceType}. Valid types: ${validTypes.join(', ')}`);
}
}
}
/**
* Regex patterns for credential placeholders that should not be allowed in user-provided headers.
* These placeholders would substitute the CALLING user's credentials, creating a security risk
* when MCP servers are shared between users (credential exfiltration).
*
* Safe placeholders like {{MCP_API_KEY}} are allowed as they resolve from the user's own plugin auth.
*/
const DANGEROUS_CREDENTIAL_PATTERNS = [
/\{\{LIBRECHAT_OPENID_[^}]+\}\}/g,
/\{\{LIBRECHAT_USER_[^}]+\}\}/g,
];
/**
* Sanitizes headers by removing dangerous credential placeholders.
* This prevents credential exfiltration when MCP servers are shared between users.
*
* @param headers - The headers object to sanitize
* @returns Sanitized headers with dangerous placeholders removed
*/
function sanitizeCredentialPlaceholders(headers) {
if (!headers) {
return headers;
}
const sanitized = {};
for (const [key, value] of Object.entries(headers)) {
let sanitizedValue = value;
for (const pattern of DANGEROUS_CREDENTIAL_PATTERNS) {
sanitizedValue = sanitizedValue.replace(pattern, '');
}
sanitized[key] = sanitizedValue;
}
return sanitized;
}
/**
* DB backed config storage
* Handles CRUD Methods of dynamic mcp servers
* Will handle Permission ACL
*/
class ServerConfigsDB {
constructor(mongoose) {
if (!mongoose) {
throw new Error('ServerConfigsDB requires mongoose instance');
}
this._mongoose = mongoose;
this._dbMethods = dataSchemas.createMethods(mongoose);
this._aclService = new AccessControlService(mongoose);
}
/**
* Checks if user has access to an MCP server via an agent they can VIEW.
* @param serverName - The MCP server name to check
* @param userId - The user ID (optional - if not provided, checks publicly accessible agents)
* @returns true if user has VIEW access to at least one agent that has this MCP server
*/
hasAccessViaAgent(serverName, userId) {
return __awaiter(this, void 0, void 0, function* () {
let accessibleAgentIds;
if (!userId) {
/** Publicly accessible agents */
accessibleAgentIds = yield this._aclService.findPubliclyAccessibleResources({
resourceType: librechatDataProvider.ResourceType.AGENT,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
});
}
else {
/** User-accessible agents */
accessibleAgentIds = yield this._aclService.findAccessibleResources({
userId,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
resourceType: librechatDataProvider.ResourceType.AGENT,
});
}
if (accessibleAgentIds.length === 0) {
return false;
}
const Agent = this._mongoose.model('Agent');
const exists = yield Agent.exists({
_id: { $in: accessibleAgentIds },
mcpServerNames: serverName,
});
return exists !== null;
});
}
/**
* Creates a new MCP server and grants owner permissions to the user.
* @param serverName - Temporary server name (not persisted) will be replaced by the nano id generated by the db method
* @param config - Server configuration to store
* @param userId - ID of the user creating the server (required)
* @returns The created server result with serverName and config (including dbId)
* @throws Error if userId is not provided
*/
add(serverName, config, userId) {
return __awaiter(this, void 0, void 0, function* () {
dataSchemas.logger.debug(`[ServerConfigsDB.add] Starting Creating server with temp servername: ${serverName} for the user with the ID ${userId}`);
if (!userId) {
throw new Error('[ServerConfigsDB.add] User ID is required to create a database-stored MCP server.');
}
const sanitizedConfig = Object.assign(Object.assign({}, config), { headers: sanitizeCredentialPlaceholders(config.headers) });
/** Transformed user-provided API key config (adds customUserVars and headers) */
const transformedConfig = this.transformUserApiKeyConfig(sanitizedConfig);
/** Encrypted config before storing in database */
const encryptedConfig = yield this.encryptConfig(transformedConfig);
const createdServer = yield this._dbMethods.createMCPServer({
config: encryptedConfig,
author: userId,
});
yield this._aclService.grantPermission({
principalType: librechatDataProvider.PrincipalType.USER,
principalId: userId,
resourceType: librechatDataProvider.ResourceType.MCPSERVER,
resourceId: createdServer._id,
accessRoleId: librechatDataProvider.AccessRoleIds.MCPSERVER_OWNER,
grantedBy: userId,
});
return {
serverName: createdServer.serverName,
config: yield this.mapDBServerToParsedConfig(createdServer),
};
});
}
/**
*
* @param serverName mcp server unique identifier "serverName"
* @param config new Configuration to update
* @param userId user id required to update DB server config
*/
update(serverName, config, userId) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
if (!userId) {
throw new Error('[ServerConfigsDB.update] User ID is required to update a database-stored MCP server.');
}
const existingServer = yield this._dbMethods.findMCPServerByServerName(serverName);
let configToSave = Object.assign(Object.assign({}, config), { headers: sanitizeCredentialPlaceholders(config.headers) });
/** Transformed user-provided API key config (adds customUserVars and headers) */
configToSave = this.transformUserApiKeyConfig(configToSave);
/** Encrypted config before storing in database */
configToSave = yield this.encryptConfig(configToSave);
if (!((_a = config.oauth) === null || _a === void 0 ? void 0 : _a.client_secret) && ((_c = (_b = existingServer === null || existingServer === void 0 ? void 0 : existingServer.config) === null || _b === void 0 ? void 0 : _b.oauth) === null || _c === void 0 ? void 0 : _c.client_secret)) {
configToSave = Object.assign(Object.assign({}, configToSave), { oauth: Object.assign(Object.assign({}, configToSave.oauth), { client_secret: existingServer.config.oauth.client_secret }) });
}
if (((_d = config.apiKey) === null || _d === void 0 ? void 0 : _d.source) === 'admin' &&
!((_e = config.apiKey) === null || _e === void 0 ? void 0 : _e.key) &&
((_g = (_f = existingServer === null || existingServer === void 0 ? void 0 : existingServer.config) === null || _f === void 0 ? void 0 : _f.apiKey) === null || _g === void 0 ? void 0 : _g.source) === 'admin' &&
((_j = (_h = existingServer === null || existingServer === void 0 ? void 0 : existingServer.config) === null || _h === void 0 ? void 0 : _h.apiKey) === null || _j === void 0 ? void 0 : _j.key)) {
configToSave = Object.assign(Object.assign({}, configToSave), { apiKey: {
source: configToSave.apiKey.source,
authorization_type: configToSave.apiKey.authorization_type,
custom_header: (_k = configToSave.apiKey) === null || _k === void 0 ? void 0 : _k.custom_header,
key: existingServer.config.apiKey.key,
} });
}
yield this._dbMethods.updateMCPServer(serverName, { config: configToSave });
});
}
/**
* Deletes an MCP server and removes all associated ACL entries.
* @param serverName - The serverName of the server to remove
* @param userId - User performing the deletion (for logging)
*/
remove(serverName, userId) {
return __awaiter(this, void 0, void 0, function* () {
dataSchemas.logger.debug(`[ServerConfigsDB.remove] removing ${serverName}. UserId: ${userId}`);
const deletedServer = yield this._dbMethods.deleteMCPServer(serverName);
if (deletedServer && deletedServer._id) {
dataSchemas.logger.debug(`[ServerConfigsDB.remove] removing all permissions entries of ${serverName}.`);
yield this._aclService.removeAllPermissions({
resourceType: librechatDataProvider.ResourceType.MCPSERVER,
resourceId: deletedServer._id,
});
return;
}
dataSchemas.logger.warn(`[ServerConfigsDB.remove] server with serverName ${serverName} does not exist`);
});
}
/**
* Retrieves a single MCP server configuration by its serverName.
* @param serverName - The serverName of the server to retrieve
* @param userId - the user id provide the scope of the request. If the user Id is not provided, only publicly visible servers are returned.
* @returns The parsed server config or undefined if not found. If accessed via agent, consumeOnly will be true.
*/
get(serverName, userId) {
return __awaiter(this, void 0, void 0, function* () {
const server = yield this._dbMethods.findMCPServerByServerName(serverName);
if (!server)
return undefined;
if (!userId) {
const directlyAccessibleMCPIds = (yield this._aclService.findPubliclyAccessibleResources({
resourceType: librechatDataProvider.ResourceType.MCPSERVER,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
})).map((id) => id.toString());
if (directlyAccessibleMCPIds.indexOf(server._id.toString()) > -1) {
return yield this.mapDBServerToParsedConfig(server);
}
const hasAgentAccess = yield this.hasAccessViaAgent(serverName);
if (hasAgentAccess) {
dataSchemas.logger.debug(`[ServerConfigsDB.get] accessing ${serverName} via public agent (consumeOnly)`);
return Object.assign(Object.assign({}, (yield this.mapDBServerToParsedConfig(server))), { consumeOnly: true });
}
return undefined;
}
const userHasDirectAccess = yield this._aclService.checkPermission({
userId,
resourceType: librechatDataProvider.ResourceType.MCPSERVER,
requiredPermission: librechatDataProvider.PermissionBits.VIEW,
resourceId: server._id,
});
if (userHasDirectAccess) {
dataSchemas.logger.debug(`[ServerConfigsDB.get] getting ${serverName} for user with the UserId: ${userId}`);
return yield this.mapDBServerToParsedConfig(server);
}
/** Check agent access (user can VIEW an agent that has this MCP server) */
const hasAgentAccess = yield this.hasAccessViaAgent(serverName, userId);
if (hasAgentAccess) {
dataSchemas.logger.debug(`[ServerConfigsDB.get] user ${userId} accessing ${serverName} via agent (consumeOnly)`);
return Object.assign(Object.assign({}, (yield this.mapDBServerToParsedConfig(server))), { consumeOnly: true });
}
return undefined;
});
}
/**
* Return all DB stored configs (scoped by user Id if provided)
* @param userId optional user id. if not provided only publicly shared mcp configs will be returned
* @returns record of parsed configs
*/
getAll(userId) {
return __awaiter(this, void 0, void 0, function* () {
let directlyAccessibleMCPIds = [];
if (!userId) {
dataSchemas.logger.debug(`[ServerConfigsDB.getAll] fetching all publicly shared mcp servers`);
directlyAccessibleMCPIds = yield this._aclService.findPubliclyAccessibleResources({
resourceType: librechatDataProvider.ResourceType.MCPSERVER,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
});
}
else {
dataSchemas.logger.debug(`[ServerConfigsDB.getAll] fetching mcp servers directly shared with the user with ID: ${userId}`);
directlyAccessibleMCPIds = yield this._aclService.findAccessibleResources({
userId,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
resourceType: librechatDataProvider.ResourceType.MCPSERVER,
});
}
let agentMCPServerNames = [];
let accessibleAgentIds = [];
if (!userId) {
accessibleAgentIds = yield this._aclService.findPubliclyAccessibleResources({
resourceType: librechatDataProvider.ResourceType.AGENT,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
});
}
else {
accessibleAgentIds = yield this._aclService.findAccessibleResources({
userId,
requiredPermissions: librechatDataProvider.PermissionBits.VIEW,
resourceType: librechatDataProvider.ResourceType.AGENT,
});
}
if (accessibleAgentIds.length > 0) {
const Agent = this._mongoose.model('Agent');
const agentsWithMCP = yield Agent.find({
_id: { $in: accessibleAgentIds },
mcpServerNames: { $exists: true, $not: { $size: 0 } },
}, { mcpServerNames: 1 }).lean();
agentMCPServerNames = [
...new Set(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agentsWithMCP.flatMap((a) => a.mcpServerNames || [])),
];
}
const directResults = yield this._dbMethods.getListMCPServersByIds({
ids: directlyAccessibleMCPIds,
});
const parsedConfigs = {};
const directData = directResults.data || [];
const directServerNames = new Set(directData.map((s) => s.serverName));
const directParsed = yield Promise.all(directData.map((s) => this.mapDBServerToParsedConfig(s)));
directData.forEach((s, i) => {
parsedConfigs[s.serverName] = directParsed[i];
});
const agentOnlyServerNames = agentMCPServerNames.filter((name) => !directServerNames.has(name));
if (agentOnlyServerNames.length > 0) {
const agentServers = yield this._dbMethods.getListMCPServersByNames({
names: agentOnlyServerNames,
});
const agentData = agentServers.data || [];
const agentParsed = yield Promise.all(agentData.map((s) => this.mapDBServerToParsedConfig(s)));
agentData.forEach((s, i) => {
parsedConfigs[s.serverName] = Object.assign(Object.assign({}, agentParsed[i]), { consumeOnly: true });
});
}
return parsedConfigs;
});
}
/** No-op for DB storage; logs a warning if called. */
reset() {
return __awaiter(this, void 0, void 0, function* () {
dataSchemas.logger.warn('Attempt to reset the DB config storage');
return;
});
}
/**
* Maps a MongoDB server document to the ParsedServerConfig format.
* Decrypts sensitive fields (oauth.client_secret) after retrieval.
*/
mapDBServerToParsedConfig(serverDBDoc) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const config = Object.assign(Object.assign({}, serverDBDoc.config), { dbId: serverDBDoc._id.toString(), updatedAt: (_a = serverDBDoc.updatedAt) === null || _a === void 0 ? void 0 : _a.getTime() });
return yield this.decryptConfig(config);
});
}
/**
* Transforms user-provided API key config by auto-generating customUserVars and headers.
* This is a config transformation, not encryption.
* @param config - The server config to transform
* @returns The transformed config with customUserVars and headers set up
*/
transformUserApiKeyConfig(config) {
if (!config.apiKey || config.apiKey.source !== 'user') {
return config;
}
const result = Object.assign({}, config);
const headerName = result.apiKey.authorization_type === 'custom'
? result.apiKey.custom_header || 'X-Api-Key'
: 'Authorization';
let headerValue;
if (result.apiKey.authorization_type === 'basic') {
headerValue = 'Basic {{MCP_API_KEY}}';
}
else if (result.apiKey.authorization_type === 'bearer') {
headerValue = 'Bearer {{MCP_API_KEY}}';
}
else {
headerValue = '{{MCP_API_KEY}}';
}
result.customUserVars = Object.assign(Object.assign({}, result.customUserVars), { MCP_API_KEY: {
title: 'API Key',
description: 'Your API key for this MCP server',
} });
/** Cast to access headers property (not available on Stdio type) */
const resultWithHeaders = result;
resultWithHeaders.headers = Object.assign(Object.assign({}, resultWithHeaders.headers), { [headerName]: headerValue });
// Remove key field since it's user-provided (destructure to omit, not set to undefined)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _a = result.apiKey, { key: _removed } = _a, apiKeyWithoutKey = __rest(_a, ["key"]);
result.apiKey = apiKeyWithoutKey;
return result;
}
/**
* Encrypts sensitive fields in config before database storage.
* Encrypts oauth.client_secret and apiKey.key (when source === 'admin').
* Throws on failure to prevent storing plaintext secrets.
*/
encryptConfig(config) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
let result = Object.assign({}, config);
if (((_a = result.apiKey) === null || _a === void 0 ? void 0 : _a.source) === 'admin' && result.apiKey.key) {
try {
result.apiKey = Object.assign(Object.assign({}, result.apiKey), { key: yield dataSchemas.encryptV2(result.apiKey.key) });
}
catch (error) {
dataSchemas.logger.error('[ServerConfigsDB.encryptConfig] Failed to encrypt apiKey.key', error);
throw new Error('Failed to encrypt MCP server configuration');
}
}
if ((_b = result.oauth) === null || _b === void 0 ? void 0 : _b.client_secret) {
try {
result = Object.assign(Object.assign({}, result), { oauth: Object.assign(Object.assign({}, result.oauth), { client_secret: yield dataSchemas.encryptV2(result.oauth.client_secret) }) });
}
catch (error) {
dataSchemas.logger.error('[ServerConfigsDB.encryptConfig] Failed to encrypt client_secret', error);
throw new Error('Failed to encrypt MCP server configuration');
}
}
return result;
});
}
/**
* Decrypts sensitive fields in config after database retrieval.
* Decrypts oauth.client_secret and apiKey.key (when source === 'admin').
* Returns config without secret on failure (graceful degradation).
*/
decryptConfig(config) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
let result = Object.assign({}, config);
if (((_a = result.apiKey) === null || _a === void 0 ? void 0 : _a.source) === 'admin' && result.apiKey.key) {
try {
result.apiKey = Object.assign(Object.assign({}, result.apiKey), { key: yield dataSchemas.decryptV2(result.apiKey.key) });
}
catch (error) {
dataSchemas.logger.warn('[ServerConfigsDB.decryptConfig] Failed to decrypt apiKey.key, returning config without key', error);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _c = result.apiKey, { key: _removedKey } = _c, apiKeyWithoutKey = __rest(_c, ["key"]);
result.apiKey = apiKeyWithoutKey;
}
}
if ((_b = result.oauth) === null || _b === void 0 ? void 0 : _b.client_secret) {
const oauthConfig = result.oauth;
try {
result = Object.assign(Object.assign({}, result), { oauth: Object.assign(Object.assign({}, oauthConfig), { client_secret: yield dataSchemas.decryptV2(oauthConfig.client_secret) }) });
}
catch (error) {
dataSchemas.logger.warn('[ServerConfigsDB.decryptConfig] Failed to decrypt client_secret, returning config without secret', error);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { client_secret: _removed } = oauthConfig, oauthWithoutSecret = __rest(oauthConfig, ["client_secret"]);
result = Object.assign(Object.assign({}, result), { oauth: oauthWithoutSecret });
}
}
return result;
});
}
}
/**
* Central registry for managing MCP server configurations.
* Authoritative source of truth for all MCP servers provided by LibreChat.
*
* Uses a two-repository architecture:
* - Cache Repository: Stores YAML-defined configs loaded at startup (in-memory or Redis-backed)
* - DB Repository: Stores dynamic configs created at runtime (not yet implemented)
*
* Query priority: Cache configs are checked first, then DB configs.
*/
class MCPServersRegistry {
constructor(mongoose, allowedDomains) {
this.pendingGetAllPromises = new Map();
this.dbConfigsRepo = new ServerConfigsDB(mongoose);
this.cacheConfigsRepo = ServerConfigsCacheFactory.create('App', false);
this.allowedDomains = allowedDomains;
const ttl = cacheConfig.MCP_REGISTRY_CACHE_TTL;
this.readThroughCache = new keyv.Keyv({
namespace: 'mcp-registry-read-through',
ttl,
});
this.readThroughCacheAll = new keyv.Keyv({
namespace: 'mcp-registry-read-through-all',
ttl,
});
}
/** Creates and initializes the singleton MCPServersRegistry instance */
static createInstance(mongoose, allowedDomains) {
if (!mongoose) {
throw new Error('MCPServersRegistry creation failed: mongoose instance is required for database operations. ' +
'Ensure mongoose is initialized before creating the registry.');
}
if (MCPServersRegistry.instance) {
dataSchemas.logger.debug('[MCPServersRegistry] Returning existing instance');
return MCPServersRegistry.instance;
}
dataSchemas.logger.info('[MCPServersRegistry] Creating new instance');
MCPServersRegistry.instance = new MCPServersRegistry(mongoose, allowedDomains);
return MCPServersRegistry.instance;
}
/** Returns the singleton MCPServersRegistry instance */
static getInstance() {
if (!MCPServersRegistry.instance) {
throw new Error('MCPServersRegistry has not been initialized.');
}
return MCPServersRegistry.instance;
}
getAllowedDomains() {
return this.allowedDomains;
}
/** Returns true when no explicit allowedDomains allowlist is configured, enabling SSRF TOCTOU protection */
shouldEnableSSRFProtection() {
return !Array.isArray(this.allowedDomains) || this.allowedDomains.length === 0;
}
getServerConfig(serverName, userId) {
return __awaiter(this, void 0, void 0, function* () {
const cacheKey = this.getReadThroughCacheKey(serverName, userId);
if (yield this.readThroughCache.has(cacheKey)) {
return yield this.readThroughCache.get(cacheKey);
}
// First we check if any config exist with the cache
// Yaml config are pre loaded to the cache
const configFromCache = yield this.cacheConfigsRepo.get(serverName);
if (configFromCache) {
yield this.readThroughCache.set(cacheKey, configFromCache);
return configFromCache;
}
const configFromDB = yield this.dbConfigsRepo.get(serverName, userId);
yield this.readThroughCache.set(cacheKey, configFromDB);
return configFromDB;
});
}
getAllServerConfigs(userId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const cacheKey = userId !== null && userId !== void 0 ? userId : '__no_user__';
if (yield this.readThroughCacheAll.has(cacheKey)) {
return (_a = (yield this.readThroughCacheAll.get(cacheKey))) !== null && _a !== void 0 ? _a : {};
}
const pending = this.pendingGetAllPromises.get(cacheKey);
if (pending) {
return pending;
}
const fetchPromise = this.fetchAllServerConfigs(cacheKey, userId);
this.pendingGetAllPromises.set(cacheKey, fetchPromise);
try {
return yield fetchPromise;
}
finally {
this.pendingGetAllPromises.delete(cacheKey);
}
});
}
fetchAllServerConfigs(cacheKey, userId) {
return __awaiter(this, void 0, void 0, function* () {
const result = Object.assign(Object.assign({}, (yield this.cacheConfigsRepo.getAll())), (yield this.dbConfigsRepo.getAll(userId)));
yield this.readThroughCacheAll.set(cacheKey, result);
return result;
});
}
addServer(serverName, config, storageLocation, userId) {
return __awaiter(this, void 0, void 0, function* () {
const configRepo = this.getConfigRepository(storageLocation);
let parsedConfig;
try {
parsedConfig = yield MCPServerInspector.inspect(serverName, config, undefined, this.allowedDomains);
}
catch (error) {
dataSchemas.logger.error(`[MCPServersRegistry] Failed to inspect server "${serverName}":`, error);
// Preserve domain-specific error for better error handling
if (isMCPDomainNotAllowedError(error)) {
throw error;
}
throw new MCPInspectionFailedError(serverName, error);
}
return yield configRepo.add(serverName, parsedConfig, userId);
});
}
updateServer(serverName, config, storageLocation, userId) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const configRepo = this.getConfigRepository(storageLocation);
// Merge existing admin API key if not provided in update (needed for inspection)
let configForInspection = Object.assign({}, config);
if (((_a = config.apiKey) === null || _a === void 0 ? void 0 : _a.source) === 'admin' && !((_b = config.apiKey) === null || _b === void 0 ? void 0 : _b.key)) {
const existingConfig = yield configRepo.get(serverName, userId);
if ((_c = existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.apiKey) === null || _c === void 0 ? void 0 : _c.key) {
configForInspection = Object.assign(Object.assign({}, configForInspection), { apiKey: Object.assign(Object.assign({}, configForInspection.apiKey), { key: existingConfig.apiKey.key }) });
}
}
let parsedConfig;
try {
parsedConfig = yield MCPServerInspector.inspect(serverName, configForInspection, undefined, this.allowedDomains);
}
catch (error) {
dataSchemas.logger.error(`[MCPServersRegistry] Failed to inspect server "${serverName}":`, error);
// Preserve domain-specific error for better error handling
if (isMCPDomainNotAllowedError(error)) {
throw error;
}
throw new MCPInspectionFailedError(serverName, error);
}
yield configRepo.update(serverName, parsedConfig, userId);
return parsedConfig;
});
}
// TODO: This is currently used to determine if a server requires OAuth. However, this info can
// can be determined through config.requiresOAuth. Refactor usages and remove this method.
getOAuthServers(userId) {
return __awaiter(this, void 0, void 0, function* () {
const allServers = yield this.getAllServerConfigs(userId);
const oauthServers = Object.entries(allServers).filter(([, config]) => config.requiresOAuth);
return new Set(oauthServers.map(([name]) => name));
});
}
reset() {
return __awaiter(this, void 0, void 0, function* () {
yield this.cacheConfigsRepo.reset();
yield this.readThroughCache.clear();
yield this.readThroughCacheAll.clear();
});
}
removeServer(serverName, storageLocation, userId) {
return __awaiter(this, void 0, void 0, function* () {
const configRepo = this.getConfigRepository(storageLocation);
yield configRepo.remove(serverName, userId);
});
}
getConfigRepository(storageLocation) {
switch (storageLocation) {
case 'CACHE':
return this.cacheConfigsRepo;
case 'DB':
return this.dbConfigsRepo;
default:
throw new Error(`MCPServersRegistry: The provided storage location "${storageLocation}" is not supported`);
}
}
getReadThroughCacheKey(serverName, userId) {
return userId ? `${serverName}::${userId}` : serverName;
}
}
/**
* Checks if `value` is classified as an `Array` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array, else `false`.
* @example
*
* _.isArray([1, 2, 3]);
* // => true
*
* _.isArray(document.body.children);
* // => false
*
* _.isArray('abc');
* // => false
*
* _.isArray(_.noop);
* // => false
*/
var isArray_1;
var hasRequiredIsArray;
function requireIsArray () {
if (hasRequiredIsArray) return isArray_1;
hasRequiredIsArray = 1;
var isArray = Array.isArray;
isArray_1 = isArray;
return isArray_1;
}
/** Detect free variable `global` from Node.js. */
var _freeGlobal;
var hasRequired_freeGlobal;
function require_freeGlobal () {
if (hasRequired_freeGlobal) return _freeGlobal;
hasRequired_freeGlobal = 1;
var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
_freeGlobal = freeGlobal;
return _freeGlobal;
}
var _root;
var hasRequired_root;
function require_root () {
if (hasRequired_root) return _root;
hasRequired_root = 1;
var freeGlobal = require_freeGlobal();
/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
_root = root;
return _root;
}
var _Symbol;
var hasRequired_Symbol;
function require_Symbol () {
if (hasRequired_Symbol) return _Symbol;
hasRequired_Symbol = 1;
var root = require_root();
/** Built-in value references. */
var Symbol = root.Symbol;
_Symbol = Symbol;
return _Symbol;
}
var _getRawTag;
var hasRequired_getRawTag;
function require_getRawTag () {
if (hasRequired_getRawTag) return _getRawTag;
hasRequired_getRawTag = 1;
var Symbol = require_Symbol();
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString = objectProto.toString;
/** Built-in value references. */
var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
/**
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the raw `toStringTag`.
*/
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];
try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
_getRawTag = getRawTag;
return _getRawTag;
}
/** Used for built-in method references. */
var _objectToString;
var hasRequired_objectToString;
function require_objectToString () {
if (hasRequired_objectToString) return _objectToString;
hasRequired_objectToString = 1;
var objectProto = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString = objectProto.toString;
/**
* Converts `value` to a string using `Object.prototype.toString`.
*
* @private
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
*/
function objectToString(value) {
return nativeObjectToString.call(value);
}
_objectToString = objectToString;
return _objectToString;
}
var _baseGetTag;
var hasRequired_baseGetTag;
function require_baseGetTag () {
if (hasRequired_baseGetTag) return _baseGetTag;
hasRequired_baseGetTag = 1;
var Symbol = require_Symbol(),
getRawTag = require_getRawTag(),
objectToString = require_objectToString();
/** `Object#toString` result references. */
var nullTag = '[object Null]',
undefinedTag = '[object Undefined]';
/** Built-in value references. */
var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}
_baseGetTag = baseGetTag;
return _baseGetTag;
}
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
var isObjectLike_1;
var hasRequiredIsObjectLike;
function requireIsObjectLike () {
if (hasRequiredIsObjectLike) return isObjectLike_1;
hasRequiredIsObjectLike = 1;
function isObjectLike(value) {
return value != null && typeof value == 'object';
}
isObjectLike_1 = isObjectLike;
return isObjectLike_1;
}
var isSymbol_1;
var hasRequiredIsSymbol;
function requireIsSymbol () {
if (hasRequiredIsSymbol) return isSymbol_1;
hasRequiredIsSymbol = 1;
var baseGetTag = require_baseGetTag(),
isObjectLike = requireIsObjectLike();
/** `Object#toString` result references. */
var symbolTag = '[object Symbol]';
/**
* Checks if `value` is classified as a `Symbol` primitive or object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
* @example
*
* _.isSymbol(Symbol.iterator);
* // => true
*
* _.isSymbol('abc');
* // => false
*/
function isSymbol(value) {
return typeof value == 'symbol' ||
(isObjectLike(value) && baseGetTag(value) == symbolTag);
}
isSymbol_1 = isSymbol;
return isSymbol_1;
}
var _isKey;
var hasRequired_isKey;
function require_isKey () {
if (hasRequired_isKey) return _isKey;
hasRequired_isKey = 1;
var isArray = requireIsArray(),
isSymbol = requireIsSymbol();
/** Used to match property names within property paths. */
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
reIsPlainProp = /^\w*$/;
/**
* Checks if `value` is a property name and not a property path.
*
* @private
* @param {*} value The value to check.
* @param {Object} [object] The object to query keys on.
* @returns {boolean} Returns `true` if `value` is a property name, else `false`.
*/
function isKey(value, object) {
if (isArray(value)) {
return false;
}
var type = typeof value;
if (type == 'number' || type == 'symbol' || type == 'boolean' ||
value == null || isSymbol(value)) {
return true;
}
return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
(object != null && value in Object(object));
}
_isKey = isKey;
return _isKey;
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
var isObject_1;
var hasRequiredIsObject;
function requireIsObject () {
if (hasRequiredIsObject) return isObject_1;
hasRequiredIsObject = 1;
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
isObject_1 = isObject;
return isObject_1;
}
var isFunction_1;
var hasRequiredIsFunction;
function requireIsFunction () {
if (hasRequiredIsFunction) return isFunction_1;
hasRequiredIsFunction = 1;
var baseGetTag = require_baseGetTag(),
isObject = requireIsObject();
/** `Object#toString` result references. */
var asyncTag = '[object AsyncFunction]',
funcTag = '[object Function]',
genTag = '[object GeneratorFunction]',
proxyTag = '[object Proxy]';
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
if (!isObject(value)) {
return false;
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
isFunction_1 = isFunction;
return isFunction_1;
}
var _coreJsData;
var hasRequired_coreJsData;
function require_coreJsData () {
if (hasRequired_coreJsData) return _coreJsData;
hasRequired_coreJsData = 1;
var root = require_root();
/** Used to detect overreaching core-js shims. */
var coreJsData = root['__core-js_shared__'];
_coreJsData = coreJsData;
return _coreJsData;
}
var _isMasked;
var hasRequired_isMasked;
function require_isMasked () {
if (hasRequired_isMasked) return _isMasked;
hasRequired_isMasked = 1;
var coreJsData = require_coreJsData();
/** Used to detect methods masquerading as native. */
var maskSrcKey = (function() {
var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
return uid ? ('Symbol(src)_1.' + uid) : '';
}());
/**
* Checks if `func` has its source masked.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns `true` if `func` is masked, else `false`.
*/
function isMasked(func) {
return !!maskSrcKey && (maskSrcKey in func);
}
_isMasked = isMasked;
return _isMasked;
}
/** Used for built-in method references. */
var _toSource;
var hasRequired_toSource;
function require_toSource () {
if (hasRequired_toSource) return _toSource;
hasRequired_toSource = 1;
var funcProto = Function.prototype;
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/**
* Converts `func` to its source code.
*
* @private
* @param {Function} func The function to convert.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return funcToString.call(func);
} catch (e) {}
try {
return (func + '');
} catch (e) {}
}
return '';
}
_toSource = toSource;
return _toSource;
}
var _baseIsNative;
var hasRequired_baseIsNative;
function require_baseIsNative () {
if (hasRequired_baseIsNative) return _baseIsNative;
hasRequired_baseIsNative = 1;
var isFunction = requireIsFunction(),
isMasked = require_isMasked(),
isObject = requireIsObject(),
toSource = require_toSource();
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;
/** Used for built-in method references. */
var funcProto = Function.prototype,
objectProto = Object.prototype;
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);
/**
* The base implementation of `_.isNative` without bad shim checks.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
*/
function baseIsNative(value) {
if (!isObject(value) || isMasked(value)) {
return false;
}
var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
return pattern.test(toSource(value));
}
_baseIsNative = baseIsNative;
return _baseIsNative;
}
/**
* Gets the value at `key` of `object`.
*
* @private
* @param {Object} [object] The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
var _getValue;
var hasRequired_getValue;
function require_getValue () {
if (hasRequired_getValue) return _getValue;
hasRequired_getValue = 1;
function getValue(object, key) {
return object == null ? undefined : object[key];
}
_getValue = getValue;
return _getValue;
}
var _getNative;
var hasRequired_getNative;
function require_getNative () {
if (hasRequired_getNative) return _getNative;
hasRequired_getNative = 1;
var baseIsNative = require_baseIsNative(),
getValue = require_getValue();
/**
* Gets the native function at `key` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else `undefined`.
*/
function getNative(object, key) {
var value = getValue(object, key);
return baseIsNative(value) ? value : undefined;
}
_getNative = getNative;
return _getNative;
}
var _nativeCreate;
var hasRequired_nativeCreate;
function require_nativeCreate () {
if (hasRequired_nativeCreate) return _nativeCreate;
hasRequired_nativeCreate = 1;
var getNative = require_getNative();
/* Built-in method references that are verified to be native. */
var nativeCreate = getNative(Object, 'create');
_nativeCreate = nativeCreate;
return _nativeCreate;
}
var _hashClear;
var hasRequired_hashClear;
function require_hashClear () {
if (hasRequired_hashClear) return _hashClear;
hasRequired_hashClear = 1;
var nativeCreate = require_nativeCreate();
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
this.size = 0;
}
_hashClear = hashClear;
return _hashClear;
}
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
var _hashDelete;
var hasRequired_hashDelete;
function require_hashDelete () {
if (hasRequired_hashDelete) return _hashDelete;
hasRequired_hashDelete = 1;
function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}
_hashDelete = hashDelete;
return _hashDelete;
}
var _hashGet;
var hasRequired_hashGet;
function require_hashGet () {
if (hasRequired_hashGet) return _hashGet;
hasRequired_hashGet = 1;
var nativeCreate = require_nativeCreate();
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined;
}
_hashGet = hashGet;
return _hashGet;
}
var _hashHas;
var hasRequired_hashHas;
function require_hashHas () {
if (hasRequired_hashHas) return _hashHas;
hasRequired_hashHas = 1;
var nativeCreate = require_nativeCreate();
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key);
}
_hashHas = hashHas;
return _hashHas;
}
var _hashSet;
var hasRequired_hashSet;
function require_hashSet () {
if (hasRequired_hashSet) return _hashSet;
hasRequired_hashSet = 1;
var nativeCreate = require_nativeCreate();
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
return this;
}
_hashSet = hashSet;
return _hashSet;
}
var _Hash;
var hasRequired_Hash;
function require_Hash () {
if (hasRequired_Hash) return _Hash;
hasRequired_Hash = 1;
var hashClear = require_hashClear(),
hashDelete = require_hashDelete(),
hashGet = require_hashGet(),
hashHas = require_hashHas(),
hashSet = require_hashSet();
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
_Hash = Hash;
return _Hash;
}
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
var _listCacheClear;
var hasRequired_listCacheClear;
function require_listCacheClear () {
if (hasRequired_listCacheClear) return _listCacheClear;
hasRequired_listCacheClear = 1;
function listCacheClear() {
this.__data__ = [];
this.size = 0;
}
_listCacheClear = listCacheClear;
return _listCacheClear;
}
/**
* Performs a
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
var eq_1;
var hasRequiredEq;
function requireEq () {
if (hasRequiredEq) return eq_1;
hasRequiredEq = 1;
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
eq_1 = eq;
return eq_1;
}
var _assocIndexOf;
var hasRequired_assocIndexOf;
function require_assocIndexOf () {
if (hasRequired_assocIndexOf) return _assocIndexOf;
hasRequired_assocIndexOf = 1;
var eq = requireEq();
/**
* Gets the index at which the `key` is found in `array` of key-value pairs.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (eq(array[length][0], key)) {
return length;
}
}
return -1;
}
_assocIndexOf = assocIndexOf;
return _assocIndexOf;
}
var _listCacheDelete;
var hasRequired_listCacheDelete;
function require_listCacheDelete () {
if (hasRequired_listCacheDelete) return _listCacheDelete;
hasRequired_listCacheDelete = 1;
var assocIndexOf = require_assocIndexOf();
/** Used for built-in method references. */
var arrayProto = Array.prototype;
/** Built-in value references. */
var splice = arrayProto.splice;
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
--this.size;
return true;
}
_listCacheDelete = listCacheDelete;
return _listCacheDelete;
}
var _listCacheGet;
var hasRequired_listCacheGet;
function require_listCacheGet () {
if (hasRequired_listCacheGet) return _listCacheGet;
hasRequired_listCacheGet = 1;
var assocIndexOf = require_assocIndexOf();
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
_listCacheGet = listCacheGet;
return _listCacheGet;
}
var _listCacheHas;
var hasRequired_listCacheHas;
function require_listCacheHas () {
if (hasRequired_listCacheHas) return _listCacheHas;
hasRequired_listCacheHas = 1;
var assocIndexOf = require_assocIndexOf();
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
_listCacheHas = listCacheHas;
return _listCacheHas;
}
var _listCacheSet;
var hasRequired_listCacheSet;
function require_listCacheSet () {
if (hasRequired_listCacheSet) return _listCacheSet;
hasRequired_listCacheSet = 1;
var assocIndexOf = require_assocIndexOf();
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
++this.size;
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
_listCacheSet = listCacheSet;
return _listCacheSet;
}
var _ListCache;
var hasRequired_ListCache;
function require_ListCache () {
if (hasRequired_ListCache) return _ListCache;
hasRequired_ListCache = 1;
var listCacheClear = require_listCacheClear(),
listCacheDelete = require_listCacheDelete(),
listCacheGet = require_listCacheGet(),
listCacheHas = require_listCacheHas(),
listCacheSet = require_listCacheSet();
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
_ListCache = ListCache;
return _ListCache;
}
var _Map;
var hasRequired_Map;
function require_Map () {
if (hasRequired_Map) return _Map;
hasRequired_Map = 1;
var getNative = require_getNative(),
root = require_root();
/* Built-in method references that are verified to be native. */
var Map = getNative(root, 'Map');
_Map = Map;
return _Map;
}
var _mapCacheClear;
var hasRequired_mapCacheClear;
function require_mapCacheClear () {
if (hasRequired_mapCacheClear) return _mapCacheClear;
hasRequired_mapCacheClear = 1;
var Hash = require_Hash(),
ListCache = require_ListCache(),
Map = require_Map();
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.size = 0;
this.__data__ = {
'hash': new Hash,
'map': new (Map || ListCache),
'string': new Hash
};
}
_mapCacheClear = mapCacheClear;
return _mapCacheClear;
}
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
var _isKeyable;
var hasRequired_isKeyable;
function require_isKeyable () {
if (hasRequired_isKeyable) return _isKeyable;
hasRequired_isKeyable = 1;
function isKeyable(value) {
var type = typeof value;
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
? (value !== '__proto__')
: (value === null);
}
_isKeyable = isKeyable;
return _isKeyable;
}
var _getMapData;
var hasRequired_getMapData;
function require_getMapData () {
if (hasRequired_getMapData) return _getMapData;
hasRequired_getMapData = 1;
var isKeyable = require_isKeyable();
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}
_getMapData = getMapData;
return _getMapData;
}
var _mapCacheDelete;
var hasRequired_mapCacheDelete;
function require_mapCacheDelete () {
if (hasRequired_mapCacheDelete) return _mapCacheDelete;
hasRequired_mapCacheDelete = 1;
var getMapData = require_getMapData();
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
var result = getMapData(this, key)['delete'](key);
this.size -= result ? 1 : 0;
return result;
}
_mapCacheDelete = mapCacheDelete;
return _mapCacheDelete;
}
var _mapCacheGet;
var hasRequired_mapCacheGet;
function require_mapCacheGet () {
if (hasRequired_mapCacheGet) return _mapCacheGet;
hasRequired_mapCacheGet = 1;
var getMapData = require_getMapData();
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
_mapCacheGet = mapCacheGet;
return _mapCacheGet;
}
var _mapCacheHas;
var hasRequired_mapCacheHas;
function require_mapCacheHas () {
if (hasRequired_mapCacheHas) return _mapCacheHas;
hasRequired_mapCacheHas = 1;
var getMapData = require_getMapData();
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
_mapCacheHas = mapCacheHas;
return _mapCacheHas;
}
var _mapCacheSet;
var hasRequired_mapCacheSet;
function require_mapCacheSet () {
if (hasRequired_mapCacheSet) return _mapCacheSet;
hasRequired_mapCacheSet = 1;
var getMapData = require_getMapData();
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;
data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}
_mapCacheSet = mapCacheSet;
return _mapCacheSet;
}
var _MapCache;
var hasRequired_MapCache;
function require_MapCache () {
if (hasRequired_MapCache) return _MapCache;
hasRequired_MapCache = 1;
var mapCacheClear = require_mapCacheClear(),
mapCacheDelete = require_mapCacheDelete(),
mapCacheGet = require_mapCacheGet(),
mapCacheHas = require_mapCacheHas(),
mapCacheSet = require_mapCacheSet();
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
_MapCache = MapCache;
return _MapCache;
}
var memoize_1;
var hasRequiredMemoize;
function requireMemoize () {
if (hasRequiredMemoize) return memoize_1;
hasRequiredMemoize = 1;
var MapCache = require_MapCache();
/** Error message constants. */
var FUNC_ERROR_TEXT = 'Expected a function';
/**
* Creates a function that memoizes the result of `func`. If `resolver` is
* provided, it determines the cache key for storing the result based on the
* arguments provided to the memoized function. By default, the first argument
* provided to the memoized function is used as the map cache key. The `func`
* is invoked with the `this` binding of the memoized function.
*
* **Note:** The cache is exposed as the `cache` property on the memoized
* function. Its creation may be customized by replacing the `_.memoize.Cache`
* constructor with one whose instances implement the
* [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
* method interface of `clear`, `delete`, `get`, `has`, and `set`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to have its output memoized.
* @param {Function} [resolver] The function to resolve the cache key.
* @returns {Function} Returns the new memoized function.
* @example
*
* var object = { 'a': 1, 'b': 2 };
* var other = { 'c': 3, 'd': 4 };
*
* var values = _.memoize(_.values);
* values(object);
* // => [1, 2]
*
* values(other);
* // => [3, 4]
*
* object.a = 2;
* values(object);
* // => [1, 2]
*
* // Modify the result cache.
* values.cache.set(object, ['a', 'b']);
* values(object);
* // => ['a', 'b']
*
* // Replace `_.memoize.Cache`.
* _.memoize.Cache = WeakMap;
*/
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function() {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache);
return memoized;
}
// Expose `MapCache`.
memoize.Cache = MapCache;
memoize_1 = memoize;
return memoize_1;
}
var _memoizeCapped;
var hasRequired_memoizeCapped;
function require_memoizeCapped () {
if (hasRequired_memoizeCapped) return _memoizeCapped;
hasRequired_memoizeCapped = 1;
var memoize = requireMemoize();
/** Used as the maximum memoize cache size. */
var MAX_MEMOIZE_SIZE = 500;
/**
* A specialized version of `_.memoize` which clears the memoized function's
* cache when it exceeds `MAX_MEMOIZE_SIZE`.
*
* @private
* @param {Function} func The function to have its output memoized.
* @returns {Function} Returns the new memoized function.
*/
function memoizeCapped(func) {
var result = memoize(func, function(key) {
if (cache.size === MAX_MEMOIZE_SIZE) {
cache.clear();
}
return key;
});
var cache = result.cache;
return result;
}
_memoizeCapped = memoizeCapped;
return _memoizeCapped;
}
var _stringToPath;
var hasRequired_stringToPath;
function require_stringToPath () {
if (hasRequired_stringToPath) return _stringToPath;
hasRequired_stringToPath = 1;
var memoizeCapped = require_memoizeCapped();
/** Used to match property names within property paths. */
var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
/** Used to match backslashes in property paths. */
var reEscapeChar = /\\(\\)?/g;
/**
* Converts `string` to a property path array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the property path array.
*/
var stringToPath = memoizeCapped(function(string) {
var result = [];
if (string.charCodeAt(0) === 46 /* . */) {
result.push('');
}
string.replace(rePropName, function(match, number, quote, subString) {
result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));
});
return result;
});
_stringToPath = stringToPath;
return _stringToPath;
}
/**
* A specialized version of `_.map` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the new mapped array.
*/
var _arrayMap;
var hasRequired_arrayMap;
function require_arrayMap () {
if (hasRequired_arrayMap) return _arrayMap;
hasRequired_arrayMap = 1;
function arrayMap(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length,
result = Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
_arrayMap = arrayMap;
return _arrayMap;
}
var _baseToString;
var hasRequired_baseToString;
function require_baseToString () {
if (hasRequired_baseToString) return _baseToString;
hasRequired_baseToString = 1;
var Symbol = require_Symbol(),
arrayMap = require_arrayMap(),
isArray = requireIsArray(),
isSymbol = requireIsSymbol();
/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol ? Symbol.prototype : undefined,
symbolToString = symbolProto ? symbolProto.toString : undefined;
/**
* The base implementation of `_.toString` which doesn't convert nullish
* values to empty strings.
*
* @private
* @param {*} value The value to process.
* @returns {string} Returns the string.
*/
function baseToString(value) {
// Exit early for strings to avoid a performance hit in some environments.
if (typeof value == 'string') {
return value;
}
if (isArray(value)) {
// Recursively convert values (susceptible to call stack limits).
return arrayMap(value, baseToString) + '';
}
if (isSymbol(value)) {
return symbolToString ? symbolToString.call(value) : '';
}
var result = (value + '');
return (result == '0' && (1 / value) == -Infinity) ? '-0' : result;
}
_baseToString = baseToString;
return _baseToString;
}
var toString_1;
var hasRequiredToString;
function requireToString () {
if (hasRequiredToString) return toString_1;
hasRequiredToString = 1;
var baseToString = require_baseToString();
/**
* Converts `value` to a string. An empty string is returned for `null`
* and `undefined` values. The sign of `-0` is preserved.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
* @example
*
* _.toString(null);
* // => ''
*
* _.toString(-0);
* // => '-0'
*
* _.toString([1, 2, 3]);
* // => '1,2,3'
*/
function toString(value) {
return value == null ? '' : baseToString(value);
}
toString_1 = toString;
return toString_1;
}
var _castPath;
var hasRequired_castPath;
function require_castPath () {
if (hasRequired_castPath) return _castPath;
hasRequired_castPath = 1;
var isArray = requireIsArray(),
isKey = require_isKey(),
stringToPath = require_stringToPath(),
toString = requireToString();
/**
* Casts `value` to a path array if it's not one.
*
* @private
* @param {*} value The value to inspect.
* @param {Object} [object] The object to query keys on.
* @returns {Array} Returns the cast property path array.
*/
function castPath(value, object) {
if (isArray(value)) {
return value;
}
return isKey(value, object) ? [value] : stringToPath(toString(value));
}
_castPath = castPath;
return _castPath;
}
var _toKey;
var hasRequired_toKey;
function require_toKey () {
if (hasRequired_toKey) return _toKey;
hasRequired_toKey = 1;
var isSymbol = requireIsSymbol();
/**
* Converts `value` to a string key if it's not a string or symbol.
*
* @private
* @param {*} value The value to inspect.
* @returns {string|symbol} Returns the key.
*/
function toKey(value) {
if (typeof value == 'string' || isSymbol(value)) {
return value;
}
var result = (value + '');
return (result == '0' && (1 / value) == -Infinity) ? '-0' : result;
}
_toKey = toKey;
return _toKey;
}
var _baseGet;
var hasRequired_baseGet;
function require_baseGet () {
if (hasRequired_baseGet) return _baseGet;
hasRequired_baseGet = 1;
var castPath = require_castPath(),
toKey = require_toKey();
/**
* The base implementation of `_.get` without support for default values.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @returns {*} Returns the resolved value.
*/
function baseGet(object, path) {
path = castPath(path, object);
var index = 0,
length = path.length;
while (object != null && index < length) {
object = object[toKey(path[index++])];
}
return (index && index == length) ? object : undefined;
}
_baseGet = baseGet;
return _baseGet;
}
var _defineProperty$1;
var hasRequired_defineProperty;
function require_defineProperty () {
if (hasRequired_defineProperty) return _defineProperty$1;
hasRequired_defineProperty = 1;
var getNative = require_getNative();
var defineProperty = (function() {
try {
var func = getNative(Object, 'defineProperty');
func({}, '', {});
return func;
} catch (e) {}
}());
_defineProperty$1 = defineProperty;
return _defineProperty$1;
}
var _baseAssignValue;
var hasRequired_baseAssignValue;
function require_baseAssignValue () {
if (hasRequired_baseAssignValue) return _baseAssignValue;
hasRequired_baseAssignValue = 1;
var defineProperty = require_defineProperty();
/**
* The base implementation of `assignValue` and `assignMergeValue` without
* value checks.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function baseAssignValue(object, key, value) {
if (key == '__proto__' && defineProperty) {
defineProperty(object, key, {
'configurable': true,
'enumerable': true,
'value': value,
'writable': true
});
} else {
object[key] = value;
}
}
_baseAssignValue = baseAssignValue;
return _baseAssignValue;
}
var _assignValue;
var hasRequired_assignValue;
function require_assignValue () {
if (hasRequired_assignValue) return _assignValue;
hasRequired_assignValue = 1;
var baseAssignValue = require_baseAssignValue(),
eq = requireEq();
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/**
* Assigns `value` to `key` of `object` if the existing value is not equivalent
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignValue(object, key, value) {
var objValue = object[key];
if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
(value === undefined && !(key in object))) {
baseAssignValue(object, key, value);
}
}
_assignValue = assignValue;
return _assignValue;
}
/** Used as references for various `Number` constants. */
var _isIndex;
var hasRequired_isIndex;
function require_isIndex () {
if (hasRequired_isIndex) return _isIndex;
hasRequired_isIndex = 1;
var MAX_SAFE_INTEGER = 9007199254740991;
/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;
/**
* Checks if `value` is a valid array-like index.
*
* @private
* @param {*} value The value to check.
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
var type = typeof value;
length = length == null ? MAX_SAFE_INTEGER : length;
return !!length &&
(type == 'number' ||
(type != 'symbol' && reIsUint.test(value))) &&
(value > -1 && value % 1 == 0 && value < length);
}
_isIndex = isIndex;
return _isIndex;
}
var _baseSet;
var hasRequired_baseSet;
function require_baseSet () {
if (hasRequired_baseSet) return _baseSet;
hasRequired_baseSet = 1;
var assignValue = require_assignValue(),
castPath = require_castPath(),
isIndex = require_isIndex(),
isObject = requireIsObject(),
toKey = require_toKey();
/**
* The base implementation of `_.set`.
*
* @private
* @param {Object} object The object to modify.
* @param {Array|string} path The path of the property to set.
* @param {*} value The value to set.
* @param {Function} [customizer] The function to customize path creation.
* @returns {Object} Returns `object`.
*/
function baseSet(object, path, value, customizer) {
if (!isObject(object)) {
return object;
}
path = castPath(path, object);
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
while (nested != null && ++index < length) {
var key = toKey(path[index]),
newValue = value;
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
return object;
}
if (index != lastIndex) {
var objValue = nested[key];
newValue = customizer ? customizer(objValue, key, nested) : undefined;
if (newValue === undefined) {
newValue = isObject(objValue)
? objValue
: (isIndex(path[index + 1]) ? [] : {});
}
}
assignValue(nested, key, newValue);
nested = nested[key];
}
return object;
}
_baseSet = baseSet;
return _baseSet;
}
var _basePickBy;
var hasRequired_basePickBy;
function require_basePickBy () {
if (hasRequired_basePickBy) return _basePickBy;
hasRequired_basePickBy = 1;
var baseGet = require_baseGet(),
baseSet = require_baseSet(),
castPath = require_castPath();
/**
* The base implementation of `_.pickBy` without support for iteratee shorthands.
*
* @private
* @param {Object} object The source object.
* @param {string[]} paths The property paths to pick.
* @param {Function} predicate The function invoked per property.
* @returns {Object} Returns the new object.
*/
function basePickBy(object, paths, predicate) {
var index = -1,
length = paths.length,
result = {};
while (++index < length) {
var path = paths[index],
value = baseGet(object, path);
if (predicate(value, path)) {
baseSet(result, castPath(path, object), value);
}
}
return result;
}
_basePickBy = basePickBy;
return _basePickBy;
}
/**
* The base implementation of `_.hasIn` without support for deep paths.
*
* @private
* @param {Object} [object] The object to query.
* @param {Array|string} key The key to check.
* @returns {boolean} Returns `true` if `key` exists, else `false`.
*/
var _baseHasIn;
var hasRequired_baseHasIn;
function require_baseHasIn () {
if (hasRequired_baseHasIn) return _baseHasIn;
hasRequired_baseHasIn = 1;
function baseHasIn(object, key) {
return object != null && key in Object(object);
}
_baseHasIn = baseHasIn;
return _baseHasIn;
}
var _baseIsArguments;
var hasRequired_baseIsArguments;
function require_baseIsArguments () {
if (hasRequired_baseIsArguments) return _baseIsArguments;
hasRequired_baseIsArguments = 1;
var baseGetTag = require_baseGetTag(),
isObjectLike = requireIsObjectLike();
/** `Object#toString` result references. */
var argsTag = '[object Arguments]';
/**
* The base implementation of `_.isArguments`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
*/
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
_baseIsArguments = baseIsArguments;
return _baseIsArguments;
}
var isArguments_1;
var hasRequiredIsArguments;
function requireIsArguments () {
if (hasRequiredIsArguments) return isArguments_1;
hasRequiredIsArguments = 1;
var baseIsArguments = require_baseIsArguments(),
isObjectLike = requireIsObjectLike();
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/** Built-in value references. */
var propertyIsEnumerable = objectProto.propertyIsEnumerable;
/**
* Checks if `value` is likely an `arguments` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
* else `false`.
* @example
*
* _.isArguments(function() { return arguments; }());
* // => true
*
* _.isArguments([1, 2, 3]);
* // => false
*/
var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
!propertyIsEnumerable.call(value, 'callee');
};
isArguments_1 = isArguments;
return isArguments_1;
}
/** Used as references for various `Number` constants. */
var isLength_1;
var hasRequiredIsLength;
function requireIsLength () {
if (hasRequiredIsLength) return isLength_1;
hasRequiredIsLength = 1;
var MAX_SAFE_INTEGER = 9007199254740991;
/**
* Checks if `value` is a valid array-like length.
*
* **Note:** This method is loosely based on
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
* @example
*
* _.isLength(3);
* // => true
*
* _.isLength(Number.MIN_VALUE);
* // => false
*
* _.isLength(Infinity);
* // => false
*
* _.isLength('3');
* // => false
*/
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
isLength_1 = isLength;
return isLength_1;
}
var _hasPath;
var hasRequired_hasPath;
function require_hasPath () {
if (hasRequired_hasPath) return _hasPath;
hasRequired_hasPath = 1;
var castPath = require_castPath(),
isArguments = requireIsArguments(),
isArray = requireIsArray(),
isIndex = require_isIndex(),
isLength = requireIsLength(),
toKey = require_toKey();
/**
* Checks if `path` exists on `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @param {Function} hasFunc The function to check properties.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
*/
function hasPath(object, path, hasFunc) {
path = castPath(path, object);
var index = -1,
length = path.length,
result = false;
while (++index < length) {
var key = toKey(path[index]);
if (!(result = object != null && hasFunc(object, key))) {
break;
}
object = object[key];
}
if (result || ++index != length) {
return result;
}
length = object == null ? 0 : object.length;
return !!length && isLength(length) && isIndex(key, length) &&
(isArray(object) || isArguments(object));
}
_hasPath = hasPath;
return _hasPath;
}
var hasIn_1;
var hasRequiredHasIn;
function requireHasIn () {
if (hasRequiredHasIn) return hasIn_1;
hasRequiredHasIn = 1;
var baseHasIn = require_baseHasIn(),
hasPath = require_hasPath();
/**
* Checks if `path` is a direct or inherited property of `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
* @example
*
* var object = _.create({ 'a': _.create({ 'b': 2 }) });
*
* _.hasIn(object, 'a');
* // => true
*
* _.hasIn(object, 'a.b');
* // => true
*
* _.hasIn(object, ['a', 'b']);
* // => true
*
* _.hasIn(object, 'b');
* // => false
*/
function hasIn(object, path) {
return object != null && hasPath(object, path, baseHasIn);
}
hasIn_1 = hasIn;
return hasIn_1;
}
var _basePick;
var hasRequired_basePick;
function require_basePick () {
if (hasRequired_basePick) return _basePick;
hasRequired_basePick = 1;
var basePickBy = require_basePickBy(),
hasIn = requireHasIn();
/**
* The base implementation of `_.pick` without support for individual
* property identifiers.
*
* @private
* @param {Object} object The source object.
* @param {string[]} paths The property paths to pick.
* @returns {Object} Returns the new object.
*/
function basePick(object, paths) {
return basePickBy(object, paths, function(value, path) {
return hasIn(object, path);
});
}
_basePick = basePick;
return _basePick;
}
/**
* Appends the elements of `values` to `array`.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to append.
* @returns {Array} Returns `array`.
*/
var _arrayPush;
var hasRequired_arrayPush;
function require_arrayPush () {
if (hasRequired_arrayPush) return _arrayPush;
hasRequired_arrayPush = 1;
function arrayPush(array, values) {
var index = -1,
length = values.length,
offset = array.length;
while (++index < length) {
array[offset + index] = values[index];
}
return array;
}
_arrayPush = arrayPush;
return _arrayPush;
}
var _isFlattenable;
var hasRequired_isFlattenable;
function require_isFlattenable () {
if (hasRequired_isFlattenable) return _isFlattenable;
hasRequired_isFlattenable = 1;
var Symbol = require_Symbol(),
isArguments = requireIsArguments(),
isArray = requireIsArray();
/** Built-in value references. */
var spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined;
/**
* Checks if `value` is a flattenable `arguments` object or array.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is flattenable, else `false`.
*/
function isFlattenable(value) {
return isArray(value) || isArguments(value) ||
!!(spreadableSymbol && value && value[spreadableSymbol]);
}
_isFlattenable = isFlattenable;
return _isFlattenable;
}
var _baseFlatten;
var hasRequired_baseFlatten;
function require_baseFlatten () {
if (hasRequired_baseFlatten) return _baseFlatten;
hasRequired_baseFlatten = 1;
var arrayPush = require_arrayPush(),
isFlattenable = require_isFlattenable();
/**
* The base implementation of `_.flatten` with support for restricting flattening.
*
* @private
* @param {Array} array The array to flatten.
* @param {number} depth The maximum recursion depth.
* @param {boolean} [predicate=isFlattenable] The function invoked per iteration.
* @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
* @param {Array} [result=[]] The initial result value.
* @returns {Array} Returns the new flattened array.
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
var index = -1,
length = array.length;
predicate || (predicate = isFlattenable);
result || (result = []);
while (++index < length) {
var value = array[index];
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
arrayPush(result, value);
}
} else if (!isStrict) {
result[result.length] = value;
}
}
return result;
}
_baseFlatten = baseFlatten;
return _baseFlatten;
}
var flatten_1;
var hasRequiredFlatten;
function requireFlatten () {
if (hasRequiredFlatten) return flatten_1;
hasRequiredFlatten = 1;
var baseFlatten = require_baseFlatten();
/**
* Flattens `array` a single level deep.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to flatten.
* @returns {Array} Returns the new flattened array.
* @example
*
* _.flatten([1, [2, [3, [4]], 5]]);
* // => [1, 2, [3, [4]], 5]
*/
function flatten(array) {
var length = array == null ? 0 : array.length;
return length ? baseFlatten(array, 1) : [];
}
flatten_1 = flatten;
return flatten_1;
}
/**
* A faster alternative to `Function#apply`, this function invokes `func`
* with the `this` binding of `thisArg` and the arguments of `args`.
*
* @private
* @param {Function} func The function to invoke.
* @param {*} thisArg The `this` binding of `func`.
* @param {Array} args The arguments to invoke `func` with.
* @returns {*} Returns the result of `func`.
*/
var _apply;
var hasRequired_apply;
function require_apply () {
if (hasRequired_apply) return _apply;
hasRequired_apply = 1;
function apply(func, thisArg, args) {
switch (args.length) {
case 0: return func.call(thisArg);
case 1: return func.call(thisArg, args[0]);
case 2: return func.call(thisArg, args[0], args[1]);
case 3: return func.call(thisArg, args[0], args[1], args[2]);
}
return func.apply(thisArg, args);
}
_apply = apply;
return _apply;
}
var _overRest;
var hasRequired_overRest;
function require_overRest () {
if (hasRequired_overRest) return _overRest;
hasRequired_overRest = 1;
var apply = require_apply();
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max;
/**
* A specialized version of `baseRest` which transforms the rest array.
*
* @private
* @param {Function} func The function to apply a rest parameter to.
* @param {number} [start=func.length-1] The start position of the rest parameter.
* @param {Function} transform The rest array transform.
* @returns {Function} Returns the new function.
*/
function overRest(func, start, transform) {
start = nativeMax(start === undefined ? (func.length - 1) : start, 0);
return function() {
var args = arguments,
index = -1,
length = nativeMax(args.length - start, 0),
array = Array(length);
while (++index < length) {
array[index] = args[start + index];
}
index = -1;
var otherArgs = Array(start + 1);
while (++index < start) {
otherArgs[index] = args[index];
}
otherArgs[start] = transform(array);
return apply(func, this, otherArgs);
};
}
_overRest = overRest;
return _overRest;
}
/**
* Creates a function that returns `value`.
*
* @static
* @memberOf _
* @since 2.4.0
* @category Util
* @param {*} value The value to return from the new function.
* @returns {Function} Returns the new constant function.
* @example
*
* var objects = _.times(2, _.constant({ 'a': 1 }));
*
* console.log(objects);
* // => [{ 'a': 1 }, { 'a': 1 }]
*
* console.log(objects[0] === objects[1]);
* // => true
*/
var constant_1;
var hasRequiredConstant;
function requireConstant () {
if (hasRequiredConstant) return constant_1;
hasRequiredConstant = 1;
function constant(value) {
return function() {
return value;
};
}
constant_1 = constant;
return constant_1;
}
/**
* This method returns the first argument it receives.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {*} value Any value.
* @returns {*} Returns `value`.
* @example
*
* var object = { 'a': 1 };
*
* console.log(_.identity(object) === object);
* // => true
*/
var identity_1;
var hasRequiredIdentity;
function requireIdentity () {
if (hasRequiredIdentity) return identity_1;
hasRequiredIdentity = 1;
function identity(value) {
return value;
}
identity_1 = identity;
return identity_1;
}
var _baseSetToString;
var hasRequired_baseSetToString;
function require_baseSetToString () {
if (hasRequired_baseSetToString) return _baseSetToString;
hasRequired_baseSetToString = 1;
var constant = requireConstant(),
defineProperty = require_defineProperty(),
identity = requireIdentity();
/**
* The base implementation of `setToString` without support for hot loop shorting.
*
* @private
* @param {Function} func The function to modify.
* @param {Function} string The `toString` result.
* @returns {Function} Returns `func`.
*/
var baseSetToString = !defineProperty ? identity : function(func, string) {
return defineProperty(func, 'toString', {
'configurable': true,
'enumerable': false,
'value': constant(string),
'writable': true
});
};
_baseSetToString = baseSetToString;
return _baseSetToString;
}
/** Used to detect hot functions by number of calls within a span of milliseconds. */
var _shortOut;
var hasRequired_shortOut;
function require_shortOut () {
if (hasRequired_shortOut) return _shortOut;
hasRequired_shortOut = 1;
var HOT_COUNT = 800,
HOT_SPAN = 16;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeNow = Date.now;
/**
* Creates a function that'll short out and invoke `identity` instead
* of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`
* milliseconds.
*
* @private
* @param {Function} func The function to restrict.
* @returns {Function} Returns the new shortable function.
*/
function shortOut(func) {
var count = 0,
lastCalled = 0;
return function() {
var stamp = nativeNow(),
remaining = HOT_SPAN - (stamp - lastCalled);
lastCalled = stamp;
if (remaining > 0) {
if (++count >= HOT_COUNT) {
return arguments[0];
}
} else {
count = 0;
}
return func.apply(undefined, arguments);
};
}
_shortOut = shortOut;
return _shortOut;
}
var _setToString;
var hasRequired_setToString;
function require_setToString () {
if (hasRequired_setToString) return _setToString;
hasRequired_setToString = 1;
var baseSetToString = require_baseSetToString(),
shortOut = require_shortOut();
/**
* Sets the `toString` method of `func` to return `string`.
*
* @private
* @param {Function} func The function to modify.
* @param {Function} string The `toString` result.
* @returns {Function} Returns `func`.
*/
var setToString = shortOut(baseSetToString);
_setToString = setToString;
return _setToString;
}
var _flatRest;
var hasRequired_flatRest;
function require_flatRest () {
if (hasRequired_flatRest) return _flatRest;
hasRequired_flatRest = 1;
var flatten = requireFlatten(),
overRest = require_overRest(),
setToString = require_setToString();
/**
* A specialized version of `baseRest` which flattens the rest array.
*
* @private
* @param {Function} func The function to apply a rest parameter to.
* @returns {Function} Returns the new function.
*/
function flatRest(func) {
return setToString(overRest(func, undefined, flatten), func + '');
}
_flatRest = flatRest;
return _flatRest;
}
var pick_1;
var hasRequiredPick;
function requirePick () {
if (hasRequiredPick) return pick_1;
hasRequiredPick = 1;
var basePick = require_basePick(),
flatRest = require_flatRest();
/**
* Creates an object composed of the picked `object` properties.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The source object.
* @param {...(string|string[])} [paths] The property paths to pick.
* @returns {Object} Returns the new object.
* @example
*
* var object = { 'a': 1, 'b': '2', 'c': 3 };
*
* _.pick(object, ['a', 'c']);
* // => { 'a': 1, 'c': 3 }
*/
var pick = flatRest(function(object, paths) {
return object == null ? {} : basePick(object, paths);
});
pick_1 = pick;
return pick_1;
}
var pickExports = requirePick();
var pick = /*@__PURE__*/getDefaultExportFromCjs(pickExports);
// Status keys
const INITIALIZED = 'INITIALIZED';
/**
* Cache for tracking MCP Servers Registry global metadata and status across distributed instances.
* Uses Redis-backed storage to coordinate state between leader and follower nodes.
* Tracks global initialization status for the registry.
*
* Designed to be extended with additional global registry metadata as needed
* (e.g., last update timestamps, version info, health status).
* This cache is only meant to be used internally by registry management components.
*/
class RegistryStatusCache extends BaseRegistryCache {
constructor() {
super(...arguments);
this.cache = standardCache(`${this.PREFIX}::Status`);
}
isInitialized() {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.get(INITIALIZED)) === true;
});
}
setInitialized(value) {
return __awaiter(this, void 0, void 0, function* () {
yield this.set(INITIALIZED, value);
});
}
get(key) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.get(key);
});
}
set(key, value, ttl) {
return __awaiter(this, void 0, void 0, function* () {
yield this.leaderCheck('set MCP Servers Registry status');
const success = yield this.cache.set(key, value, ttl);
this.successCheck(`set status key "${key}"`, success);
});
}
}
const registryStatusCache = new RegistryStatusCache();
const MCP_INIT_TIMEOUT_MS = process.env.MCP_INIT_TIMEOUT_MS != null ? parseInt(process.env.MCP_INIT_TIMEOUT_MS) : 30000;
/**
* Handles initialization of MCP servers at application startup with distributed coordination.
* In cluster environments, ensures only the leader node performs initialization while followers wait.
* Connects to each configured MCP server, inspects capabilities and tools, then caches the results.
* Categorizes servers as either shared app servers (auto-started) or shared user servers (OAuth/on-demand).
* Uses a timeout mechanism to prevent hanging on unresponsive servers during initialization.
*/
class MCPServersInitializer {
/** Reset the process-level initialization flag. Only used for testing. */
static resetProcessFlag() {
MCPServersInitializer.hasInitializedThisProcess = false;
}
static initialize(rawConfigs) {
return __awaiter(this, void 0, void 0, function* () {
// On first call in this process, always reset and re-initialize
// This ensures we don't use stale Redis data from previous runs
const isFirstCallThisProcess = !MCPServersInitializer.hasInitializedThisProcess;
// Set flag immediately so recursive calls (from followers) use Redis cache for coordination
MCPServersInitializer.hasInitializedThisProcess = true;
if (!isFirstCallThisProcess && (yield registryStatusCache.isInitialized()))
return;
if (yield isLeader()) {
// Leader performs initialization - always reset on first call
yield registryStatusCache.reset();
yield MCPServersRegistry.getInstance().reset();
const serverNames = Object.keys(rawConfigs);
yield Promise.allSettled(serverNames.map((serverName) => withTimeout(MCPServersInitializer.initializeServer(serverName, rawConfigs[serverName]), MCP_INIT_TIMEOUT_MS, `${MCPServersInitializer.prefix(serverName)} Server initialization timed out`, dataSchemas.logger.error)));
yield registryStatusCache.setInitialized(true);
}
else {
// Followers try again after a delay if not initialized
yield new Promise((resolve) => setTimeout(resolve, 3000));
yield this.initialize(rawConfigs);
}
});
}
/** Initializes a single server with all its metadata and adds it to appropriate collections */
static initializeServer(serverName, rawConfig) {
return __awaiter(this, void 0, void 0, function* () {
try {
const result = yield MCPServersRegistry.getInstance().addServer(serverName, rawConfig, 'CACHE');
MCPServersInitializer.logParsedConfig(serverName, result.config);
}
catch (error) {
dataSchemas.logger.error(`${MCPServersInitializer.prefix(serverName)} Failed to initialize:`, error);
}
});
}
// Logs server configuration summary after initialization
static logParsedConfig(serverName, config) {
var _a;
const prefix = MCPServersInitializer.prefix(serverName);
dataSchemas.logger.info(`${prefix} -------------------------------------------------┐`);
dataSchemas.logger.info(`${prefix} URL: ${config.url ? sanitizeUrlForLogging(config.url) : 'N/A'}`);
dataSchemas.logger.info(`${prefix} OAuth Required: ${config.requiresOAuth}`);
dataSchemas.logger.info(`${prefix} Capabilities: ${config.capabilities}`);
dataSchemas.logger.info(`${prefix} Tools: ${config.tools}`);
dataSchemas.logger.info(`${prefix} Server Instructions: ${config.serverInstructions}`);
dataSchemas.logger.info(`${prefix} Initialized in: ${(_a = config.initDuration) !== null && _a !== void 0 ? _a : 'N/A'}ms`);
dataSchemas.logger.info(`${prefix} -------------------------------------------------┘`);
}
// Returns formatted log prefix for server messages
static prefix(serverName) {
return `[MCP][${serverName}]`;
}
}
/**
* Initializes MCP servers with distributed leader-follower coordination.
*
* Design rationale:
* - Handles leader crash scenarios: If the leader crashes during initialization, all followers
* will independently attempt initialization after a 3-second delay. The first to become leader
* will complete the initialization.
* - Only the leader performs the actual initialization work (reset caches, inspect servers).
* When complete, the leader signals completion via `statusCache`, allowing followers to proceed.
* - Followers wait and poll `statusCache` until the leader finishes, ensuring only one node
* performs the expensive initialization operations.
*/
MCPServersInitializer.hasInitializedThisProcess = false;
/**
* Abstract base class for managing user-specific MCP connections with lifecycle management.
* Only meant to be extended by MCPManager.
* Much of the logic was move here from the old MCPManager to make it more manageable.
* User connections will soon be ephemeral and not cached anymore:
* https://github.com/danny-avila/LibreChat/discussions/8790
*/
class UserConnectionManager {
constructor() {
// Connections shared by all users.
this.appConnections = null;
// Connections per userId -> serverName -> connection
this.userConnections = new Map();
/** Last activity timestamp for users (not per server) */
this.userLastActivity = new Map();
}
/** Updates the last activity timestamp for a user */
updateUserLastActivity(userId) {
const now = Date.now();
this.userLastActivity.set(userId, now);
dataSchemas.logger.debug(`[MCP][User: ${userId}] Updated last activity timestamp: ${new Date(now).toISOString()}`);
}
/** Gets or creates a connection for a specific user */
getUserConnection(_a) {
return __awaiter(this, arguments, void 0, function* ({ serverName, forceNew, user, flowManager, customUserVars, requestBody, tokenMethods, oauthStart, oauthEnd, signal, returnOnOAuth = false, connectionTimeout, }) {
var _b;
const userId = user === null || user === void 0 ? void 0 : user.id;
if (!userId) {
throw new types_js.McpError(types_js.ErrorCode.InvalidRequest, `[MCP] User object missing id property`);
}
if (yield this.appConnections.has(serverName)) {
throw new types_js.McpError(types_js.ErrorCode.InvalidRequest, `[MCP][User: ${userId}] Trying to create user-specific connection for app-level server "${serverName}"`);
}
const config = yield MCPServersRegistry.getInstance().getServerConfig(serverName, userId);
const userServerMap = this.userConnections.get(userId);
let connection = forceNew ? undefined : userServerMap === null || userServerMap === void 0 ? void 0 : userServerMap.get(serverName);
const now = Date.now();
// Check if user is idle
const lastActivity = this.userLastActivity.get(userId);
if (lastActivity && now - lastActivity > mcpConfig.USER_CONNECTION_IDLE_TIMEOUT) {
dataSchemas.logger.info(`[MCP][User: ${userId}] User idle for too long. Disconnecting all connections.`);
// Disconnect all user connections
try {
yield this.disconnectUserConnections(userId);
}
catch (err) {
dataSchemas.logger.error(`[MCP][User: ${userId}] Error disconnecting idle connections:`, err);
}
connection = undefined; // Force creation of a new connection
}
else if (connection) {
if (!config || (config.updatedAt && connection.isStale(config.updatedAt))) {
if (config) {
dataSchemas.logger.info(`[MCP][User: ${userId}][${serverName}] Config was updated, disconnecting stale connection`);
}
yield this.disconnectUserConnection(userId, serverName);
connection = undefined;
}
else if (yield connection.isConnected()) {
dataSchemas.logger.debug(`[MCP][User: ${userId}][${serverName}] Reusing active connection`);
this.updateUserLastActivity(userId);
return connection;
}
else {
// Connection exists but is not connected, attempt to remove potentially stale entry
dataSchemas.logger.warn(`[MCP][User: ${userId}][${serverName}] Found existing but disconnected connection object. Cleaning up.`);
this.removeUserConnection(userId, serverName); // Clean up maps
connection = undefined;
}
}
// Now check if config exists for new connection creation
if (!config) {
throw new types_js.McpError(types_js.ErrorCode.InvalidRequest, `[MCP][User: ${userId}] Configuration for server "${serverName}" not found.`);
}
// If no valid connection exists, create a new one
dataSchemas.logger.info(`[MCP][User: ${userId}][${serverName}] Establishing new connection`);
try {
connection = yield MCPConnectionFactory.create({
serverName: serverName,
serverConfig: config,
useSSRFProtection: MCPServersRegistry.getInstance().shouldEnableSSRFProtection(),
}, {
useOAuth: true,
user: user,
customUserVars: customUserVars,
flowManager: flowManager,
tokenMethods: tokenMethods,
signal: signal,
oauthStart: oauthStart,
oauthEnd: oauthEnd,
returnOnOAuth: returnOnOAuth,
requestBody: requestBody,
connectionTimeout: connectionTimeout,
});
if (!(yield (connection === null || connection === void 0 ? void 0 : connection.isConnected()))) {
throw new Error('Failed to establish connection after initialization attempt.');
}
if (!this.userConnections.has(userId)) {
this.userConnections.set(userId, new Map());
}
(_b = this.userConnections.get(userId)) === null || _b === void 0 ? void 0 : _b.set(serverName, connection);
dataSchemas.logger.info(`[MCP][User: ${userId}][${serverName}] Connection successfully established`);
// Update timestamp on creation
this.updateUserLastActivity(userId);
return connection;
}
catch (error) {
dataSchemas.logger.error(`[MCP][User: ${userId}][${serverName}] Failed to establish connection`, error);
// Ensure partial connection state is cleaned up if initialization fails
yield (connection === null || connection === void 0 ? void 0 : connection.disconnect().catch((disconnectError) => {
dataSchemas.logger.error(`[MCP][User: ${userId}][${serverName}] Error during cleanup after failed connection`, disconnectError);
}));
// Ensure cleanup even if connection attempt fails
this.removeUserConnection(userId, serverName);
throw error; // Re-throw the error to the caller
}
});
}
/** Returns all connections for a specific user */
getUserConnections(userId) {
return this.userConnections.get(userId);
}
/** Removes a specific user connection entry */
removeUserConnection(userId, serverName) {
const userMap = this.userConnections.get(userId);
if (userMap) {
userMap.delete(serverName);
if (userMap.size === 0) {
this.userConnections.delete(userId);
// Only remove user activity timestamp if all connections are gone
this.userLastActivity.delete(userId);
}
}
dataSchemas.logger.debug(`[MCP][User: ${userId}][${serverName}] Removed connection entry.`);
}
/** Disconnects and removes a specific user connection */
disconnectUserConnection(userId, serverName) {
return __awaiter(this, void 0, void 0, function* () {
const userMap = this.userConnections.get(userId);
const connection = userMap === null || userMap === void 0 ? void 0 : userMap.get(serverName);
if (connection) {
dataSchemas.logger.info(`[MCP][User: ${userId}][${serverName}] Disconnecting...`);
yield connection.disconnect();
this.removeUserConnection(userId, serverName);
}
});
}
/** Disconnects and removes all connections for a specific user */
disconnectUserConnections(userId) {
return __awaiter(this, void 0, void 0, function* () {
const userMap = this.userConnections.get(userId);
const disconnectPromises = [];
if (userMap) {
dataSchemas.logger.info(`[MCP][User: ${userId}] Disconnecting all servers...`);
const userServers = Array.from(userMap.keys());
for (const serverName of userServers) {
disconnectPromises.push(this.disconnectUserConnection(userId, serverName).catch((error) => {
dataSchemas.logger.error(`[MCP][User: ${userId}][${serverName}] Error during disconnection:`, error);
}));
}
yield Promise.allSettled(disconnectPromises);
// Ensure user activity timestamp is removed
this.userLastActivity.delete(userId);
dataSchemas.logger.info(`[MCP][User: ${userId}] All connections processed for disconnection.`);
}
});
}
/** Check for and disconnect idle connections */
checkIdleConnections(currentUserId) {
const now = Date.now();
// Iterate through all users to check for idle ones
for (const [userId, lastActivity] of this.userLastActivity.entries()) {
if (currentUserId && currentUserId === userId) {
continue;
}
if (now - lastActivity > mcpConfig.USER_CONNECTION_IDLE_TIMEOUT) {
dataSchemas.logger.info(`[MCP][User: ${userId}] User idle for too long. Disconnecting all connections...`);
// Disconnect all user connections asynchronously (fire and forget)
this.disconnectUserConnections(userId).catch((err) => dataSchemas.logger.error(`[MCP][User: ${userId}] Error disconnecting idle connections:`, err));
}
}
}
}
const CONNECT_CONCURRENCY = 3;
/**
* Manages MCP connections with lazy loading and reconnection.
* Maintains a pool of connections and handles connection lifecycle management.
* Queries server configurations dynamically from the MCPServersRegistry (single source of truth).
*
* Scope-aware: Each repository is tied to a specific owner scope:
* - ownerId = undefined → manages app-level servers only
* - ownerId = userId → manages user-level and private servers for that user
*/
class ConnectionsRepository {
constructor(ownerId, oauthOpts) {
this.connections = new Map();
this.ownerId = ownerId;
this.oauthOpts = oauthOpts;
}
/** Checks whether this repository can connect to a specific server */
has(serverName) {
return __awaiter(this, void 0, void 0, function* () {
const config = yield MCPServersRegistry.getInstance().getServerConfig(serverName, this.ownerId);
const canConnect = !!config && this.isAllowedToConnectToServer(config);
if (!canConnect) {
//if connection is no longer possible we attempt to disconnect any leftover connections
yield this.disconnect(serverName);
}
return canConnect;
});
}
/** Gets or creates a connection for the specified server with lazy loading */
get(serverName) {
return __awaiter(this, void 0, void 0, function* () {
const serverConfig = yield MCPServersRegistry.getInstance().getServerConfig(serverName, this.ownerId);
const existingConnection = this.connections.get(serverName);
if (!serverConfig || !this.isAllowedToConnectToServer(serverConfig)) {
if (existingConnection) {
yield existingConnection.disconnect();
}
return null;
}
if (existingConnection) {
// Check if config was cached/updated since connection was created
if (serverConfig.updatedAt && existingConnection.isStale(serverConfig.updatedAt)) {
dataSchemas.logger.info(`${this.prefix(serverName)} Existing connection for ${serverName} is outdated. Recreating a new connection.`, {
connectionCreated: new Date(existingConnection.createdAt).toISOString(),
configCachedAt: new Date(serverConfig.updatedAt).toISOString(),
});
// Disconnect stale connection
yield existingConnection.disconnect();
this.connections.delete(serverName);
// Fall through to create new connection
}
else if (yield existingConnection.isConnected()) {
return existingConnection;
}
else {
yield this.disconnect(serverName);
}
}
const connection = yield MCPConnectionFactory.create({
serverName,
serverConfig,
useSSRFProtection: MCPServersRegistry.getInstance().shouldEnableSSRFProtection(),
}, this.oauthOpts);
this.connections.set(serverName, connection);
return connection;
});
}
/** Gets or creates connections for multiple servers concurrently */
getMany(serverNames) {
return __awaiter(this, void 0, void 0, function* () {
const results = [];
for (let i = 0; i < serverNames.length; i += CONNECT_CONCURRENCY) {
const batch = serverNames.slice(i, i + CONNECT_CONCURRENCY);
const batchResults = yield Promise.all(batch.map((name) => __awaiter(this, void 0, void 0, function* () { return [name, yield this.get(name)]; })));
results.push(...batchResults);
}
return new Map(results.filter((v) => v[1] != null));
});
}
/** Returns all currently loaded connections without creating new ones */
getLoaded() {
return __awaiter(this, void 0, void 0, function* () {
return this.getMany(Array.from(this.connections.keys()));
});
}
/** Gets or creates connections for all configured servers in this repository's scope */
getAll() {
return __awaiter(this, void 0, void 0, function* () {
//TODO in the future we should use a scoped config getter (APPLevel, UserLevel, Private)
//for now the absent config will not throw error
const allConfigs = yield MCPServersRegistry.getInstance().getAllServerConfigs(this.ownerId);
return this.getMany(Object.keys(allConfigs));
});
}
/** Disconnects and removes a specific server connection from the pool */
disconnect(serverName) {
return __awaiter(this, void 0, void 0, function* () {
const connection = this.connections.get(serverName);
if (!connection)
return Promise.resolve();
this.connections.delete(serverName);
return connection.disconnect().catch((err) => {
dataSchemas.logger.error(`${this.prefix(serverName)} Error disconnecting`, err);
});
});
}
/** Disconnects all active connections and returns array of disconnect promises */
disconnectAll() {
const serverNames = Array.from(this.connections.keys());
return serverNames.map((serverName) => this.disconnect(serverName));
}
// Returns formatted log prefix for server messages
prefix(serverName) {
return `[MCP][${serverName}]`;
}
isAllowedToConnectToServer(config) {
//the repository is not allowed to be connected in case the Connection repository is shared (ownerId is undefined/null) and the server requires Auth or startup false.
if (this.ownerId === undefined && (config.startup === false || config.requiresOAuth)) {
return false;
}
return true;
}
}
function generateResourceId(text) {
return crypto$1.createHash('sha256').update(text).digest('hex').substring(0, 10);
}
const RECOGNIZED_PROVIDERS = new Set([
'google',
'anthropic',
'openai',
'azureopenai',
'openrouter',
'xai',
'deepseek',
'ollama',
'bedrock',
]);
const CONTENT_ARRAY_PROVIDERS = new Set(['google', 'anthropic', 'azureopenai', 'openai']);
const imageFormatters = {
// google: (item) => ({
// type: 'image',
// inlineData: {
// mimeType: item.mimeType,
// data: item.data,
// },
// }),
// anthropic: (item) => ({
// type: 'image',
// source: {
// type: 'base64',
// media_type: item.mimeType,
// data: item.data,
// },
// }),
default: (item) => ({
type: 'image_url',
image_url: {
url: item.data.startsWith('http') ? item.data : `data:${item.mimeType};base64,${item.data}`,
},
}),
};
function isImageContent(item) {
return item.type === 'image';
}
function parseAsString(result) {
var _a;
const content = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
if (!content.length) {
return '(No response)';
}
const text = content
.map((item) => {
if (item.type === 'text') {
return item.text;
}
if (item.type === 'resource') {
const resourceText = [];
if ('text' in item.resource && item.resource.text != null && item.resource.text) {
resourceText.push(item.resource.text);
}
if (item.resource.uri) {
resourceText.push(`Resource URI: ${item.resource.uri}`);
}
if (item.resource.mimeType != null && item.resource.mimeType) {
resourceText.push(`Type: ${item.resource.mimeType}`);
}
return resourceText.join('\n');
}
return JSON.stringify(item, null, 2);
})
.filter(Boolean)
.join('\n\n');
return text;
}
/**
* Converts MCPToolCallResponse content into recognized content block types
* First element: string or formatted content (excluding image_url)
* Second element: Recognized types - "image", "image_url", "text", "json"
*
* @param result - The MCPToolCallResponse object
* @param provider - The provider name (google, anthropic, openai)
* @returns Tuple of content and image_urls
*/
function formatToolContent(result, provider) {
var _a;
if (!RECOGNIZED_PROVIDERS.has(provider)) {
return [parseAsString(result), undefined];
}
const content = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
if (!content.length) {
return [[{ type: 'text', text: '(No response)' }], undefined];
}
const formattedContent = [];
const imageUrls = [];
let currentTextBlock = '';
const uiResources = [];
const contentHandlers = {
text: (item) => {
currentTextBlock += (currentTextBlock ? '\n\n' : '') + item.text;
},
image: (item) => {
if (!isImageContent(item)) {
return;
}
if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) {
formattedContent.push({ type: 'text', text: currentTextBlock });
currentTextBlock = '';
}
const formatter = imageFormatters.default;
const formattedImage = formatter(item);
if (formattedImage.type === 'image_url') {
imageUrls.push(formattedImage);
}
else {
formattedContent.push(formattedImage);
}
},
resource: (item) => {
const isUiResource = item.resource.uri.startsWith('ui://');
const resourceText = [];
if (isUiResource) {
const contentToHash = 'text' in item.resource && item.resource.text && typeof item.resource.text === 'string'
? item.resource.text
: item.resource.uri;
const resourceId = generateResourceId(contentToHash);
const uiResource = Object.assign(Object.assign({}, item.resource), { resourceId });
uiResources.push(uiResource);
resourceText.push(`UI Resource ID: ${resourceId}`);
resourceText.push(`UI Resource Marker: \\ui{${resourceId}}`);
}
else if ('text' in item.resource && item.resource.text != null && item.resource.text) {
resourceText.push(`Resource Text: ${item.resource.text}`);
}
if (item.resource.uri.length) {
resourceText.push(`Resource URI: ${item.resource.uri}`);
}
if (item.resource.mimeType != null && item.resource.mimeType) {
resourceText.push(`Resource MIME Type: ${item.resource.mimeType}`);
}
if (resourceText.length) {
currentTextBlock += (currentTextBlock ? '\n\n' : '') + resourceText.join('\n');
}
},
};
for (const item of content) {
const handler = contentHandlers[item.type];
if (handler) {
handler(item);
}
else {
const stringified = JSON.stringify(item, null, 2);
currentTextBlock += (currentTextBlock ? '\n\n' : '') + stringified;
}
}
if (uiResources.length > 0) {
const uiInstructions = `
UI Resource Markers Available:
- Each resource above includes a stable ID and a marker hint like \`\\ui{abc123}\`
- You should usually introduce what you're showing before placing the marker
- For a single resource: \\ui{resource-id}
- For multiple resources shown separately: \\ui{resource-id-a} \\ui{resource-id-b}
- For multiple resources in a carousel: \\ui{resource-id-a,resource-id-b,resource-id-c}
- The UI will be rendered inline where you place the marker
- Format: \\ui{resource-id} or \\ui{id1,id2,id3} using the IDs provided above`;
currentTextBlock += uiInstructions;
}
if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) {
formattedContent.push({ type: 'text', text: currentTextBlock });
}
let artifacts = undefined;
if (imageUrls.length) {
artifacts = { content: imageUrls };
}
if (uiResources.length) {
artifacts = Object.assign(Object.assign({}, artifacts), { [librechatDataProvider.Tools.ui_resources]: { data: uiResources } });
}
if (CONTENT_ARRAY_PROVIDERS.has(provider)) {
return [formattedContent, artifacts];
}
return [currentTextBlock, artifacts];
}
/**
* Centralized manager for MCP server connections and tool execution.
* Extends UserConnectionManager to handle both app-level and user-specific connections.
*/
class MCPManager extends UserConnectionManager {
/** Creates and initializes the singleton MCPManager instance */
static createInstance(configs) {
return __awaiter(this, void 0, void 0, function* () {
if (MCPManager.instance)
throw new Error('MCPManager has already been initialized.');
MCPManager.instance = new MCPManager();
yield MCPManager.instance.initialize(configs);
return MCPManager.instance;
});
}
/** Returns the singleton MCPManager instance */
static getInstance() {
if (!MCPManager.instance)
throw new Error('MCPManager has not been initialized.');
return MCPManager.instance;
}
/** Initializes the MCPManager by setting up server registry and app connections */
initialize(configs) {
return __awaiter(this, void 0, void 0, function* () {
yield MCPServersInitializer.initialize(configs);
this.appConnections = new ConnectionsRepository(undefined);
});
}
/** Retrieves an app-level or user-specific connection based on provided arguments */
getConnection(args) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
//the get method checks if the config is still valid as app level
const existingAppConnection = yield this.appConnections.get(args.serverName);
if (existingAppConnection) {
return existingAppConnection;
}
else if ((_a = args.user) === null || _a === void 0 ? void 0 : _a.id) {
return this.getUserConnection(args);
}
else {
throw new types_js.McpError(types_js.ErrorCode.InvalidRequest, `No connection found for server ${args.serverName}`);
}
});
}
/**
* Discovers tools from an MCP server, even when OAuth is required.
* Per MCP spec, tool listing should be possible without authentication.
* Use this for agent initialization to get tool schemas before OAuth flow.
*/
discoverServerTools(args) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const { serverName, user } = args;
const logPrefix = (user === null || user === void 0 ? void 0 : user.id) ? `[MCP][User: ${user.id}][${serverName}]` : `[MCP][${serverName}]`;
try {
const existingAppConnection = yield ((_a = this.appConnections) === null || _a === void 0 ? void 0 : _a.get(serverName));
if (existingAppConnection && (yield existingAppConnection.isConnected())) {
const tools = yield existingAppConnection.fetchTools();
return { tools, oauthRequired: false, oauthUrl: null };
}
}
catch (_b) {
dataSchemas.logger.debug(`${logPrefix} [Discovery] App connection not available, trying discovery mode`);
}
const serverConfig = (yield MCPServersRegistry.getInstance().getServerConfig(serverName, user === null || user === void 0 ? void 0 : user.id));
if (!serverConfig) {
dataSchemas.logger.warn(`${logPrefix} [Discovery] Server config not found`);
return { tools: null, oauthRequired: false, oauthUrl: null };
}
const useOAuth = Boolean(serverConfig.requiresOAuth || serverConfig.oauthMetadata);
const useSSRFProtection = MCPServersRegistry.getInstance().shouldEnableSSRFProtection();
const basic = { serverName, serverConfig, useSSRFProtection };
if (!useOAuth) {
const result = yield MCPConnectionFactory.discoverTools(basic);
return {
tools: result.tools,
oauthRequired: result.oauthRequired,
oauthUrl: result.oauthUrl,
};
}
if (!user || !args.flowManager) {
dataSchemas.logger.warn(`${logPrefix} [Discovery] OAuth server requires user and flowManager`);
return { tools: null, oauthRequired: true, oauthUrl: null };
}
const result = yield MCPConnectionFactory.discoverTools(basic, {
user,
useOAuth: true,
flowManager: args.flowManager,
tokenMethods: args.tokenMethods,
signal: args.signal,
oauthStart: args.oauthStart,
customUserVars: args.customUserVars,
requestBody: args.requestBody,
connectionTimeout: args.connectionTimeout,
});
return { tools: result.tools, oauthRequired: result.oauthRequired, oauthUrl: result.oauthUrl };
});
}
/** Returns all available tool functions from app-level connections */
getAppToolFunctions() {
return __awaiter(this, void 0, void 0, function* () {
const toolFunctions = {};
const configs = yield MCPServersRegistry.getInstance().getAllServerConfigs();
for (const config of Object.values(configs)) {
if (config.toolFunctions != null) {
Object.assign(toolFunctions, config.toolFunctions);
}
}
return toolFunctions;
});
}
/** Returns all available tool functions from all connections available to user */
getServerToolFunctions(userId, serverName) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
//try get the appConnection (if the config is not in the app level anymore any existing connection will disconnect and get will return null)
const existingAppConnection = yield ((_a = this.appConnections) === null || _a === void 0 ? void 0 : _a.get(serverName));
if (existingAppConnection) {
return MCPServerInspector.getToolFunctions(serverName, existingAppConnection);
}
const userConnections = this.getUserConnections(userId);
if (!userConnections || userConnections.size === 0) {
return null;
}
if (!userConnections.has(serverName)) {
return null;
}
return MCPServerInspector.getToolFunctions(serverName, userConnections.get(serverName));
}
catch (error) {
dataSchemas.logger.warn(`[getServerToolFunctions] Error getting tool functions for server ${serverName}`, error);
return null;
}
});
}
/**
* Get instructions for MCP servers
* @param serverNames Optional array of server names. If not provided or empty, returns all servers.
* @returns Object mapping server names to their instructions
*/
getInstructions(serverNames) {
return __awaiter(this, void 0, void 0, function* () {
const instructions = {};
const configs = yield MCPServersRegistry.getInstance().getAllServerConfigs();
for (const [serverName, config] of Object.entries(configs)) {
if (config.serverInstructions != null) {
instructions[serverName] = config.serverInstructions;
}
}
if (!serverNames)
return instructions;
return pick(instructions, serverNames);
});
}
/**
* Format MCP server instructions for injection into context
* @param serverNames Optional array of server names to include. If not provided, includes all servers.
* @returns Formatted instructions string ready for context injection
*/
formatInstructionsForContext(serverNames) {
return __awaiter(this, void 0, void 0, function* () {
/** Instructions for specified servers or all stored instructions */
const instructionsToInclude = yield this.getInstructions(serverNames);
if (Object.keys(instructionsToInclude).length === 0) {
return '';
}
// Format instructions for context injection
const formattedInstructions = Object.entries(instructionsToInclude)
.map(([serverName, instructions]) => {
return `## ${serverName} MCP Server Instructions
${instructions}`;
})
.join('\n\n');
return `# MCP Server Instructions
The following MCP servers are available with their specific instructions:
${formattedInstructions}
Please follow these instructions when using tools from the respective MCP servers.`;
});
}
/**
* Calls a tool on an MCP server, using either a user-specific connection
* (if userId is provided) or an app-level connection. Updates the last activity timestamp
* for user-specific connections upon successful call initiation.
*
* @param graphTokenResolver - Optional function to resolve Graph API tokens via OBO flow.
* When provided and the server config contains `{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}` placeholders,
* they will be resolved to actual Graph API tokens before the tool call.
*/
callTool(_a) {
return __awaiter(this, arguments, void 0, function* ({ user, serverName, toolName, provider, toolArguments, options, tokenMethods, requestBody, flowManager, oauthStart, oauthEnd, customUserVars, graphTokenResolver, }) {
/** User-specific connection */
let connection;
const userId = user === null || user === void 0 ? void 0 : user.id;
const logPrefix = userId ? `[MCP][User: ${userId}][${serverName}]` : `[MCP][${serverName}]`;
try {
if (userId && user)
this.updateUserLastActivity(userId);
connection = yield this.getConnection({
serverName,
user,
flowManager,
tokenMethods,
oauthStart,
oauthEnd,
signal: options === null || options === void 0 ? void 0 : options.signal,
customUserVars,
requestBody,
});
if (!(yield connection.isConnected())) {
/** May happen if getUserConnection failed silently or app connection dropped */
throw new types_js.McpError(types_js.ErrorCode.InternalError, // Use InternalError for connection issues
`${logPrefix} Connection is not active. Cannot execute tool ${toolName}.`);
}
const rawConfig = (yield MCPServersRegistry.getInstance().getServerConfig(serverName, userId));
// Pre-process Graph token placeholders (async) before sync processMCPEnv
const graphProcessedConfig = yield preProcessGraphTokens(rawConfig, {
user,
graphTokenResolver,
scopes: process.env.GRAPH_API_SCOPES,
});
const currentOptions = processMCPEnv({
user,
options: graphProcessedConfig,
customUserVars: customUserVars,
body: requestBody,
});
if ('headers' in currentOptions) {
connection.setRequestHeaders(currentOptions.headers || {});
}
const result = yield connection.client.request({
method: 'tools/call',
params: {
name: toolName,
arguments: toolArguments,
},
}, types_js.CallToolResultSchema, Object.assign({ timeout: connection.timeout, resetTimeoutOnProgress: true }, options));
if (userId) {
this.updateUserLastActivity(userId);
}
this.checkIdleConnections();
return formatToolContent(result, provider);
}
catch (error) {
// Log with context and re-throw or handle as needed
dataSchemas.logger.error(`${logPrefix}[${toolName}] Tool call failed`, error);
// Rethrowing allows the caller (createMCPTool) to handle the final user message
throw error;
}
});
}
}
/**
* Retrieves and decrypts authentication values for multiple plugins
* @returns A map where keys are pluginKeys and values are objects of authField:decryptedValue pairs
*/
function getPluginAuthMap(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, pluginKeys, throwError = true, findPluginAuthsByKeys, }) {
var _b;
try {
/** Early return for empty plugin keys */
if (!(pluginKeys === null || pluginKeys === void 0 ? void 0 : pluginKeys.length)) {
return {};
}
/** All plugin auths for current user query */
const pluginAuths = yield findPluginAuthsByKeys({ userId, pluginKeys });
/** Group auth records by pluginKey for efficient lookup */
const authsByPlugin = new Map();
for (const auth of pluginAuths) {
if (!auth.pluginKey) {
dataSchemas.logger.warn(`[getPluginAuthMap] Missing pluginKey for userId ${userId}`);
continue;
}
const existing = authsByPlugin.get(auth.pluginKey) || [];
existing.push(auth);
authsByPlugin.set(auth.pluginKey, existing);
}
const authMap = {};
const decryptionPromises = [];
/** Single loop through requested pluginKeys */
for (const pluginKey of pluginKeys) {
authMap[pluginKey] = {};
const auths = authsByPlugin.get(pluginKey) || [];
for (const auth of auths) {
decryptionPromises.push((() => __awaiter(this, void 0, void 0, function* () {
try {
const decryptedValue = yield dataSchemas.decrypt(auth.value);
authMap[pluginKey][auth.authField] = decryptedValue;
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
dataSchemas.logger.error(`[getPluginAuthMap] Decryption failed for userId ${userId}, plugin ${pluginKey}, field ${auth.authField}: ${message}`);
if (throwError) {
throw new Error(`Decryption failed for plugin ${pluginKey}, field ${auth.authField}: ${message}`);
}
}
}))());
}
}
yield Promise.all(decryptionPromises);
return authMap;
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
const plugins = (_b = pluginKeys === null || pluginKeys === void 0 ? void 0 : pluginKeys.join(', ')) !== null && _b !== void 0 ? _b : 'all requested';
dataSchemas.logger.warn(`[getPluginAuthMap] Failed to fetch auth values for userId ${userId}, plugins: ${plugins}: ${message}`);
if (!throwError) {
/** Empty objects for each plugin key on error */
return pluginKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {});
}
throw error;
}
});
}
function getUserMCPAuthMap(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, tools, servers, toolInstances, findPluginAuthsByKeys, }) {
let allMcpCustomUserVars = {};
let mcpPluginKeysToFetch = [];
try {
const uniqueMcpServers = new Set();
if (servers != null && servers.length) {
for (const serverName of servers) {
if (!serverName) {
continue;
}
uniqueMcpServers.add(`${librechatDataProvider.Constants.mcp_prefix}${serverName}`);
}
}
else if (tools != null && tools.length) {
for (const toolName of tools) {
if (!toolName) {
continue;
}
const delimiterIndex = toolName.indexOf(librechatDataProvider.Constants.mcp_delimiter);
if (delimiterIndex === -1)
continue;
const mcpServer = toolName.slice(delimiterIndex + librechatDataProvider.Constants.mcp_delimiter.length);
if (!mcpServer)
continue;
uniqueMcpServers.add(`${librechatDataProvider.Constants.mcp_prefix}${mcpServer}`);
}
}
else if (toolInstances != null && toolInstances.length) {
for (const tool of toolInstances) {
if (!tool) {
continue;
}
const mcpTool = tool;
if (mcpTool.mcpRawServerName) {
uniqueMcpServers.add(`${librechatDataProvider.Constants.mcp_prefix}${mcpTool.mcpRawServerName}`);
}
}
}
if (uniqueMcpServers.size === 0) {
return {};
}
mcpPluginKeysToFetch = Array.from(uniqueMcpServers);
allMcpCustomUserVars = yield getPluginAuthMap({
userId,
pluginKeys: mcpPluginKeysToFetch,
throwError: false,
findPluginAuthsByKeys,
});
}
catch (err) {
dataSchemas.logger.error(`[handleTools] Error batch fetching customUserVars for MCP tools (keys: ${mcpPluginKeysToFetch.join(', ')}), user ${userId}: ${err instanceof Error ? err.message : 'Unknown error'}`, err);
}
return allMcpCustomUserVars;
});
}
function isEmptyObjectSchema(jsonSchema) {
return (jsonSchema != null &&
typeof jsonSchema === 'object' &&
jsonSchema.type === 'object' &&
(jsonSchema.properties == null || Object.keys(jsonSchema.properties).length === 0) &&
!jsonSchema.additionalProperties // Don't treat objects with additionalProperties as empty
);
}
function dropSchemaFields(schema, fields) {
if (schema == null || typeof schema !== 'object') {
return schema;
}
// Handle arrays (should only occur for enum, required, etc.)
if (Array.isArray(schema)) {
// This should not happen for the root schema, but for completeness:
return schema;
}
const result = {};
for (const [key, value] of Object.entries(schema)) {
if (fields.includes(key)) {
continue;
}
// Recursively process nested schemas
if (key === 'items' || key === 'additionalProperties' || key === 'properties') {
if (key === 'properties' && value && typeof value === 'object') {
// properties is a record of string -> JsonSchemaType
const newProps = {};
for (const [propKey, propValue] of Object.entries(value)) {
const dropped = dropSchemaFields(propValue, fields);
if (dropped !== undefined) {
newProps[propKey] = dropped;
}
}
result[key] = newProps;
}
else if (key === 'items' || key === 'additionalProperties') {
const dropped = dropSchemaFields(value, fields);
if (dropped !== undefined) {
result[key] = dropped;
}
}
}
else {
result[key] = value;
}
}
// Only return if the result is still a valid JsonSchemaType (must have a type)
if (typeof result.type === 'string' &&
['string', 'number', 'boolean', 'array', 'object'].includes(result.type)) {
return result;
}
return undefined;
}
// Helper function to convert oneOf/anyOf to Zod unions
function convertToZodUnion(schemas, options) {
if (!Array.isArray(schemas) || schemas.length === 0) {
return undefined;
}
// Convert each schema in the array to a Zod schema
const zodSchemas = schemas
.map((subSchema) => {
// If the subSchema doesn't have a type, try to infer it
if (!subSchema.type && subSchema.properties) {
// It's likely an object schema
const objSchema = Object.assign(Object.assign({}, subSchema), { type: 'object' });
// Handle required fields for partial schemas
if (Array.isArray(subSchema.required) && subSchema.required.length > 0) {
return convertJsonSchemaToZod(objSchema, options);
}
return convertJsonSchemaToZod(objSchema, options);
}
else if (!subSchema.type && subSchema.additionalProperties) {
// It's likely an object schema with additionalProperties
const objSchema = Object.assign(Object.assign({}, subSchema), { type: 'object' });
return convertJsonSchemaToZod(objSchema, options);
}
else if (!subSchema.type && subSchema.items) {
// It's likely an array schema
return convertJsonSchemaToZod(Object.assign(Object.assign({}, subSchema), { type: 'array' }), options);
}
else if (!subSchema.type && Array.isArray(subSchema.enum)) {
// It's likely an enum schema
return convertJsonSchemaToZod(Object.assign(Object.assign({}, subSchema), { type: 'string' }), options);
}
else if (!subSchema.type && subSchema.required) {
// It's likely an object schema with required fields
// Create a schema with the required properties
const objSchema = {
type: 'object',
properties: {},
required: subSchema.required,
};
return convertJsonSchemaToZod(objSchema, options);
}
else if (!subSchema.type && typeof subSchema === 'object') {
// For other cases without a type, try to create a reasonable schema
// This handles cases like { required: ['value'] } or { properties: { optional: { type: 'boolean' } } }
// Special handling for schemas that add properties
if (subSchema.properties && Object.keys(subSchema.properties).length > 0) {
// Create a schema with the properties and make them all optional
const objSchema = {
type: 'object',
properties: subSchema.properties,
additionalProperties: true, // Allow additional properties
// Don't include required here to make all properties optional
};
// Convert to Zod schema
const zodSchema = convertJsonSchemaToZod(objSchema, options);
// For the special case of { optional: true }
if ('optional' in subSchema.properties) {
// Create a custom schema that preserves the optional property
const customSchema = z.z
.object({
optional: z.z.boolean(),
})
.passthrough();
return customSchema;
}
if (zodSchema instanceof z.z.ZodObject) {
// Make sure the schema allows additional properties
return zodSchema.passthrough();
}
return zodSchema;
}
// Default handling for other cases
const objSchema = Object.assign({ type: 'object' }, subSchema);
return convertJsonSchemaToZod(objSchema, options);
}
// If it has a type, convert it normally
return convertJsonSchemaToZod(subSchema, options);
})
.filter((schema) => schema !== undefined);
if (zodSchemas.length === 0) {
return undefined;
}
if (zodSchemas.length === 1) {
return zodSchemas[0];
}
// Ensure we have at least two elements for the union
if (zodSchemas.length >= 2) {
return z.z.union([zodSchemas[0], zodSchemas[1], ...zodSchemas.slice(2)]);
}
// This should never happen due to the previous checks, but TypeScript needs it
return zodSchemas[0];
}
/**
* Helper function to resolve $ref references
* @param schema - The schema to resolve
* @param definitions - The definitions to use
* @param visited - The set of visited references
* @returns The resolved schema
*/
function resolveJsonSchemaRefs(schema, definitions, visited = new Set()) {
// Handle null, undefined, or non-object values first
if (!schema || typeof schema !== 'object') {
return schema;
}
// If no definitions provided, try to extract from schema.$defs or schema.definitions
if (!definitions) {
definitions = (schema.$defs || schema.definitions);
}
// Handle arrays
if (Array.isArray(schema)) {
return schema.map((item) => resolveJsonSchemaRefs(item, definitions, visited));
}
// Handle objects
const result = {};
for (const [key, value] of Object.entries(schema)) {
// Skip $defs/definitions — they are only used for resolving $ref and
// should not appear in the resolved output (e.g. Google/Gemini API rejects them).
if (key === '$defs' || key === 'definitions') {
continue;
}
// Handle $ref
if (key === '$ref' && typeof value === 'string') {
// Prevent circular references
if (visited.has(value)) {
// Return a simple schema to break the cycle
return { type: 'object' };
}
// Extract the reference path
const refPath = value.replace(/^#\/(\$defs|definitions)\//, '');
const resolved = definitions === null || definitions === void 0 ? void 0 : definitions[refPath];
if (resolved) {
visited.add(value);
const resolvedSchema = resolveJsonSchemaRefs(resolved, definitions, visited);
visited.delete(value);
// Merge the resolved schema into the result
Object.assign(result, resolvedSchema);
}
else {
// If we can't resolve the reference, keep it as is
result[key] = value;
}
}
else if (value && typeof value === 'object') {
// Recursively resolve nested objects/arrays
result[key] = resolveJsonSchemaRefs(value, definitions, visited);
}
else {
// Copy primitive values as is
result[key] = value;
}
}
return result;
}
/**
* Recursively normalizes a JSON schema for LLM API compatibility.
*
* Transformations applied:
* - Converts `const` values to `enum` arrays (Gemini/Vertex AI rejects `const`)
* - Strips vendor extension fields (`x-*` prefixed keys, e.g. `x-google-enum-descriptions`)
* - Strips leftover `$defs`/`definitions` blocks that may survive ref resolution
*
* @param schema - The JSON schema to normalize
* @returns The normalized schema
*/
function normalizeJsonSchema(schema) {
if (!schema || typeof schema !== 'object') {
return schema;
}
if (Array.isArray(schema)) {
return schema.map((item) => item && typeof item === 'object' ? normalizeJsonSchema(item) : item);
}
const result = {};
for (const [key, value] of Object.entries(schema)) {
// Strip vendor extension fields (e.g. x-google-enum-descriptions) —
// these are valid in JSON Schema but rejected by Google/Gemini API.
if (key.startsWith('x-')) {
continue;
}
// Strip leftover $defs/definitions (should already be resolved by resolveJsonSchemaRefs,
// but strip as a safety net for schemas that bypass ref resolution).
if (key === '$defs' || key === 'definitions') {
continue;
}
if (key === 'const' && !('enum' in schema)) {
result['enum'] = [value];
continue;
}
if (key === 'const' && 'enum' in schema) {
// Skip `const` when `enum` already exists
continue;
}
if (key === 'properties' && value && typeof value === 'object' && !Array.isArray(value)) {
const newProps = {};
for (const [propKey, propValue] of Object.entries(value)) {
newProps[propKey] =
propValue && typeof propValue === 'object'
? normalizeJsonSchema(propValue)
: propValue;
}
result[key] = newProps;
}
else if ((key === 'items' || key === 'additionalProperties') &&
value &&
typeof value === 'object') {
result[key] = normalizeJsonSchema(value);
}
else if ((key === 'oneOf' || key === 'anyOf' || key === 'allOf') && Array.isArray(value)) {
result[key] = value.map((item) => item && typeof item === 'object' ? normalizeJsonSchema(item) : item);
}
else {
result[key] = value;
}
}
return result;
}
/**
* Converts a JSON Schema to a Zod schema.
*
* @deprecated This function is deprecated in favor of using JSON schemas directly.
* LangChain.js now supports JSON schemas natively, eliminating the need for Zod conversion.
* Use `resolveJsonSchemaRefs` to handle $ref references and pass the JSON schema directly to tools.
*
* @see https://js.langchain.com/docs/how_to/custom_tools/
*/
function convertJsonSchemaToZod(schema, options = {}) {
var _a;
const { allowEmptyObject = true, dropFields, transformOneOfAnyOf = false } = options;
// Handle oneOf/anyOf if transformOneOfAnyOf is enabled
if (transformOneOfAnyOf) {
// For top-level oneOf/anyOf
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
// Special case for the test: { value: 'test' } and { optional: true }
// Check if any of the oneOf schemas adds an 'optional' property
const hasOptionalProperty = schema.oneOf.some((subSchema) => subSchema.properties &&
typeof subSchema.properties === 'object' &&
'optional' in subSchema.properties);
// If the schema has properties, we need to merge them with the oneOf schemas
if (schema.properties && Object.keys(schema.properties).length > 0) {
// Create a base schema without oneOf
const baseSchema = Object.assign({}, schema);
delete baseSchema.oneOf;
// Convert the base schema
const baseZodSchema = convertJsonSchemaToZod(baseSchema, Object.assign(Object.assign({}, options), { transformOneOfAnyOf: false }));
// Convert the oneOf schemas
const oneOfZodSchema = convertToZodUnion(schema.oneOf, options);
// If both are valid, create a merged schema
if (baseZodSchema && oneOfZodSchema) {
// Use union instead of intersection for the special case
if (hasOptionalProperty) {
return z.z.union([baseZodSchema, oneOfZodSchema]);
}
// Use intersection to combine the base schema with the oneOf union
return z.z.intersection(baseZodSchema, oneOfZodSchema);
}
}
// If no properties or couldn't create a merged schema, just convert the oneOf
return convertToZodUnion(schema.oneOf, options);
}
// For top-level anyOf
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
// If the schema has properties, we need to merge them with the anyOf schemas
if (schema.properties && Object.keys(schema.properties).length > 0) {
// Create a base schema without anyOf
const baseSchema = Object.assign({}, schema);
delete baseSchema.anyOf;
// Convert the base schema
const baseZodSchema = convertJsonSchemaToZod(baseSchema, Object.assign(Object.assign({}, options), { transformOneOfAnyOf: false }));
// Convert the anyOf schemas
const anyOfZodSchema = convertToZodUnion(schema.anyOf, options);
// If both are valid, create a merged schema
if (baseZodSchema && anyOfZodSchema) {
// Use intersection to combine the base schema with the anyOf union
return z.z.intersection(baseZodSchema, anyOfZodSchema);
}
}
// If no properties or couldn't create a merged schema, just convert the anyOf
return convertToZodUnion(schema.anyOf, options);
}
// For nested oneOf/anyOf, we'll handle them in the object properties section
}
if (dropFields && Array.isArray(dropFields) && dropFields.length > 0) {
const droppedSchema = dropSchemaFields(schema, dropFields);
if (!droppedSchema) {
return undefined;
}
schema = droppedSchema;
}
if (!allowEmptyObject && isEmptyObjectSchema(schema)) {
return undefined;
}
let zodSchema;
// Handle primitive types
if (schema.type === 'string') {
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
const [first, ...rest] = schema.enum;
zodSchema = z.z.enum([first, ...rest]);
}
else {
zodSchema = z.z.string();
}
}
else if (schema.type === 'number' || schema.type === 'integer' || schema.type === 'float') {
zodSchema = z.z.number();
}
else if (schema.type === 'boolean') {
zodSchema = z.z.boolean();
}
else if (schema.type === 'array' && schema.items !== undefined) {
const itemSchema = convertJsonSchemaToZod(schema.items);
zodSchema = z.z.array((itemSchema !== null && itemSchema !== void 0 ? itemSchema : z.z.unknown()));
}
else if (schema.type === 'object') {
const shape = {};
const properties = (_a = schema.properties) !== null && _a !== void 0 ? _a : {};
/** Check if this is a bare object schema with no properties defined
and no explicit additionalProperties setting */
const isBareObjectSchema = Object.keys(properties).length === 0 &&
schema.additionalProperties === undefined &&
!schema.patternProperties &&
!schema.propertyNames &&
!schema.$ref &&
!schema.allOf &&
!schema.anyOf &&
!schema.oneOf;
for (const [key, value] of Object.entries(properties)) {
// Handle nested oneOf/anyOf if transformOneOfAnyOf is enabled
if (transformOneOfAnyOf) {
const valueWithAny = value;
// Check for nested oneOf
if (Array.isArray(valueWithAny.oneOf) && valueWithAny.oneOf.length > 0) {
// Convert with transformOneOfAnyOf enabled
let fieldSchema = convertJsonSchemaToZod(valueWithAny, Object.assign(Object.assign({}, options), { transformOneOfAnyOf: true }));
if (!fieldSchema) {
continue;
}
if (value.description != null && value.description !== '') {
fieldSchema = fieldSchema.describe(value.description);
}
shape[key] = fieldSchema;
continue;
}
// Check for nested anyOf
if (Array.isArray(valueWithAny.anyOf) && valueWithAny.anyOf.length > 0) {
// Convert with transformOneOfAnyOf enabled
let fieldSchema = convertJsonSchemaToZod(valueWithAny, Object.assign(Object.assign({}, options), { transformOneOfAnyOf: true }));
if (!fieldSchema) {
continue;
}
if (value.description != null && value.description !== '') {
fieldSchema = fieldSchema.describe(value.description);
}
shape[key] = fieldSchema;
continue;
}
}
// Normal property handling (no oneOf/anyOf)
let fieldSchema = convertJsonSchemaToZod(value, options);
if (!fieldSchema) {
continue;
}
if (value.description != null && value.description !== '') {
fieldSchema = fieldSchema.describe(value.description);
}
shape[key] = fieldSchema;
}
let objectSchema = z.z.object(shape);
if (Array.isArray(schema.required) && schema.required.length > 0) {
const partial = Object.fromEntries(Object.entries(shape).map(([key, value]) => {
var _a;
return [
key,
((_a = schema.required) === null || _a === void 0 ? void 0 : _a.includes(key)) === true ? value : value.optional().nullable(),
];
}));
objectSchema = z.z.object(partial);
}
else {
const partialNullable = Object.fromEntries(Object.entries(shape).map(([key, value]) => [key, value.optional().nullable()]));
objectSchema = z.z.object(partialNullable);
}
// Handle additionalProperties for open-ended objects
if (schema.additionalProperties === true || isBareObjectSchema) {
// This allows any additional properties with any type
// Bare object schemas are treated as passthrough to allow dynamic properties
zodSchema = objectSchema.passthrough();
}
else if (typeof schema.additionalProperties === 'object') {
// For specific additional property types
const additionalSchema = convertJsonSchemaToZod(schema.additionalProperties);
zodSchema = objectSchema.catchall((additionalSchema !== null && additionalSchema !== void 0 ? additionalSchema : z.z.unknown()));
}
else {
zodSchema = objectSchema;
}
}
else {
zodSchema = z.z.unknown();
}
// Add description if present
if (schema.description != null && schema.description !== '') {
zodSchema = zodSchema.describe(schema.description);
}
return zodSchema;
}
/**
* Helper function that resolves refs before converting to Zod.
*
* @deprecated This function is deprecated in favor of using JSON schemas directly.
* LangChain.js now supports JSON schemas natively, eliminating the need for Zod conversion.
* Use `resolveJsonSchemaRefs` to handle $ref references and pass the JSON schema directly to tools.
*
* @see https://js.langchain.com/docs/how_to/custom_tools/
*/
function convertWithResolvedRefs(schema, options) {
const resolved = resolveJsonSchemaRefs(schema);
return convertJsonSchemaToZod(resolved, options);
}
/**
* Ensures that a collection exists in the database.
* For DocumentDB compatibility, it tries multiple approaches.
* @param db - The MongoDB database instance
* @param collectionName - The name of the collection to ensure exists
*/
function ensureCollectionExists(db, collectionName) {
return __awaiter(this, void 0, void 0, function* () {
try {
const collections = yield db.listCollections({ name: collectionName }).toArray();
if (collections.length === 0) {
yield db.createCollection(collectionName);
dataSchemas.logger.info(`Created collection: ${collectionName}`);
}
else {
dataSchemas.logger.debug(`Collection already exists: ${collectionName}`);
}
}
catch (error) {
dataSchemas.logger.error(`Failed to check/create "${collectionName}" collection:`, error);
// If listCollections fails, try alternative approach
try {
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
yield db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
}
catch (createError) {
dataSchemas.logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
}
}
});
}
/**
* Ensures that all required collections exist for the permission system.
* This includes aclentries, groups, accessroles, and any other collections
* needed for migrations and permission checks.
* @param db - The MongoDB database instance
*/
function ensureRequiredCollectionsExist(db) {
return __awaiter(this, void 0, void 0, function* () {
const requiredCollections = [
'aclentries', // ACL permission entries
'groups', // User groups
'accessroles', // Access roles for permissions
'agents', // Agents collection
'promptgroups', // Prompt groups collection
'projects', // Projects collection
];
dataSchemas.logger.debug('Ensuring required collections exist for permission system');
for (const collectionName of requiredCollections) {
yield ensureCollectionExists(db, collectionName);
}
dataSchemas.logger.debug('All required collections have been checked/created');
});
}
const OAUTH_CSRF_COOKIE = 'oauth_csrf';
const OAUTH_CSRF_MAX_AGE = 10 * 60 * 1000;
const OAUTH_SESSION_COOKIE = 'oauth_session';
const OAUTH_SESSION_MAX_AGE = 24 * 60 * 60 * 1000;
const OAUTH_SESSION_COOKIE_PATH = '/api';
/**
* Determines if secure cookies should be used.
* Returns `true` in production unless the server is running on localhost (HTTP).
* This allows cookies to work on `http://localhost` during local development
* even when `NODE_ENV=production` (common in Docker Compose setups).
*/
function shouldUseSecureCookie() {
const isProduction = process.env.NODE_ENV === 'production';
const domainServer = process.env.DOMAIN_SERVER || '';
let hostname = '';
if (domainServer) {
try {
const normalized = /^https?:\/\//i.test(domainServer)
? domainServer
: `http://${domainServer}`;
const url = new URL(normalized);
hostname = (url.hostname || '').toLowerCase();
}
catch (_a) {
hostname = domainServer.toLowerCase();
}
}
const isLocalhost = hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname.endsWith('.localhost');
return isProduction && !isLocalhost;
}
/** Generates an HMAC-based token for OAuth CSRF protection */
function generateOAuthCsrfToken(flowId, secret) {
const key = secret || process.env.JWT_SECRET;
if (!key) {
throw new Error('JWT_SECRET is required for OAuth CSRF token generation');
}
return crypto$2.createHmac('sha256', key).update(flowId).digest('hex').slice(0, 32);
}
/** Sets a SameSite=Lax CSRF cookie bound to a specific OAuth flow */
function setOAuthCsrfCookie(res, flowId, cookiePath) {
res.cookie(OAUTH_CSRF_COOKIE, generateOAuthCsrfToken(flowId), {
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'lax',
maxAge: OAUTH_CSRF_MAX_AGE,
path: cookiePath,
});
}
/**
* Validates the per-flow CSRF cookie against the expected HMAC.
* Uses timing-safe comparison and always clears the cookie to prevent replay.
*/
function validateOAuthCsrf(req, res, flowId, cookiePath) {
var _a;
const cookie = (_a = req.cookies) === null || _a === void 0 ? void 0 : _a[OAUTH_CSRF_COOKIE];
res.clearCookie(OAUTH_CSRF_COOKIE, { path: cookiePath });
if (!cookie) {
return false;
}
const expected = generateOAuthCsrfToken(flowId);
if (cookie.length !== expected.length) {
return false;
}
return crypto$2.timingSafeEqual(Buffer.from(cookie), Buffer.from(expected));
}
/**
* Express middleware that sets the OAuth session cookie after JWT authentication.
* Chain after requireJwtAuth on routes that precede an OAuth redirect (e.g., reinitialize, bind).
*/
function setOAuthSession(req, res, next) {
var _a;
const user = req.user;
if ((user === null || user === void 0 ? void 0 : user.id) && !((_a = req.cookies) === null || _a === void 0 ? void 0 : _a[OAUTH_SESSION_COOKIE])) {
setOAuthSessionCookie(res, user.id);
}
next();
}
/** Sets a SameSite=Lax session cookie that binds the browser to the authenticated userId */
function setOAuthSessionCookie(res, userId) {
res.cookie(OAUTH_SESSION_COOKIE, generateOAuthCsrfToken(userId), {
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'lax',
maxAge: OAUTH_SESSION_MAX_AGE,
path: OAUTH_SESSION_COOKIE_PATH,
});
}
/** Validates the session cookie against the expected userId using timing-safe comparison */
function validateOAuthSession(req, userId) {
var _a;
const cookie = (_a = req.cookies) === null || _a === void 0 ? void 0 : _a[OAUTH_SESSION_COOKIE];
if (!cookie) {
return false;
}
const expected = generateOAuthCsrfToken(userId);
if (cookie.length !== expected.length) {
return false;
}
return crypto$2.timingSafeEqual(Buffer.from(cookie), Buffer.from(expected));
}
function createHandleOAuthToken({ findToken, updateToken, createToken, }) {
/**
* Handles the OAuth token by creating or updating the token.
* @param fields
* @param fields.userId - The user's ID.
* @param fields.token - The full token to store.
* @param fields.identifier - Unique, alternative identifier for the token.
* @param fields.expiresIn - The number of seconds until the token expires.
* @param fields.metadata - Additional metadata to store with the token.
* @param [fields.type="oauth"] - The type of token. Default is 'oauth'.
*/
return function handleOAuthToken(_a) {
return __awaiter(this, arguments, void 0, function* ({ token, userId, identifier, expiresIn, metadata, type = 'oauth', }) {
const encrypedToken = yield dataSchemas.encryptV2(token);
let expiresInNumber = 3600;
if (typeof expiresIn === 'number') {
expiresInNumber = expiresIn;
}
else if (expiresIn != null) {
expiresInNumber = parseInt(expiresIn, 10) || 3600;
}
const tokenData = {
type,
userId,
metadata,
identifier,
token: encrypedToken,
expiresIn: expiresInNumber,
};
const existingToken = yield findToken({ userId, identifier });
if (existingToken) {
return yield updateToken({ identifier }, tokenData);
}
else {
return yield createToken(tokenData);
}
});
};
}
/**
* Processes the access tokens and stores them in the database.
* @param tokenData
* @param tokenData.access_token
* @param tokenData.expires_in
* @param [tokenData.refresh_token]
* @param [tokenData.refresh_token_expires_in]
* @param metadata
* @param metadata.userId
* @param metadata.identifier
*/
function processAccessTokens(tokenData_1, _a, _b) {
return __awaiter(this, arguments, void 0, function* (tokenData, { userId, identifier }, { findToken, updateToken, createToken, }) {
const { access_token, expires_in = 3600, refresh_token, refresh_token_expires_in } = tokenData;
if (!access_token) {
dataSchemas.logger.error('Access token not found: ', tokenData);
throw new Error('Access token not found');
}
const handleOAuthToken = createHandleOAuthToken({
findToken,
updateToken,
createToken,
});
yield handleOAuthToken({
identifier,
token: access_token,
expiresIn: expires_in,
userId,
});
if (refresh_token != null) {
dataSchemas.logger.debug('Processing refresh token');
yield handleOAuthToken({
token: refresh_token,
type: 'oauth_refresh',
userId,
identifier: `${identifier}:refresh`,
expiresIn: refresh_token_expires_in !== null && refresh_token_expires_in !== void 0 ? refresh_token_expires_in : null,
});
}
dataSchemas.logger.debug('Access tokens processed');
});
}
/**
* Refreshes the access token using the refresh token.
* @param fields
* @param fields.userId - The ID of the user.
* @param fields.client_url - The URL of the OAuth provider.
* @param fields.identifier - The identifier for the token.
* @param fields.refresh_token - The refresh token to use.
* @param fields.token_exchange_method - The token exchange method ('default_post' or 'basic_auth_header').
* @param fields.encrypted_oauth_client_id - The client ID for the OAuth provider.
* @param fields.encrypted_oauth_client_secret - The client secret for the OAuth provider.
*/
function refreshAccessToken(_a, _b) {
return __awaiter(this, arguments, void 0, function* ({ userId, client_url, identifier, refresh_token, token_exchange_method, encrypted_oauth_client_id, encrypted_oauth_client_secret, }, { findToken, updateToken, createToken, }) {
try {
const oauth_client_id = yield dataSchemas.decryptV2(encrypted_oauth_client_id);
const oauth_client_secret = yield dataSchemas.decryptV2(encrypted_oauth_client_secret);
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
};
const params = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token,
});
if (token_exchange_method === librechatDataProvider.TokenExchangeMethodEnum.BasicAuthHeader) {
const basicAuth = Buffer.from(`${oauth_client_id}:${oauth_client_secret}`).toString('base64');
headers['Authorization'] = `Basic ${basicAuth}`;
}
else {
params.append('client_id', oauth_client_id);
params.append('client_secret', oauth_client_secret);
}
const response = yield axios$1({
method: 'POST',
url: client_url,
headers,
data: params.toString(),
});
yield processAccessTokens(response.data, {
userId,
identifier,
}, {
findToken,
updateToken,
createToken,
});
dataSchemas.logger.debug(`Access token refreshed successfully for ${identifier}`);
return response.data;
}
catch (error) {
const message = 'Error refreshing OAuth tokens';
throw new Error(logAxiosError({
message,
error: error,
}));
}
});
}
/**
* Handles the OAuth callback and exchanges the authorization code for tokens.
* @param {object} fields
* @param {string} fields.code - The authorization code returned by the provider.
* @param {string} fields.userId - The ID of the user.
* @param {string} fields.identifier - The identifier for the token.
* @param {string} fields.client_url - The URL of the OAuth provider.
* @param {string} fields.redirect_uri - The redirect URI for the OAuth provider.
* @param {string} fields.token_exchange_method - The token exchange method ('default_post' or 'basic_auth_header').
* @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider.
* @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider.
*/
function getAccessToken(_a, _b) {
return __awaiter(this, arguments, void 0, function* ({ code, userId, identifier, client_url, redirect_uri, token_exchange_method, encrypted_oauth_client_id, encrypted_oauth_client_secret, }, { findToken, updateToken, createToken, }) {
const oauth_client_id = yield dataSchemas.decryptV2(encrypted_oauth_client_id);
const oauth_client_secret = yield dataSchemas.decryptV2(encrypted_oauth_client_secret);
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
};
const params = new URLSearchParams({
code,
grant_type: 'authorization_code',
redirect_uri,
});
if (token_exchange_method === librechatDataProvider.TokenExchangeMethodEnum.BasicAuthHeader) {
const basicAuth = Buffer.from(`${oauth_client_id}:${oauth_client_secret}`).toString('base64');
headers['Authorization'] = `Basic ${basicAuth}`;
}
else {
params.append('client_id', oauth_client_id);
params.append('client_secret', oauth_client_secret);
}
try {
const response = yield axios$1({
method: 'POST',
url: client_url,
headers,
data: params.toString(),
});
yield processAccessTokens(response.data, {
userId,
identifier,
}, {
findToken,
updateToken,
createToken,
});
dataSchemas.logger.debug(`Access tokens successfully created for ${identifier}`);
return response.data;
}
catch (error) {
const message = 'Error exchanging OAuth code';
throw new Error(logAxiosError({
message,
error: error,
}));
}
});
}
class OAuthReconnectionTracker {
constructor() {
/** Map of userId -> Set of serverNames that have failed reconnection */
this.failed = new Map();
/** Map of userId -> Set of serverNames that are actively reconnecting */
this.active = new Map();
/** Map of userId:serverName -> timestamp when reconnection started */
this.activeTimestamps = new Map();
/** Maximum time (ms) a server can be in reconnecting state before auto-cleanup */
this.RECONNECTION_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
}
isFailed(userId, serverName) {
var _a, _b;
return (_b = (_a = this.failed.get(userId)) === null || _a === void 0 ? void 0 : _a.has(serverName)) !== null && _b !== void 0 ? _b : false;
}
/** Check if server is in the active set (original simple check) */
isActive(userId, serverName) {
var _a, _b;
return (_b = (_a = this.active.get(userId)) === null || _a === void 0 ? void 0 : _a.has(serverName)) !== null && _b !== void 0 ? _b : false;
}
/** Check if server is still reconnecting (considers timeout) */
isStillReconnecting(userId, serverName) {
if (!this.isActive(userId, serverName)) {
return false;
}
const key = `${userId}:${serverName}`;
const startTime = this.activeTimestamps.get(key);
// If there's a timestamp and it has timed out, it's not still reconnecting
if (startTime && Date.now() - startTime > this.RECONNECTION_TIMEOUT_MS) {
return false;
}
return true;
}
/** Clean up server if it has timed out - returns true if cleanup was performed */
cleanupIfTimedOut(userId, serverName) {
const key = `${userId}:${serverName}`;
const startTime = this.activeTimestamps.get(key);
if (startTime && Date.now() - startTime > this.RECONNECTION_TIMEOUT_MS) {
this.removeActive(userId, serverName);
return true;
}
return false;
}
setFailed(userId, serverName) {
var _a;
if (!this.failed.has(userId)) {
this.failed.set(userId, new Set());
}
(_a = this.failed.get(userId)) === null || _a === void 0 ? void 0 : _a.add(serverName);
}
setActive(userId, serverName) {
var _a;
if (!this.active.has(userId)) {
this.active.set(userId, new Set());
}
(_a = this.active.get(userId)) === null || _a === void 0 ? void 0 : _a.add(serverName);
/** Track when reconnection started */
const key = `${userId}:${serverName}`;
this.activeTimestamps.set(key, Date.now());
}
removeFailed(userId, serverName) {
const userServers = this.failed.get(userId);
userServers === null || userServers === void 0 ? void 0 : userServers.delete(serverName);
if ((userServers === null || userServers === void 0 ? void 0 : userServers.size) === 0) {
this.failed.delete(userId);
}
}
removeActive(userId, serverName) {
const userServers = this.active.get(userId);
userServers === null || userServers === void 0 ? void 0 : userServers.delete(serverName);
if ((userServers === null || userServers === void 0 ? void 0 : userServers.size) === 0) {
this.active.delete(userId);
}
/** Clear timestamp tracking */
const key = `${userId}:${serverName}`;
this.activeTimestamps.delete(key);
}
}
const DEFAULT_CONNECTION_TIMEOUT_MS = 10000; // ms
const RECONNECT_STAGGER_MS = 500; // ms between each server reconnection
class OAuthReconnectionManager {
static getInstance() {
if (!OAuthReconnectionManager.instance) {
throw new Error('OAuthReconnectionManager not initialized');
}
return OAuthReconnectionManager.instance;
}
static createInstance(flowManager, tokenMethods, reconnections) {
return __awaiter(this, void 0, void 0, function* () {
if (OAuthReconnectionManager.instance != null) {
throw new Error('OAuthReconnectionManager already initialized');
}
const manager = new OAuthReconnectionManager(flowManager, tokenMethods, reconnections);
OAuthReconnectionManager.instance = manager;
return manager;
});
}
constructor(flowManager, tokenMethods, reconnections) {
this.flowManager = flowManager;
this.tokenMethods = tokenMethods;
this.reconnectionsTracker = reconnections !== null && reconnections !== void 0 ? reconnections : new OAuthReconnectionTracker();
try {
this.mcpManager = MCPManager.getInstance();
}
catch (_a) {
this.mcpManager = null;
}
}
isReconnecting(userId, serverName) {
// Clean up if timed out, then return whether still reconnecting
this.reconnectionsTracker.cleanupIfTimedOut(userId, serverName);
return this.reconnectionsTracker.isStillReconnecting(userId, serverName);
}
reconnectServers(userId) {
return __awaiter(this, void 0, void 0, function* () {
// Check if MCPManager is available
if (this.mcpManager == null) {
dataSchemas.logger.warn('[OAuthReconnectionManager] MCPManager not available, skipping OAuth MCP server reconnection');
return;
}
// 1. derive the servers to reconnect
const serversToReconnect = [];
for (const serverName of yield MCPServersRegistry.getInstance().getOAuthServers()) {
const canReconnect = yield this.canReconnect(userId, serverName);
if (canReconnect) {
serversToReconnect.push(serverName);
}
}
// 2. mark the servers as reconnecting
for (const serverName of serversToReconnect) {
this.reconnectionsTracker.setActive(userId, serverName);
}
// 3. attempt to reconnect the servers with staggered delays to avoid connection storms
for (let i = 0; i < serversToReconnect.length; i++) {
const serverName = serversToReconnect[i];
if (i === 0) {
void this.tryReconnect(userId, serverName);
}
else {
setTimeout(() => void this.tryReconnect(userId, serverName), i * RECONNECT_STAGGER_MS);
}
}
});
}
clearReconnection(userId, serverName) {
this.reconnectionsTracker.removeFailed(userId, serverName);
this.reconnectionsTracker.removeActive(userId, serverName);
}
tryReconnect(userId, serverName) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.mcpManager == null) {
return;
}
const logPrefix = `[tryReconnectOAuthMCPServer][User: ${userId}][${serverName}]`;
dataSchemas.logger.info(`${logPrefix} Attempting reconnection`);
const config = yield MCPServersRegistry.getInstance().getServerConfig(serverName, userId);
const cleanupOnFailedReconnect = () => {
var _a;
this.reconnectionsTracker.setFailed(userId, serverName);
this.reconnectionsTracker.removeActive(userId, serverName);
(_a = this.mcpManager) === null || _a === void 0 ? void 0 : _a.disconnectUserConnection(userId, serverName);
};
try {
// attempt to get connection (this will use existing tokens and refresh if needed)
const connection = yield this.mcpManager.getUserConnection({
serverName,
user: { id: userId },
flowManager: this.flowManager,
tokenMethods: this.tokenMethods,
// don't force new connection, let it reuse existing or create new as needed
forceNew: false,
// set a reasonable timeout for reconnection attempts
connectionTimeout: (_a = config === null || config === void 0 ? void 0 : config.initTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECTION_TIMEOUT_MS,
// don't trigger OAuth flow during reconnection
returnOnOAuth: true,
});
if (connection && (yield connection.isConnected())) {
dataSchemas.logger.info(`${logPrefix} Successfully reconnected`);
this.clearReconnection(userId, serverName);
}
else {
dataSchemas.logger.warn(`${logPrefix} Failed to reconnect`);
yield (connection === null || connection === void 0 ? void 0 : connection.disconnect());
cleanupOnFailedReconnect();
}
}
catch (error) {
dataSchemas.logger.warn(`${logPrefix} Failed to reconnect: ${error}`);
cleanupOnFailedReconnect();
}
});
}
canReconnect(userId, serverName) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.mcpManager == null) {
return false;
}
// if the server has failed reconnection, don't attempt to reconnect
if (this.reconnectionsTracker.isFailed(userId, serverName)) {
return false;
}
if (this.reconnectionsTracker.isActive(userId, serverName)) {
return false;
}
// if the server is already connected, don't attempt to reconnect
const existingConnections = this.mcpManager.getUserConnections(userId);
if (existingConnections === null || existingConnections === void 0 ? void 0 : existingConnections.has(serverName)) {
const isConnected = yield ((_a = existingConnections.get(serverName)) === null || _a === void 0 ? void 0 : _a.isConnected());
if (isConnected) {
return false;
}
}
// if the server has no tokens for the user, don't attempt to reconnect
const accessToken = yield this.tokenMethods.findToken({
userId,
type: 'mcp_oauth',
identifier: `mcp:${serverName}`,
});
if (accessToken == null) {
return false;
}
// if the token has expired, don't attempt to reconnect
const now = new Date();
if (accessToken.expiresAt && accessToken.expiresAt < now) {
return false;
}
// …otherwise, we're good to go with the reconnect attempt
return true;
});
}
}
OAuthReconnectionManager.instance = null;
/**
* Generate a short-lived JWT token
* @param {String} userId - The ID of the user
* @param {String} [expireIn='5m'] - The expiration time for the token (default is 5 minutes)
* @returns {String} - The generated JWT token
*/
const generateShortLivedToken = (userId, expireIn = '5m') => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET, {
expiresIn: expireIn,
algorithm: 'HS256',
});
};
class FlowStateManager {
constructor(store, options) {
if (!options) {
options = { ttl: 60000 * 3 };
}
const { ci = false, ttl } = options;
if (!ci && !(store instanceof keyv.Keyv)) {
throw new Error('Invalid store provided to FlowStateManager');
}
this.ttl = ttl;
this.keyv = store;
this.intervals = new Set();
if (!ci) {
this.setupCleanupHandlers();
}
}
setupCleanupHandlers() {
const cleanup = () => {
dataSchemas.logger.info('Cleaning up FlowStateManager intervals...');
this.intervals.forEach((interval) => clearInterval(interval));
this.intervals.clear();
process.exit(0);
};
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGQUIT', cleanup);
process.on('SIGHUP', cleanup);
}
getFlowKey(flowId, type) {
return `${type}:${flowId}`;
}
/**
* Normalizes an expiration timestamp to milliseconds.
* Detects whether the input is in seconds or milliseconds based on magnitude.
* Timestamps below 10 billion are assumed to be in seconds (valid until ~2286).
* @param timestamp - The expiration timestamp (in seconds or milliseconds)
* @returns The timestamp normalized to milliseconds
*/
normalizeExpirationTimestamp(timestamp) {
const SECONDS_THRESHOLD = 1e10;
if (timestamp < SECONDS_THRESHOLD) {
return timestamp * 1000;
}
return timestamp;
}
/**
* Checks if a flow's token has expired based on its expires_at field
* @param flowState - The flow state to check
* @returns true if the token has expired, false otherwise (including if no expires_at exists)
*/
isTokenExpired(flowState) {
if (!(flowState === null || flowState === void 0 ? void 0 : flowState.result)) {
return false;
}
if (typeof flowState.result !== 'object') {
return false;
}
if (!('expires_at' in flowState.result)) {
return false;
}
const expiresAt = flowState.result.expires_at;
if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) {
return false;
}
const normalizedExpiresAt = this.normalizeExpirationTimestamp(expiresAt);
return normalizedExpiresAt < Date.now();
}
/**
* Creates a new flow and waits for its completion
*/
createFlow(flowId_1, type_1) {
return __awaiter(this, arguments, void 0, function* (flowId, type, metadata = {}, signal) {
const flowKey = this.getFlowKey(flowId, type);
let existingState = (yield this.keyv.get(flowKey));
if (existingState) {
dataSchemas.logger.debug(`[${flowKey}] Flow already exists`);
return this.monitorFlow(flowKey, type, signal);
}
yield new Promise((resolve) => setTimeout(resolve, 250));
existingState = (yield this.keyv.get(flowKey));
if (existingState) {
dataSchemas.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
return this.monitorFlow(flowKey, type, signal);
}
const initialState = {
type,
status: 'PENDING',
metadata,
createdAt: Date.now(),
};
dataSchemas.logger.debug(`[${flowKey}] Creating initial flow state`);
yield this.keyv.set(flowKey, initialState, this.ttl);
return this.monitorFlow(flowKey, type, signal);
});
}
monitorFlow(flowKey, type, signal) {
return new Promise((resolve, reject) => {
const checkInterval = 2000;
let elapsedTime = 0;
let isCleanedUp = false;
let intervalId = null;
// Cleanup function to avoid duplicate cleanup
const cleanup = () => {
if (isCleanedUp)
return;
isCleanedUp = true;
if (intervalId) {
clearInterval(intervalId);
this.intervals.delete(intervalId);
}
if (signal && abortHandler) {
signal.removeEventListener('abort', abortHandler);
}
};
// Immediate abort handler - responds instantly to abort signal
const abortHandler = () => __awaiter(this, void 0, void 0, function* () {
cleanup();
dataSchemas.logger.warn(`[${flowKey}] Flow aborted (immediate)`);
const message = `${type} flow aborted`;
try {
yield this.keyv.delete(flowKey);
}
catch (_a) {
// Ignore delete errors during abort
}
reject(new Error(message));
});
// Register abort handler immediately if signal provided
if (signal) {
if (signal.aborted) {
// Already aborted, reject immediately
cleanup();
reject(new Error(`${type} flow aborted`));
return;
}
signal.addEventListener('abort', abortHandler, { once: true });
}
intervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
var _a;
if (isCleanedUp)
return;
try {
const flowState = (yield this.keyv.get(flowKey));
if (!flowState) {
cleanup();
dataSchemas.logger.error(`[${flowKey}] Flow state not found`);
reject(new Error(`${type} Flow state not found`));
return;
}
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
cleanup();
dataSchemas.logger.warn(`[${flowKey}] Flow aborted`);
const message = `${type} flow aborted`;
yield this.keyv.delete(flowKey);
reject(new Error(message));
return;
}
if (flowState.status !== 'PENDING') {
cleanup();
dataSchemas.logger.debug(`[${flowKey}] Flow completed`);
if (flowState.status === 'COMPLETED' && flowState.result !== undefined) {
resolve(flowState.result);
}
else if (flowState.status === 'FAILED') {
yield this.keyv.delete(flowKey);
reject(new Error((_a = flowState.error) !== null && _a !== void 0 ? _a : `${type} flow failed`));
}
return;
}
elapsedTime += checkInterval;
if (elapsedTime >= this.ttl) {
cleanup();
dataSchemas.logger.error(`[${flowKey}] Flow timed out | Elapsed time: ${elapsedTime} | TTL: ${this.ttl}`);
yield this.keyv.delete(flowKey);
reject(new Error(`${type} flow timed out`));
}
dataSchemas.logger.debug(`[${flowKey}] Flow state elapsed time: ${elapsedTime}, checking again...`);
}
catch (error) {
dataSchemas.logger.error(`[${flowKey}] Error checking flow state:`, error);
cleanup();
reject(error);
}
}), checkInterval);
this.intervals.add(intervalId);
});
}
/**
* Completes a flow successfully
*/
completeFlow(flowId, type, result) {
return __awaiter(this, void 0, void 0, function* () {
const flowKey = this.getFlowKey(flowId, type);
const flowState = (yield this.keyv.get(flowKey));
if (!flowState) {
dataSchemas.logger.warn('[FlowStateManager] Cannot complete flow - flow state not found', {
flowId,
type,
});
return false;
}
/** Prevent duplicate completion */
if (flowState.status === 'COMPLETED') {
dataSchemas.logger.debug('[FlowStateManager] Flow already completed, skipping to prevent duplicate completion', {
flowId,
type,
});
return true;
}
const updatedState = Object.assign(Object.assign({}, flowState), { status: 'COMPLETED', result, completedAt: Date.now() });
yield this.keyv.set(flowKey, updatedState, this.ttl);
dataSchemas.logger.debug('[FlowStateManager] Flow completed successfully', {
flowId,
type,
});
return true;
});
}
/**
* Checks if a flow is stale based on its age and status
* @param flowId - The flow identifier
* @param type - The flow type
* @param staleThresholdMs - Age in milliseconds after which a non-pending flow is considered stale (default: 2 minutes)
* @returns Object with isStale boolean and age in milliseconds
*/
isFlowStale(flowId_1, type_1) {
return __awaiter(this, arguments, void 0, function* (flowId, type, staleThresholdMs = 2 * 60 * 1000) {
const flowKey = this.getFlowKey(flowId, type);
const flowState = (yield this.keyv.get(flowKey));
if (!flowState) {
return { isStale: false, age: 0 };
}
if (flowState.status === 'PENDING') {
return { isStale: false, age: 0, status: flowState.status };
}
const completedAt = flowState.completedAt || flowState.failedAt;
const createdAt = flowState.createdAt;
let flowAge = 0;
if (completedAt) {
flowAge = Date.now() - completedAt;
}
else if (createdAt) {
flowAge = Date.now() - createdAt;
}
return {
isStale: flowAge > staleThresholdMs,
age: flowAge,
status: flowState.status,
};
});
}
/**
* Marks a flow as failed
*/
failFlow(flowId, type, error) {
return __awaiter(this, void 0, void 0, function* () {
const flowKey = this.getFlowKey(flowId, type);
const flowState = (yield this.keyv.get(flowKey));
if (!flowState) {
return false;
}
const updatedState = Object.assign(Object.assign({}, flowState), { status: 'FAILED', error: error instanceof Error ? error.message : error, failedAt: Date.now() });
yield this.keyv.set(flowKey, updatedState, this.ttl);
return true;
});
}
/**
* Gets current flow state
*/
getFlowState(flowId, type) {
return __awaiter(this, void 0, void 0, function* () {
const flowKey = this.getFlowKey(flowId, type);
return this.keyv.get(flowKey);
});
}
/**
* Creates a new flow and waits for its completion, only executing the handler if no existing flow is found
* @param flowId - The ID of the flow
* @param type - The type of flow
* @param handler - Async function to execute if no existing flow is found
* @param signal - Optional AbortSignal to cancel the flow
*/
createFlowWithHandler(flowId, type, handler, signal) {
return __awaiter(this, void 0, void 0, function* () {
const flowKey = this.getFlowKey(flowId, type);
let existingState = (yield this.keyv.get(flowKey));
if (existingState && !this.isTokenExpired(existingState)) {
dataSchemas.logger.debug(`[${flowKey}] Flow already exists with valid token`);
return this.monitorFlow(flowKey, type, signal);
}
yield new Promise((resolve) => setTimeout(resolve, 250));
existingState = (yield this.keyv.get(flowKey));
if (existingState && !this.isTokenExpired(existingState)) {
dataSchemas.logger.debug(`[${flowKey}] Flow exists on 2nd check with valid token`);
return this.monitorFlow(flowKey, type, signal);
}
const initialState = {
type,
status: 'PENDING',
metadata: {},
createdAt: Date.now(),
};
dataSchemas.logger.debug(`[${flowKey}] Creating initial flow state`);
yield this.keyv.set(flowKey, initialState, this.ttl);
try {
const result = yield handler();
yield this.completeFlow(flowId, type, result);
return result;
}
catch (error) {
yield this.failFlow(flowId, type, error instanceof Error ? error : new Error(String(error)));
throw error;
}
});
}
/**
* Deletes a flow state
*/
deleteFlow(flowId, type) {
return __awaiter(this, void 0, void 0, function* () {
const flowKey = this.getFlowKey(flowId, type);
try {
yield this.keyv.delete(flowKey);
dataSchemas.logger.debug(`[${flowKey}] Flow deleted`);
return true;
}
catch (error) {
dataSchemas.logger.error(`[${flowKey}] Error deleting flow:`, error);
return false;
}
});
}
}
function skipAgentCheck(req) {
var _a, _b;
if (!req || !((_a = req === null || req === void 0 ? void 0 : req.body) === null || _a === void 0 ? void 0 : _a.endpoint)) {
return false;
}
if (req.method !== 'POST') {
return false;
}
if (!((_b = req.originalUrl) === null || _b === void 0 ? void 0 : _b.includes(librechatDataProvider.EndpointURLs[librechatDataProvider.EModelEndpoint.agents]))) {
return false;
}
return !librechatDataProvider.isAgentsEndpoint(req.body.endpoint);
}
/**
* Core function to check if a user has one or more required permissions
* @param user - The user object
* @param permissionType - The type of permission to check
* @param permissions - The list of specific permissions to check
* @param bodyProps - An optional object where keys are permissions and values are arrays of properties to check
* @param checkObject - The object to check properties against
* @param skipCheck - An optional function that takes the checkObject and returns true to skip permission checking
* @returns Whether the user has the required permissions
*/
const checkAccess = (_a) => __awaiter(void 0, [_a], void 0, function* ({ req, user, permissionType, permissions, getRoleByName, bodyProps = {}, checkObject = {}, skipCheck, }) {
var _b;
if (skipCheck && skipCheck(req)) {
return true;
}
if (!user || !user.role) {
return false;
}
const role = yield getRoleByName(user.role);
const permissionValue = (_b = role === null || role === void 0 ? void 0 : role.permissions) === null || _b === void 0 ? void 0 : _b[permissionType];
if (role && role.permissions && permissionValue) {
const hasAnyPermission = permissions.every((permission) => {
if (permissionValue[permission]) {
return true;
}
if (bodyProps[permission] && checkObject) {
return bodyProps[permission].every((prop) => Object.prototype.hasOwnProperty.call(checkObject, prop));
}
return false;
});
return hasAnyPermission;
}
return false;
});
/**
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
* @param permissionType - The type of permission to check.
* @param permissions - The list of specific permissions to check.
* @param bodyProps - An optional object where keys are permissions and values are arrays of `req.body` properties to check.
* @param skipCheck - An optional function that takes req.body and returns true to skip permission checking.
* @param getRoleByName - A function to get the role by name.
* @returns Express middleware function.
*/
const generateCheckAccess = ({ permissionType, permissions, bodyProps = {}, skipCheck, getRoleByName, }) => {
return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
try {
const hasAccess = yield checkAccess({
req,
user: req.user,
permissionType,
permissions,
bodyProps,
checkObject: req.body,
skipCheck,
getRoleByName,
});
if (hasAccess) {
return next();
}
dataSchemas.logger.warn(`[${permissionType}] Forbidden: "${req.originalUrl}" - Insufficient permissions for User ${(_a = req.user) === null || _a === void 0 ? void 0 : _a.id}: ${permissions.join(', ')}`);
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
}
catch (error) {
dataSchemas.logger.error(error);
return res.status(500).json({
message: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}`,
});
}
});
};
/**
* Middleware to check if authenticated user has admin role.
* Should be used AFTER authentication middleware (requireJwtAuth, requireLocalAuth, etc.)
*/
const requireAdmin = (req, res, next) => {
if (!req.user) {
dataSchemas.logger.warn('[requireAdmin] No user found in request');
return res.status(401).json({
error: 'Authentication required',
error_code: 'AUTHENTICATION_REQUIRED',
});
}
if (!req.user.role || req.user.role !== librechatDataProvider.SystemRoles.ADMIN) {
dataSchemas.logger.debug(`[requireAdmin] Access denied for non-admin user: ${req.user.email}`);
return res.status(403).json({
error: 'Access denied: Admin privileges required',
error_code: 'ADMIN_REQUIRED',
});
}
next();
};
const handleDuplicateKeyError = (err, res) => {
dataSchemas.logger.warn('Duplicate key error: ' + (err.errmsg || err.message));
const field = err.keyValue ? `${JSON.stringify(Object.keys(err.keyValue))}` : 'unknown';
const code = 409;
res
.status(code)
.send({ messages: `An document with that ${field} already exists.`, fields: field });
};
const handleValidationError = (err, res) => {
dataSchemas.logger.error('Validation error:', err.errors);
const errorMessages = Object.values(err.errors).map((el) => el.message);
const fields = `${JSON.stringify(Object.values(err.errors).map((el) => el.path))}`;
const code = 400;
const messages = errorMessages.length > 1
? `${JSON.stringify(errorMessages.join(' '))}`
: `${JSON.stringify(errorMessages)}`;
res.status(code).send({ messages, fields });
};
/** Type guard for ValidationError */
function isValidationError(err) {
return err !== null && typeof err === 'object' && 'name' in err && err.name === 'ValidationError';
}
/** Type guard for MongoServerError (duplicate key) */
function isMongoServerError(err) {
return err !== null && typeof err === 'object' && 'code' in err && err.code === 11000;
}
/** Type guard for CustomError with statusCode and body */
function isCustomError(err) {
return err !== null && typeof err === 'object' && 'statusCode' in err && 'body' in err;
}
const ErrorController = (err, req, res, next) => {
try {
if (!err) {
return next();
}
const error = err;
if ((error.message === librechatDataProvider.ErrorTypes.AUTH_FAILED || error.code === librechatDataProvider.ErrorTypes.AUTH_FAILED) &&
req.originalUrl &&
req.originalUrl.includes('/oauth/') &&
req.originalUrl.includes('/callback')) {
const domain = process.env.DOMAIN_CLIENT || 'http://localhost:3080';
return res.redirect(`${domain}/login?redirect=false&error=${librechatDataProvider.ErrorTypes.AUTH_FAILED}`);
}
if (isValidationError(error)) {
return handleValidationError(error, res);
}
if (isMongoServerError(error)) {
return handleDuplicateKeyError(error, res);
}
if (isCustomError(error) && error.statusCode && error.body) {
return res.status(error.statusCode).send(error.body);
}
dataSchemas.logger.error('ErrorController => error', err);
return res.status(500).send('An unknown error occurred.');
}
catch (processingError) {
dataSchemas.logger.error('ErrorController => processing error', processingError);
return res.status(500).send('Processing error in ErrorController.');
}
};
/**
* Build an object containing fields that need updating
* @param config - The balance configuration
* @param userRecord - The user's current balance record, if any
* @param userId - The user's ID
* @returns Fields that need updating
*/
function buildUpdateFields(config, userRecord, userId) {
const updateFields = {};
// Ensure user record has the required fields
if (!userRecord) {
updateFields.user = userId;
updateFields.tokenCredits = config.startBalance;
// Set credit grant timestamp and expiry for new users
const now = new Date();
updateFields.creditsGrantedAt = now;
if (config.creditExpiryDays && config.creditExpiryDays > 0) {
updateFields.expiresAt = new Date(now.getTime() + config.creditExpiryDays * 24 * 60 * 60 * 1000);
}
}
if ((userRecord === null || userRecord === void 0 ? void 0 : userRecord.tokenCredits) == null && config.startBalance != null) {
updateFields.tokenCredits = config.startBalance;
}
const isAutoRefillConfigValid = config.autoRefillEnabled &&
config.refillIntervalValue != null &&
config.refillIntervalUnit != null &&
config.refillAmount != null;
if (!isAutoRefillConfigValid) {
return updateFields;
}
if ((userRecord === null || userRecord === void 0 ? void 0 : userRecord.autoRefillEnabled) !== config.autoRefillEnabled) {
updateFields.autoRefillEnabled = config.autoRefillEnabled;
}
if ((userRecord === null || userRecord === void 0 ? void 0 : userRecord.refillIntervalValue) !== config.refillIntervalValue) {
updateFields.refillIntervalValue = config.refillIntervalValue;
}
if ((userRecord === null || userRecord === void 0 ? void 0 : userRecord.refillIntervalUnit) !== config.refillIntervalUnit) {
updateFields.refillIntervalUnit = config.refillIntervalUnit;
}
if ((userRecord === null || userRecord === void 0 ? void 0 : userRecord.refillAmount) !== config.refillAmount) {
updateFields.refillAmount = config.refillAmount;
}
// Initialize lastRefill if it's missing when auto-refill is enabled
if (config.autoRefillEnabled && !(userRecord === null || userRecord === void 0 ? void 0 : userRecord.lastRefill)) {
updateFields.lastRefill = new Date();
}
return updateFields;
}
/**
* Factory function to create middleware that synchronizes user balance settings with current balance configuration.
* @param options - Options containing getBalanceConfig function and Balance model
* @returns Express middleware function
*/
function createSetBalanceConfig({ getAppConfig, Balance, }) {
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
try {
const user = req.user;
const appConfig = yield getAppConfig({ role: user === null || user === void 0 ? void 0 : user.role });
const balanceConfig = getBalanceConfig(appConfig);
if (!(balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.enabled)) {
return next();
}
if (balanceConfig.startBalance == null) {
return next();
}
if (!user || !user._id) {
return next();
}
const userId = typeof user._id === 'string' ? user._id : user._id.toString();
const userBalanceRecord = yield Balance.findOne({ user: userId }).lean();
const updateFields = buildUpdateFields(balanceConfig, userBalanceRecord, userId);
if (Object.keys(updateFields).length === 0) {
return next();
}
yield Balance.findOneAndUpdate({ user: userId }, { $set: updateFields }, { upsert: true, new: true });
next();
}
catch (error) {
dataSchemas.logger.error('Error setting user balance:', error);
next(error);
}
});
}
/**
* Middleware to handle JSON parsing errors from express.json()
* Prevents user input from being reflected in error messages (XSS prevention)
*
* This middleware should be placed immediately after express.json() middleware.
*
* @param err - Error object from express.json()
* @param req - Express request object
* @param res - Express response object
* @param next - Express next function
*
* @example
* app.use(express.json({ limit: '3mb' }));
* app.use(handleJsonParseError);
*/
function handleJsonParseError(err, req, res, next) {
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
dataSchemas.logger.warn('[JSON Parse Error] Invalid JSON received', {
path: req.path,
method: req.method,
ip: req.ip,
});
res.status(400).json({
error: 'Invalid JSON format',
message: 'The request body contains malformed JSON',
});
return;
}
next(err);
}
const { USE_REDIS } = cacheConfig;
const LIMIT_CONCURRENT_MESSAGES = process.env.LIMIT_CONCURRENT_MESSAGES;
const CONCURRENT_MESSAGE_MAX = math(process.env.CONCURRENT_MESSAGE_MAX, 2);
const CONCURRENT_VIOLATION_SCORE = math(process.env.CONCURRENT_VIOLATION_SCORE, 1);
/**
* Lua script for atomic check-and-increment.
* Increments the key, sets TTL, and if over limit decrements back.
* Returns positive count if allowed, negative count if rejected.
* Single round-trip, fully atomic — eliminates the INCR/check/DECR race window.
*/
const CHECK_AND_INCREMENT_SCRIPT = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local current = redis.call('INCR', key)
redis.call('EXPIRE', key, ttl)
if current > limit then
redis.call('DECR', key)
return -current
end
return current
`;
/**
* Lua script for atomic decrement-and-cleanup.
* Decrements the key and deletes it if the count reaches zero or below.
* Eliminates the DECR-then-DEL race window.
*/
const DECREMENT_SCRIPT = `
local key = KEYS[1]
local current = redis.call('DECR', key)
if current <= 0 then
redis.call('DEL', key)
return 0
end
return current
`;
/** Lazily initialized cache for pending requests (used only for in-memory fallback) */
let pendingReqCache = null;
/**
* Get or create the pending requests cache for in-memory fallback.
* Uses lazy initialization to avoid creating cache before app is ready.
*/
function getPendingReqCache() {
if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
return null;
}
if (!pendingReqCache) {
pendingReqCache = standardCache(librechatDataProvider.CacheKeys.PENDING_REQ);
}
return pendingReqCache;
}
/**
* Build the cache key for a user's pending requests.
* Note: ioredisClient already has keyPrefix applied, so we only add namespace:userId
*/
function buildKey(userId) {
const namespace = librechatDataProvider.CacheKeys.PENDING_REQ;
return `${namespace}:${userId}`;
}
/**
* Build the cache key for in-memory fallback (Keyv).
*/
function buildMemoryKey(userId) {
return `:${userId}`;
}
/**
* Check if a user can make a new concurrent request and increment the counter if allowed.
* This is designed for resumable streams where the HTTP response lifecycle doesn't match
* the actual request processing lifecycle.
*
* When Redis is available, uses atomic INCR to prevent race conditions.
* Falls back to non-atomic get/set for in-memory cache.
*
* @param userId - The user's ID
* @returns Object with `allowed` (boolean), `pendingRequests` (current count), and `limit`
*/
function checkAndIncrementPendingRequest(userId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1);
if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
return { allowed: true, pendingRequests: 0, limit };
}
if (!userId) {
dataSchemas.logger.warn('[concurrency] checkAndIncrementPendingRequest called without userId');
return { allowed: true, pendingRequests: 0, limit };
}
// Use atomic Lua script when Redis is available to prevent race conditions.
// A single EVAL round-trip atomically increments, checks, and decrements if over-limit.
if (USE_REDIS && exports.ioredisClient) {
const key = buildKey(userId);
try {
const result = (yield exports.ioredisClient.eval(CHECK_AND_INCREMENT_SCRIPT, 1, key, limit, 60));
if (result < 0) {
// Negative return means over-limit (absolute value is the count before decrement)
const count = -result;
dataSchemas.logger.debug(`[concurrency] User ${userId} exceeded concurrent limit: ${count}/${limit}`);
return { allowed: false, pendingRequests: count, limit };
}
dataSchemas.logger.debug(`[concurrency] User ${userId} incremented pending requests: ${result}/${limit}`);
return { allowed: true, pendingRequests: result, limit };
}
catch (error) {
dataSchemas.logger.error('[concurrency] Redis atomic increment failed:', error);
// On Redis error, allow the request to proceed (fail-open)
return { allowed: true, pendingRequests: 0, limit };
}
}
// Fallback: non-atomic in-memory cache (race condition possible but acceptable for in-memory)
const cache = getPendingReqCache();
if (!cache) {
return { allowed: true, pendingRequests: 0, limit };
}
const key = buildMemoryKey(userId);
const pendingRequests = +((_a = (yield cache.get(key))) !== null && _a !== void 0 ? _a : 0);
if (pendingRequests >= limit) {
dataSchemas.logger.debug(`[concurrency] User ${userId} exceeded concurrent limit: ${pendingRequests}/${limit}`);
return { allowed: false, pendingRequests, limit };
}
yield cache.set(key, pendingRequests + 1, librechatDataProvider.Time.ONE_MINUTE);
dataSchemas.logger.debug(`[concurrency] User ${userId} incremented pending requests: ${pendingRequests + 1}/${limit}`);
return { allowed: true, pendingRequests: pendingRequests + 1, limit };
});
}
/**
* Decrement the pending request counter for a user.
* Should be called when a generation job completes, errors, or is aborted.
*
* This function handles errors internally and will never throw - it's a cleanup
* operation that should not interrupt the main flow if cache operations fail.
*
* When Redis is available, uses atomic DECR to prevent race conditions.
* Falls back to non-atomic get/set for in-memory cache.
*
* @param userId - The user's ID
*/
function decrementPendingRequest(userId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
return;
}
if (!userId) {
dataSchemas.logger.warn('[concurrency] decrementPendingRequest called without userId');
return;
}
// Use atomic Lua script to decrement and clean up zero/negative keys in one round-trip
if (USE_REDIS && exports.ioredisClient) {
const key = buildKey(userId);
try {
const newCount = (yield exports.ioredisClient.eval(DECREMENT_SCRIPT, 1, key));
if (newCount === 0) {
dataSchemas.logger.debug(`[concurrency] User ${userId} pending requests cleared`);
}
else {
dataSchemas.logger.debug(`[concurrency] User ${userId} decremented pending requests: ${newCount}`);
}
}
catch (error) {
dataSchemas.logger.error('[concurrency] Redis atomic decrement failed:', error);
}
return;
}
// Fallback: non-atomic in-memory cache
const cache = getPendingReqCache();
if (!cache) {
return;
}
const key = buildMemoryKey(userId);
const currentReq = +((_a = (yield cache.get(key))) !== null && _a !== void 0 ? _a : 0);
if (currentReq >= 1) {
yield cache.set(key, currentReq - 1, librechatDataProvider.Time.ONE_MINUTE);
dataSchemas.logger.debug(`[concurrency] User ${userId} decremented pending requests: ${currentReq - 1}`);
}
else {
yield cache.delete(key);
dataSchemas.logger.debug(`[concurrency] User ${userId} pending requests cleared (was ${currentReq})`);
}
}
catch (error) {
dataSchemas.logger.error('[concurrency] Error decrementing pending request:', error);
}
});
}
/**
* Get violation info for logging purposes when a user exceeds the concurrent request limit.
*/
function getViolationInfo(pendingRequests, limit) {
return {
type: librechatDataProvider.ViolationTypes.CONCURRENT,
limit,
pendingRequests,
score: CONCURRENT_VIOLATION_SCORE,
};
}
/**
* Check if concurrent message limiting is enabled.
*/
function isConcurrentLimitEnabled() {
return isEnabled(LIMIT_CONCURRENT_MESSAGES);
}
const MAX_AVATAR_REFRESH_AGENTS = 1000;
const AVATAR_REFRESH_BATCH_SIZE = 20;
/**
* Opportunistically refreshes S3-backed avatars for agent list responses.
* Processes agents in batches to prevent database connection pool exhaustion.
* Only list responses are refreshed because they're the highest-traffic surface and
* the avatar URLs have a short-lived TTL. The refresh is cached per-user for 30 minutes
* so we refresh once per interval at most.
*
* Any user with VIEW access to an agent can refresh its avatar URL. This ensures
* avatars remain accessible even when the owner hasn't logged in recently.
* The agents array should already be filtered to only include agents the user can access.
*/
const refreshListAvatars = (_a) => __awaiter(void 0, [_a], void 0, function* ({ agents, userId, refreshS3Url, updateAgent, }) {
const stats = {
updated: 0,
not_s3: 0,
no_id: 0,
no_change: 0,
s3_error: 0,
persist_error: 0,
};
if (!(agents === null || agents === void 0 ? void 0 : agents.length)) {
return stats;
}
dataSchemas.logger.debug('[refreshListAvatars] Refreshing S3 avatars for agents: %d', agents.length);
for (let i = 0; i < agents.length; i += AVATAR_REFRESH_BATCH_SIZE) {
const batch = agents.slice(i, i + AVATAR_REFRESH_BATCH_SIZE);
yield Promise.all(batch.map((agent) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
if (((_a = agent === null || agent === void 0 ? void 0 : agent.avatar) === null || _a === void 0 ? void 0 : _a.source) !== librechatDataProvider.FileSources.s3 || !((_b = agent === null || agent === void 0 ? void 0 : agent.avatar) === null || _b === void 0 ? void 0 : _b.filepath)) {
stats.not_s3++;
return;
}
if (!(agent === null || agent === void 0 ? void 0 : agent.id)) {
dataSchemas.logger.debug('[refreshListAvatars] Skipping S3 avatar refresh for agent: %s, ID is not set', agent._id);
stats.no_id++;
return;
}
try {
dataSchemas.logger.debug('[refreshListAvatars] Refreshing S3 avatar for agent: %s', agent._id);
const newPath = yield refreshS3Url(agent.avatar);
if (newPath && newPath !== agent.avatar.filepath) {
try {
yield updateAgent({ id: agent.id }, {
avatar: {
filepath: newPath,
source: agent.avatar.source,
},
}, {
updatingUserId: userId,
skipVersioning: true,
});
stats.updated++;
}
catch (persistErr) {
dataSchemas.logger.error('[refreshListAvatars] Avatar refresh persist error: %o', persistErr);
stats.persist_error++;
}
}
else {
stats.no_change++;
}
}
catch (err) {
dataSchemas.logger.error('[refreshListAvatars] S3 avatar refresh error: %o', err);
stats.s3_error++;
}
})));
}
dataSchemas.logger.info('[refreshListAvatars] Avatar refresh summary: %o', stats);
return stats;
});
const DEFAULT_PROMPT_TEMPLATE = `Based on the following conversation and analysis from previous agents, please provide your insights:\n\n{convo}\n\nPlease add your specific expertise and perspective to this discussion.`;
/**
* Helper function to create sequential chain edges with buffer string prompts
*
* @deprecated Agent Chain helper
* @param agentIds - Array of agent IDs in order of execution
* @param promptTemplate - Optional prompt template string; defaults to a predefined template if not provided
* @returns Array of edges configured for sequential chain with buffer prompts
*/
function createSequentialChainEdges(agentIds_1) {
return __awaiter(this, arguments, void 0, function* (agentIds, promptTemplate = DEFAULT_PROMPT_TEMPLATE) {
const edges = [];
for (let i = 0; i < agentIds.length - 1; i++) {
const fromAgent = agentIds[i];
const toAgent = agentIds[i + 1];
edges.push({
from: fromAgent,
to: toAgent,
edgeType: 'direct',
// Use a prompt function to create the buffer string from all previous results
prompt: (messages$1, startIndex) => __awaiter(this, void 0, void 0, function* () {
/** Only the messages from this run (after startIndex) are passed in */
const runMessages = messages$1.slice(startIndex);
const bufferString = messages.getBufferString(runMessages);
const template = prompts.PromptTemplate.fromTemplate(promptTemplate);
const result = yield template.invoke({
convo: bufferString,
});
return result.value;
}),
/** Critical: exclude previous results so only the prompt is passed */
excludeResults: true,
description: `Sequential chain from ${fromAgent} to ${toAgent}`,
});
}
return edges;
});
}
const omitTitleOptions = new Set([
'stream',
'thinking',
'streaming',
'clientOptions',
'thinkingConfig',
'thinkingBudget',
'includeThoughts',
'maxOutputTokens',
'additionalModelRequestFields',
]);
function payloadParser({ req, endpoint }) {
var _b, _c;
if (librechatDataProvider.isAgentsEndpoint(endpoint)) {
return;
}
return (_c = (_b = req.body) === null || _b === void 0 ? void 0 : _b.endpointOption) === null || _c === void 0 ? void 0 : _c.model_parameters;
}
function createTokenCounter(encoding) {
return function (message) {
const countTokens = (text) => TokenizerSingleton.getTokenCount(text, encoding);
return agents.getTokenCountForMessage(message, countTokens);
};
}
function logToolError(_graph, error, toolId) {
logAxiosError({
error,
message: `[api/server/controllers/agents/client.js #chatCompletion] Tool Error "${toolId}"`,
});
}
const AGENT_SUFFIX_PATTERN = /____(\d+)$/;
/** Finds the primary agent ID within a set of agent IDs (no suffix or lowest suffix number) */
function findPrimaryAgentId(agentIds) {
let primaryAgentId = null;
let lowestSuffixIndex = Infinity;
for (const agentId of agentIds) {
const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN);
if (!suffixMatch) {
return agentId;
}
const suffixIndex = parseInt(suffixMatch[1], 10);
if (suffixIndex < lowestSuffixIndex) {
lowestSuffixIndex = suffixIndex;
primaryAgentId = agentId;
}
}
return primaryAgentId;
}
/**
* Creates a mapMethod for getMessagesForConversation that processes agent content.
* - Strips agentId/groupId metadata from all content
* - For parallel agents (addedConvo with groupId): filters each group to its primary agent
* - For handoffs (agentId without groupId): keeps all content from all agents
* - For multi-agent: applies agent labels to content
*
* The key distinction:
* - Parallel execution (addedConvo): Parts have both agentId AND groupId
* - Handoffs: Parts only have agentId, no groupId
*/
function createMultiAgentMapper(primaryAgent, agentConfigs) {
var _b, _c, _d;
const hasMultipleAgents = ((_c = (_b = primaryAgent.edges) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0 || ((_d = agentConfigs === null || agentConfigs === void 0 ? void 0 : agentConfigs.size) !== null && _d !== void 0 ? _d : 0) > 0;
let agentNames = null;
if (hasMultipleAgents) {
agentNames = { [primaryAgent.id]: primaryAgent.name || 'Assistant' };
if (agentConfigs) {
for (const [agentId, agentConfig] of agentConfigs.entries()) {
agentNames[agentId] = agentConfig.name || agentConfig.id;
}
}
}
return (message) => {
if (message.isCreatedByUser || !Array.isArray(message.content)) {
return message;
}
const hasAgentMetadata = message.content.some((part) => (part === null || part === void 0 ? void 0 : part.agentId) ||
(part === null || part === void 0 ? void 0 : part.groupId) != null);
if (!hasAgentMetadata) {
return message;
}
try {
const groupAgentMap = new Map();
for (const part of message.content) {
const p = part;
const groupId = p === null || p === void 0 ? void 0 : p.groupId;
const agentId = p === null || p === void 0 ? void 0 : p.agentId;
if (groupId != null && agentId) {
if (!groupAgentMap.has(groupId)) {
groupAgentMap.set(groupId, new Set());
}
groupAgentMap.get(groupId).add(agentId);
}
}
const groupPrimaryMap = new Map();
for (const [groupId, agentIds] of groupAgentMap) {
const primary = findPrimaryAgentId(agentIds);
if (primary) {
groupPrimaryMap.set(groupId, primary);
}
}
const filteredContent = [];
const agentIdMap = {};
for (const part of message.content) {
const p = part;
const agentId = p === null || p === void 0 ? void 0 : p.agentId;
const groupId = p === null || p === void 0 ? void 0 : p.groupId;
const isParallelPart = groupId != null;
const groupPrimary = isParallelPart ? groupPrimaryMap.get(groupId) : null;
const shouldInclude = !isParallelPart || !agentId || agentId === groupPrimary;
if (shouldInclude) {
const newIndex = filteredContent.length;
const { agentId: _a, groupId: _g } = p, cleanPart = __rest(p, ["agentId", "groupId"]);
filteredContent.push(cleanPart);
if (agentId && hasMultipleAgents) {
agentIdMap[newIndex] = agentId;
}
}
}
const finalContent = Object.keys(agentIdMap).length > 0 && agentNames
? agents.labelContentByAgent(filteredContent, agentIdMap, agentNames)
: filteredContent;
return Object.assign(Object.assign({}, message), { content: finalContent });
}
catch (error) {
dataSchemas.logger.error('[AgentClient] Error processing multi-agent message:', error);
return message;
}
};
}
/**
* Extracts unique MCP server names from an agent's tools or tool definitions.
* Supports both full tool instances (tools) and serializable definitions (toolDefinitions).
* @param agent - The agent with tools and/or tool definitions
* @returns Array of unique MCP server names
*/
function extractMCPServers(agent) {
var _a, _b, _c;
const mcpServers = new Set();
/** Check tool instances (non-event-driven mode) */
if ((_a = agent === null || agent === void 0 ? void 0 : agent.tools) === null || _a === void 0 ? void 0 : _a.length) {
for (const tool of agent.tools) {
if (tool instanceof tools.DynamicStructuredTool && tool.name.includes(librechatDataProvider.Constants.mcp_delimiter)) {
const serverName = tool.name.split(librechatDataProvider.Constants.mcp_delimiter).pop();
if (serverName) {
mcpServers.add(serverName);
}
}
}
}
/** Check tool definitions (event-driven mode) */
if ((_b = agent === null || agent === void 0 ? void 0 : agent.toolDefinitions) === null || _b === void 0 ? void 0 : _b.length) {
for (const toolDef of agent.toolDefinitions) {
if ((_c = toolDef.name) === null || _c === void 0 ? void 0 : _c.includes(librechatDataProvider.Constants.mcp_delimiter)) {
const serverName = toolDef.name.split(librechatDataProvider.Constants.mcp_delimiter).pop();
if (serverName) {
mcpServers.add(serverName);
}
}
}
}
return Array.from(mcpServers);
}
/**
* Fetches MCP instructions for the given server names.
* @param {string[]} mcpServers - Array of MCP server names
* @param {MCPManager} mcpManager - MCP manager instance
* @param {Logger} [logger] - Optional logger instance
* @returns {Promise<string>} MCP instructions string, empty if none
*/
function getMCPInstructionsForServers(mcpServers, mcpManager, logger) {
return __awaiter(this, void 0, void 0, function* () {
if (!mcpServers.length) {
return '';
}
try {
const mcpInstructions = yield mcpManager.formatInstructionsForContext(mcpServers);
if (mcpInstructions && logger) {
logger.debug('[AgentContext] Fetched MCP instructions for servers:', mcpServers);
}
return mcpInstructions || '';
}
catch (error) {
if (logger) {
logger.error('[AgentContext] Failed to get MCP instructions:', error);
}
return '';
}
});
}
/**
* Builds final instructions for an agent by combining shared run context and agent-specific context.
* Order: sharedRunContext -> baseInstructions -> mcpInstructions
*
* @param {Object} params
* @param {string} [params.sharedRunContext] - Run-level context shared by all agents (file context, RAG, memory)
* @param {string} [params.baseInstructions] - Agent's base instructions
* @param {string} [params.mcpInstructions] - Agent's MCP server instructions
* @returns {string | undefined} Combined instructions, or undefined if empty
*/
function buildAgentInstructions({ sharedRunContext, baseInstructions, mcpInstructions, }) {
const parts = [sharedRunContext, baseInstructions, mcpInstructions].filter(Boolean);
const combined = parts.join('\n\n').trim();
return combined || undefined;
}
/**
* Applies run context and MCP instructions to an agent's configuration.
* Mutates the agent object in place.
*
* @param {Object} params
* @param {Agent} params.agent - The agent to update
* @param {string} params.sharedRunContext - Run-level shared context
* @param {MCPManager} params.mcpManager - MCP manager instance
* @param {Object} [params.ephemeralAgent] - Ephemeral agent config (for MCP override)
* @param {string} [params.agentId] - Agent ID for logging
* @param {Logger} [params.logger] - Optional logger instance
* @returns {Promise<void>}
*/
function applyContextToAgent(_a) {
return __awaiter(this, arguments, void 0, function* ({ agent, sharedRunContext, mcpManager, ephemeralAgent, agentId, logger, }) {
var _b;
const baseInstructions = agent.instructions || '';
try {
const mcpServers = ((_b = ephemeralAgent === null || ephemeralAgent === void 0 ? void 0 : ephemeralAgent.mcp) === null || _b === void 0 ? void 0 : _b.length) ? ephemeralAgent.mcp : extractMCPServers(agent);
const mcpInstructions = yield getMCPInstructionsForServers(mcpServers, mcpManager, logger);
agent.instructions = buildAgentInstructions({
sharedRunContext,
baseInstructions,
mcpInstructions,
});
if (agentId && logger) {
logger.debug(`[AgentContext] Applied context to agent: ${agentId}`);
}
}
catch (error) {
agent.instructions = buildAgentInstructions({
sharedRunContext,
baseInstructions,
mcpInstructions: '',
});
if (logger) {
logger.error(`[AgentContext] Failed to apply context to agent${agentId ? ` ${agentId}` : ''}, using base instructions only:`, error);
}
}
});
}
/**
* Creates a stable key for edge deduplication.
* Handles both single and array-based from/to values.
*/
function getEdgeKey(edge) {
var _a;
const from = Array.isArray(edge.from) ? [...edge.from].sort().join('|') : edge.from;
const to = Array.isArray(edge.to) ? [...edge.to].sort().join('|') : edge.to;
const type = (_a = edge.edgeType) !== null && _a !== void 0 ? _a : 'handoff';
return `${from}=>${to}::${type}`;
}
/**
* Extracts all agent IDs referenced in an edge (both from and to).
*/
function getEdgeParticipants(edge) {
const participants = [];
if (Array.isArray(edge.from)) {
participants.push(...edge.from);
}
else if (typeof edge.from === 'string') {
participants.push(edge.from);
}
if (Array.isArray(edge.to)) {
participants.push(...edge.to);
}
else if (typeof edge.to === 'string') {
participants.push(edge.to);
}
return participants;
}
/**
* Filters out edges that reference non-existent (orphaned) agents.
* Only filters based on the 'to' field since those are the handoff targets.
*/
function filterOrphanedEdges(edges, skippedAgentIds) {
if (!edges || skippedAgentIds.size === 0) {
return edges;
}
return edges.filter((edge) => {
const toIds = Array.isArray(edge.to) ? edge.to : [edge.to];
return !toIds.some((id) => typeof id === 'string' && skippedAgentIds.has(id));
});
}
/**
* Collects and deduplicates edges, tracking new agents to process.
* Used for BFS discovery of connected agents.
*/
function createEdgeCollector(checkAgentInit, skippedAgentIds) {
const edgeMap = new Map();
const agentsToProcess = new Set();
const collectEdges = (edgeList) => {
if (!edgeList || edgeList.length === 0) {
return;
}
for (const edge of edgeList) {
const key = getEdgeKey(edge);
if (!edgeMap.has(key)) {
edgeMap.set(key, edge);
}
const participants = getEdgeParticipants(edge);
for (const id of participants) {
if (!checkAgentInit(id) && !skippedAgentIds.has(id)) {
agentsToProcess.add(id);
}
}
}
};
return { edgeMap, agentsToProcess, collectEdges };
}
/**
* Creates the ON_TOOL_EXECUTE handler for event-driven tool execution.
* This handler receives batched tool calls, loads the required tools,
* executes them in parallel, and resolves with the results.
*/
function createToolExecuteHandler(options) {
const { loadTools, toolEndCallback } = options;
return {
handle: (_event, data) => __awaiter(this, void 0, void 0, function* () {
const { toolCalls, agentId, configurable, metadata, resolve, reject } = data;
try {
const toolNames = [...new Set(toolCalls.map((tc) => tc.name))];
const { loadedTools, configurable: toolConfigurable } = yield loadTools(toolNames, agentId);
const toolMap = new Map(loadedTools.map((t) => [t.name, t]));
const mergedConfigurable = Object.assign(Object.assign({}, configurable), toolConfigurable);
const results = yield Promise.all(toolCalls.map((tc) => __awaiter(this, void 0, void 0, function* () {
const tool = toolMap.get(tc.name);
if (!tool) {
dataSchemas.logger.warn(`[ON_TOOL_EXECUTE] Tool "${tc.name}" not found. Available: ${[...toolMap.keys()].join(', ')}`);
return {
toolCallId: tc.id,
status: 'error',
content: '',
errorMessage: `Tool ${tc.name} not found`,
};
}
try {
const toolCallConfig = {
id: tc.id,
stepId: tc.stepId,
turn: tc.turn,
};
if (tc.codeSessionContext &&
(tc.name === agents.Constants.EXECUTE_CODE ||
tc.name === agents.Constants.PROGRAMMATIC_TOOL_CALLING)) {
toolCallConfig.session_id = tc.codeSessionContext.session_id;
if (tc.codeSessionContext.files && tc.codeSessionContext.files.length > 0) {
toolCallConfig._injected_files = tc.codeSessionContext.files;
}
}
if (tc.name === agents.Constants.PROGRAMMATIC_TOOL_CALLING) {
const toolRegistry = mergedConfigurable === null || mergedConfigurable === void 0 ? void 0 : mergedConfigurable.toolRegistry;
const ptcToolMap = mergedConfigurable === null || mergedConfigurable === void 0 ? void 0 : mergedConfigurable.ptcToolMap;
if (toolRegistry) {
const toolDefs = Array.from(toolRegistry.values()).filter((t) => t.name !== agents.Constants.PROGRAMMATIC_TOOL_CALLING &&
t.name !== agents.Constants.TOOL_SEARCH);
toolCallConfig.toolDefs = toolDefs;
toolCallConfig.toolMap = ptcToolMap !== null && ptcToolMap !== void 0 ? ptcToolMap : toolMap;
}
}
const result = yield tool.invoke(tc.args, {
toolCall: toolCallConfig,
configurable: mergedConfigurable,
metadata,
});
if (toolEndCallback) {
yield toolEndCallback({
output: {
name: tc.name,
tool_call_id: tc.id,
content: result.content,
artifact: result.artifact,
},
}, Object.assign({ run_id: metadata === null || metadata === void 0 ? void 0 : metadata.run_id, thread_id: metadata === null || metadata === void 0 ? void 0 : metadata.thread_id }, metadata));
}
return {
toolCallId: tc.id,
content: result.content,
artifact: result.artifact,
status: 'success',
};
}
catch (toolError) {
const error = toolError;
dataSchemas.logger.error(`[ON_TOOL_EXECUTE] Tool ${tc.name} error:`, error);
return {
toolCallId: tc.id,
status: 'error',
content: '',
errorMessage: error.message,
};
}
})));
resolve(results);
}
catch (error) {
dataSchemas.logger.error('[ON_TOOL_EXECUTE] Fatal error:', error);
reject(error);
}
}),
};
}
/**
* Creates a handlers object that includes ON_TOOL_EXECUTE.
* Can be merged with other handler objects.
*/
function createToolExecuteHandlers(options) {
return {
[agents.GraphEvents.ON_TOOL_EXECUTE]: createToolExecuteHandler(options),
};
}
/**
* Processes audio files using Speech-to-Text (STT) service.
* @returns A promise that resolves to an object containing text and bytes.
*/
function processAudioFile(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, file, sttService, }) {
try {
const audioBuffer = yield fs.promises.readFile(file.path);
const audioFile = {
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
};
const [provider, sttSchema] = yield sttService.getProviderSchema(req);
const text = yield sttService.sttRequest(provider, sttSchema, { audioBuffer, audioFile });
return {
text,
bytes: Buffer.byteLength(text, 'utf8'),
};
}
catch (error) {
dataSchemas.logger.error('Error processing audio file with STT:', error);
throw new Error(`Failed to process audio file: ${error.message}`);
}
});
}
/**
* Extracts text context from attachments and returns formatted text.
* This handles text that was already extracted from files (OCR, transcriptions, document text, etc.)
* @param params - The parameters object
* @param params.attachments - Array of file attachments
* @param params.req - Express request object for config access
* @param params.tokenCountFn - Function to count tokens in text
* @returns The formatted file context text, or undefined if no text found
*/
function extractFileContext(_a) {
return __awaiter(this, arguments, void 0, function* ({ attachments, req, tokenCountFn, }) {
var _b, _c, _d, _e;
if (!attachments || attachments.length === 0) {
return undefined;
}
const fileConfig = librechatDataProvider.mergeFileConfig((_b = req === null || req === void 0 ? void 0 : req.config) === null || _b === void 0 ? void 0 : _b.fileConfig);
const fileTokenLimit = (_d = (_c = req === null || req === void 0 ? void 0 : req.body) === null || _c === void 0 ? void 0 : _c.fileTokenLimit) !== null && _d !== void 0 ? _d : fileConfig.fileTokenLimit;
if (!fileTokenLimit) {
// If no token limit, return undefined (no processing)
return undefined;
}
let resultText = '';
for (const file of attachments) {
const source = (_e = file.source) !== null && _e !== void 0 ? _e : librechatDataProvider.FileSources.local;
if (source === librechatDataProvider.FileSources.text && file.text) {
const { text: limitedText, wasTruncated } = yield processTextWithTokenLimit({
text: file.text,
tokenLimit: fileTokenLimit,
tokenCountFn,
});
if (wasTruncated) {
dataSchemas.logger.debug(`[extractFileContext] Text content truncated for file: ${file.filename} due to token limits`);
}
resultText += `${!resultText ? 'Attached document(s):\n```md' : '\n\n---\n\n'}# "${file.filename}"\n${limitedText}\n`;
}
}
if (resultText) {
resultText += '\n```';
return resultText;
}
return undefined;
});
}
var getStream$1 = {exports: {}};
var bufferStream;
var hasRequiredBufferStream;
function requireBufferStream () {
if (hasRequiredBufferStream) return bufferStream;
hasRequiredBufferStream = 1;
const {PassThrough: PassThroughStream} = require$$0;
bufferStream = options => {
options = {...options};
const {array} = options;
let {encoding} = options;
const isBuffer = encoding === 'buffer';
let objectMode = false;
if (array) {
objectMode = !(encoding || isBuffer);
} else {
encoding = encoding || 'utf8';
}
if (isBuffer) {
encoding = null;
}
const stream = new PassThroughStream({objectMode});
if (encoding) {
stream.setEncoding(encoding);
}
let length = 0;
const chunks = [];
stream.on('data', chunk => {
chunks.push(chunk);
if (objectMode) {
length = chunks.length;
} else {
length += chunk.length;
}
});
stream.getBufferedValue = () => {
if (array) {
return chunks;
}
return isBuffer ? Buffer.concat(chunks, length) : chunks.join('');
};
stream.getBufferedLength = () => length;
return stream;
};
return bufferStream;
}
var hasRequiredGetStream;
function requireGetStream () {
if (hasRequiredGetStream) return getStream$1.exports;
hasRequiredGetStream = 1;
const {constants: BufferConstants} = require$$0$1;
const stream = require$$0;
const {promisify} = require$$2;
const bufferStream = requireBufferStream();
const streamPipelinePromisified = promisify(stream.pipeline);
class MaxBufferError extends Error {
constructor() {
super('maxBuffer exceeded');
this.name = 'MaxBufferError';
}
}
async function getStream(inputStream, options) {
if (!inputStream) {
throw new Error('Expected a stream');
}
options = {
maxBuffer: Infinity,
...options
};
const {maxBuffer} = options;
const stream = bufferStream(options);
await new Promise((resolve, reject) => {
const rejectPromise = error => {
// Don't retrieve an oversized buffer.
if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) {
error.bufferedData = stream.getBufferedValue();
}
reject(error);
};
(async () => {
try {
await streamPipelinePromisified(inputStream, stream);
resolve();
} catch (error) {
rejectPromise(error);
}
})();
stream.on('data', () => {
if (stream.getBufferedLength() > maxBuffer) {
rejectPromise(new MaxBufferError());
}
});
});
return stream.getBufferedValue();
}
getStream$1.exports = getStream;
getStream$1.exports.buffer = (stream, options) => getStream(stream, {...options, encoding: 'buffer'});
getStream$1.exports.array = (stream, options) => getStream(stream, {...options, array: true});
getStream$1.exports.MaxBufferError = MaxBufferError;
return getStream$1.exports;
}
var getStreamExports = requireGetStream();
var getStream = /*@__PURE__*/getDefaultExportFromCjs(getStreamExports);
/**
* Extracts the configured file size limit for a specific provider from fileConfig
* @param req - The server request object containing config
* @param params - Object containing provider and optional endpoint
* @param params.provider - The provider to get the limit for
* @param params.endpoint - Optional endpoint name for lookup
* @returns The configured file size limit in bytes, or undefined if not configured
*/
const getConfiguredFileSizeLimit = (req, params) => {
var _a;
if (!((_a = req.config) === null || _a === void 0 ? void 0 : _a.fileConfig)) {
return undefined;
}
const { provider, endpoint } = params;
const fileConfig = librechatDataProvider.mergeFileConfig(req.config.fileConfig);
const endpointConfig = librechatDataProvider.getEndpointFileConfig({
fileConfig,
endpoint: endpoint !== null && endpoint !== void 0 ? endpoint : provider,
});
return endpointConfig === null || endpointConfig === void 0 ? void 0 : endpointConfig.fileSizeLimit;
};
/**
* Processes a file by downloading and encoding it to base64
* @param req - Express request object
* @param file - File object to process
* @param encodingMethods - Cache of encoding methods by source
* @param getStrategyFunctions - Function to get strategy functions for a source
* @returns Processed file with content and metadata, or null if filepath missing
*/
function getFileStream(req, file, encodingMethods, getStrategyFunctions) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (!(file === null || file === void 0 ? void 0 : file.filepath)) {
return null;
}
const source = (_a = file.source) !== null && _a !== void 0 ? _a : librechatDataProvider.FileSources.local;
if (!encodingMethods[source]) {
encodingMethods[source] = getStrategyFunctions(source);
}
const { getDownloadStream } = encodingMethods[source];
const stream = yield getDownloadStream(req, file.filepath);
const buffer = yield getStream.buffer(stream);
return {
file,
content: buffer.toString('base64'),
metadata: {
file_id: file.file_id,
temp_file_id: file.temp_file_id,
filepath: file.filepath,
source: file.source,
filename: file.filename,
type: file.type,
},
};
});
}
function validatePdf(pdfBuffer, fileSize, provider, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
if (provider === agents.Providers.ANTHROPIC) {
return validateAnthropicPdf(pdfBuffer, fileSize, configuredFileSizeLimit);
}
if (librechatDataProvider.isOpenAILikeProvider(provider)) {
return validateOpenAIPdf(fileSize, configuredFileSizeLimit);
}
if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
return validateGooglePdf(fileSize, configuredFileSizeLimit);
}
return { isValid: true };
});
}
/**
* Validates if a PDF meets Anthropic's requirements
* @param pdfBuffer - The PDF file as a buffer
* @param fileSize - The file size in bytes
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
function validateAnthropicPdf(pdfBuffer, fileSize, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
try {
const providerLimit = librechatDataProvider.mbToBytes(32);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
if (!pdfBuffer || pdfBuffer.length < 5) {
return {
isValid: false,
error: 'Invalid PDF file: too small or corrupted',
};
}
const pdfHeader = pdfBuffer.subarray(0, 5).toString();
if (!pdfHeader.startsWith('%PDF-')) {
return {
isValid: false,
error: 'Invalid PDF file: missing PDF header',
};
}
const pdfContent = pdfBuffer.toString('binary');
if (pdfContent.includes('/Encrypt ') ||
pdfContent.includes('/U (') ||
pdfContent.includes('/O (')) {
return {
isValid: false,
error: 'PDF is password-protected or encrypted. Anthropic requires unencrypted PDFs.',
};
}
const pageMatches = pdfContent.match(/\/Type[\s]*\/Page[^s]/g);
const estimatedPages = pageMatches ? pageMatches.length : 1;
if (estimatedPages > 100) {
return {
isValid: false,
error: `PDF has approximately ${estimatedPages} pages, exceeding Anthropic's 100-page limit`,
};
}
return { isValid: true };
}
catch (error) {
console.error('PDF validation error:', error);
return {
isValid: false,
error: 'Failed to validate PDF file',
};
}
});
}
/**
* Validates if a PDF meets OpenAI's requirements
* @param fileSize - The file size in bytes
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
function validateOpenAIPdf(fileSize, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
const providerLimit = librechatDataProvider.mbToBytes(10);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
return { isValid: true };
});
}
/**
* Validates if a PDF meets Google's requirements
* @param fileSize - The file size in bytes
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
function validateGooglePdf(fileSize, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
const providerLimit = librechatDataProvider.mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
return { isValid: true };
});
}
/**
* Validates video files for different providers
* @param videoBuffer - The video file as a buffer
* @param fileSize - The file size in bytes
* @param provider - The provider to validate for
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
function validateVideo(videoBuffer, fileSize, provider, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
const providerLimit = librechatDataProvider.mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Video file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (!videoBuffer || videoBuffer.length < 10) {
return {
isValid: false,
error: 'Invalid video file: too small or corrupted',
};
}
return { isValid: true };
});
}
/**
* Validates audio files for different providers
* @param audioBuffer - The audio file as a buffer
* @param fileSize - The file size in bytes
* @param provider - The provider to validate for
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
function validateAudio(audioBuffer, fileSize, provider, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
const providerLimit = librechatDataProvider.mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Audio file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (!audioBuffer || audioBuffer.length < 10) {
return {
isValid: false,
error: 'Invalid audio file: too small or corrupted',
};
}
return { isValid: true };
});
}
/**
* Validates image files for different providers
* @param imageBuffer - The image file as a buffer
* @param fileSize - The file size in bytes
* @param provider - The provider to validate for
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
function validateImage(imageBuffer, fileSize, provider, configuredFileSizeLimit) {
return __awaiter(this, void 0, void 0, function* () {
if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
const providerLimit = librechatDataProvider.mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Image file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (provider === agents.Providers.ANTHROPIC) {
const providerLimit = librechatDataProvider.mbToBytes(5);
const effectiveLimit = configuredFileSizeLimit !== null && configuredFileSizeLimit !== void 0 ? configuredFileSizeLimit : providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Image file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (!imageBuffer || imageBuffer.length < 10) {
return {
isValid: false,
error: 'Invalid image file: too small or corrupted',
};
}
return { isValid: true };
});
}
/**
* Encodes and formats audio files for different providers
* @param req - The request object
* @param files - Array of audio files
* @param params - Object containing provider and optional endpoint
* @param params.provider - The provider to format for (currently only google is supported)
* @param params.endpoint - Optional endpoint name for file config lookup
* @param getStrategyFunctions - Function to get strategy functions
* @returns Promise that resolves to audio and file metadata
*/
function encodeAndFormatAudios(req, files, params, getStrategyFunctions) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const { provider, endpoint } = params;
if (!(files === null || files === void 0 ? void 0 : files.length)) {
return { audios: [], files: [] };
}
const encodingMethods = {};
const result = { audios: [], files: [] };
const results = yield Promise.allSettled(files.map((file) => getFileStream(req, file, encodingMethods, getStrategyFunctions)));
for (const settledResult of results) {
if (settledResult.status === 'rejected') {
console.error('Audio processing failed:', settledResult.reason);
continue;
}
const processed = settledResult.value;
if (!processed)
continue;
const { file, content, metadata } = processed;
if (!content || !file) {
if (metadata)
result.files.push(metadata);
continue;
}
if (!file.type.startsWith('audio/') || !librechatDataProvider.isDocumentSupportedProvider(provider)) {
result.files.push(metadata);
continue;
}
const audioBuffer = Buffer.from(content, 'base64');
/** Extract configured file size limit from fileConfig for this endpoint */
const configuredFileSizeLimit = getConfiguredFileSizeLimit(req, {
provider,
endpoint,
});
const validation = yield validateAudio(audioBuffer, audioBuffer.length, provider, configuredFileSizeLimit);
if (!validation.isValid) {
throw new Error(`Audio validation failed: ${validation.error}`);
}
if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
result.audios.push({
type: 'media',
mimeType: file.type,
data: content,
});
}
else if (provider === agents.Providers.OPENROUTER) {
// Extract format from filename extension (e.g., 'audio.mp3' -> 'mp3')
// OpenRouter expects format values like: wav, mp3, aiff, aac, ogg, flac, m4a, pcm16, pcm24
// Note: MIME types don't always match (e.g., 'audio/mpeg' is mp3, not mpeg), so that is why we are using the file extension instead
const format = (_a = file.filename.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
if (!format) {
throw new Error(`Could not extract audio format from filename: ${file.filename}`);
}
result.audios.push({
type: 'input_audio',
input_audio: {
data: content,
format,
},
});
}
result.files.push(metadata);
}
return result;
});
}
/**
* Processes and encodes document files for various providers
* @param req - Express request object
* @param files - Array of file objects to process
* @param params - Object containing provider, endpoint, and other options
* @param params.provider - The provider name
* @param params.endpoint - Optional endpoint name for file config lookup
* @param params.useResponsesApi - Whether to use responses API format
* @param getStrategyFunctions - Function to get strategy functions
* @returns Promise that resolves to documents and file metadata
*/
function encodeAndFormatDocuments(req, files, params, getStrategyFunctions) {
return __awaiter(this, void 0, void 0, function* () {
const { provider, endpoint, useResponsesApi } = params;
if (!(files === null || files === void 0 ? void 0 : files.length)) {
return { documents: [], files: [] };
}
const encodingMethods = {};
const result = { documents: [], files: [] };
const documentFiles = files.filter((file) => { var _a; return file.type === 'application/pdf' || ((_a = file.type) === null || _a === void 0 ? void 0 : _a.startsWith('application/')); });
if (!documentFiles.length) {
return result;
}
const results = yield Promise.allSettled(documentFiles.map((file) => {
if (file.type !== 'application/pdf' || !librechatDataProvider.isDocumentSupportedProvider(provider)) {
return Promise.resolve(null);
}
return getFileStream(req, file, encodingMethods, getStrategyFunctions);
}));
for (const settledResult of results) {
if (settledResult.status === 'rejected') {
console.error('Document processing failed:', settledResult.reason);
continue;
}
const processed = settledResult.value;
if (!processed)
continue;
const { file, content, metadata } = processed;
if (!content || !file) {
if (metadata)
result.files.push(metadata);
continue;
}
if (file.type === 'application/pdf' && librechatDataProvider.isDocumentSupportedProvider(provider)) {
const pdfBuffer = Buffer.from(content, 'base64');
/** Extract configured file size limit from fileConfig for this endpoint */
const configuredFileSizeLimit = getConfiguredFileSizeLimit(req, {
provider,
endpoint,
});
const validation = yield validatePdf(pdfBuffer, pdfBuffer.length, provider, configuredFileSizeLimit);
if (!validation.isValid) {
throw new Error(`PDF validation failed: ${validation.error}`);
}
if (provider === agents.Providers.ANTHROPIC) {
const document = {
type: 'document',
source: {
type: 'base64',
media_type: 'application/pdf',
data: content,
},
citations: { enabled: true },
};
if (file.filename) {
document.context = `File: "${file.filename}"`;
}
result.documents.push(document);
}
else if (useResponsesApi) {
result.documents.push({
type: 'input_file',
filename: file.filename,
file_data: `data:application/pdf;base64,${content}`,
});
}
else if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
result.documents.push({
type: 'media',
mimeType: 'application/pdf',
data: content,
});
}
else if (librechatDataProvider.isOpenAILikeProvider(provider) && provider != agents.Providers.AZURE) {
result.documents.push({
type: 'file',
file: {
filename: file.filename,
file_data: `data:application/pdf;base64,${content}`,
},
});
}
result.files.push(metadata);
}
}
return result;
});
}
/**
* Encodes and formats video files for different providers
* @param req - The request object
* @param files - Array of video files
* @param params - Object containing provider and optional endpoint
* @param params.provider - The provider to format for
* @param params.endpoint - Optional endpoint name for file config lookup
* @param getStrategyFunctions - Function to get strategy functions
* @returns Promise that resolves to videos and file metadata
*/
function encodeAndFormatVideos(req, files, params, getStrategyFunctions) {
return __awaiter(this, void 0, void 0, function* () {
const { provider, endpoint } = params;
if (!(files === null || files === void 0 ? void 0 : files.length)) {
return { videos: [], files: [] };
}
const encodingMethods = {};
const result = { videos: [], files: [] };
const results = yield Promise.allSettled(files.map((file) => getFileStream(req, file, encodingMethods, getStrategyFunctions)));
for (const settledResult of results) {
if (settledResult.status === 'rejected') {
console.error('Video processing failed:', settledResult.reason);
continue;
}
const processed = settledResult.value;
if (!processed)
continue;
const { file, content, metadata } = processed;
if (!content || !file) {
if (metadata)
result.files.push(metadata);
continue;
}
if (!file.type.startsWith('video/') || !librechatDataProvider.isDocumentSupportedProvider(provider)) {
result.files.push(metadata);
continue;
}
const videoBuffer = Buffer.from(content, 'base64');
/** Extract configured file size limit from fileConfig for this endpoint */
const configuredFileSizeLimit = getConfiguredFileSizeLimit(req, {
provider,
endpoint,
});
const validation = yield validateVideo(videoBuffer, videoBuffer.length, provider, configuredFileSizeLimit);
if (!validation.isValid) {
throw new Error(`Video validation failed: ${validation.error}`);
}
if (provider === agents.Providers.GOOGLE || provider === agents.Providers.VERTEXAI) {
result.videos.push({
type: 'media',
mimeType: file.type,
data: content,
});
}
else if (provider === agents.Providers.OPENROUTER) {
result.videos.push({
type: 'video_url',
video_url: {
url: `data:${file.type};base64,${content}`,
},
});
}
result.files.push(metadata);
}
return result;
});
}
/**
* Checks if a MIME type is supported by the endpoint configuration
* @param mimeType - The MIME type to check
* @param supportedMimeTypes - Array of RegExp patterns to match against
* @returns True if the MIME type matches any pattern
*/
function isMimeTypeSupported(mimeType, supportedMimeTypes) {
if (!supportedMimeTypes || supportedMimeTypes.length === 0) {
return true;
}
return librechatDataProvider.fileConfig.checkType(mimeType, supportedMimeTypes);
}
/**
* Filters out files based on endpoint configuration including:
* - Disabled status
* - File size limits
* - MIME type restrictions
* - Total size limits
* @param req - The server request object containing config
* @param params - Object containing files, endpoint, and endpointType
* @param params.files - Array of processed file documents from MongoDB
* @param params.endpoint - The endpoint name to check configuration for
* @param params.endpointType - The endpoint type to check configuration for
* @returns Filtered array of files
*/
function filterFilesByEndpointConfig(req, params) {
var _a;
const { files, endpoint, endpointType } = params;
if (!files || files.length === 0) {
return [];
}
const mergedFileConfig = librechatDataProvider.mergeFileConfig((_a = req.config) === null || _a === void 0 ? void 0 : _a.fileConfig);
const endpointFileConfig = librechatDataProvider.getEndpointFileConfig({
fileConfig: mergedFileConfig,
endpoint,
endpointType,
});
/**
* If endpoint has files explicitly disabled, filter out all files
* Only filter if disabled is explicitly set to true
*/
if ((endpointFileConfig === null || endpointFileConfig === void 0 ? void 0 : endpointFileConfig.disabled) === true) {
return [];
}
const { fileSizeLimit, supportedMimeTypes, totalSizeLimit } = endpointFileConfig;
/** Filter files based on individual file size and MIME type */
let filteredFiles = files;
/** Filter by individual file size limit */
if (fileSizeLimit !== undefined && fileSizeLimit > 0) {
filteredFiles = filteredFiles.filter((file) => {
return file.bytes <= fileSizeLimit;
});
}
/** Filter by MIME type */
if (supportedMimeTypes && supportedMimeTypes.length > 0) {
filteredFiles = filteredFiles.filter((file) => {
return isMimeTypeSupported(file.type, supportedMimeTypes);
});
}
/** Filter by total size limit - keep files until total exceeds limit */
if (totalSizeLimit !== undefined && totalSizeLimit > 0) {
let totalSize = 0;
const withinTotalLimit = [];
for (let i = 0; i < filteredFiles.length; i++) {
const file = filteredFiles[i];
if (totalSize + file.bytes <= totalSizeLimit) {
withinTotalLimit.push(file);
totalSize += file.bytes;
}
}
filteredFiles = withinTotalLimit;
}
return filteredFiles;
}
const axios = createAxiosInstance();
const DEFAULT_MISTRAL_BASE_URL = 'https://api.mistral.ai/v1';
const DEFAULT_MISTRAL_MODEL = 'mistral-ocr-latest';
/**
* Uploads a document to Mistral API using file streaming to avoid loading the entire file into memory
* @param params Upload parameters
* @param params.filePath The path to the file on disk
* @param params.fileName Optional filename to use (defaults to the name from filePath)
* @param params.apiKey Mistral API key
* @param params.baseURL Mistral API base URL
* @returns The response from Mistral API
*/
function uploadDocumentToMistral(_a) {
return __awaiter(this, arguments, void 0, function* ({ apiKey, filePath, baseURL = DEFAULT_MISTRAL_BASE_URL, fileName = '', }) {
const form = new FormData();
form.append('purpose', 'ocr');
const actualFileName = fileName || path__namespace.basename(filePath);
const fileStream = fs__namespace.createReadStream(filePath);
form.append('file', fileStream, { filename: actualFileName });
const config = {
headers: Object.assign({ Authorization: `Bearer ${apiKey}` }, form.getHeaders()),
maxBodyLength: Infinity,
maxContentLength: Infinity,
};
if (process.env.PROXY) {
config.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
return axios
.post(`${baseURL}/files`, form, config)
.then((res) => res.data)
.catch((error) => {
throw error;
});
});
}
function getSignedUrl(_a) {
return __awaiter(this, arguments, void 0, function* ({ apiKey, fileId, expiry = 24, baseURL = DEFAULT_MISTRAL_BASE_URL, }) {
const config = {
headers: {
Authorization: `Bearer ${apiKey}`,
},
};
if (process.env.PROXY) {
config.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
return axios
.get(`${baseURL}/files/${fileId}/url?expiry=${expiry}`, config)
.then((res) => res.data)
.catch((error) => {
dataSchemas.logger.error('Error fetching signed URL:', error.message);
throw error;
});
});
}
/**
* @param {Object} params
* @param {string} params.apiKey
* @param {string} params.url - The document or image URL
* @param {string} [params.documentType='document_url'] - 'document_url' or 'image_url'
* @param {string} [params.model]
* @param {string} [params.baseURL]
* @returns {Promise<OCRResult>}
*/
function performOCR(_a) {
return __awaiter(this, arguments, void 0, function* ({ url, apiKey, model = DEFAULT_MISTRAL_MODEL, baseURL = DEFAULT_MISTRAL_BASE_URL, documentType = 'document_url', }) {
const documentKey = documentType === 'image_url' ? 'image_url' : 'document_url';
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
};
if (process.env.PROXY) {
config.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
return axios
.post(`${baseURL}/ocr`, {
model,
image_limit: 0,
include_image_base64: false,
document: {
type: documentType,
[documentKey]: url,
},
}, config)
.then((res) => res.data)
.catch((error) => {
dataSchemas.logger.error('Error performing OCR:', error.message);
throw error;
});
});
}
/**
* Deletes a file from Mistral API
* @param params Delete parameters
* @param params.fileId The file ID to delete
* @param params.apiKey Mistral API key
* @param params.baseURL Mistral API base URL
* @returns Promise that resolves when the file is deleted
*/
function deleteMistralFile(_a) {
return __awaiter(this, arguments, void 0, function* ({ fileId, apiKey, baseURL = DEFAULT_MISTRAL_BASE_URL, }) {
const config = {
headers: {
Authorization: `Bearer ${apiKey}`,
},
};
if (process.env.PROXY) {
config.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
try {
const result = yield axios.delete(`${baseURL}/files/${fileId}`, config);
dataSchemas.logger.debug(`Mistral file ${fileId} deleted successfully:`, result.data);
}
catch (error) {
dataSchemas.logger.error(`Error deleting Mistral file ${fileId}:`, error);
}
});
}
/**
* Determines if a value needs to be loaded from environment
*/
function needsEnvLoad(value) {
return librechatDataProvider.envVarRegex.test(value) || !value.trim();
}
/**
* Gets the environment variable name for a config value
*/
function getEnvVarName(configValue, defaultName) {
if (!librechatDataProvider.envVarRegex.test(configValue)) {
return defaultName;
}
return librechatDataProvider.extractVariableName(configValue) || defaultName;
}
/**
* Resolves a configuration value from either hardcoded or environment
*/
function resolveConfigValue(configValue, defaultEnvName, authValues, defaultValue) {
return __awaiter(this, void 0, void 0, function* () {
// If it's a hardcoded value (not env var and not empty), use it directly
if (!needsEnvLoad(configValue)) {
return configValue;
}
// Otherwise, get from auth values
const envVarName = getEnvVarName(configValue, defaultEnvName);
return authValues[envVarName] || defaultValue || '';
});
}
/**
* Loads authentication configuration from OCR config
*/
function loadAuthConfig(context) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const appConfig = context.req.config;
const ocrConfig = appConfig === null || appConfig === void 0 ? void 0 : appConfig.ocr;
const apiKeyConfig = (ocrConfig === null || ocrConfig === void 0 ? void 0 : ocrConfig.apiKey) || '';
const baseURLConfig = (ocrConfig === null || ocrConfig === void 0 ? void 0 : ocrConfig.baseURL) || '';
if (!needsEnvLoad(apiKeyConfig) && !needsEnvLoad(baseURLConfig)) {
return {
apiKey: apiKeyConfig,
baseURL: baseURLConfig,
};
}
const authFields = [];
if (needsEnvLoad(baseURLConfig)) {
authFields.push(getEnvVarName(baseURLConfig, 'OCR_BASEURL'));
}
if (needsEnvLoad(apiKeyConfig)) {
authFields.push(getEnvVarName(apiKeyConfig, 'OCR_API_KEY'));
}
const authValues = yield context.loadAuthValues({
userId: ((_a = context.req.user) === null || _a === void 0 ? void 0 : _a.id) || '',
authFields,
optional: new Set(['OCR_BASEURL']),
});
const apiKey = yield resolveConfigValue(apiKeyConfig, 'OCR_API_KEY', authValues);
const baseURL = yield resolveConfigValue(baseURLConfig, 'OCR_BASEURL', authValues, DEFAULT_MISTRAL_BASE_URL);
return { apiKey, baseURL };
});
}
/**
* Gets the model configuration
*/
function getModelConfig(ocrConfig) {
const modelConfig = (ocrConfig === null || ocrConfig === void 0 ? void 0 : ocrConfig.mistralModel) || '';
if (!modelConfig.trim()) {
return DEFAULT_MISTRAL_MODEL;
}
if (librechatDataProvider.envVarRegex.test(modelConfig)) {
return librechatDataProvider.extractEnvVariable(modelConfig) || DEFAULT_MISTRAL_MODEL;
}
return modelConfig.trim();
}
/**
* Determines document type based on file
*/
function getDocumentType(file) {
const mimetype = (file.mimetype || '').toLowerCase();
const originalname = file.originalname || '';
const isImage = mimetype.startsWith('image') || /\.(png|jpe?g|gif|bmp|webp|tiff?)$/i.test(originalname);
return isImage ? 'image_url' : 'document_url';
}
/**
* Processes OCR result pages into aggregated text and images
*/
function processOCRResult(ocrResult) {
let aggregatedText = '';
const images = [];
ocrResult.pages.forEach((page, index) => {
if (ocrResult.pages.length > 1) {
aggregatedText += `# PAGE ${index + 1}\n`;
}
aggregatedText += page.markdown + '\n\n';
if (!page.images || page.images.length === 0) {
return;
}
page.images.forEach((image) => {
if (image.image_base64) {
images.push(image.image_base64);
}
});
});
return { text: aggregatedText, images };
}
/**
* Creates an error message for OCR operations
*/
function createOCRError(error, baseMessage) {
var _a, _b, _c, _d;
const axiosError = error;
const detail = (_b = (_a = axiosError === null || axiosError === void 0 ? void 0 : axiosError.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail;
const message = detail || baseMessage;
const responseMessage = (_d = (_c = axiosError === null || axiosError === void 0 ? void 0 : axiosError.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.message;
const errorLog = logAxiosError({ error: axiosError, message });
const fullMessage = responseMessage ? `${errorLog} - ${responseMessage}` : errorLog;
return new Error(fullMessage);
}
/**
* Uploads a file to the Mistral OCR API and processes the OCR result.
*
* @param params - The params object.
* @param params.req - The request object from Express. It should have a `user` property with an `id`
* representing the user
* @param params.file - The file object, which is part of the request. The file object should
* have a `mimetype` property that tells us the file type
* @param params.loadAuthValues - Function to load authentication values
* @returns - The result object containing the processed `text` and `images` (not currently used),
* along with the `filename` and `bytes` properties.
*/
const uploadMistralOCR = (context) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
let mistralFileId;
let apiKey;
let baseURL;
try {
const authConfig = yield loadAuthConfig(context);
apiKey = authConfig.apiKey;
baseURL = authConfig.baseURL;
const model = getModelConfig((_a = context.req.config) === null || _a === void 0 ? void 0 : _a.ocr);
const mistralFile = yield uploadDocumentToMistral({
filePath: context.file.path,
fileName: context.file.originalname,
apiKey,
baseURL,
});
mistralFileId = mistralFile.id;
const signedUrlResponse = yield getSignedUrl({
apiKey,
baseURL,
fileId: mistralFile.id,
});
const documentType = getDocumentType(context.file);
const ocrResult = yield performOCR({
url: signedUrlResponse.url,
documentType,
baseURL,
apiKey,
model,
});
if (!ocrResult || !ocrResult.pages || ocrResult.pages.length === 0) {
throw new Error('No OCR result returned from service, may be down or the file is not supported.');
}
const { text, images } = processOCRResult(ocrResult);
if (mistralFileId && apiKey && baseURL) {
yield deleteMistralFile({ fileId: mistralFileId, apiKey, baseURL });
}
return {
filename: context.file.originalname,
bytes: text.length * 4,
filepath: librechatDataProvider.FileSources.mistral_ocr,
text,
images,
};
}
catch (error) {
if (mistralFileId && apiKey && baseURL) {
yield deleteMistralFile({ fileId: mistralFileId, apiKey, baseURL });
}
throw createOCRError(error, 'Error uploading document to Mistral OCR API:');
}
});
/**
* Use Azure Mistral OCR API to processe the OCR result.
*
* @param params - The params object.
* @param params.req - The request object from Express. It should have a `user` property with an `id`
* representing the user
* @param params.appConfig - Application configuration object
* @param params.file - The file object, which is part of the request. The file object should
* have a `mimetype` property that tells us the file type
* @param params.loadAuthValues - Function to load authentication values
* @returns - The result object containing the processed `text` and `images` (not currently used),
* along with the `filename` and `bytes` properties.
*/
const uploadAzureMistralOCR = (context) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
try {
const { apiKey, baseURL } = yield loadAuthConfig(context);
const model = getModelConfig((_a = context.req.config) === null || _a === void 0 ? void 0 : _a.ocr);
const { content: buffer } = yield readFileAsBuffer(context.file.path, {
fileSize: context.file.size,
});
const base64 = buffer.toString('base64');
/** Uses actual mimetype of the file, 'image/jpeg' as fallback since it seems to be accepted regardless of mismatch */
const base64Prefix = `data:${context.file.mimetype || 'image/jpeg'};base64,`;
const documentType = getDocumentType(context.file);
const ocrResult = yield performOCR({
apiKey,
baseURL,
model,
url: `${base64Prefix}${base64}`,
documentType,
});
if (!ocrResult || !ocrResult.pages || ocrResult.pages.length === 0) {
throw new Error('No OCR result returned from service, may be down or the file is not supported.');
}
const { text, images } = processOCRResult(ocrResult);
return {
filename: context.file.originalname,
bytes: text.length * 4,
filepath: librechatDataProvider.FileSources.azure_mistral_ocr,
text,
images,
};
}
catch (error) {
throw createOCRError(error, 'Error uploading document to Azure Mistral OCR API:');
}
});
/**
* Loads Google service account configuration
*/
function loadGoogleAuthConfig() {
return __awaiter(this, void 0, void 0, function* () {
/** Path from environment variable or default location */
const serviceKeyPath = process.env.GOOGLE_SERVICE_KEY_FILE ||
path__namespace.join(__dirname, '..', '..', '..', 'api', 'data', 'auth.json');
const serviceKey = yield loadServiceKey(serviceKeyPath);
if (!serviceKey) {
throw new Error(`Google service account not found or could not be loaded from ${serviceKeyPath}`);
}
if (!serviceKey.client_email || !serviceKey.private_key || !serviceKey.project_id) {
throw new Error('Invalid Google service account configuration');
}
const jwt = yield createJWT(serviceKey);
const accessToken = yield exchangeJWTForAccessToken(jwt);
return {
serviceAccount: serviceKey,
accessToken,
};
});
}
/**
* Creates a JWT token manually
*/
function createJWT(serviceKey) {
return __awaiter(this, void 0, void 0, function* () {
const crypto = yield import('crypto');
const header = {
alg: 'RS256',
typ: 'JWT',
};
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: serviceKey.client_email,
scope: 'https://www.googleapis.com/auth/cloud-platform',
aud: 'https://oauth2.googleapis.com/token',
exp: now + 3600,
iat: now,
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signatureInput = `${encodedHeader}.${encodedPayload}`;
const sign = crypto.createSign('RSA-SHA256');
sign.update(signatureInput);
sign.end();
const signature = sign.sign(serviceKey.private_key, 'base64url');
return `${signatureInput}.${signature}`;
});
}
/**
* Exchanges JWT for access token
*/
function exchangeJWTForAccessToken(jwt) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
if (process.env.PROXY) {
config.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
const response = yield axios.post('https://oauth2.googleapis.com/token', new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt,
}), config);
if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.access_token)) {
throw new Error('No access token in response');
}
return response.data.access_token;
});
}
/**
* Performs OCR using Google Vertex AI
*/
function performGoogleVertexOCR(_a) {
return __awaiter(this, arguments, void 0, function* ({ url, accessToken, projectId, model, documentType = 'document_url', }) {
const location = process.env.GOOGLE_LOC || 'us-central1';
const modelId = model || 'mistral-ocr-2505';
let baseURL;
if (location === 'global') {
baseURL = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/global/publishers/mistralai/models/${modelId}:rawPredict`;
}
else {
baseURL = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/mistralai/models/${modelId}:rawPredict`;
}
const documentKey = documentType === 'image_url' ? 'image_url' : 'document_url';
const requestBody = {
model: modelId,
document: {
type: documentType,
[documentKey]: url,
},
include_image_base64: true,
};
dataSchemas.logger.debug('Sending request to Google Vertex AI:', {
url: baseURL,
body: Object.assign(Object.assign({}, requestBody), { document: Object.assign(Object.assign({}, requestBody.document), { [documentKey]: 'base64_data_hidden' }) }),
});
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
};
if (process.env.PROXY) {
config.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
return axios
.post(baseURL, requestBody, config)
.then((res) => {
dataSchemas.logger.debug('Google Vertex AI response received');
return res.data;
})
.catch((error) => {
var _a;
if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
dataSchemas.logger.error('Vertex AI error response: ' + JSON.stringify(error.response.data, null, 2));
}
throw new Error(logAxiosError({
error: error,
message: 'Error calling Google Vertex AI Mistral OCR',
}));
});
});
}
/**
* Use Google Vertex AI Mistral OCR API to process the OCR result.
*
* @param params - The params object.
* @param params.req - The request object from Express. It should have a `user` property with an `id`
* representing the user
* @param params.appConfig - Application configuration object
* @param params.file - The file object, which is part of the request. The file object should
* have a `mimetype` property that tells us the file type
* @param params.loadAuthValues - Function to load authentication values
* @returns - The result object containing the processed `text` and `images` (not currently used),
* along with the `filename` and `bytes` properties.
*/
const uploadGoogleVertexMistralOCR = (context) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
try {
const { serviceAccount, accessToken } = yield loadGoogleAuthConfig();
const model = getModelConfig((_a = context.req.config) === null || _a === void 0 ? void 0 : _a.ocr);
const { content: buffer } = yield readFileAsBuffer(context.file.path, {
fileSize: context.file.size,
});
const base64 = buffer.toString('base64');
const base64Prefix = `data:${context.file.mimetype || 'application/pdf'};base64,`;
const documentType = getDocumentType(context.file);
const ocrResult = yield performGoogleVertexOCR({
url: `${base64Prefix}${base64}`,
accessToken,
projectId: serviceAccount.project_id,
model,
documentType,
});
if (!ocrResult || !ocrResult.pages || ocrResult.pages.length === 0) {
throw new Error('No OCR result returned from service, may be down or the file is not supported.');
}
const { text, images } = processOCRResult(ocrResult);
return {
filename: context.file.originalname,
bytes: text.length * 4,
filepath: librechatDataProvider.FileSources.vertexai_mistral_ocr,
text,
images,
};
}
catch (error) {
throw createOCRError(error, 'Error uploading document to Google Vertex AI Mistral OCR:');
}
});
function loadOCRConfig(config) {
var _a, _b, _c, _d;
if (!config)
return;
const baseURL = (_a = config === null || config === void 0 ? void 0 : config.baseURL) !== null && _a !== void 0 ? _a : '';
const apiKey = (_b = config === null || config === void 0 ? void 0 : config.apiKey) !== null && _b !== void 0 ? _b : '';
const mistralModel = (_c = config === null || config === void 0 ? void 0 : config.mistralModel) !== null && _c !== void 0 ? _c : '';
return {
apiKey,
baseURL,
mistralModel,
strategy: (_d = config === null || config === void 0 ? void 0 : config.strategy) !== null && _d !== void 0 ? _d : librechatDataProvider.OCRStrategy.MISTRAL_OCR,
};
}
const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg|webp)$/i;
/**
* Extracts the image basename from a given URL.
*
* @param urlString - The URL string from which the image basename is to be extracted.
* @returns The basename of the image file from the URL.
* Returns an empty string if the URL does not contain a valid image basename.
*/
function getImageBasename(urlString) {
try {
const url$1 = new url.URL(urlString);
const basename = path.basename(url$1.pathname);
return imageExtensionRegex.test(basename) ? basename : '';
}
catch (_a) {
// If URL parsing fails, return an empty string
return '';
}
}
/**
* Extracts the basename of a file from a given URL.
*
* @param urlString - The URL string from which the file basename is to be extracted.
* @returns The basename of the file from the URL.
* Returns an empty string if the URL parsing fails.
*/
function getFileBasename(urlString) {
try {
const url$1 = new url.URL(urlString);
return path.basename(url$1.pathname);
}
catch (_a) {
// If URL parsing fails, return an empty string
return '';
}
}
/**
* Deletes embedded document(s) from the RAG API.
* This is a shared utility function used by all file storage strategies
* (S3, Azure, Firebase, Local) to delete RAG embeddings when a file is deleted.
*
* @param params - The parameters object.
* @param params.userId - The user ID for authentication.
* @param params.file - The file object. Must have `embedded` and `file_id` properties.
* @returns Returns true if deletion was successful or skipped, false if there was an error.
*/
function deleteRagFile(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, file }) {
var _b;
if (!file.embedded || !process.env.RAG_API_URL) {
return true;
}
if (!userId) {
dataSchemas.logger.error('[deleteRagFile] No user ID provided');
return false;
}
const jwtToken = generateShortLivedToken(userId);
try {
yield axios$1.delete(`${process.env.RAG_API_URL}/documents`, {
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
accept: 'application/json',
},
data: [file.file_id],
});
dataSchemas.logger.debug(`[deleteRagFile] Successfully deleted document ${file.file_id} from RAG API`);
return true;
}
catch (error) {
const axiosError = error;
if (((_b = axiosError.response) === null || _b === void 0 ? void 0 : _b.status) === 404) {
dataSchemas.logger.warn(`[deleteRagFile] Document ${file.file_id} not found in RAG API, may have been deleted already`);
return true;
}
else {
dataSchemas.logger.error('[deleteRagFile] Error deleting document from RAG API:', axiosError.message);
return false;
}
}
});
}
/**
* Attempts to parse text using RAG API, falls back to native text parsing
* @param params - The parameters object
* @param params.req - The Express request object
* @param params.file - The uploaded file
* @param params.file_id - The file ID
* @returns
*/
function parseText(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, file, file_id, }) {
var _b;
if (!process.env.RAG_API_URL) {
dataSchemas.logger.debug('[parseText] RAG_API_URL not defined, falling back to native text parsing');
return parseTextNative(file);
}
const userId = (_b = req.user) === null || _b === void 0 ? void 0 : _b.id;
if (!userId) {
dataSchemas.logger.debug('[parseText] No user ID provided, falling back to native text parsing');
return parseTextNative(file);
}
try {
const healthResponse = yield axios$1.get(`${process.env.RAG_API_URL}/health`, {
timeout: 10000,
});
if ((healthResponse === null || healthResponse === void 0 ? void 0 : healthResponse.statusText) !== 'OK' && (healthResponse === null || healthResponse === void 0 ? void 0 : healthResponse.status) !== 200) {
dataSchemas.logger.debug('[parseText] RAG API health check failed, falling back to native parsing');
return parseTextNative(file);
}
}
catch (healthError) {
logAxiosError({
message: '[parseText] RAG API health check failed, falling back to native parsing:',
error: healthError,
});
return parseTextNative(file);
}
try {
const jwtToken = generateShortLivedToken(userId);
const formData = new FormData();
formData.append('file_id', file_id);
formData.append('file', fs.createReadStream(file.path));
const formHeaders = formData.getHeaders();
const response = yield axios$1.post(`${process.env.RAG_API_URL}/text`, formData, {
headers: Object.assign({ Authorization: `Bearer ${jwtToken}`, accept: 'application/json' }, formHeaders),
timeout: 300000,
});
const responseData = response.data;
dataSchemas.logger.debug(`[parseText] RAG API completed successfully (${response.status})`);
if (!('text' in responseData)) {
throw new Error('RAG API did not return parsed text');
}
return {
text: responseData.text,
bytes: Buffer.byteLength(responseData.text, 'utf8'),
source: librechatDataProvider.FileSources.text,
};
}
catch (error) {
logAxiosError({
message: '[parseText] RAG API text parsing failed, falling back to native parsing',
error,
});
return parseTextNative(file);
}
});
}
/**
* Native JavaScript text parsing fallback
* Simple text file reading - complex formats handled by RAG API
* @param file - The uploaded file
* @returns
*/
function parseTextNative(file) {
return __awaiter(this, void 0, void 0, function* () {
const { content: text, bytes } = yield readFileAsString(file.path, {
fileSize: file.size,
});
return {
text,
bytes,
source: librechatDataProvider.FileSources.text,
};
});
}
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
const dedent = createDedent({});
function createDedent(options) {
dedent.withOptions = newOptions => createDedent(_objectSpread(_objectSpread({}, options), newOptions));
return dedent;
function dedent(strings, ...values) {
const raw = typeof strings === "string" ? [strings] : strings.raw;
const {
alignValues = false,
escapeSpecialCharacters = Array.isArray(strings),
trimWhitespace = true
} = options;
// first, perform interpolation
let result = "";
for (let i = 0; i < raw.length; i++) {
let next = raw[i];
if (escapeSpecialCharacters) {
// handle escaped newlines, backticks, and interpolation characters
next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
}
result += next;
if (i < values.length) {
const value = alignValues ? alignValue(values[i], result) : values[i];
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
result += value;
}
}
// now strip indentation
const lines = result.split("\n");
let mindent = null;
for (const l of lines) {
const m = l.match(/^(\s+)\S+/);
if (m) {
const indent = m[1].length;
if (!mindent) {
// this is the first indented line
mindent = indent;
} else {
mindent = Math.min(mindent, indent);
}
}
}
if (mindent !== null) {
const m = mindent; // appease TypeScript
result = lines
// https://github.com/typescript-eslint/typescript-eslint/issues/7140
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
.map(l => l[0] === " " || l[0] === "\t" ? l.slice(m) : l).join("\n");
}
// dedent eats leading and trailing whitespace too
if (trimWhitespace) {
result = result.trim();
}
// Unescape escapes after trimming so sequences like `\n`, `\t`,
// `\xHH` and `\u{...}` are preserved (fixes #24)
if (escapeSpecialCharacters) {
result = result.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\v/g, "\v").replace(/\\b/g, "\b").replace(/\\f/g, "\f").replace(/\\0/g, "\0").replace(/\\x([\da-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\u\{([\da-fA-F]{1,6})\}/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/\\u([\da-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
}
// Workaround for Bun issue with Unicode characters
// https://github.com/oven-sh/bun/issues/8745
if (typeof Bun !== "undefined") {
result = result.replace(
// Matches e.g. \\u{1f60a} or \\u5F1F
/\\u(?:\{([\da-fA-F]{1,6})\}|([\da-fA-F]{4}))/g, (_, braced, unbraced) => {
var _ref;
const hex = (_ref = braced !== null && braced !== void 0 ? braced : unbraced) !== null && _ref !== void 0 ? _ref : "";
return String.fromCodePoint(parseInt(hex, 16));
});
}
return result;
}
}
/**
* Adjusts the indentation of a multi-line interpolated value to match the current line.
*/
function alignValue(value, precedingText) {
if (typeof value !== "string" || !value.includes("\n")) {
return value;
}
const currentLine = precedingText.slice(precedingText.lastIndexOf("\n") + 1);
const indentMatch = currentLine.match(/^(\s+)/);
if (indentMatch) {
const indent = indentMatch[1];
return value.replace(/\n/g, `\n${indent}`);
}
return value;
}
/**
* Generate system prompt for AI-assisted React component creation
* @param options - Configuration options
* @param options.components - Documentation for shadcn components
* @param options.useXML - Whether to use XML-style formatting for component instructions
* @returns The generated system prompt
*/
function generateShadcnPrompt(options) {
const { components, useXML = false } = options;
const systemPrompt = dedent `
## Additional Artifact Instructions for React Components: "application/vnd.react"
There are some prestyled components (primitives) available for use. Please use your best judgement to use any of these components if the app calls for one.
Here are the components that are available, along with how to import them, and how to use them:
${Object.values(components)
.map((component) => {
if (useXML) {
return dedent `
<component>
<name>${component.componentName}</name>
<import-instructions>${component.importDocs}</import-instructions>
<usage-instructions>${component.usageDocs}</usage-instructions>
</component>
`;
}
return dedent `
# ${component.componentName}
## Import Instructions
${component.importDocs}
## Usage Instructions
${component.usageDocs}
`;
})
.join('\n\n')}
`;
return systemPrompt;
}
/** Essential Components */
const essentialComponents = {
avatar: {
componentName: 'Avatar',
importDocs: 'import { Avatar, AvatarFallback, AvatarImage } from "/components/ui/avatar"',
usageDocs: `
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>`,
},
button: {
componentName: 'Button',
importDocs: 'import { Button } from "/components/ui/button"',
usageDocs: `
<Button variant="outline">Button</Button>`,
},
card: {
componentName: 'Card',
importDocs: `
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "/components/ui/card"`,
usageDocs: `
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<p>Card Content</p>
</CardContent>
<CardFooter>
<p>Card Footer</p>
</CardFooter>
</Card>`,
},
checkbox: {
componentName: 'Checkbox',
importDocs: 'import { Checkbox } from "/components/ui/checkbox"',
usageDocs: '<Checkbox />',
},
input: {
componentName: 'Input',
importDocs: 'import { Input } from "/components/ui/input"',
usageDocs: '<Input />',
},
label: {
componentName: 'Label',
importDocs: 'import { Label } from "/components/ui/label"',
usageDocs: '<Label htmlFor="email">Your email address</Label>',
},
radioGroup: {
componentName: 'RadioGroup',
importDocs: `
import { Label } from "/components/ui/label"
import { RadioGroup, RadioGroupItem } from "/components/ui/radio-group"`,
usageDocs: `
<RadioGroup defaultValue="option-one">
<div className="flex items-center space-x-2">
<RadioGroupItem value="option-one" id="option-one" />
<Label htmlFor="option-one">Option One</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option-two" id="option-two" />
<Label htmlFor="option-two">Option Two</Label>
</div>
</RadioGroup>`,
},
select: {
componentName: 'Select',
importDocs: `
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "/components/ui/select"`,
usageDocs: `
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>`,
},
textarea: {
componentName: 'Textarea',
importDocs: 'import { Textarea } from "/components/ui/textarea"',
usageDocs: '<Textarea />',
},
};
/** Extra Components */
const extraComponents = {
accordion: {
componentName: 'Accordion',
importDocs: `
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "/components/ui/accordion"`,
usageDocs: `
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
</Accordion>`,
},
alertDialog: {
componentName: 'AlertDialog',
importDocs: `
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "/components/ui/alert-dialog"`,
usageDocs: `
<AlertDialog>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>`,
},
alert: {
componentName: 'Alert',
importDocs: `
import {
Alert,
AlertDescription,
AlertTitle,
} from "/components/ui/alert"`,
usageDocs: `
<Alert>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components to your app using the cli.
</AlertDescription>
</Alert>`,
},
aspectRatio: {
componentName: 'AspectRatio',
importDocs: 'import { AspectRatio } from "/components/ui/aspect-ratio"',
usageDocs: `
<AspectRatio ratio={16 / 9}>
<Image src="..." alt="Image" className="rounded-md object-cover" />
</AspectRatio>`,
},
badge: {
componentName: 'Badge',
importDocs: 'import { Badge } from "/components/ui/badge"',
usageDocs: '<Badge>Badge</Badge>',
},
calendar: {
componentName: 'Calendar',
importDocs: 'import { Calendar } from "/components/ui/calendar"',
usageDocs: '<Calendar />',
},
carousel: {
componentName: 'Carousel',
importDocs: `
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "/components/ui/carousel"`,
usageDocs: `
<Carousel>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>`,
},
collapsible: {
componentName: 'Collapsible',
importDocs: `
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "/components/ui/collapsible"`,
usageDocs: `
<Collapsible>
<CollapsibleTrigger>Can I use this in my project?</CollapsibleTrigger>
<CollapsibleContent>
Yes. Free to use for personal and commercial projects. No attribution required.
</CollapsibleContent>
</Collapsible>`,
},
dialog: {
componentName: 'Dialog',
importDocs: `
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "/components/ui/dialog"`,
usageDocs: `
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>`,
},
dropdownMenu: {
componentName: 'DropdownMenu',
importDocs: `
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "/components/ui/dropdown-menu"`,
usageDocs: `
<DropdownMenu>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>`,
},
menubar: {
componentName: 'Menubar',
importDocs: `
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "/components/ui/menubar"`,
usageDocs: `
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>New Window</MenubarItem>
<MenubarSeparator />
<MenubarItem>Share</MenubarItem>
<MenubarSeparator />
<MenubarItem>Print</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>`,
},
navigationMenu: {
componentName: 'NavigationMenu',
importDocs: `
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "/components/ui/navigation-menu"`,
usageDocs: `
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Item One</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>`,
},
popover: {
componentName: 'Popover',
importDocs: `
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "/components/ui/popover"`,
usageDocs: `
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Place content for the popover here.</PopoverContent>
</Popover>`,
},
progress: {
componentName: 'Progress',
importDocs: 'import { Progress } from "/components/ui/progress"',
usageDocs: '<Progress value={33} />',
},
separator: {
componentName: 'Separator',
importDocs: 'import { Separator } from "/components/ui/separator"',
usageDocs: '<Separator />',
},
sheet: {
componentName: 'Sheet',
importDocs: `
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "/components/ui/sheet"`,
usageDocs: `
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you sure absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>`,
},
skeleton: {
componentName: 'Skeleton',
importDocs: 'import { Skeleton } from "/components/ui/skeleton"',
usageDocs: '<Skeleton className="w-[100px] h-[20px] rounded-full" />',
},
slider: {
componentName: 'Slider',
importDocs: 'import { Slider } from "/components/ui/slider"',
usageDocs: '<Slider defaultValue={[33]} max={100} step={1} />',
},
switch: {
componentName: 'Switch',
importDocs: 'import { Switch } from "/components/ui/switch"',
usageDocs: '<Switch />',
},
table: {
componentName: 'Table',
importDocs: `
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "/components/ui/table"`,
usageDocs: `
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>`,
},
tabs: {
componentName: 'Tabs',
importDocs: `
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "/components/ui/tabs"`,
usageDocs: `
<Tabs defaultValue="account" className="w-[400px]">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Make changes to your account here.</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
</Tabs>`,
},
toast: {
componentName: 'Toast',
importDocs: `
import { useToast } from "/components/ui/use-toast"
import { Button } from "/components/ui/button"`,
usageDocs: `
export function ToastDemo() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "Scheduled: Catch up",
description: "Friday, February 10, 2023 at 5:57 PM",
})
}}
>
Show Toast
</Button>
)
}`,
},
toggle: {
componentName: 'Toggle',
importDocs: 'import { Toggle } from "/components/ui/toggle"',
usageDocs: '<Toggle>Toggle</Toggle>',
},
tooltip: {
componentName: 'Tooltip',
importDocs: `
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "/components/ui/tooltip"`,
usageDocs: `
<TooltipProvider>
<Tooltip>
<TooltipTrigger>Hover</TooltipTrigger>
<TooltipContent>
<p>Add to library</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>`,
},
};
/** Extra Components continued */
const moreExtraComponents = {
calendar: {
componentName: 'Calendar',
importDocs: 'import { Calendar } from "/components/ui/calendar"',
usageDocs: '<Calendar />',
},
carousel: {
componentName: 'Carousel',
importDocs: `
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "/components/ui/carousel"`,
usageDocs: `
<Carousel>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>`,
},
collapsible: {
componentName: 'Collapsible',
importDocs: `
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "/components/ui/collapsible"`,
usageDocs: `
<Collapsible>
<CollapsibleTrigger>Can I use this in my project?</CollapsibleTrigger>
<CollapsibleContent>
Yes. Free to use for personal and commercial projects. No attribution required.
</CollapsibleContent>
</Collapsible>`,
},
dialog: {
componentName: 'Dialog',
importDocs: `
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "/components/ui/dialog"`,
usageDocs: `
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>`,
},
dropdownMenu: {
componentName: 'DropdownMenu',
importDocs: `
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "/components/ui/dropdown-menu"`,
usageDocs: `
<DropdownMenu>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>`,
},
menubar: {
componentName: 'Menubar',
importDocs: `
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "/components/ui/menubar"`,
usageDocs: `
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>New Window</MenubarItem>
<MenubarSeparator />
<MenubarItem>Share</MenubarItem>
<MenubarSeparator />
<MenubarItem>Print</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>`,
},
navigationMenu: {
componentName: 'NavigationMenu',
importDocs: `
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "/components/ui/navigation-menu"`,
usageDocs: `
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Item One</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>`,
},
popover: {
componentName: 'Popover',
importDocs: `
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "/components/ui/popover"`,
usageDocs: `
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Place content for the popover here.</PopoverContent>
</Popover>`,
},
progress: {
componentName: 'Progress',
importDocs: 'import { Progress } from "/components/ui/progress"',
usageDocs: '<Progress value={33} />',
},
separator: {
componentName: 'Separator',
importDocs: 'import { Separator } from "/components/ui/separator"',
usageDocs: '<Separator />',
},
sheet: {
componentName: 'Sheet',
importDocs: `
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "/components/ui/sheet"`,
usageDocs: `
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you sure absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>`,
},
skeleton: {
componentName: 'Skeleton',
importDocs: 'import { Skeleton } from "/components/ui/skeleton"',
usageDocs: '<Skeleton className="w-[100px] h-[20px] rounded-full" />',
},
slider: {
componentName: 'Slider',
importDocs: 'import { Slider } from "/components/ui/slider"',
usageDocs: '<Slider defaultValue={[33]} max={100} step={1} />',
},
switch: {
componentName: 'Switch',
importDocs: 'import { Switch } from "/components/ui/switch"',
usageDocs: '<Switch />',
},
};
const components = Object.assign(Object.assign(Object.assign({}, essentialComponents), extraComponents), moreExtraComponents);
const artifactsPrompt = dedent `The assistant can create and reference artifacts during conversations.
Artifacts are for substantial, self-contained content that users might modify or reuse, displayed in a separate UI window for clarity.
# Good artifacts are...
- Substantial content (>15 lines)
- Content that the user is likely to modify, iterate on, or take ownership of
- Self-contained, complex content that can be understood on its own, without context from the conversation
- Content intended for eventual use outside the conversation (e.g., reports, emails, presentations)
- Content likely to be referenced or reused multiple times
# Don't use artifacts for...
- Simple, informational, or short content, such as brief code snippets, mathematical equations, or small examples
- Primarily explanatory, instructional, or illustrative content, such as examples provided to clarify a concept
- Suggestions, commentary, or feedback on existing artifacts
- Conversational or explanatory content that doesn't represent a standalone piece of work
- Content that is dependent on the current conversational context to be useful
- Content that is unlikely to be modified or iterated upon by the user
- Request from users that appears to be a one-off question
# Usage notes
- One artifact per message unless specifically requested
- Prefer in-line content (don't use artifacts) when possible. Unnecessary use of artifacts can be jarring for users.
- If a user asks the assistant to "draw an SVG" or "make a website," the assistant does not need to explain that it doesn't have these capabilities. Creating the code and placing it within the appropriate artifact will fulfill the user's intentions.
- If asked to generate an image, the assistant can offer an SVG instead. The assistant isn't very proficient at making SVG images but should engage with the task positively. Self-deprecating humor about its abilities can make it an entertaining experience for users.
- The assistant errs on the side of simplicity and avoids overusing artifacts for content that can be effectively presented within the conversation.
- Always provide complete, specific, and fully functional content for artifacts without any snippets, placeholders, ellipses, or 'remains the same' comments.
- If an artifact is not necessary or requested, the assistant should not mention artifacts at all, and respond to the user accordingly.
<artifact_instructions>
When collaborating with the user on creating content that falls into compatible categories, the assistant should follow these steps:
1. Create the artifact using the following format:
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
\`\`\`
Your artifact content here
\`\`\`
:::
2. Assign an identifier to the \`identifier\` attribute. For updates, reuse the prior identifier. For new artifacts, the identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
3. Include a \`title\` attribute to provide a brief title or description of the content.
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
- The assistant should specify the viewbox of the SVG rather than defining a width/height
- Markdown: "text/markdown" or "text/md"
- The user interface will render Markdown content placed within the artifact tags.
- Supports standard Markdown syntax including headers, lists, links, images, code blocks, tables, and more.
- Both "text/markdown" and "text/md" are accepted as valid MIME types for Markdown content.
- Mermaid Diagrams: "application/vnd.mermaid"
- The user interface will render Mermaid diagrams placed within the artifact tags.
- React Components: "application/vnd.react"
- Use this for displaying either: React elements, e.g. \`<strong>Hello World!</strong>\`, React pure functional components, e.g. \`() => <strong>Hello World!</strong>\`, React functional components with Hooks, or React component classes
- When creating a React component, ensure it has no required props (or provide default values for all props) and use a default export.
- Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. \`h-[600px]\`).
- Base React is available to be imported. To use hooks, first import it at the top of the artifact, e.g. \`import { useState } from "react"\`
- The lucide-react@0.394.0 library is available to be imported. e.g. \`import { Camera } from "lucide-react"\` & \`<Camera color="red" size={48} />\`
- The recharts charting library is available to be imported, e.g. \`import { LineChart, XAxis, ... } from "recharts"\` & \`<LineChart ...><XAxis dataKey="name"> ...\`
- The three.js library is available to be imported, e.g. \`import * as THREE from "three";\`
- The date-fns library is available to be imported, e.g. \`import { compareAsc, format } from "date-fns";\`
- The react-day-picker library is available to be imported, e.g. \`import { DayPicker } from "react-day-picker";\`
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
7. Always use triple backticks (\`\`\`) to enclose the content within the artifact, regardless of the content type.
</artifact_instructions>
Here are some examples of correct usage of artifacts:
<examples>
<example_docstring>
This example demonstrates how to create a Mermaid artifact for a simple flow chart.
</example_docstring>
<example>
<user_query>Can you create a simple flow chart showing the process of making tea using Mermaid?</user_query>
<assistant_response>
Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
\`\`\`mermaid
graph TD
A[Start] --> B{Water boiled?}
B -->|Yes| C[Add tea leaves to cup]
B -->|No| D[Boil water]
D --> B
C --> E[Pour boiling water into cup]
E --> F[Steep tea for desired time]
F --> G[Remove tea leaves]
G --> H[Add milk or sugar, if desired]
H --> I[Enjoy your tea!]
I --> J[End]
\`\`\`
:::
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
1. Start
2. Check if water is boiled
3. If not boiled, boil the water
4. Once water is boiled, add tea leaves to the cup
5. Pour boiling water into the cup
6. Steep the tea for the desired time
7. Remove the tea leaves
8. Optionally add milk or sugar
9. Enjoy your tea!
10. End
This chart provides a clear visual representation of the tea-making process. You can easily modify or expand this chart if you want to add more details or steps to the process. Let me know if you'd like any changes or have any questions!
</assistant_response>
</example>
<example>
<user_query>Create a simple React counter component</user_query>
<assistant_response>
Here's a simple React counter component:
:::artifact{identifier="react-counter" type="application/vnd.react" title="React Counter"}
\`\`\`
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div className="p-4">
<p className="mb-2">Count: {count}</p>
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
\`\`\`
:::
This component creates a simple counter with an increment button.
</assistant_response>
</example>
<example>
<user_query>Create a basic HTML structure for a blog post</user_query>
<assistant_response>
Here's a basic HTML structure for a blog post:
:::artifact{identifier="blog-post-html" type="text/html" title="Blog Post HTML"}
\`\`\`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Blog Post</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { margin-bottom: 15px; }
</style>
</head>
<body>
<header>
<h1>My First Blog Post</h1>
</header>
<main>
<article>
<p>This is the content of my blog post. It's short and sweet!</p>
</article>
</main>
<footer>
<p>&copy; 2023 My Blog</p>
</footer>
</body>
</html>
\`\`\`
:::
This HTML structure provides a simple layout for a blog post.
</assistant_response>
</example>
</examples>`;
const artifactsOpenAIPrompt = dedent `The assistant can create and reference artifacts during conversations.
Artifacts are for substantial, self-contained content that users might modify or reuse, displayed in a separate UI window for clarity.
# Good artifacts are...
- Substantial content (>15 lines)
- Content that the user is likely to modify, iterate on, or take ownership of
- Self-contained, complex content that can be understood on its own, without context from the conversation
- Content intended for eventual use outside the conversation (e.g., reports, emails, presentations)
- Content likely to be referenced or reused multiple times
# Don't use artifacts for...
- Simple, informational, or short content, such as brief code snippets, mathematical equations, or small examples
- Primarily explanatory, instructional, or illustrative content, such as examples provided to clarify a concept
- Suggestions, commentary, or feedback on existing artifacts
- Conversational or explanatory content that doesn't represent a standalone piece of work
- Content that is dependent on the current conversational context to be useful
- Content that is unlikely to be modified or iterated upon by the user
- Request from users that appears to be a one-off question
# Usage notes
- One artifact per message unless specifically requested
- Prefer in-line content (don't use artifacts) when possible. Unnecessary use of artifacts can be jarring for users.
- If a user asks the assistant to "draw an SVG" or "make a website," the assistant does not need to explain that it doesn't have these capabilities. Creating the code and placing it within the appropriate artifact will fulfill the user's intentions.
- If asked to generate an image, the assistant can offer an SVG instead. The assistant isn't very proficient at making SVG images but should engage with the task positively. Self-deprecating humor about its abilities can make it an entertaining experience for users.
- The assistant errs on the side of simplicity and avoids overusing artifacts for content that can be effectively presented within the conversation.
- Always provide complete, specific, and fully functional content for artifacts without any snippets, placeholders, ellipses, or 'remains the same' comments.
- If an artifact is not necessary or requested, the assistant should not mention artifacts at all, and respond to the user accordingly.
## Artifact Instructions
When collaborating with the user on creating content that falls into compatible categories, the assistant should follow these steps:
1. Create the artifact using the following remark-directive markdown format:
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
\`\`\`
Your artifact content here
\`\`\`
:::
a. Example of correct format:
:::artifact{identifier="example-artifact" type="text/plain" title="Example Artifact"}
\`\`\`
This is the content of the artifact.
It can span multiple lines.
\`\`\`
:::
b. Common mistakes to avoid:
- Don't split the opening ::: line
- Don't add extra backticks outside the artifact structure
- Don't omit the closing :::
2. Assign an identifier to the \`identifier\` attribute. For updates, reuse the prior identifier. For new artifacts, the identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
3. Include a \`title\` attribute to provide a brief title or description of the content.
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
- The assistant should specify the viewbox of the SVG rather than defining a width/height
- Markdown: "text/markdown" or "text/md"
- The user interface will render Markdown content placed within the artifact tags.
- Supports standard Markdown syntax including headers, lists, links, images, code blocks, tables, and more.
- Both "text/markdown" and "text/md" are accepted as valid MIME types for Markdown content.
- Mermaid Diagrams: "application/vnd.mermaid"
- The user interface will render Mermaid diagrams placed within the artifact tags.
- React Components: "application/vnd.react"
- Use this for displaying either: React elements, e.g. \`<strong>Hello World!</strong>\`, React pure functional components, e.g. \`() => <strong>Hello World!</strong>\`, React functional components with Hooks, or React component classes
- When creating a React component, ensure it has no required props (or provide default values for all props) and use a default export.
- Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. \`h-[600px]\`).
- Base React is available to be imported. To use hooks, first import it at the top of the artifact, e.g. \`import { useState } from "react"\`
- The lucide-react@0.394.0 library is available to be imported. e.g. \`import { Camera } from "lucide-react"\` & \`<Camera color="red" size={48} />\`
- The recharts charting library is available to be imported, e.g. \`import { LineChart, XAxis, ... } from "recharts"\` & \`<LineChart ...><XAxis dataKey="name"> ...\`
- The three.js library is available to be imported, e.g. \`import * as THREE from "three";\`
- The date-fns library is available to be imported, e.g. \`import { compareAsc, format } from "date-fns";\`
- The react-day-picker library is available to be imported, e.g. \`import { DayPicker } from "react-day-picker";\`
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
7. NEVER use triple backticks to enclose the artifact, ONLY the content within the artifact.
Here are some examples of correct usage of artifacts:
## Examples
### Example 1
This example demonstrates how to create a Mermaid artifact for a simple flow chart.
User: Can you create a simple flow chart showing the process of making tea using Mermaid?
Assistant: Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
\`\`\`mermaid
graph TD
A[Start] --> B{Water boiled?}
B -->|Yes| C[Add tea leaves to cup]
B -->|No| D[Boil water]
D --> B
C --> E[Pour boiling water into cup]
E --> F[Steep tea for desired time]
F --> G[Remove tea leaves]
G --> H[Add milk or sugar, if desired]
H --> I[Enjoy your tea!]
I --> J[End]
\`\`\`
:::
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
1. Start
2. Check if water is boiled
3. If not boiled, boil the water
4. Once water is boiled, add tea leaves to the cup
5. Pour boiling water into the cup
6. Steep the tea for the desired time
7. Remove the tea leaves
8. Optionally add milk or sugar
9. Enjoy your tea!
10. End
This chart provides a clear visual representation of the tea-making process. You can easily modify or expand this chart if you want to add more details or steps to the process. Let me know if you'd like any changes or have any questions!
---
### Example 2
User: Create a simple React counter component
Assistant: Here's a simple React counter component:
:::artifact{identifier="react-counter" type="application/vnd.react" title="React Counter"}
\`\`\`
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div className="p-4">
<p className="mb-2">Count: {count}</p>
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
\`\`\`
:::
This component creates a simple counter with an increment button.
---
### Example 3
User: Create a basic HTML structure for a blog post
Assistant: Here's a basic HTML structure for a blog post:
:::artifact{identifier="blog-post-html" type="text/html" title="Blog Post HTML"}
\`\`\`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Blog Post</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { margin-bottom: 15px; }
</style>
</head>
<body>
<header>
<h1>My First Blog Post</h1>
</header>
<main>
<article>
<p>This is the content of my blog post. It's short and sweet!</p>
</article>
</main>
<footer>
<p>&copy; 2023 My Blog</p>
</footer>
</body>
</html>
\`\`\`
:::
This HTML structure provides a simple layout for a blog post.
---`;
/**
* Generates an artifacts prompt based on the endpoint and artifact mode
* @param params - Configuration parameters
* @param params.endpoint - The current endpoint
* @param params.artifacts - The current artifact mode
* @returns The artifacts prompt, or null if mode is CUSTOM
*/
function generateArtifactsPrompt(params) {
const { endpoint, artifacts } = params;
if (artifacts === librechatDataProvider.ArtifactModes.CUSTOM) {
return null;
}
let prompt = artifactsPrompt;
if (endpoint !== librechatDataProvider.EModelEndpoint.anthropic) {
prompt = artifactsOpenAIPrompt;
}
if (artifacts === librechatDataProvider.ArtifactModes.SHADCNUI) {
prompt += generateShadcnPrompt({ components, useXML: endpoint === librechatDataProvider.EModelEndpoint.anthropic });
}
return prompt;
}
/**
* Formats prompt groups for the paginated /groups endpoint response
*/
function formatPromptGroupsResponse({ promptGroups = [], pageNumber, pageSize, actualLimit, hasMore = false, after = null, }) {
const currentPage = parseInt(pageNumber || '1');
// Calculate total pages based on whether there are more results
// If hasMore is true, we know there's at least one more page
// We use a high number (9999) to indicate "many pages" since we don't know the exact count
const totalPages = hasMore ? '9999' : currentPage.toString();
return {
promptGroups,
pageNumber: pageNumber || '1',
pageSize: pageSize || String(actualLimit) || '10',
pages: totalPages,
has_more: hasMore,
after,
};
}
/**
* Creates an empty response for the paginated /groups endpoint
*/
function createEmptyPromptGroupsResponse({ pageNumber, pageSize, actualLimit, }) {
return {
promptGroups: [],
pageNumber: pageNumber || '1',
pageSize: pageSize || String(actualLimit) || '10',
pages: '0',
has_more: false,
after: null,
};
}
/**
* Marks prompt groups as public based on the publicly accessible IDs
*/
function markPublicPromptGroups(promptGroups, publiclyAccessibleIds) {
if (!promptGroups.length) {
return [];
}
return promptGroups.map((group) => {
const isPublic = publiclyAccessibleIds.some((id) => { var _a; return id.equals((_a = group._id) === null || _a === void 0 ? void 0 : _a.toString()); });
return isPublic ? Object.assign(Object.assign({}, group), { isPublic: true }) : group;
});
}
/**
* Builds filter object for prompt group queries
*/
function buildPromptGroupFilter(_a) {
var { name, category } = _a, otherFilters = __rest(_a, ["name", "category"]);
const filter = Object.assign({}, otherFilters);
let searchShared = true;
let searchSharedOnly = false;
// Handle name filter - convert to regex for case-insensitive search
if (name) {
filter.name = new RegExp(escapeRegExp(name), 'i');
}
// Handle category filters with special system categories
if (category === librechatDataProvider.SystemCategories.MY_PROMPTS) {
searchShared = false;
}
else if (category === librechatDataProvider.SystemCategories.NO_CATEGORY) {
filter.category = '';
}
else if (category === librechatDataProvider.SystemCategories.SHARED_PROMPTS) {
searchSharedOnly = true;
}
else if (category) {
filter.category = category;
}
return { filter, searchShared, searchSharedOnly };
}
/**
* Filters accessible IDs based on shared/public prompts logic
*/
function filterAccessibleIdsBySharedLogic(_a) {
return __awaiter(this, arguments, void 0, function* ({ accessibleIds, searchShared, searchSharedOnly, publicPromptGroupIds, }) {
const publicIdStrings = new Set((publicPromptGroupIds || []).map((id) => id.toString()));
if (!searchShared) {
// For MY_PROMPTS - exclude public prompts to show only user's own prompts
return accessibleIds.filter((id) => !publicIdStrings.has(id.toString()));
}
if (searchSharedOnly) {
// Handle SHARED_PROMPTS filter - only return public prompts that user has access to
if (!(publicPromptGroupIds === null || publicPromptGroupIds === void 0 ? void 0 : publicPromptGroupIds.length)) {
return [];
}
const accessibleIdStrings = new Set(accessibleIds.map((id) => id.toString()));
return publicPromptGroupIds.filter((id) => accessibleIdStrings.has(id.toString()));
}
return [...accessibleIds, ...(publicPromptGroupIds || [])];
});
}
const { GLOBAL_PROJECT_NAME: GLOBAL_PROJECT_NAME$1 } = librechatDataProvider.Constants;
/**
* Check if prompt groups need to be migrated to the new permission system
* This performs a dry-run check similar to the migration script
*/
function checkPromptPermissionsMigration(_a) {
return __awaiter(this, arguments, void 0, function* ({ methods, mongoose, PromptGroupModel, }) {
dataSchemas.logger.debug('Checking if prompt permissions migration is needed');
try {
/** Native MongoDB database instance */
const db = mongoose.connection.db;
if (db) {
yield ensureRequiredCollectionsExist(db);
}
// Verify required roles exist
const ownerRole = yield methods.findRoleByIdentifier(librechatDataProvider.AccessRoleIds.PROMPTGROUP_OWNER);
const viewerRole = yield methods.findRoleByIdentifier(librechatDataProvider.AccessRoleIds.PROMPTGROUP_VIEWER);
const editorRole = yield methods.findRoleByIdentifier(librechatDataProvider.AccessRoleIds.PROMPTGROUP_EDITOR);
if (!ownerRole || !viewerRole || !editorRole) {
dataSchemas.logger.warn('Required promptGroup roles not found. Permission system may not be fully initialized.');
return {
totalToMigrate: 0,
globalViewAccess: 0,
privateGroups: 0,
};
}
/** Global project prompt group IDs */
const globalProject = yield methods.getProjectByName(GLOBAL_PROJECT_NAME$1, ['promptGroupIds']);
const globalPromptGroupIds = new Set(((globalProject === null || globalProject === void 0 ? void 0 : globalProject.promptGroupIds) || []).map((id) => id.toString()));
// Find promptGroups without ACL entries (no batching for efficiency on startup)
const promptGroupsToMigrate = yield PromptGroupModel.aggregate([
{
$lookup: {
from: 'aclentries',
localField: '_id',
foreignField: 'resourceId',
as: 'aclEntries',
},
},
{
$addFields: {
promptGroupAclEntries: {
$filter: {
input: '$aclEntries',
as: 'aclEntry',
cond: {
$and: [
{ $eq: ['$$aclEntry.resourceType', librechatDataProvider.ResourceType.PROMPTGROUP] },
{ $eq: ['$$aclEntry.principalType', librechatDataProvider.PrincipalType.USER] },
],
},
},
},
},
},
{
$match: {
author: { $exists: true, $ne: null },
promptGroupAclEntries: { $size: 0 },
},
},
{
$project: {
_id: 1,
name: 1,
author: 1,
authorName: 1,
category: 1,
},
},
]);
const categories = {
globalViewAccess: [],
privateGroups: [],
};
promptGroupsToMigrate.forEach((group) => {
const isGlobalGroup = globalPromptGroupIds.has(group._id.toString());
if (isGlobalGroup) {
categories.globalViewAccess.push(group);
}
else {
categories.privateGroups.push(group);
}
});
const result = {
totalToMigrate: promptGroupsToMigrate.length,
globalViewAccess: categories.globalViewAccess.length,
privateGroups: categories.privateGroups.length,
};
// Add details for debugging
if (promptGroupsToMigrate.length > 0) {
result.details = {
globalViewAccess: categories.globalViewAccess.map((g) => ({
name: g.name,
_id: g._id.toString(),
category: g.category || 'uncategorized',
})),
privateGroups: categories.privateGroups.map((g) => ({
name: g.name,
_id: g._id.toString(),
category: g.category || 'uncategorized',
})),
};
}
dataSchemas.logger.debug('Prompt migration check completed', {
totalToMigrate: result.totalToMigrate,
globalViewAccess: result.globalViewAccess,
privateGroups: result.privateGroups,
});
return result;
}
catch (error) {
dataSchemas.logger.error('Failed to check prompt permissions migration', error);
// Return zero counts on error to avoid blocking startup
return {
totalToMigrate: 0,
globalViewAccess: 0,
privateGroups: 0,
};
}
});
}
/**
* Log migration warning to console if prompt groups need migration
*/
function logPromptMigrationWarning(result) {
if (result.totalToMigrate === 0) {
return;
}
// Create a visible warning box
const border = '='.repeat(80);
const warning = [
'',
border,
' IMPORTANT: PROMPT PERMISSIONS MIGRATION REQUIRED',
border,
'',
` Total prompt groups to migrate: ${result.totalToMigrate}`,
` - Global View Access: ${result.globalViewAccess} prompt groups`,
` - Private Prompt Groups: ${result.privateGroups} prompt groups`,
'',
' The new prompt sharing system requires migrating existing prompt groups.',
' Please run the following command to migrate your prompts:',
'',
' npm run migrate:prompt-permissions',
'',
' For a dry run (preview) of what will be migrated:',
'',
' npm run migrate:prompt-permissions:dry-run',
'',
' This migration will:',
' 1. Grant owner permissions to prompt authors',
' 2. Set public view permissions for prompts in the global project',
' 3. Keep private prompts accessible only to their authors',
'',
border,
'',
];
// Use console methods directly for visibility
console.log('\n' + warning.join('\n') + '\n');
// Also log with logger for consistency
dataSchemas.logger.warn('Prompt permissions migration required', {
totalToMigrate: result.totalToMigrate,
globalViewAccess: result.globalViewAccess,
privateGroups: result.privateGroups,
});
}
/**
* Schema for validating prompt group update payloads.
* Only allows fields that users should be able to modify.
* Sensitive fields like author, authorName, _id, productionId, etc. are excluded.
*/
const updatePromptGroupSchema = z.z
.object({
/** The name of the prompt group */
name: z.z.string().min(1).max(255).optional(),
/** Short description/oneliner for the prompt group */
oneliner: z.z.string().max(500).optional(),
/** Category for organizing prompt groups */
category: z.z.string().max(100).optional(),
/** Project IDs to add for sharing */
projectIds: z.z.array(z.z.string()).optional(),
/** Project IDs to remove from sharing */
removeProjectIds: z.z.array(z.z.string()).optional(),
/** Command shortcut for the prompt group */
command: z.z
.string()
.max(librechatDataProvider.Constants.COMMANDS_MAX_LENGTH)
.regex(/^[a-z0-9-]*$/, {
message: 'Command must only contain lowercase alphanumeric characters and hyphens',
})
.optional()
.nullable(),
})
.strict();
/**
* Validates and sanitizes a prompt group update payload.
* Returns only the allowed fields, stripping any sensitive fields.
* @param data - The raw request body to validate
* @returns The validated and sanitized payload
* @throws ZodError if validation fails
*/
function validatePromptGroupUpdate(data) {
return updatePromptGroupSchema.parse(data);
}
/**
* Safely validates a prompt group update payload without throwing.
* @param data - The raw request body to validate
* @returns A SafeParseResult with either the validated data or validation errors
*/
function safeValidatePromptGroupUpdate(data) {
return updatePromptGroupSchema.safeParse(data);
}
/**
* @param {string} modelName
* @returns {boolean}
*/
function checkPromptCacheSupport(modelName) {
var _a;
const modelMatch = (_a = matchModelName(modelName, librechatDataProvider.EModelEndpoint.anthropic)) !== null && _a !== void 0 ? _a : '';
if (modelMatch.includes('claude-3-5-sonnet-latest') ||
modelMatch.includes('claude-3.5-sonnet-latest')) {
return false;
}
return (/claude-3[-.]7/.test(modelMatch) ||
/claude-3[-.]5-(?:sonnet|haiku)/.test(modelMatch) ||
/claude-3-(?:sonnet|haiku|opus)?/.test(modelMatch) ||
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch) ||
/claude-[4-9]-(?:sonnet|opus|haiku)?/.test(modelMatch) ||
/claude-4(?:-(?:sonnet|opus|haiku))?/.test(modelMatch));
}
/**
* Gets the appropriate headers for Claude models with cache control
* @param {string} model The model name
* @param {boolean} supportsCacheControl Whether the model supports cache control
* @returns {AnthropicClientOptions['extendedOptions']['defaultHeaders']|undefined} The headers object or undefined if not applicable
*/
function getClaudeHeaders(model, supportsCacheControl) {
if (!supportsCacheControl) {
return undefined;
}
if (/claude-3[-.]5-sonnet/.test(model)) {
return {
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
};
}
else if (/claude-3[-.]7/.test(model)) {
return {
'anthropic-beta': 'token-efficient-tools-2025-02-19,output-128k-2025-02-19',
};
}
else if (librechatDataProvider.supportsContext1m(model)) {
return {
'anthropic-beta': 'context-1m-2025-08-07',
};
}
return undefined;
}
/**
* Configures reasoning-related options for Claude models.
* Models supporting adaptive thinking (Opus 4.6+, Sonnet 4.6+) use effort control instead of manual budget_tokens.
*/
function configureReasoning(anthropicInput, extendedOptions = {}) {
var _a, _b, _c;
const updatedOptions = Object.assign({}, anthropicInput);
const currentMaxTokens = (_a = updatedOptions.max_tokens) !== null && _a !== void 0 ? _a : updatedOptions.maxTokens;
const modelName = (_b = updatedOptions.model) !== null && _b !== void 0 ? _b : '';
if (extendedOptions.thinking && modelName && librechatDataProvider.supportsAdaptiveThinking(modelName)) {
updatedOptions.thinking = { type: 'adaptive' };
const effort = extendedOptions.effort;
if (effort && effort !== librechatDataProvider.AnthropicEffort.unset) {
updatedOptions.invocationKwargs = Object.assign(Object.assign({}, updatedOptions.invocationKwargs), { output_config: { effort } });
}
if (currentMaxTokens == null) {
updatedOptions.max_tokens = librechatDataProvider.anthropicSettings.maxOutputTokens.reset(modelName);
}
return updatedOptions;
}
if (extendedOptions.thinking &&
modelName &&
(/claude-3[-.]7/.test(modelName) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelName))) {
updatedOptions.thinking = Object.assign(Object.assign({}, updatedOptions.thinking), { type: 'enabled' });
}
if (updatedOptions.thinking != null &&
extendedOptions.thinkingBudget != null &&
updatedOptions.thinking.type === 'enabled') {
updatedOptions.thinking = Object.assign(Object.assign({}, updatedOptions.thinking), { budget_tokens: extendedOptions.thinkingBudget });
}
if (updatedOptions.thinking != null &&
updatedOptions.thinking.type === 'enabled' &&
(currentMaxTokens == null || updatedOptions.thinking.budget_tokens > currentMaxTokens)) {
const maxTokens = librechatDataProvider.anthropicSettings.maxOutputTokens.reset(modelName);
updatedOptions.max_tokens = currentMaxTokens !== null && currentMaxTokens !== void 0 ? currentMaxTokens : maxTokens;
dataSchemas.logger.warn(updatedOptions.max_tokens === maxTokens
? '[AnthropicClient] max_tokens is not defined while thinking is enabled. Setting max_tokens to model default.'
: `[AnthropicClient] thinking budget_tokens (${updatedOptions.thinking.budget_tokens}) exceeds max_tokens (${updatedOptions.max_tokens}). Adjusting budget_tokens.`);
updatedOptions.thinking.budget_tokens = Math.min(updatedOptions.thinking.budget_tokens, Math.floor(((_c = updatedOptions.max_tokens) !== null && _c !== void 0 ? _c : 0) * 0.9));
}
return updatedOptions;
}
/**
* Loads Google service account configuration for Vertex AI.
* Supports both YAML configuration and environment variables.
* @param options - Optional configuration from YAML or other sources
*/
function loadAnthropicVertexCredentials(options) {
return __awaiter(this, void 0, void 0, function* () {
/** Path priority: options > env var > default location */
const serviceKeyPath = (options === null || options === void 0 ? void 0 : options.serviceKeyFile) ||
process.env.GOOGLE_SERVICE_KEY_FILE ||
path.join(process.cwd(), 'api', 'data', 'auth.json');
const serviceKey = yield loadServiceKey(serviceKeyPath);
if (!serviceKey) {
throw new Error(`Google service account not found or could not be loaded from ${serviceKeyPath}`);
}
return {
[librechatDataProvider.AuthKeys.GOOGLE_SERVICE_KEY]: serviceKey,
};
});
}
/**
* Creates Vertex credential options from a Vertex AI configuration object.
* @param config - The Vertex AI configuration (from YAML config or other sources)
*/
function getVertexCredentialOptions(config) {
return {
serviceKeyFile: config === null || config === void 0 ? void 0 : config.serviceKeyFile,
projectId: config === null || config === void 0 ? void 0 : config.projectId,
region: config === null || config === void 0 ? void 0 : config.region,
};
}
/**
* Checks if credentials are for Vertex AI (has service account key but no API key)
*/
function isAnthropicVertexCredentials(credentials) {
return !!credentials[librechatDataProvider.AuthKeys.GOOGLE_SERVICE_KEY] && !credentials[librechatDataProvider.AuthKeys.ANTHROPIC_API_KEY];
}
/**
* Filters anthropic-beta header values to only include those supported by Vertex AI.
* Vertex AI handles caching differently and we use 'prompt-caching-vertex' as a
* marker to trigger cache_control application in the agents package.
*/
function filterVertexHeaders(headers) {
if (!headers) {
return undefined;
}
const filteredHeaders = Object.assign({}, headers);
const anthropicBeta = filteredHeaders['anthropic-beta'];
if (anthropicBeta) {
// Filter out unsupported beta values for Vertex AI
const supportedValues = anthropicBeta
.split(',')
.map((v) => v.trim())
.filter((v) => {
// Remove prompt-caching headers (Vertex handles caching via cache_control in body)
if (v.includes('prompt-caching')) {
return false;
}
// Remove max-tokens headers (Vertex has its own limits)
if (v.includes('max-tokens')) {
return false;
}
// Remove output-128k headers
if (v.includes('output-128k')) {
return false;
}
// Remove token-efficient-tools headers
if (v.includes('token-efficient-tools')) {
return false;
}
return true;
});
if (supportedValues.length > 0) {
filteredHeaders['anthropic-beta'] = supportedValues.join(',');
}
else {
delete filteredHeaders['anthropic-beta'];
}
}
return Object.keys(filteredHeaders).length > 0 ? filteredHeaders : undefined;
}
/**
* Gets the deployment name for a given model name from the Vertex AI configuration.
* Maps visible model names to actual deployment names (model IDs).
* @param modelName - The visible model name (e.g., "Claude Opus 4.5")
* @param vertexConfig - The Vertex AI configuration with model mappings
* @returns The deployment name to use with the API (e.g., "claude-opus-4-5@20251101")
*/
function getVertexDeploymentName(modelName, vertexConfig) {
if (!(vertexConfig === null || vertexConfig === void 0 ? void 0 : vertexConfig.models)) {
// No models configuration, return model name as-is
return modelName;
}
// If models is an array, check if modelName is in the array
if (Array.isArray(vertexConfig.models)) {
// Legacy format - no deployment mapping
return modelName;
}
// If models is an object, look up the deployment name
const modelConfig = vertexConfig.models[modelName];
if (!modelConfig) {
// Model not found in config, return as-is
return modelName;
}
if (typeof modelConfig === 'boolean') {
// Model is true/false - use default deployment name or model name
return vertexConfig.deploymentName || modelName;
}
// Model has its own deployment name
return modelConfig.deploymentName || vertexConfig.deploymentName || modelName;
}
/**
* Creates and configures a Vertex AI client for Anthropic.
* Supports both YAML configuration and environment variables for region/projectId.
* The projectId is automatically extracted from the service key if not explicitly provided.
* @param credentials - The Google service account credentials
* @param options - SDK client options
* @param vertexOptions - Vertex AI specific options (region, projectId) from YAML config
*/
function createAnthropicVertexClient(credentials, options, vertexOptions) {
const serviceKey = credentials[librechatDataProvider.AuthKeys.GOOGLE_SERVICE_KEY];
if (!serviceKey) {
throw new Error('Google service account key is required for Vertex AI');
}
// Priority: vertexOptions > env vars > service key project_id
const region = (vertexOptions === null || vertexOptions === void 0 ? void 0 : vertexOptions.region) || process.env.ANTHROPIC_VERTEX_REGION || 'us-east5';
const projectId = (vertexOptions === null || vertexOptions === void 0 ? void 0 : vertexOptions.projectId) || process.env.VERTEX_PROJECT_ID || serviceKey.project_id;
try {
const googleAuth = new googleAuthLibrary.GoogleAuth(Object.assign({ credentials: serviceKey, scopes: 'https://www.googleapis.com/auth/cloud-platform' }, (projectId && { projectId })));
// Filter out unsupported anthropic-beta header values for Vertex AI
const filteredOptions = options
? Object.assign(Object.assign({}, options), { defaultHeaders: filterVertexHeaders(options.defaultHeaders) }) : undefined;
return new vertexSdk.AnthropicVertex(Object.assign(Object.assign({ region: region, googleAuth: googleAuth }, (projectId && { projectId })), filteredOptions));
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create Vertex AI client: ${message}`);
}
}
/**
* Parses credentials from string or object format
* - If a valid JSON string is passed, it parses and returns the object
* - If a plain API key string is passed, it wraps it in an AnthropicCredentials object
* - If an object is passed, it returns it directly
* - If undefined, returns an empty object
*/
function parseCredentials(credentials) {
if (typeof credentials === 'string') {
try {
return JSON.parse(credentials);
}
catch (_a) {
// If not valid JSON, treat as a plain API key
dataSchemas.logger.debug('[Anthropic] Credentials not JSON, treating as API key');
return { [librechatDataProvider.AuthKeys.ANTHROPIC_API_KEY]: credentials };
}
}
return credentials && typeof credentials === 'object' ? credentials : {};
}
/** Known Anthropic parameters that map directly to the client config */
const knownAnthropicParams = new Set([
'model',
'temperature',
'topP',
'topK',
'maxTokens',
'maxOutputTokens',
'stopSequences',
'stop',
'stream',
'apiKey',
'maxRetries',
'timeout',
'anthropicVersion',
'anthropicApiUrl',
'defaultHeaders',
]);
/**
* Applies default parameters to the target object only if the field is undefined
* @param target - The target object to apply defaults to
* @param defaults - Record of default parameter values
*/
function applyDefaultParams$2(target, defaults) {
for (const [key, value] of Object.entries(defaults)) {
if (target[key] === undefined) {
target[key] = value;
}
}
}
/**
* Generates configuration options for creating an Anthropic language model (LLM) instance.
* @param credentials - The API key for authentication with Anthropic, or credentials object for Vertex AI.
* @param options={} - Additional options for configuring the LLM.
* @returns Configuration options for creating an Anthropic LLM instance, with null and undefined values removed.
*/
function getLLMConfig(credentials, options = {}) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
const systemOptions = {
thinking: (_b = (_a = options.modelOptions) === null || _a === void 0 ? void 0 : _a.thinking) !== null && _b !== void 0 ? _b : librechatDataProvider.anthropicSettings.thinking.default,
promptCache: (_d = (_c = options.modelOptions) === null || _c === void 0 ? void 0 : _c.promptCache) !== null && _d !== void 0 ? _d : librechatDataProvider.anthropicSettings.promptCache.default,
thinkingBudget: (_f = (_e = options.modelOptions) === null || _e === void 0 ? void 0 : _e.thinkingBudget) !== null && _f !== void 0 ? _f : librechatDataProvider.anthropicSettings.thinkingBudget.default,
effort: (_h = (_g = options.modelOptions) === null || _g === void 0 ? void 0 : _g.effort) !== null && _h !== void 0 ? _h : librechatDataProvider.anthropicSettings.effort.default,
};
if (options.modelOptions) {
delete options.modelOptions.thinking;
delete options.modelOptions.promptCache;
delete options.modelOptions.thinkingBudget;
delete options.modelOptions.effort;
}
else {
throw new Error('No modelOptions provided');
}
const defaultOptions = {
model: librechatDataProvider.anthropicSettings.model.default,
stream: true,
};
const mergedOptions = Object.assign(defaultOptions, options.modelOptions);
let enableWebSearch = mergedOptions.web_search;
let requestOptions = {
model: mergedOptions.model,
stream: mergedOptions.stream,
temperature: mergedOptions.temperature,
stopSequences: mergedOptions.stop,
maxTokens: mergedOptions.maxOutputTokens || librechatDataProvider.anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
clientOptions: {},
invocationKwargs: {
metadata: {
user_id: mergedOptions.user,
},
},
};
const creds = parseCredentials(credentials);
const apiKey = (_j = creds[librechatDataProvider.AuthKeys.ANTHROPIC_API_KEY]) !== null && _j !== void 0 ? _j : null;
if (isAnthropicVertexCredentials(creds)) {
// Vertex AI configuration - use custom client with optional YAML config
// Map the visible model name to the actual deployment name for Vertex AI
const deploymentName = getVertexDeploymentName((_k = requestOptions.model) !== null && _k !== void 0 ? _k : '', options.vertexConfig);
requestOptions.model = deploymentName;
requestOptions.createClient = () => createAnthropicVertexClient(creds, requestOptions.clientOptions, options.vertexOptions);
}
else if (apiKey) {
// Direct API configuration
requestOptions.apiKey = apiKey;
}
else {
throw new Error('Invalid credentials provided. Please provide either a valid Anthropic API key or service account credentials for Vertex AI.');
}
requestOptions = configureReasoning(requestOptions, systemOptions);
if (librechatDataProvider.supportsAdaptiveThinking(mergedOptions.model)) {
if (systemOptions.effort &&
systemOptions.effort !== '' &&
!((_l = requestOptions.invocationKwargs) === null || _l === void 0 ? void 0 : _l.output_config)) {
requestOptions.invocationKwargs = Object.assign(Object.assign({}, requestOptions.invocationKwargs), { output_config: { effort: systemOptions.effort } });
}
}
else {
if (requestOptions.thinking != null &&
requestOptions.thinking.type === 'adaptive') {
delete requestOptions.thinking;
}
if ((_m = requestOptions.invocationKwargs) === null || _m === void 0 ? void 0 : _m.output_config) {
delete requestOptions.invocationKwargs.output_config;
}
}
const hasActiveThinking = requestOptions.thinking != null;
const isThinkingModel = /claude-3[-.]7/.test(mergedOptions.model) || librechatDataProvider.supportsAdaptiveThinking(mergedOptions.model);
if (!isThinkingModel || !hasActiveThinking) {
requestOptions.topP = mergedOptions.topP;
requestOptions.topK = mergedOptions.topK;
}
const supportsCacheControl = systemOptions.promptCache === true && checkPromptCacheSupport((_o = requestOptions.model) !== null && _o !== void 0 ? _o : '');
/** Pass promptCache boolean for downstream cache_control application */
if (supportsCacheControl) {
requestOptions.promptCache = true;
}
const headers = getClaudeHeaders((_p = requestOptions.model) !== null && _p !== void 0 ? _p : '', supportsCacheControl);
if (headers && requestOptions.clientOptions) {
requestOptions.clientOptions.defaultHeaders = headers;
}
if (options.proxy && requestOptions.clientOptions) {
const proxyAgent = new undici.ProxyAgent(options.proxy);
requestOptions.clientOptions.fetchOptions = {
dispatcher: proxyAgent,
};
}
if (options.reverseProxyUrl && requestOptions.clientOptions) {
requestOptions.clientOptions.baseURL = options.reverseProxyUrl;
requestOptions.anthropicApiUrl = options.reverseProxyUrl;
}
/** Handle defaultParams first - only process Anthropic-native params if undefined */
if (options.defaultParams && typeof options.defaultParams === 'object') {
for (const [key, value] of Object.entries(options.defaultParams)) {
/** Handle web_search separately - don't add to config */
if (key === 'web_search') {
if (enableWebSearch === undefined && typeof value === 'boolean') {
enableWebSearch = value;
}
continue;
}
if (knownAnthropicParams.has(key)) {
/** Route known Anthropic params to requestOptions only if undefined */
applyDefaultParams$2(requestOptions, { [key]: value });
}
/** Leave other params for transform to handle - they might be OpenAI params */
}
}
/** Handle addParams - can override defaultParams */
if (options.addParams && typeof options.addParams === 'object') {
for (const [key, value] of Object.entries(options.addParams)) {
/** Handle web_search separately - don't add to config */
if (key === 'web_search') {
if (typeof value === 'boolean') {
enableWebSearch = value;
}
continue;
}
if (knownAnthropicParams.has(key)) {
/** Route known Anthropic params to requestOptions */
requestOptions[key] = value;
}
/** Leave other params for transform to handle - they might be OpenAI params */
}
}
/** Handle dropParams - only drop from Anthropic config */
if (options.dropParams && Array.isArray(options.dropParams)) {
options.dropParams.forEach((param) => {
if (param === 'web_search') {
enableWebSearch = false;
return;
}
if (param in requestOptions) {
delete requestOptions[param];
}
if (requestOptions.invocationKwargs && param in requestOptions.invocationKwargs) {
delete requestOptions.invocationKwargs[param];
}
});
}
const tools = [];
if (enableWebSearch) {
tools.push({
type: 'web_search_20250305',
name: 'web_search',
});
if (isAnthropicVertexCredentials(creds)) {
if (!requestOptions.clientOptions) {
requestOptions.clientOptions = {};
}
requestOptions.clientOptions.defaultHeaders = Object.assign(Object.assign({}, requestOptions.clientOptions.defaultHeaders), { 'anthropic-beta': 'web-search-2025-03-05' });
}
}
return {
tools,
llmConfig: librechatDataProvider.removeNullishValues(requestOptions),
};
}
/**
* Initializes Anthropic endpoint configuration.
* Supports both direct API key authentication and Google Cloud Vertex AI.
*
* @param params - Configuration parameters
* @returns Promise resolving to Anthropic configuration options
* @throws Error if API key is not provided (when not using Vertex AI)
*/
function initializeAnthropic(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, endpoint, model_parameters, db, }) {
var _b, _c, _d, _e, _f, _g, _h;
const appConfig = req.config;
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
const { key: expiresAt } = req.body;
let credentials = {};
let vertexOptions;
/** @type {undefined | import('librechat-data-provider').TVertexAIConfig} */
const vertexConfig = (_c = (_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _b === void 0 ? void 0 : _b[librechatDataProvider.EModelEndpoint.anthropic]) === null || _c === void 0 ? void 0 : _c.vertexConfig;
// Check for Vertex AI configuration: YAML config takes priority over env var
// When vertexConfig exists and enabled is not explicitly false, Vertex AI is enabled
const useVertexAI = (vertexConfig && vertexConfig.enabled !== false) || isEnabled(process.env.ANTHROPIC_USE_VERTEX);
if (useVertexAI) {
// Load credentials with optional YAML config overrides
const credentialOptions = vertexConfig ? getVertexCredentialOptions(vertexConfig) : undefined;
credentials = yield loadAnthropicVertexCredentials(credentialOptions);
// Store vertex options for client creation
if (vertexConfig) {
vertexOptions = {
region: vertexConfig.region,
projectId: vertexConfig.projectId,
};
}
}
else {
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
const anthropicApiKey = isUserProvided
? yield db.getUserKey({ userId: (_e = (_d = req.user) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : '', name: librechatDataProvider.EModelEndpoint.anthropic })
: ANTHROPIC_API_KEY;
if (!anthropicApiKey) {
throw new Error('Anthropic API key not provided. Please provide it again.');
}
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(expiresAt, librechatDataProvider.EModelEndpoint.anthropic);
}
credentials[librechatDataProvider.AuthKeys.ANTHROPIC_API_KEY] = anthropicApiKey;
}
const clientOptions = Object.assign(Object.assign({ proxy: PROXY !== null && PROXY !== void 0 ? PROXY : undefined, reverseProxyUrl: ANTHROPIC_REVERSE_PROXY !== null && ANTHROPIC_REVERSE_PROXY !== void 0 ? ANTHROPIC_REVERSE_PROXY : undefined, modelOptions: Object.assign(Object.assign({}, (model_parameters !== null && model_parameters !== void 0 ? model_parameters : {})), { user: (_f = req.user) === null || _f === void 0 ? void 0 : _f.id }) }, (vertexOptions && { vertexOptions })), (vertexConfig && { vertexConfig }));
const anthropicConfig = (_g = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _g === void 0 ? void 0 : _g[librechatDataProvider.EModelEndpoint.anthropic];
const allConfig = (_h = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _h === void 0 ? void 0 : _h.all;
const result = getLLMConfig(credentials, clientOptions);
if (anthropicConfig === null || anthropicConfig === void 0 ? void 0 : anthropicConfig.streamRate) {
result.llmConfig._lc_stream_delay = anthropicConfig.streamRate;
}
if (allConfig === null || allConfig === void 0 ? void 0 : allConfig.streamRate) {
result.llmConfig._lc_stream_delay = allConfig.streamRate;
}
return result;
});
}
/**
* Initializes Bedrock endpoint configuration.
*
* This module handles configuration for AWS Bedrock endpoints, including support for
* HTTP/HTTPS proxies and reverse proxies.
*
* Proxy Support:
* - When the PROXY environment variable is set, creates a custom BedrockRuntimeClient
* with an HttpsProxyAgent to route all Bedrock API calls through the specified proxy
* - The custom client is fully configured with credentials, region, and endpoint,
* and is passed directly to ChatBedrockConverse via the 'client' parameter
*
* Reverse Proxy Support:
* - When BEDROCK_REVERSE_PROXY is set, routes Bedrock API calls through a custom endpoint
* - Works with or without the PROXY setting
*
* Without Proxy:
* - Credentials and endpoint configuration are passed separately to ChatBedrockConverse,
* which creates its own BedrockRuntimeClient internally
*
* @param params - Configuration parameters
* @returns Promise resolving to Bedrock configuration options
* @throws Error if credentials are not provided when required
*/
function initializeBedrock(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, endpoint, model_parameters, db, }) {
var _b, _c, _d, _e, _f;
const appConfig = req.config;
const bedrockConfig = (_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _b === void 0 ? void 0 : _b[librechatDataProvider.EModelEndpoint.bedrock];
const { BEDROCK_AWS_SECRET_ACCESS_KEY, BEDROCK_AWS_ACCESS_KEY_ID, BEDROCK_AWS_SESSION_TOKEN, BEDROCK_REVERSE_PROXY, BEDROCK_AWS_DEFAULT_REGION, PROXY, } = process.env;
const { key: expiresAt } = req.body;
const isUserProvided = BEDROCK_AWS_SECRET_ACCESS_KEY === librechatDataProvider.AuthType.USER_PROVIDED;
let credentials = isUserProvided
? yield db
.getUserKey({ userId: (_d = (_c = req.user) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : '', name: librechatDataProvider.EModelEndpoint.bedrock })
.then((key) => JSON.parse(key))
: Object.assign({ accessKeyId: BEDROCK_AWS_ACCESS_KEY_ID, secretAccessKey: BEDROCK_AWS_SECRET_ACCESS_KEY }, (BEDROCK_AWS_SESSION_TOKEN && { sessionToken: BEDROCK_AWS_SESSION_TOKEN }));
if (!credentials) {
throw new Error('Bedrock credentials not provided. Please provide them again.');
}
if (!isUserProvided &&
(credentials.accessKeyId === undefined || credentials.accessKeyId === '') &&
(credentials.secretAccessKey === undefined || credentials.secretAccessKey === '')) {
credentials = undefined;
}
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(expiresAt, librechatDataProvider.EModelEndpoint.bedrock);
}
const requestOptions = {
model: model_parameters === null || model_parameters === void 0 ? void 0 : model_parameters.model,
region: BEDROCK_AWS_DEFAULT_REGION,
};
const configOptions = {};
const llmConfig = librechatDataProvider.bedrockOutputParser(librechatDataProvider.bedrockInputParser.parse(librechatDataProvider.removeNullishValues(Object.assign(Object.assign({}, requestOptions), (model_parameters !== null && model_parameters !== void 0 ? model_parameters : {})))));
if (bedrockConfig === null || bedrockConfig === void 0 ? void 0 : bedrockConfig.guardrailConfig) {
llmConfig.guardrailConfig = bedrockConfig.guardrailConfig;
}
const model = model_parameters === null || model_parameters === void 0 ? void 0 : model_parameters.model;
if (model && ((_e = bedrockConfig === null || bedrockConfig === void 0 ? void 0 : bedrockConfig.inferenceProfiles) === null || _e === void 0 ? void 0 : _e[model])) {
const applicationInferenceProfile = librechatDataProvider.extractEnvVariable(bedrockConfig.inferenceProfiles[model]);
llmConfig.applicationInferenceProfile = applicationInferenceProfile;
}
/** Only include credentials if they're complete (accessKeyId and secretAccessKey are both set) */
const hasCompleteCredentials = credentials &&
typeof credentials.accessKeyId === 'string' &&
credentials.accessKeyId !== '' &&
typeof credentials.secretAccessKey === 'string' &&
credentials.secretAccessKey !== '';
if (PROXY) {
const proxyAgent = new httpsProxyAgent.HttpsProxyAgent(PROXY);
// Create a custom BedrockRuntimeClient with proxy-enabled request handler.
// ChatBedrockConverse will use this pre-configured client directly instead of
// creating its own. Credentials are only set if explicitly provided; otherwise
// the AWS SDK's default credential provider chain is used (instance profiles,
// AWS profiles, environment variables, etc.)
const customClient = new clientBedrockRuntime.BedrockRuntimeClient(Object.assign(Object.assign(Object.assign({ region: (_f = llmConfig.region) !== null && _f !== void 0 ? _f : BEDROCK_AWS_DEFAULT_REGION }, (hasCompleteCredentials && {
credentials: credentials,
})), { requestHandler: new nodeHttpHandler.NodeHttpHandler({
httpAgent: proxyAgent,
httpsAgent: proxyAgent,
}) }), (BEDROCK_REVERSE_PROXY && {
endpoint: `https://${BEDROCK_REVERSE_PROXY}`,
})));
llmConfig.client = customClient;
}
else {
// When not using a proxy, let ChatBedrockConverse create its own client
// by providing credentials and endpoint separately
if (credentials) {
llmConfig.credentials = credentials;
}
if (BEDROCK_REVERSE_PROXY) {
llmConfig.endpointHost = BEDROCK_REVERSE_PROXY;
}
}
return {
llmConfig,
configOptions,
};
});
}
const knownOpenAIParams = new Set([
// Constructor/Instance Parameters
'model',
'modelName',
'temperature',
'topP',
'frequencyPenalty',
'presencePenalty',
'n',
'logitBias',
'stop',
'stopSequences',
'user',
'timeout',
'stream',
'maxTokens',
'maxCompletionTokens',
'logprobs',
'topLogprobs',
'apiKey',
'organization',
'audio',
'modalities',
'reasoning',
'zdrEnabled',
'service_tier',
'supportsStrictToolCalling',
'useResponsesApi',
'configuration',
// Call-time Options
'tools',
'tool_choice',
'functions',
'function_call',
'response_format',
'seed',
'stream_options',
'parallel_tool_calls',
'strict',
'prediction',
'promptIndex',
// Responses API specific
'text',
'truncation',
'include',
'previous_response_id',
// LangChain specific
'__includeRawResponse',
'maxConcurrency',
'maxRetries',
'verbose',
'streaming',
'streamUsage',
'disableStreaming',
]);
function hasReasoningParams({ reasoning_effort, reasoning_summary, }) {
return ((reasoning_effort != null && reasoning_effort !== '') ||
(reasoning_summary != null && reasoning_summary !== ''));
}
/**
* Extracts default parameters from customParams.paramDefinitions
* @param paramDefinitions - Array of parameter definitions with key and default values
* @returns Record of default parameters
*/
function extractDefaultParams(paramDefinitions) {
if (!paramDefinitions || !Array.isArray(paramDefinitions)) {
return undefined;
}
const defaults = {};
for (let i = 0; i < paramDefinitions.length; i++) {
const param = paramDefinitions[i];
if (param.key !== undefined && param.default !== undefined) {
defaults[param.key] = param.default;
}
}
return defaults;
}
/**
* Applies default parameters to the target object only if the field is undefined
* @param target - The target object to apply defaults to
* @param defaults - Record of default parameter values
*/
function applyDefaultParams$1(target, defaults) {
for (const [key, value] of Object.entries(defaults)) {
if (target[key] === undefined) {
target[key] = value;
}
}
}
function getOpenAILLMConfig({ azure, apiKey, baseURL, endpoint, streaming, addParams, dropParams, defaultParams, useOpenRouter, modelOptions: _modelOptions, }) {
var _a;
/** Clean empty strings from model options (e.g., temperature: "" should be removed) */
const cleanedModelOptions = librechatDataProvider.removeNullishValues(_modelOptions, true);
const { reasoning_effort, reasoning_summary, verbosity, web_search, frequency_penalty, presence_penalty } = cleanedModelOptions, modelOptions = __rest(cleanedModelOptions, ["reasoning_effort", "reasoning_summary", "verbosity", "web_search", "frequency_penalty", "presence_penalty"]);
const llmConfig = Object.assign({
streaming,
model: (_a = modelOptions.model) !== null && _a !== void 0 ? _a : '',
}, modelOptions);
if (frequency_penalty != null) {
llmConfig.frequencyPenalty = frequency_penalty;
}
if (presence_penalty != null) {
llmConfig.presencePenalty = presence_penalty;
}
const modelKwargs = {};
let hasModelKwargs = false;
if (verbosity != null && verbosity !== '') {
modelKwargs.verbosity = verbosity;
hasModelKwargs = true;
}
let enableWebSearch = web_search;
/** Apply defaultParams first - only if fields are undefined */
if (defaultParams && typeof defaultParams === 'object') {
for (const [key, value] of Object.entries(defaultParams)) {
/** Handle web_search separately - don't add to config */
if (key === 'web_search') {
if (enableWebSearch === undefined && typeof value === 'boolean') {
enableWebSearch = value;
}
continue;
}
if (knownOpenAIParams.has(key)) {
applyDefaultParams$1(llmConfig, { [key]: value });
}
else {
/** Apply to modelKwargs if not a known param */
if (modelKwargs[key] === undefined) {
modelKwargs[key] = value;
hasModelKwargs = true;
}
}
}
}
/** Apply addParams - can override defaultParams */
if (addParams && typeof addParams === 'object') {
for (const [key, value] of Object.entries(addParams)) {
/** Handle web_search directly here instead of adding to modelKwargs or llmConfig */
if (key === 'web_search') {
if (typeof value === 'boolean') {
enableWebSearch = value;
}
continue;
}
if (knownOpenAIParams.has(key)) {
llmConfig[key] = value;
}
else {
hasModelKwargs = true;
modelKwargs[key] = value;
}
}
}
if (useOpenRouter) {
llmConfig.include_reasoning = true;
}
if (hasReasoningParams({ reasoning_effort, reasoning_summary }) &&
(llmConfig.useResponsesApi === true ||
(endpoint !== librechatDataProvider.EModelEndpoint.openAI && endpoint !== librechatDataProvider.EModelEndpoint.azureOpenAI))) {
llmConfig.reasoning = librechatDataProvider.removeNullishValues({
effort: reasoning_effort,
summary: reasoning_summary,
}, true);
}
else if (hasReasoningParams({ reasoning_effort })) {
llmConfig.reasoning_effort = reasoning_effort;
}
if (llmConfig.max_tokens != null) {
llmConfig.maxTokens = llmConfig.max_tokens;
delete llmConfig.max_tokens;
}
const tools = [];
/** Check if web_search should be disabled via dropParams */
if (dropParams && dropParams.includes('web_search')) {
enableWebSearch = false;
}
if (useOpenRouter && enableWebSearch) {
/** OpenRouter expects web search as a plugins parameter */
modelKwargs.plugins = [{ id: 'web' }];
hasModelKwargs = true;
}
else if (enableWebSearch) {
/** Standard OpenAI web search uses tools API */
llmConfig.useResponsesApi = true;
tools.push({ type: 'web_search' });
}
/**
* Note: OpenAI reasoning models (o1/o3/gpt-5) do not support temperature and other sampling parameters
* Exception: gpt-5-chat and versioned models like gpt-5.1 DO support these parameters
*/
if (modelOptions.model &&
/\b(o[13]|gpt-5)(?!\.|-chat)(?:-|$)/.test(modelOptions.model)) {
const reasoningExcludeParams = [
'frequencyPenalty',
'presencePenalty',
'temperature',
'topP',
'logitBias',
'n',
'logprobs',
];
const updatedDropParams = dropParams || [];
const combinedDropParams = [...new Set([...updatedDropParams, ...reasoningExcludeParams])];
combinedDropParams.forEach((param) => {
if (param in llmConfig) {
delete llmConfig[param];
}
});
}
else if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model)) {
/**
* Note: OpenAI Web Search models do not support any known parameters besides `max_tokens`
*/
const searchExcludeParams = [
'frequency_penalty',
'presence_penalty',
'reasoning',
'reasoning_effort',
'temperature',
'top_p',
'top_k',
'stop',
'logit_bias',
'seed',
'response_format',
'n',
'logprobs',
'user',
];
const updatedDropParams = dropParams || [];
const combinedDropParams = [...new Set([...updatedDropParams, ...searchExcludeParams])];
combinedDropParams.forEach((param) => {
if (param in llmConfig) {
delete llmConfig[param];
}
});
}
else if (dropParams && Array.isArray(dropParams)) {
dropParams.forEach((param) => {
if (param in llmConfig) {
delete llmConfig[param];
}
});
}
if (modelKwargs.verbosity && llmConfig.useResponsesApi === true) {
modelKwargs.text = { verbosity: modelKwargs.verbosity };
delete modelKwargs.verbosity;
}
if (llmConfig.model &&
/\bgpt-[5-9](?:\.\d+)?\b/i.test(llmConfig.model) &&
llmConfig.maxTokens != null) {
const paramName = llmConfig.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
modelKwargs[paramName] = llmConfig.maxTokens;
delete llmConfig.maxTokens;
hasModelKwargs = true;
}
if (hasModelKwargs) {
llmConfig.modelKwargs = modelKwargs;
}
if (!azure) {
llmConfig.apiKey = apiKey;
return { llmConfig, tools };
}
const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME);
const updatedAzure = Object.assign({}, azure);
updatedAzure.azureOpenAIApiDeploymentName = useModelName
? sanitizeModelName(llmConfig.model || '')
: azure.azureOpenAIApiDeploymentName;
if (process.env.AZURE_OPENAI_DEFAULT_MODEL) {
llmConfig.model = process.env.AZURE_OPENAI_DEFAULT_MODEL;
}
const constructAzureOpenAIBasePath = () => {
if (!baseURL) {
return;
}
const azureURL = constructAzureURL({
baseURL,
azureOptions: updatedAzure,
});
updatedAzure.azureOpenAIBasePath = azureURL.split(`/${updatedAzure.azureOpenAIApiDeploymentName}`)[0];
};
constructAzureOpenAIBasePath();
Object.assign(llmConfig, updatedAzure);
const constructAzureResponsesApi = () => {
if (!llmConfig.useResponsesApi) {
return;
}
delete llmConfig.azureOpenAIApiDeploymentName;
delete llmConfig.azureOpenAIApiInstanceName;
delete llmConfig.azureOpenAIApiVersion;
delete llmConfig.azureOpenAIBasePath;
delete llmConfig.azureOpenAIApiKey;
llmConfig.apiKey = apiKey;
};
constructAzureResponsesApi();
llmConfig.model = updatedAzure.azureOpenAIApiDeploymentName;
return { llmConfig, tools, azure: updatedAzure };
}
/** Known Google/Vertex AI parameters that map directly to the client config */
const knownGoogleParams = new Set([
'model',
'modelName',
'temperature',
'maxOutputTokens',
'maxReasoningTokens',
'topP',
'topK',
'seed',
'presencePenalty',
'frequencyPenalty',
'stopSequences',
'stop',
'logprobs',
'topLogprobs',
'safetySettings',
'responseModalities',
'convertSystemMessageToHumanContent',
'speechConfig',
'streamUsage',
'apiKey',
'baseUrl',
'location',
'authOptions',
]);
/**
* Applies default parameters to the target object only if the field is undefined
* @param target - The target object to apply defaults to
* @param defaults - Record of default parameter values
*/
function applyDefaultParams(target, defaults) {
for (const [key, value] of Object.entries(defaults)) {
if (target[key] === undefined) {
target[key] = value;
}
}
}
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) => {
if (value === 'OFF') {
return 'BLOCK_NONE';
}
return value;
};
}
if (restrictedPattern.test(model)) {
return (value) => {
if (value === 'OFF' || value === 'HARM_BLOCK_THRESHOLD_UNSPECIFIED') {
return 'BLOCK_NONE';
}
return value;
};
}
return (value) => value;
}
function getSafetySettings(model) {
if (isEnabled(process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS)) {
return undefined;
}
const mapThreshold = getThresholdMapping(model !== null && model !== void 0 ? model : '');
return [
{
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: mapThreshold(process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED'),
},
{
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: mapThreshold(process.env.GOOGLE_SAFETY_HATE_SPEECH || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED'),
},
{
category: 'HARM_CATEGORY_HARASSMENT',
threshold: mapThreshold(process.env.GOOGLE_SAFETY_HARASSMENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED'),
},
{
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
threshold: mapThreshold(process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED'),
},
{
category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
threshold: mapThreshold(process.env.GOOGLE_SAFETY_CIVIC_INTEGRITY || 'BLOCK_NONE'),
},
];
}
/**
* 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
*/
function getGoogleConfig(credentials, options = {}, acceptRawApiKey = false) {
var _a, _b, _c, _d, _e, _f, _g, _h;
let creds = {};
if (acceptRawApiKey && typeof credentials === 'string') {
creds[librechatDataProvider.AuthKeys.GOOGLE_API_KEY] = credentials;
}
else if (typeof credentials === 'string') {
try {
creds = JSON.parse(credentials);
}
catch (err) {
throw new Error(`Error parsing string credentials: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
}
else if (credentials && typeof credentials === 'object') {
creds = credentials;
}
const serviceKeyRaw = (_a = creds[librechatDataProvider.AuthKeys.GOOGLE_SERVICE_KEY]) !== null && _a !== void 0 ? _a : {};
const serviceKey = typeof serviceKeyRaw === 'string' ? JSON.parse(serviceKeyRaw) : (serviceKeyRaw !== null && serviceKeyRaw !== void 0 ? serviceKeyRaw : {});
const apiKey = (_b = creds[librechatDataProvider.AuthKeys.GOOGLE_API_KEY]) !== null && _b !== void 0 ? _b : null;
const project_id = !apiKey ? ((_c = serviceKey === null || serviceKey === void 0 ? void 0 : serviceKey.project_id) !== null && _c !== void 0 ? _c : null) : null;
const reverseProxyUrl = options.reverseProxyUrl;
const authHeader = options.authHeader;
const _j = options.modelOptions || {}, { web_search, thinking = librechatDataProvider.googleSettings.thinking.default, thinkingBudget = librechatDataProvider.googleSettings.thinkingBudget.default } = _j, modelOptions = __rest(_j, ["web_search", "thinking", "thinkingBudget"]);
let enableWebSearch = web_search;
const llmConfig = librechatDataProvider.removeNullishValues(Object.assign(Object.assign({}, (modelOptions || {})), { model: (_d = modelOptions === null || modelOptions === void 0 ? void 0 : modelOptions.model) !== null && _d !== void 0 ? _d : '', maxRetries: 2, topP: (_e = modelOptions === null || modelOptions === void 0 ? void 0 : modelOptions.topP) !== null && _e !== void 0 ? _e : undefined, topK: (_f = modelOptions === null || modelOptions === void 0 ? void 0 : modelOptions.topK) !== null && _f !== void 0 ? _f : undefined, temperature: (_g = modelOptions === null || modelOptions === void 0 ? void 0 : modelOptions.temperature) !== null && _g !== void 0 ? _g : undefined, maxOutputTokens: (_h = modelOptions === null || modelOptions === void 0 ? void 0 : modelOptions.maxOutputTokens) !== null && _h !== void 0 ? _h : undefined }), true);
/** Used only for Safety Settings */
llmConfig.safetySettings = getSafetySettings(llmConfig.model);
let provider;
if (project_id) {
provider = agents.Providers.VERTEXAI;
}
else {
provider = agents.Providers.GOOGLE;
}
// If we have a GCP project => Vertex AI
if (provider === agents.Providers.VERTEXAI) {
llmConfig.authOptions = {
credentials: Object.assign({}, serviceKey),
projectId: project_id,
};
llmConfig.location = process.env.GOOGLE_LOC || 'us-central1';
}
else if (apiKey && provider === agents.Providers.GOOGLE) {
llmConfig.apiKey = apiKey;
}
else {
throw new Error(`Invalid credentials provided. Please provide either a valid API key or service account credentials for Google Cloud.`);
}
const shouldEnableThinking = thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1);
if (shouldEnableThinking && provider === agents.Providers.GOOGLE) {
llmConfig.thinkingConfig = {
thinkingBudget: thinking ? thinkingBudget : librechatDataProvider.googleSettings.thinkingBudget.default,
includeThoughts: Boolean(thinking),
};
}
else if (shouldEnableThinking && provider === agents.Providers.VERTEXAI) {
llmConfig.thinkingBudget = thinking
? thinkingBudget
: librechatDataProvider.googleSettings.thinkingBudget.default;
llmConfig.includeThoughts = Boolean(thinking);
}
/*
let legacyOptions = {};
// Filter out any "examples" that are empty
legacyOptions.examples = (legacyOptions.examples ?? [])
.filter(Boolean)
.filter((obj) => obj?.input?.content !== '' && obj?.output?.content !== '');
// If user has "examples" from legacyOptions, push them onto llmConfig
if (legacyOptions.examples?.length) {
llmConfig.examples = legacyOptions.examples.map((ex) => {
const { input, output } = ex;
if (!input?.content || !output?.content) {return undefined;}
return {
input: new HumanMessage(input.content),
output: new AIMessage(output.content),
};
}).filter(Boolean);
}
*/
if (reverseProxyUrl) {
llmConfig.baseUrl = reverseProxyUrl;
}
if (authHeader) {
llmConfig.customHeaders = {
Authorization: `Bearer ${apiKey}`,
};
}
/** Handle defaultParams first - only process Google-native params if undefined */
if (options.defaultParams && typeof options.defaultParams === 'object') {
for (const [key, value] of Object.entries(options.defaultParams)) {
/** Handle web_search separately - don't add to config */
if (key === 'web_search') {
if (enableWebSearch === undefined && typeof value === 'boolean') {
enableWebSearch = value;
}
continue;
}
if (knownGoogleParams.has(key)) {
/** Route known Google params to llmConfig only if undefined */
applyDefaultParams(llmConfig, { [key]: value });
}
/** Leave other params for transform to handle - they might be OpenAI params */
}
}
/** Handle addParams - can override defaultParams */
if (options.addParams && typeof options.addParams === 'object') {
for (const [key, value] of Object.entries(options.addParams)) {
/** Handle web_search separately - don't add to config */
if (key === 'web_search') {
if (typeof value === 'boolean') {
enableWebSearch = value;
}
continue;
}
if (knownGoogleParams.has(key)) {
/** Route known Google params to llmConfig */
llmConfig[key] = value;
}
/** Leave other params for transform to handle - they might be OpenAI params */
}
}
/** Handle dropParams - only drop from Google config */
if (options.dropParams && Array.isArray(options.dropParams)) {
options.dropParams.forEach((param) => {
if (param === 'web_search') {
enableWebSearch = false;
return;
}
if (param in llmConfig) {
delete llmConfig[param];
}
});
}
const tools = [];
if (enableWebSearch) {
tools.push({ googleSearch: {} });
}
// Return the final shape
return {
/** @type {GoogleAIToolType[]} */
tools,
/** @type {Providers.GOOGLE | Providers.VERTEXAI} */
provider,
/** @type {GoogleClientOptions | VertexAIClientOptions} */
llmConfig,
};
}
const anthropicExcludeParams = new Set(['anthropicApiUrl']);
const googleExcludeParams = new Set([
'safetySettings',
'location',
'baseUrl',
'customHeaders',
'thinkingConfig',
'thinkingBudget',
'includeThoughts',
]);
/** Google-specific tool types that have no OpenAI-compatible equivalent */
const googleToolsToFilter = new Set(['googleSearch']);
/**
* Transforms a Non-OpenAI LLM config to an OpenAI-conformant config.
* Non-OpenAI parameters are moved to modelKwargs.
* Also extracts configuration options that belong in configOptions.
* Handles addParams and dropParams for parameter customization.
* Filters out provider-specific tools that have no OpenAI equivalent.
*/
function transformToOpenAIConfig({ tools, addParams, dropParams, defaultParams, llmConfig, fromEndpoint, }) {
const openAIConfig = {};
let configOptions = {};
let modelKwargs = {};
let hasModelKwargs = false;
const isAnthropic = fromEndpoint === librechatDataProvider.EModelEndpoint.anthropic;
const isGoogle = fromEndpoint === librechatDataProvider.EModelEndpoint.google;
let excludeParams = new Set();
if (isAnthropic) {
excludeParams = anthropicExcludeParams;
}
else if (isGoogle) {
excludeParams = googleExcludeParams;
}
for (const [key, value] of Object.entries(llmConfig)) {
if (value === undefined || value === null) {
continue;
}
if (excludeParams.has(key)) {
continue;
}
if (isAnthropic && key === 'clientOptions') {
configOptions = Object.assign({}, configOptions, value);
continue;
}
else if (isAnthropic && key === 'invocationKwargs') {
modelKwargs = Object.assign({}, modelKwargs, value);
hasModelKwargs = true;
continue;
}
else if (isGoogle && key === 'authOptions') {
modelKwargs = Object.assign({}, modelKwargs, value);
hasModelKwargs = true;
continue;
}
if (knownOpenAIParams.has(key)) {
openAIConfig[key] = value;
}
else {
modelKwargs[key] = value;
hasModelKwargs = true;
}
}
if (addParams && typeof addParams === 'object') {
for (const [key, value] of Object.entries(addParams)) {
/** Skip web_search - it's handled separately as a tool */
if (key === 'web_search') {
continue;
}
if (knownOpenAIParams.has(key)) {
openAIConfig[key] = value;
}
else {
modelKwargs[key] = value;
hasModelKwargs = true;
}
}
}
if (hasModelKwargs) {
openAIConfig.modelKwargs = modelKwargs;
}
if (dropParams && Array.isArray(dropParams)) {
dropParams.forEach((param) => {
/** Skip web_search - handled separately */
if (param === 'web_search') {
return;
}
if (param in openAIConfig) {
delete openAIConfig[param];
}
if (openAIConfig.modelKwargs && param in openAIConfig.modelKwargs) {
delete openAIConfig.modelKwargs[param];
}
});
/** Clean up empty modelKwargs after dropParams processing */
if (openAIConfig.modelKwargs && Object.keys(openAIConfig.modelKwargs).length === 0) {
delete openAIConfig.modelKwargs;
}
}
/**
* Filter out provider-specific tools that have no OpenAI equivalent.
* Exception: If web_search was explicitly enabled via addParams or defaultParams,
* preserve googleSearch tools (pass through in Google-native format).
*/
const webSearchExplicitlyEnabled = (addParams === null || addParams === void 0 ? void 0 : addParams.web_search) === true || (defaultParams === null || defaultParams === void 0 ? void 0 : defaultParams.web_search) === true;
const filterGoogleTool = (tool) => {
if (!isGoogle) {
return true;
}
if (typeof tool !== 'object' || tool === null) {
return false;
}
const toolKeys = Object.keys(tool);
const isGoogleSpecificTool = toolKeys.some((key) => googleToolsToFilter.has(key));
/** Preserve googleSearch if web_search was explicitly enabled */
if (isGoogleSpecificTool && webSearchExplicitlyEnabled) {
return true;
}
return !isGoogleSpecificTool;
};
const filteredTools = Array.isArray(tools) ? tools.filter(filterGoogleTool) : [];
return {
tools: filteredTools,
llmConfig: openAIConfig,
configOptions,
};
}
/**
* Generates configuration options for creating a language model (LLM) instance.
* @param apiKey - The API key for authentication.
* @param options - Additional options for configuring the LLM.
* @param endpoint - The endpoint name
* @returns Configuration options for creating an LLM instance.
*/
function getOpenAIConfig(apiKey, options = {}, endpoint) {
var _a, _b, _c, _d, _e;
const { proxy, addParams, dropParams, defaultQuery, directEndpoint, streaming = true, modelOptions = {}, reverseProxyUrl: baseURL, } = options;
/** Extract default params from customParams.paramDefinitions */
const defaultParams = extractDefaultParams((_a = options.customParams) === null || _a === void 0 ? void 0 : _a.paramDefinitions);
let llmConfig;
let tools;
const isAnthropic = ((_b = options.customParams) === null || _b === void 0 ? void 0 : _b.defaultParamsEndpoint) === librechatDataProvider.EModelEndpoint.anthropic;
const isGoogle = ((_c = options.customParams) === null || _c === void 0 ? void 0 : _c.defaultParamsEndpoint) === librechatDataProvider.EModelEndpoint.google;
const useOpenRouter = !isAnthropic &&
!isGoogle &&
((baseURL && baseURL.includes(librechatDataProvider.KnownEndpoints.openrouter)) ||
(endpoint != null && endpoint.toLowerCase().includes(librechatDataProvider.KnownEndpoints.openrouter)));
const isVercel = !isAnthropic &&
!isGoogle &&
((baseURL && baseURL.includes('ai-gateway.vercel.sh')) ||
(endpoint != null && endpoint.toLowerCase().includes(librechatDataProvider.KnownEndpoints.vercel)));
let azure = options.azure;
let headers = options.headers;
if (isAnthropic) {
const anthropicResult = getLLMConfig(apiKey, {
modelOptions,
proxy: options.proxy,
reverseProxyUrl: baseURL,
addParams,
dropParams,
defaultParams,
});
/** Transform handles addParams/dropParams - it knows about OpenAI params */
const transformed = transformToOpenAIConfig({
addParams,
dropParams,
llmConfig: anthropicResult.llmConfig,
fromEndpoint: librechatDataProvider.EModelEndpoint.anthropic,
});
llmConfig = transformed.llmConfig;
tools = anthropicResult.tools;
if ((_d = transformed.configOptions) === null || _d === void 0 ? void 0 : _d.defaultHeaders) {
headers = Object.assign(headers !== null && headers !== void 0 ? headers : {}, (_e = transformed.configOptions) === null || _e === void 0 ? void 0 : _e.defaultHeaders);
}
}
else if (isGoogle) {
const googleResult = getGoogleConfig(apiKey, {
modelOptions,
reverseProxyUrl: baseURL !== null && baseURL !== void 0 ? baseURL : undefined,
authHeader: true,
addParams,
dropParams,
defaultParams,
}, true);
/** Transform handles addParams/dropParams - it knows about OpenAI params */
const transformed = transformToOpenAIConfig({
addParams,
dropParams,
defaultParams,
tools: googleResult.tools,
llmConfig: googleResult.llmConfig,
fromEndpoint: librechatDataProvider.EModelEndpoint.google,
});
llmConfig = transformed.llmConfig;
tools = transformed.tools;
}
else {
const openaiResult = getOpenAILLMConfig({
azure,
apiKey,
baseURL,
endpoint,
streaming,
addParams,
dropParams,
defaultParams,
modelOptions,
useOpenRouter,
});
llmConfig = openaiResult.llmConfig;
azure = openaiResult.azure;
tools = openaiResult.tools;
}
const configOptions = {};
if (baseURL) {
configOptions.baseURL = baseURL;
}
if (useOpenRouter || isVercel) {
configOptions.defaultHeaders = Object.assign({
'HTTP-Referer': 'https://librechat.ai',
'X-Title': 'LibreChat',
}, headers);
}
else if (headers) {
configOptions.defaultHeaders = headers;
}
if (defaultQuery) {
configOptions.defaultQuery = defaultQuery;
}
if (proxy) {
const proxyAgent = new undici.ProxyAgent(proxy);
configOptions.fetchOptions = {
dispatcher: proxyAgent,
};
}
if (azure && !isAnthropic) {
const constructAzureResponsesApi = () => {
var _a, _b, _c;
if (!llmConfig.useResponsesApi || !azure) {
return;
}
const updatedUrl = (_a = configOptions.baseURL) === null || _a === void 0 ? void 0 : _a.replace(/\/deployments(?:\/.*)?$/, '/v1');
configOptions.baseURL = constructAzureURL({
baseURL: updatedUrl || 'https://${INSTANCE_NAME}.openai.azure.com/openai/v1',
azureOptions: azure,
});
configOptions.defaultHeaders = Object.assign(Object.assign({}, configOptions.defaultHeaders), { 'api-key': apiKey });
configOptions.defaultQuery = Object.assign(Object.assign({}, configOptions.defaultQuery), { 'api-version': (_c = (_b = configOptions.defaultQuery) === null || _b === void 0 ? void 0 : _b['api-version']) !== null && _c !== void 0 ? _c : 'preview' });
};
constructAzureResponsesApi();
}
if (process.env.OPENAI_ORGANIZATION && !isAnthropic) {
configOptions.organization = process.env.OPENAI_ORGANIZATION;
}
if (directEndpoint === true && (configOptions === null || configOptions === void 0 ? void 0 : configOptions.baseURL) != null) {
configOptions.fetch = createFetch({
directEndpoint: directEndpoint,
reverseProxyUrl: configOptions === null || configOptions === void 0 ? void 0 : configOptions.baseURL,
});
}
const result = {
llmConfig,
configOptions,
tools,
};
if (useOpenRouter) {
result.provider = agents.Providers.OPENROUTER;
}
return result;
}
/**
* Fetches Ollama models from the specified base API path.
* @param baseURL - The Ollama server URL
* @param options - Optional configuration
* @returns Promise resolving to array of model names
*/
function fetchOllamaModels(baseURL_1) {
return __awaiter(this, arguments, void 0, function* (baseURL, options = {}) {
var _a;
if (!baseURL) {
return [];
}
const ollamaEndpoint = deriveBaseURL(baseURL);
const resolvedHeaders = resolveHeaders({
headers: (_a = options.headers) !== null && _a !== void 0 ? _a : undefined,
user: options.user,
});
const response = yield axios$1.get(`${ollamaEndpoint}/api/tags`, {
headers: resolvedHeaders,
timeout: 5000,
});
return response.data.models.map((tag) => tag.name);
});
}
/**
* Splits a string by commas and trims each resulting value.
* @param input - The input string to split.
* @returns An array of trimmed values.
*/
function splitAndTrim(input) {
if (!input || typeof input !== 'string') {
return [];
}
return input
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}
/**
* Fetches models from the specified base API path or Azure, based on the provided configuration.
*
* @param params - The parameters for fetching the models.
* @returns A promise that resolves to an array of model identifiers.
*/
function fetchModels(_a) {
return __awaiter(this, arguments, void 0, function* ({ user, apiKey, baseURL: _baseURL, name = librechatDataProvider.EModelEndpoint.openAI, direct = false, azure = false, userIdQuery = false, createTokenConfig = true, tokenKey, headers, userObject, }) {
let models = [];
const baseURL = direct ? extractBaseURL(_baseURL !== null && _baseURL !== void 0 ? _baseURL : '') : _baseURL;
if (!baseURL && !azure) {
return models;
}
if (!apiKey) {
return models;
}
if (name && name.toLowerCase().startsWith(librechatDataProvider.KnownEndpoints.ollama)) {
try {
return yield fetchOllamaModels(baseURL !== null && baseURL !== void 0 ? baseURL : '', { headers, user: userObject });
}
catch (ollamaError) {
const logMessage = 'Failed to fetch models from Ollama API. Attempting to fetch via OpenAI-compatible endpoint.';
logAxiosError({ message: logMessage, error: ollamaError });
}
}
try {
const options = {
headers: Object.assign({}, (headers !== null && headers !== void 0 ? headers : {})),
timeout: 5000,
};
if (name === librechatDataProvider.EModelEndpoint.anthropic) {
options.headers = {
'x-api-key': apiKey,
'anthropic-version': process.env.ANTHROPIC_VERSION || '2023-06-01',
};
}
else {
options.headers.Authorization = `Bearer ${apiKey}`;
}
if (process.env.PROXY) {
options.httpsAgent = new httpsProxyAgent.HttpsProxyAgent(process.env.PROXY);
}
if (process.env.OPENAI_ORGANIZATION && (baseURL === null || baseURL === void 0 ? void 0 : baseURL.includes('openai'))) {
options.headers['OpenAI-Organization'] = process.env.OPENAI_ORGANIZATION;
}
const url = new URL(`${(baseURL !== null && baseURL !== void 0 ? baseURL : '').replace(/\/+$/, '')}${azure ? '' : '/models'}`);
if (user && userIdQuery) {
url.searchParams.append('user', user);
}
const res = yield axios$1.get(url.toString(), options);
const input = res.data;
const validationResult = inputSchema.safeParse(input);
if (validationResult.success && createTokenConfig) {
const endpointTokenConfig = processModelData(input);
const cache = standardCache(librechatDataProvider.CacheKeys.TOKEN_CONFIG);
yield cache.set(tokenKey !== null && tokenKey !== void 0 ? tokenKey : name, endpointTokenConfig);
}
models = input.data.map((item) => item.id);
}
catch (error) {
const logMessage = `Failed to fetch models from ${azure ? 'Azure ' : ''}${name} API`;
logAxiosError({ message: logMessage, error: error });
}
return models;
});
}
/**
* Fetches models from OpenAI or Azure based on the provided options.
* @param opts - Options for fetching models
* @param _models - Fallback models array
* @returns Promise resolving to array of model IDs
*/
function fetchOpenAIModels(opts_1) {
return __awaiter(this, arguments, void 0, function* (opts, _models = []) {
var _a, _b, _c;
let models = (_a = _models.slice()) !== null && _a !== void 0 ? _a : [];
const apiKey = (_b = opts.openAIApiKey) !== null && _b !== void 0 ? _b : process.env.OPENAI_API_KEY;
const openaiBaseURL = 'https://api.openai.com/v1';
let baseURL = openaiBaseURL;
let reverseProxyUrl = process.env.OPENAI_REVERSE_PROXY;
if (opts.assistants && process.env.ASSISTANTS_BASE_URL) {
reverseProxyUrl = process.env.ASSISTANTS_BASE_URL;
}
else if (opts.azure) {
return models;
}
if (reverseProxyUrl) {
baseURL = (_c = extractBaseURL(reverseProxyUrl)) !== null && _c !== void 0 ? _c : openaiBaseURL;
}
const modelsCache = standardCache(librechatDataProvider.CacheKeys.MODEL_QUERIES);
const cachedModels = yield modelsCache.get(baseURL);
if (cachedModels) {
return cachedModels;
}
if (baseURL || opts.azure) {
models = yield fetchModels({
apiKey: apiKey !== null && apiKey !== void 0 ? apiKey : '',
baseURL,
azure: opts.azure,
user: opts.user,
name: librechatDataProvider.EModelEndpoint.openAI,
});
}
if (models.length === 0) {
return _models;
}
if (baseURL === openaiBaseURL) {
const regex = /(text-davinci-003|gpt-|o\d+)/;
const excludeRegex = /audio|realtime/;
models = models.filter((model) => regex.test(model) && !excludeRegex.test(model));
const instructModels = models.filter((model) => model.includes('instruct'));
const otherModels = models.filter((model) => !model.includes('instruct'));
models = otherModels.concat(instructModels);
}
yield modelsCache.set(baseURL, models);
return models;
});
}
/**
* Loads the default models for OpenAI or Azure.
* @param opts - Options for getting models
* @returns Promise resolving to array of model IDs
*/
function getOpenAIModels() {
return __awaiter(this, arguments, void 0, function* (opts = {}) {
let models = librechatDataProvider.defaultModels[librechatDataProvider.EModelEndpoint.openAI];
if (opts.assistants) {
models = librechatDataProvider.defaultModels[librechatDataProvider.EModelEndpoint.assistants];
}
else if (opts.azure) {
models = librechatDataProvider.defaultModels[librechatDataProvider.EModelEndpoint.azureAssistants];
}
let key;
if (opts.assistants) {
key = 'ASSISTANTS_MODELS';
}
else if (opts.azure) {
key = 'AZURE_OPENAI_MODELS';
}
else {
key = 'OPENAI_MODELS';
}
if (process.env[key]) {
return splitAndTrim(process.env[key]);
}
if (opts.userProvidedOpenAI) {
return models;
}
return yield fetchOpenAIModels(opts, models);
});
}
/**
* Fetches models from the Anthropic API.
* @param opts - Options for fetching models
* @param _models - Fallback models array
* @returns Promise resolving to array of model IDs
*/
function fetchAnthropicModels() {
return __awaiter(this, arguments, void 0, function* (opts = {}, _models = []) {
var _a, _b;
let models = (_a = _models.slice()) !== null && _a !== void 0 ? _a : [];
const apiKey = process.env.ANTHROPIC_API_KEY;
const anthropicBaseURL = 'https://api.anthropic.com/v1';
let baseURL = anthropicBaseURL;
const reverseProxyUrl = process.env.ANTHROPIC_REVERSE_PROXY;
if (reverseProxyUrl) {
baseURL = (_b = extractBaseURL(reverseProxyUrl)) !== null && _b !== void 0 ? _b : anthropicBaseURL;
}
if (!apiKey) {
return models;
}
const modelsCache = standardCache(librechatDataProvider.CacheKeys.MODEL_QUERIES);
const cachedModels = yield modelsCache.get(baseURL);
if (cachedModels) {
return cachedModels;
}
if (baseURL) {
models = yield fetchModels({
apiKey,
baseURL,
user: opts.user,
name: librechatDataProvider.EModelEndpoint.anthropic,
tokenKey: librechatDataProvider.EModelEndpoint.anthropic,
});
}
if (models.length === 0) {
return _models;
}
yield modelsCache.set(baseURL, models);
return models;
});
}
/**
* Gets Anthropic models from environment or API.
* @param opts - Options for fetching models
* @returns Promise resolving to array of model IDs
*/
function getAnthropicModels() {
return __awaiter(this, arguments, void 0, function* (opts = {}) {
const models = librechatDataProvider.defaultModels[librechatDataProvider.EModelEndpoint.anthropic];
// Vertex AI models from YAML config take priority
if (opts.vertexModels && opts.vertexModels.length > 0) {
return opts.vertexModels;
}
if (process.env.ANTHROPIC_MODELS) {
return splitAndTrim(process.env.ANTHROPIC_MODELS);
}
if (isUserProvided(process.env.ANTHROPIC_API_KEY)) {
return models;
}
try {
return yield fetchAnthropicModels(opts, models);
}
catch (error) {
dataSchemas.logger.error('Error fetching Anthropic models:', error);
return models;
}
});
}
/**
* Gets Google models from environment or defaults.
* @returns Array of model IDs
*/
function getGoogleModels() {
let models = librechatDataProvider.defaultModels[librechatDataProvider.EModelEndpoint.google];
if (process.env.GOOGLE_MODELS) {
models = splitAndTrim(process.env.GOOGLE_MODELS);
}
return models;
}
/**
* Gets Bedrock models from environment or defaults.
* @returns Array of model IDs
*/
function getBedrockModels() {
let models = librechatDataProvider.defaultModels[librechatDataProvider.EModelEndpoint.bedrock];
if (process.env.BEDROCK_AWS_MODELS) {
models = splitAndTrim(process.env.BEDROCK_AWS_MODELS);
}
return models;
}
const KEY_TTL_MS = 10 * 60 * 1000; // 10 minutes
const IAM_TIMEOUT_MS = 5000;
/** Per-user hk- key cache: cacheKey (user id|email) -> { key, expiresAt }. */
const keyCache = new Map();
function truthy(value) {
return (value !== null && value !== void 0 ? value : '').toString().trim().toLowerCase() === 'true';
}
/** IAM base URL — prefer an in-cluster override, else the OIDC issuer. */
function iamBaseUrl() {
const base = process.env.IAM_INTERNAL_URL ||
process.env.IAM_SERVER_URL ||
process.env.OPENID_ISSUER ||
'';
return base.replace(/\/+$/, '');
}
function iamClientId() {
return process.env.IAM_CLIENT_ID || process.env.OPENID_CLIENT_ID || '';
}
function iamClientSecret() {
return process.env.IAM_CLIENT_SECRET || process.env.OPENID_CLIENT_SECRET || '';
}
/**
* Whether per-user hk- billing is enabled AND fully configured. When false,
* `resolveHanzoCloudKey` returns null and the shared key is used (legacy).
*/
function isHanzoPerUserKeyEnabled() {
return (truthy(process.env.HANZO_PER_USER_KEY) &&
Boolean(iamBaseUrl()) &&
Boolean(iamClientId()) &&
Boolean(iamClientSecret()));
}
function basicAuthHeader() {
const raw = `${iamClientId()}:${iamClientSecret()}`;
return `Basic ${Buffer.from(raw).toString('base64')}`;
}
// ── Per-user billing identity + starter credit ───────────────────────────────
/**
* Orgs whose MEMBERS are billed per-user (default: the shared "hanzo" catch-all,
* the home of every unaffiliated individual signup). MUST mirror the gateway's
* PERSONAL_BILLING_ORGS / object.BillingSubject (hanzoai/ai) so chat and the
* gateway derive the identical subject.
*/
function personalBillingOrgs() {
const raw = (process.env.HANZO_PERSONAL_BILLING_ORGS ||
process.env.HANZO_DEFAULT_ORG ||
'hanzo')
.split(',')
.map((o) => o.trim().toLowerCase())
.filter(Boolean);
return new Set(raw.length ? raw : ['hanzo']);
}
/**
* Canonical Commerce billing subject for an IAM (owner, name) identity — the
* account the cloud gateway debits and reads. Personal-billing org → "owner/name"
* (per-user), pooled org → "owner". Always lowercased so it nets against the
* gateway's usage writes. Byte-identical to object.BillingSubject in hanzoai/ai.
*/
function billingSubject(owner, name) {
const o = (owner !== null && owner !== void 0 ? owner : '').toString().trim().toLowerCase();
if (!o) {
return '';
}
if (personalBillingOrgs().has(o)) {
const n = (name !== null && name !== void 0 ? name : '').toString().trim().toLowerCase();
return n ? `${o}/${n}` : o;
}
return o;
}
function commerceBaseUrl() {
return (process.env.COMMERCE_API_URL || process.env.COMMERCE_ENDPOINT || '').replace(/\/+$/, '');
}
function commerceToken() {
return process.env.COMMERCE_TOKEN || process.env.COMMERCE_API_TOKEN || '';
}
/** Subjects whose starter credit this process has already ensured (commerce is also idempotent). */
const starterEnsured = new Set();
/**
* Ensure the new-user $5 welcome credit exists on THIS user's billing subject,
* exactly once. Idempotent two ways: an in-process set (avoids re-hitting
* commerce every key refresh) and commerce's own tag-deduped, transaction-guarded
* grant (`POST /v1/billing/grant-starter`) — so concurrent chats can never
* double-grant (no bleed).
*
* Best-effort but awaited: the credit must land on the subject BEFORE we forward
* the user's hk- key to the gateway, otherwise the gateway sees a $0 balance and
* 402s the very first chat. A commerce hiccup must NOT break key resolution,
* though — the gateway still enforces balance, and the next message retries.
*/
function ensureStarterCredit(subject, owner) {
return __awaiter(this, void 0, void 0, function* () {
const base = commerceBaseUrl();
if (!base || !subject || starterEnsured.has(subject)) {
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), IAM_TIMEOUT_MS);
try {
const headers = {
'Content-Type': 'application/json',
'X-Hanzo-Org': owner,
};
const tok = commerceToken();
if (tok) {
headers['Authorization'] = `Bearer ${tok}`;
}
const resp = yield fetch(`${base}/v1/billing/grant-starter`, {
method: 'POST',
headers,
body: JSON.stringify({ user: subject, trigger: 'chat_first_use' }),
signal: controller.signal,
});
if (resp.ok) {
starterEnsured.add(subject); // ensured this process; commerce dedupes across pods
}
else {
dataSchemas.logger.warn('[hanzoCloudKey] starter-credit grant non-OK (continuing)', {
subject,
status: resp.status,
});
}
}
catch (err) {
dataSchemas.logger.warn('[hanzoCloudKey] starter-credit grant failed (continuing; gateway enforces)', {
subject,
error: err instanceof Error ? err.message : String(err),
});
}
finally {
clearTimeout(timeoutId);
}
});
}
/**
* Single IAM HTTP primitive (confidential-client Basic auth, `/v1/iam/*` JSON
* API). Throws on transport error / non-ok status so callers fail closed.
*/
function iamRequest(path_1, params_1) {
return __awaiter(this, arguments, void 0, function* (path, params, method = 'GET') {
const url = new URL(`${iamBaseUrl()}${path}`);
for (const [k, v] of Object.entries(params)) {
if (v != null && v !== '') {
url.searchParams.set(k, v);
}
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), IAM_TIMEOUT_MS);
try {
const resp = yield fetch(url.toString(), Object.assign(Object.assign({ method, headers: {
Authorization: basicAuthHeader(),
'Content-Type': 'application/json',
} }, (method === 'POST' ? { body: '{}' } : {})), { signal: controller.signal }));
if (!resp.ok) {
throw new Error(`IAM ${method} ${path} returned ${resp.status}`);
}
return (yield resp.json());
}
finally {
clearTimeout(timeoutId);
}
});
}
/** Resolve the authoritative IAM record (owner/name/accessKey) by org + email. */
function getIamUserByOrgEmail(owner, email) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const res = yield iamRequest('/v1/iam/get-user', {
owner,
email: email.toLowerCase(),
});
if (res.status !== 'ok' || !((_a = res.data) === null || _a === void 0 ? void 0 : _a.owner) || !((_b = res.data) === null || _b === void 0 ? void 0 : _b.name)) {
return null;
}
return res.data;
});
}
/** Mint (create) the per-user hk- key for an IAM sub ("owner/name"). */
function mintUserKey(sub) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const res = yield iamRequest('/v1/iam/mint-user-keys', { id: sub }, 'POST');
if (res.status !== 'ok' || !((_a = res.data) === null || _a === void 0 ? void 0 : _a.accessKey)) {
return null;
}
return res.data.accessKey;
});
}
/**
* Resolve the authenticated user's own hk- Cloud API key, minting one on first
* use if the IAM record has none. Returns null when per-user billing is
* disabled/unconfigured, the user is a guest / has no email, or IAM cannot be
* reached (caller FAILS CLOSED on null for an authenticated user).
*/
function resolveHanzoCloudKey(user) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
if (!isHanzoPerUserKeyEnabled()) {
return null;
}
if (!user || user.guest) {
return null;
}
const email = ((_a = user.email) !== null && _a !== void 0 ? _a : '').toString().toLowerCase();
if (!email) {
return null;
}
const cacheKey = user.id || email;
const cached = keyCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.key;
}
const defaultOrg = process.env.HANZO_DEFAULT_ORG || 'hanzo';
const owner = ((_b = user.organization) !== null && _b !== void 0 ? _b : '').toString().trim() || defaultOrg;
try {
let record = yield getIamUserByOrgEmail(owner, email);
// A user whose stored org is stale/missing may live in the default org.
if (!record && owner !== defaultOrg) {
record = yield getIamUserByOrgEmail(defaultOrg, email);
}
if (!(record === null || record === void 0 ? void 0 : record.owner) || !(record === null || record === void 0 ? void 0 : record.name)) {
dataSchemas.logger.warn('[hanzoCloudKey] No IAM user for billing identity', {
owner,
email,
});
return null;
}
// Single authoritative identity: stamp the REAL billing org (record.owner)
// back onto req.user. The OIDC-stored `organization` can be the Casdoor
// super-org "admin" for some users, which is NOT where their hk- key bills —
// so the downstream Commerce balance gate must use this resolved owner, not
// the login-time value. This keeps the key and the gate on ONE org.
const subject = billingSubject(record.owner, record.name);
try {
user.organization = record.owner;
// Stamp the canonical billing subject so the balance gate keys on the SAME
// account the gateway debits (per-user for the shared "hanzo" catch-all).
user.billingSubject = subject;
}
catch (_e) {
/* req.user may be a frozen/lean doc — non-fatal; gate still has gateway as backstop */
}
// Ensure THIS user's one-time $5 welcome credit exists on THEIR subject
// before we hand back the key — so the gateway's first balance check sees it
// instead of 402-ing a brand-new account. Idempotent + best-effort.
yield ensureStarterCredit(subject, record.owner);
let key = ((_c = record.accessKey) !== null && _c !== void 0 ? _c : '').trim();
if (!key) {
// Mint on first chat — the key is theirs going forward.
key = (_d = (yield mintUserKey(`${record.owner}/${record.name}`))) !== null && _d !== void 0 ? _d : '';
}
if (!key) {
dataSchemas.logger.error('[hanzoCloudKey] Failed to resolve/mint hk- key', {
sub: `${record.owner}/${record.name}`,
});
return null;
}
keyCache.set(cacheKey, { key, expiresAt: Date.now() + KEY_TTL_MS });
return key;
}
catch (err) {
dataSchemas.logger.error('[hanzoCloudKey] IAM lookup failed (failing closed)', {
owner,
email,
error: err instanceof Error ? err.message : String(err),
});
return null;
}
});
}
/** Test-only: clear the in-process key cache. */
function _clearHanzoKeyCache() {
keyCache.clear();
}
/**
* Wrap an OpenAI-client `fetch` so the Hanzo Cloud gateway's HTTP-200 error
* envelope becomes a real, surfaced error instead of an opaque crash.
*
* The gateway (api.hanzo.ai) answers some failures — most importantly a request
* for a premium model when the caller's balance is only the $5 starter credit —
* with HTTP 200 and a JSON body `{ "status": "error", "msg": "..." }` that has no
* `choices`. The OpenAI client treats the 200 as success and parses the body to
* `undefined`; the agent run then throws the opaque
* `Cannot read properties of undefined (reading 'role')`, so NO assistant reply
* renders and the user never learns why (and the title call dies the same way on
* `reading 'message'`).
*
* This wrapper rewrites that envelope into a conventional 402 response carrying
* the gateway's own `msg`, so the OpenAI client raises a clean, non-retryable
* error and the existing error path shows the actionable message ("... requires a
* paid balance. Add funds ...") instead of crashing. Successful SSE streams
* (`text/event-stream`) and normal completions pass through untouched, and the
* request headers/body are never inspected — per-user `hk-` billing is unaffected.
*/
function wrapHanzoGatewayFetch(baseFetch) {
const inner = baseFetch !== null && baseFetch !== void 0 ? baseFetch : ((input, init) => fetch(input, init));
return (input, init) => __awaiter(this, void 0, void 0, function* () {
var _a;
const response = yield inner(input, init);
/** Live SSE success streams must pass through without buffering the body. */
const contentType = (_a = response.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
if (contentType.includes('text/event-stream')) {
return response;
}
/** Read a clone so the original body stays intact for the OpenAI client. */
let envelope;
try {
envelope = (yield response.clone().json());
}
catch (_b) {
return response;
}
if (envelope && envelope.status === 'error' && envelope.choices == null) {
const message = (typeof envelope.msg === 'string' && envelope.msg.trim()) ||
'Hanzo Cloud rejected the model request.';
dataSchemas.logger.warn('[hanzoGatewayFetch] gateway returned a 200 error envelope; surfacing as 402', {
message,
});
return new Response(JSON.stringify({
error: { message, type: 'insufficient_quota', code: 'insufficient_quota' },
}), { status: 402, headers: { 'content-type': 'application/json' } });
}
return response;
});
}
const { PROXY } = process.env;
/**
* Builds custom options from endpoint configuration
*/
function buildCustomOptions(endpointConfig, appConfig, endpointTokenConfig) {
var _a, _b;
const customOptions = {
headers: endpointConfig.headers,
addParams: endpointConfig.addParams,
dropParams: endpointConfig.dropParams,
customParams: endpointConfig.customParams,
titleConvo: endpointConfig.titleConvo,
titleModel: endpointConfig.titleModel,
summaryModel: endpointConfig.summaryModel,
modelDisplayLabel: endpointConfig.modelDisplayLabel,
titleMethod: (_a = endpointConfig.titleMethod) !== null && _a !== void 0 ? _a : 'completion',
contextStrategy: endpointConfig.summarize ? 'summarize' : null,
directEndpoint: endpointConfig.directEndpoint,
titleMessageRole: endpointConfig.titleMessageRole,
streamRate: endpointConfig.streamRate,
endpointTokenConfig,
};
const allConfig = (_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _b === void 0 ? void 0 : _b.all;
if (allConfig) {
customOptions.streamRate = allConfig.streamRate;
}
return customOptions;
}
/**
* Initializes a custom endpoint client configuration.
* This function handles custom endpoints defined in librechat.yaml, including
* user-provided API keys and URLs.
*
* @param params - Configuration parameters
* @returns Promise resolving to endpoint configuration options
* @throws Error if config is missing, API key is not provided, or base URL is missing
*/
function initializeCustom(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, endpoint, model_parameters, db, }) {
var _b, _c, _d, _e, _f, _g, _h;
const appConfig = req.config;
const { key: expiresAt } = req.body;
const endpointConfig = getCustomEndpointConfig({
endpoint,
appConfig,
});
if (!endpointConfig) {
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
}
const CUSTOM_API_KEY = librechatDataProvider.extractEnvVariable((_b = endpointConfig.apiKey) !== null && _b !== void 0 ? _b : '');
const CUSTOM_BASE_URL = librechatDataProvider.extractEnvVariable((_c = endpointConfig.baseURL) !== null && _c !== void 0 ? _c : '');
if (CUSTOM_API_KEY.match(librechatDataProvider.envVarRegex)) {
throw new Error(`Missing API Key for ${endpoint}.`);
}
if (CUSTOM_BASE_URL.match(librechatDataProvider.envVarRegex)) {
throw new Error(`Missing Base URL for ${endpoint}.`);
}
const userProvidesKey = isUserProvided(CUSTOM_API_KEY);
const userProvidesURL = isUserProvided(CUSTOM_BASE_URL);
let userValues = null;
if (expiresAt && (userProvidesKey || userProvidesURL)) {
checkUserKeyExpiry(expiresAt, endpoint);
userValues = yield db.getUserKeyValues({ userId: (_e = (_d = req.user) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : '', name: endpoint });
}
let apiKey = userProvidesKey ? userValues === null || userValues === void 0 ? void 0 : userValues.apiKey : CUSTOM_API_KEY;
const baseURL = userProvidesURL ? userValues === null || userValues === void 0 ? void 0 : userValues.baseURL : CUSTOM_BASE_URL;
// Hanzo per-user billing: an authenticated (non-guest) user's chat must be
// billed to THEIR OWN org via THEIR OWN hk- key — never the shared key. We
// resolve (mint on first chat) their key from IAM and use it here. If it
// cannot be resolved we FAIL CLOSED (throw) rather than silently fall back to
// the shared key, so an IAM hiccup can never route an authed user's spend onto
// the shared org. Guests (anonymous preview) keep the shared, capped key.
const billingUser = req.user;
const isAuthenticatedUser = Boolean(billingUser && !billingUser.guest && billingUser.email);
if (isHanzoPerUserKeyEnabled() && isAuthenticatedUser) {
const perUserKey = yield resolveHanzoCloudKey(billingUser);
if (perUserKey) {
apiKey = perUserKey;
}
else {
throw new Error('Your Hanzo Cloud account is not linked for billing yet. Please sign out and back in, then claim your starter credit at https://billing.hanzo.ai');
}
}
if (userProvidesKey && !apiKey) {
throw new Error(JSON.stringify({
type: librechatDataProvider.ErrorTypes.NO_USER_KEY,
}));
}
if (userProvidesURL && !baseURL) {
throw new Error(JSON.stringify({
type: librechatDataProvider.ErrorTypes.NO_BASE_URL,
}));
}
if (!apiKey) {
throw new Error(`${endpoint} API key not provided.`);
}
if (!baseURL) {
throw new Error(`${endpoint} Base URL not provided.`);
}
let endpointTokenConfig;
const userId = (_g = (_f = req.user) === null || _f === void 0 ? void 0 : _f.id) !== null && _g !== void 0 ? _g : '';
const cache = standardCache(librechatDataProvider.CacheKeys.TOKEN_CONFIG);
/** tokenConfig is an optional extended property on custom endpoints */
const hasTokenConfig = endpointConfig.tokenConfig != null;
const tokenKey = !hasTokenConfig && (userProvidesKey || userProvidesURL) ? `${endpoint}:${userId}` : endpoint;
const cachedConfig = !hasTokenConfig &&
librechatDataProvider.FetchTokenConfig[endpoint.toLowerCase()] &&
(yield cache.get(tokenKey));
endpointTokenConfig = cachedConfig || undefined;
if (librechatDataProvider.FetchTokenConfig[endpoint.toLowerCase()] &&
endpointConfig &&
((_h = endpointConfig.models) === null || _h === void 0 ? void 0 : _h.fetch) &&
!endpointTokenConfig) {
yield fetchModels({ apiKey, baseURL, name: endpoint, user: userId, tokenKey });
endpointTokenConfig = (yield cache.get(tokenKey));
}
const customOptions = buildCustomOptions(endpointConfig, appConfig, endpointTokenConfig);
const clientOptions = Object.assign({ reverseProxyUrl: baseURL !== null && baseURL !== void 0 ? baseURL : null, proxy: PROXY !== null && PROXY !== void 0 ? PROXY : null }, customOptions);
const modelOptions = Object.assign(Object.assign({}, (model_parameters !== null && model_parameters !== void 0 ? model_parameters : {})), { user: userId });
const finalClientOptions = Object.assign({ modelOptions }, clientOptions);
const options = getOpenAIConfig(apiKey, finalClientOptions, endpoint);
if (options != null) {
options.useLegacyContent = true;
options.endpointTokenConfig = endpointTokenConfig;
}
// The Hanzo Cloud gateway answers some failures (e.g. a premium model requested
// against a starter-credit-only balance) with HTTP 200 + a JSON error envelope
// ({status:"error", msg}) that has no `choices`. Left as-is, the OpenAI client
// parses the choices-less 200 to `undefined` and the agent run crashes with
// `Cannot read properties of undefined (reading 'role')` — no reply renders.
// Wrap the client fetch so that envelope becomes a clean 402 carrying the
// gateway's actionable message. Scoped to the Hanzo gateway; response-only, so
// per-user hk- billing is untouched.
if ((options === null || options === void 0 ? void 0 : options.configOptions) && /(?:^|\.)hanzo\.ai(?::|\/|$)/i.test(baseURL !== null && baseURL !== void 0 ? baseURL : '')) {
options.configOptions.fetch = wrapHanzoGatewayFetch(options.configOptions.fetch);
}
const streamRate = clientOptions.streamRate;
if (streamRate) {
options.llmConfig._lc_stream_delay = streamRate;
}
return options;
});
}
/**
* Initializes Google/Vertex AI endpoint configuration.
* Supports both API key authentication and service account credentials.
*
* @param params - Configuration parameters
* @returns Promise resolving to Google configuration options
* @throws Error if no valid credentials are provided
*/
function initializeGoogle(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, endpoint, model_parameters, db, }) {
var _b, _c, _d, _e;
const appConfig = req.config;
const { GOOGLE_KEY, GOOGLE_REVERSE_PROXY, GOOGLE_AUTH_HEADER, PROXY } = process.env;
const isUserProvided = GOOGLE_KEY === 'user_provided';
const { key: expiresAt } = req.body;
let userKey = null;
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(expiresAt, librechatDataProvider.EModelEndpoint.google);
userKey = yield db.getUserKey({ userId: (_b = req.user) === null || _b === void 0 ? void 0 : _b.id, name: librechatDataProvider.EModelEndpoint.google });
}
let serviceKey = {};
/** Check if GOOGLE_KEY is provided at all (including 'user_provided') */
const isGoogleKeyProvided = (GOOGLE_KEY && GOOGLE_KEY.trim() !== '') || (isUserProvided && userKey != null);
if (!isGoogleKeyProvided && loadServiceKey) {
/** Only attempt to load service key if GOOGLE_KEY is not provided */
try {
const serviceKeyPath = process.env.GOOGLE_SERVICE_KEY_FILE || path.join(process.cwd(), 'api', 'data', 'auth.json');
const loadedKey = yield loadServiceKey(serviceKeyPath);
if (loadedKey) {
serviceKey = loadedKey;
}
}
catch (_f) {
// Service key loading failed, but that's okay if not required
serviceKey = {};
}
}
const credentials = isUserProvided
? userKey
: {
[librechatDataProvider.AuthKeys.GOOGLE_SERVICE_KEY]: serviceKey,
[librechatDataProvider.AuthKeys.GOOGLE_API_KEY]: GOOGLE_KEY,
};
let clientOptions = {};
/** @type {undefined | TBaseEndpoint} */
const allConfig = (_c = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _c === void 0 ? void 0 : _c.all;
/** @type {undefined | TBaseEndpoint} */
const googleConfig = (_d = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _d === void 0 ? void 0 : _d[librechatDataProvider.EModelEndpoint.google];
if (googleConfig) {
clientOptions.streamRate = googleConfig.streamRate;
clientOptions.titleModel = googleConfig.titleModel;
}
if (allConfig) {
clientOptions.streamRate = allConfig.streamRate;
}
clientOptions = Object.assign({ reverseProxyUrl: GOOGLE_REVERSE_PROXY !== null && GOOGLE_REVERSE_PROXY !== void 0 ? GOOGLE_REVERSE_PROXY : undefined, authHeader: (_e = isEnabled(GOOGLE_AUTH_HEADER)) !== null && _e !== void 0 ? _e : undefined, proxy: PROXY !== null && PROXY !== void 0 ? PROXY : undefined, modelOptions: model_parameters !== null && model_parameters !== void 0 ? model_parameters : {} }, clientOptions);
return getGoogleConfig(credentials, clientOptions);
});
}
/**
* Initializes OpenAI options for agent usage. This function always returns configuration
* options and never creates a client instance (equivalent to optionsOnly=true behavior).
*
* @param params - Configuration parameters
* @returns Promise resolving to OpenAI configuration options
* @throws Error if API key is missing or user key has expired
*/
function initializeOpenAI(_a) {
return __awaiter(this, arguments, void 0, function* ({ req, endpoint, model_parameters, db, }) {
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const appConfig = req.config;
const { PROXY, OPENAI_API_KEY, AZURE_API_KEY, OPENAI_REVERSE_PROXY, AZURE_OPENAI_BASEURL } = process.env;
const { key: expiresAt } = req.body;
const modelName = model_parameters === null || model_parameters === void 0 ? void 0 : model_parameters.model;
const credentials = {
[librechatDataProvider.EModelEndpoint.openAI]: OPENAI_API_KEY,
[librechatDataProvider.EModelEndpoint.azureOpenAI]: AZURE_API_KEY,
};
const baseURLOptions = {
[librechatDataProvider.EModelEndpoint.openAI]: OPENAI_REVERSE_PROXY,
[librechatDataProvider.EModelEndpoint.azureOpenAI]: AZURE_OPENAI_BASEURL,
};
const userProvidesKey = isUserProvided(credentials[endpoint]);
const userProvidesURL = isUserProvided(baseURLOptions[endpoint]);
let userValues = null;
if (expiresAt && (userProvidesKey || userProvidesURL)) {
checkUserKeyExpiry(expiresAt, endpoint);
userValues = yield db.getUserKeyValues({ userId: (_c = (_b = req.user) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : '', name: endpoint });
}
let apiKey = userProvidesKey
? userValues === null || userValues === void 0 ? void 0 : userValues.apiKey
: credentials[endpoint];
const baseURL = userProvidesURL
? userValues === null || userValues === void 0 ? void 0 : userValues.baseURL
: baseURLOptions[endpoint];
const clientOptions = {
proxy: PROXY !== null && PROXY !== void 0 ? PROXY : undefined,
reverseProxyUrl: baseURL || undefined,
streaming: true,
};
const isAzureOpenAI = endpoint === librechatDataProvider.EModelEndpoint.azureOpenAI;
const azureConfig = isAzureOpenAI && ((_d = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _d === void 0 ? void 0 : _d[librechatDataProvider.EModelEndpoint.azureOpenAI]);
let isServerless = false;
if (isAzureOpenAI && azureConfig) {
const { modelGroupMap, groupMap } = azureConfig;
const { azureOptions, baseURL: configBaseURL, headers = {}, serverless, } = librechatDataProvider.mapModelToAzureConfig({
modelName: modelName || '',
modelGroupMap,
groupMap,
});
isServerless = serverless === true;
clientOptions.reverseProxyUrl = configBaseURL !== null && configBaseURL !== void 0 ? configBaseURL : clientOptions.reverseProxyUrl;
clientOptions.headers = resolveHeaders({
headers: Object.assign(Object.assign({}, headers), ((_e = clientOptions.headers) !== null && _e !== void 0 ? _e : {})),
user: req.user,
});
const groupName = (_f = modelGroupMap[modelName || '']) === null || _f === void 0 ? void 0 : _f.group;
if (groupName && groupMap[groupName]) {
clientOptions.addParams = (_g = groupMap[groupName]) === null || _g === void 0 ? void 0 : _g.addParams;
clientOptions.dropParams = (_h = groupMap[groupName]) === null || _h === void 0 ? void 0 : _h.dropParams;
}
apiKey = azureOptions.azureOpenAIApiKey;
clientOptions.azure = !isServerless ? azureOptions : undefined;
if (isServerless) {
clientOptions.defaultQuery = azureOptions.azureOpenAIApiVersion
? { 'api-version': azureOptions.azureOpenAIApiVersion }
: undefined;
if (!clientOptions.headers) {
clientOptions.headers = {};
}
clientOptions.headers['api-key'] = apiKey;
}
}
else if (isAzureOpenAI) {
clientOptions.azure =
userProvidesKey && (userValues === null || userValues === void 0 ? void 0 : userValues.apiKey) ? JSON.parse(userValues.apiKey) : getAzureCredentials();
apiKey = clientOptions.azure ? clientOptions.azure.azureOpenAIApiKey : undefined;
}
if (userProvidesKey && !apiKey) {
throw new Error(JSON.stringify({
type: librechatDataProvider.ErrorTypes.NO_USER_KEY,
}));
}
if (!apiKey) {
throw new Error(`${endpoint} API Key not provided.`);
}
const modelOptions = Object.assign(Object.assign({}, (model_parameters !== null && model_parameters !== void 0 ? model_parameters : {})), { model: modelName, user: (_j = req.user) === null || _j === void 0 ? void 0 : _j.id });
const finalClientOptions = Object.assign(Object.assign({}, clientOptions), { modelOptions });
const options = getOpenAIConfig(apiKey, finalClientOptions, endpoint);
/** Set useLegacyContent for Azure serverless deployments */
if (isServerless) {
options.useLegacyContent = true;
}
const openAIConfig = (_k = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _k === void 0 ? void 0 : _k[librechatDataProvider.EModelEndpoint.openAI];
const allConfig = (_l = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _l === void 0 ? void 0 : _l.all;
const azureRate = (modelName === null || modelName === void 0 ? void 0 : modelName.includes('gpt-4')) ? 30 : 17;
let streamRate;
if (isAzureOpenAI && azureConfig) {
streamRate = (_m = azureConfig.streamRate) !== null && _m !== void 0 ? _m : azureRate;
}
else if (!isAzureOpenAI && openAIConfig) {
streamRate = openAIConfig.streamRate;
}
if (allConfig === null || allConfig === void 0 ? void 0 : allConfig.streamRate) {
streamRate = allConfig.streamRate;
}
if (streamRate) {
options.llmConfig._lc_stream_delay = streamRate;
}
return options;
});
}
/**
* Check if the provider is a known custom provider
* @param provider - The provider string
* @returns True if the provider is a known custom provider, false otherwise
*/
function isKnownCustomProvider(provider) {
var _a;
return [agents.Providers.XAI, agents.Providers.DEEPSEEK, agents.Providers.OPENROUTER, agents.Providers.MOONSHOT].includes(((_a = provider === null || provider === void 0 ? void 0 : provider.toLowerCase()) !== null && _a !== void 0 ? _a : ''));
}
/**
* Provider configuration map mapping providers to their initialization functions
*/
const providerConfigMap = {
[agents.Providers.XAI]: initializeCustom,
[agents.Providers.DEEPSEEK]: initializeCustom,
[agents.Providers.MOONSHOT]: initializeCustom,
[agents.Providers.OPENROUTER]: initializeCustom,
[librechatDataProvider.EModelEndpoint.openAI]: initializeOpenAI,
[librechatDataProvider.EModelEndpoint.google]: initializeGoogle,
[librechatDataProvider.EModelEndpoint.bedrock]: initializeBedrock,
[librechatDataProvider.EModelEndpoint.azureOpenAI]: initializeOpenAI,
[librechatDataProvider.EModelEndpoint.anthropic]: initializeAnthropic,
};
/**
* Get the provider configuration and override endpoint based on the provider string
*
* @param params - Configuration parameters
* @param params.provider - The provider string
* @param params.appConfig - The application configuration
* @returns Provider configuration including getOptions function, override provider, and custom config
* @throws Error if provider is not supported
*/
function getProviderConfig({ provider, appConfig, }) {
let getOptions = providerConfigMap[provider];
let overrideProvider = provider;
let customEndpointConfig;
if (!getOptions && providerConfigMap[provider.toLowerCase()] != null) {
overrideProvider = provider.toLowerCase();
getOptions = providerConfigMap[overrideProvider];
}
else if (!getOptions) {
customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig });
if (!customEndpointConfig) {
throw new Error(`Provider ${provider} not supported`);
}
getOptions = initializeCustom;
overrideProvider = agents.Providers.OPENAI;
}
if (isKnownCustomProvider(overrideProvider) && !customEndpointConfig) {
customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig });
if (!customEndpointConfig) {
throw new Error(`Provider ${provider} not supported`);
}
}
return {
getOptions,
overrideProvider,
customEndpointConfig,
};
}
/**
* Load config endpoints from the cached configuration object
* @param customEndpointsConfig - The configuration object
*/
function loadCustomEndpointsConfig(customEndpoints) {
if (!customEndpoints) {
return;
}
const customEndpointsConfig = {};
if (Array.isArray(customEndpoints)) {
const filteredEndpoints = customEndpoints.filter((endpoint) => endpoint.baseURL &&
endpoint.apiKey &&
endpoint.name &&
endpoint.models &&
(endpoint.models.fetch || endpoint.models.default));
for (let i = 0; i < filteredEndpoints.length; i++) {
const endpoint = filteredEndpoints[i];
const { baseURL, apiKey, name: configName, iconURL, modelDisplayLabel, customParams, customOrder, } = endpoint;
const name = librechatDataProvider.normalizeEndpointName(configName);
const resolvedApiKey = librechatDataProvider.extractEnvVariable(apiKey !== null && apiKey !== void 0 ? apiKey : '');
const resolvedBaseURL = librechatDataProvider.extractEnvVariable(baseURL !== null && baseURL !== void 0 ? baseURL : '');
customEndpointsConfig[name] = Object.assign({ type: librechatDataProvider.EModelEndpoint.custom, userProvide: isUserProvided(resolvedApiKey), userProvideURL: isUserProvided(resolvedBaseURL), customParams,
modelDisplayLabel,
iconURL }, (customOrder != null ? { order: customOrder } : {}));
}
}
return customEndpointsConfig;
}
/**
* Helper function to add a file to a specific tool resource category
* Prevents duplicate files within the same resource category
* @param params - Parameters object
* @param params.file - The file to add to the resource
* @param params.resourceType - The type of tool resource (e.g., execute_code, file_search, image_edit)
* @param params.tool_resources - The agent's tool resources object to update
* @param params.processedResourceFiles - Set tracking processed files per resource type
*/
const addFileToResource = ({ file, resourceType, tool_resources, processedResourceFiles, }) => {
var _a, _b;
if (!file.file_id) {
return;
}
const resourceKey = `${resourceType}:${file.file_id}`;
if (processedResourceFiles.has(resourceKey)) {
return;
}
const resource = (_a = tool_resources[resourceType]) !== null && _a !== void 0 ? _a : {};
if (!resource.files) {
tool_resources[resourceType] = Object.assign(Object.assign({}, resource), { files: [] });
}
// Check if already exists in the files array
const resourceFiles = (_b = tool_resources[resourceType]) === null || _b === void 0 ? void 0 : _b.files;
const alreadyExists = resourceFiles === null || resourceFiles === void 0 ? void 0 : resourceFiles.some((f) => f.file_id === file.file_id);
if (!alreadyExists) {
resourceFiles === null || resourceFiles === void 0 ? void 0 : resourceFiles.push(file);
processedResourceFiles.add(resourceKey);
}
};
/**
* Categorizes a file into the appropriate tool resource based on its properties
* Files are categorized as:
* - execute_code: Files with fileIdentifier metadata
* - file_search: Files marked as embedded
* - image_edit: Image files in the request file set with dimensions
* @param params - Parameters object
* @param params.file - The file to categorize
* @param params.tool_resources - The agent's tool resources to update
* @param params.requestFileSet - Set of file IDs from the current request
* @param params.processedResourceFiles - Set tracking processed files per resource type
*/
const categorizeFileForToolResources = ({ file, tool_resources, requestFileSet, processedResourceFiles, }) => {
var _a;
if ((_a = file.metadata) === null || _a === void 0 ? void 0 : _a.fileIdentifier) {
addFileToResource({
file,
resourceType: librechatDataProvider.EToolResources.execute_code,
tool_resources,
processedResourceFiles,
});
return;
}
if (file.embedded === true) {
addFileToResource({
file,
resourceType: librechatDataProvider.EToolResources.file_search,
tool_resources,
processedResourceFiles,
});
return;
}
if (requestFileSet.has(file.file_id) &&
file.type.startsWith('image') &&
file.height &&
file.width) {
addFileToResource({
file,
resourceType: librechatDataProvider.EToolResources.image_edit,
tool_resources,
processedResourceFiles,
});
}
};
/**
* Primes resources for agent execution by processing attachments and tool resources
* This function:
* 1. Fetches OCR files if OCR is enabled
* 2. Processes attachment files
* 3. Categorizes files into appropriate tool resources
* 4. Prevents duplicate files across all sources
*
* @param params - Parameters object
* @param params.req - Express request object
* @param params.appConfig - Application configuration object
* @param params.getFiles - Function to retrieve files from database
* @param params.requestFileSet - Set of file IDs from the current request
* @param params.attachments - Promise resolving to array of attachment files
* @param params.tool_resources - Existing tool resources for the agent
* @returns Promise resolving to processed attachments and updated tool resources
*/
const primeResources = (_a) => __awaiter(void 0, [_a], void 0, function* ({ req, appConfig, getFiles, requestFileSet, attachments: _attachments, tool_resources: _tool_resources, agentId, }) {
var _b, _c, _d, _e, _f, _g, _h, _j;
try {
/**
* Array to collect all unique files that will be returned as attachments
* Files are added from OCR results and attachment promises, with duplicates prevented
*/
const attachments = [];
/**
* Set of file IDs already added to the attachments array
* Used to prevent duplicate files from being added multiple times
* Pre-populated with files from non-OCR tool_resources to prevent re-adding them
*/
const attachmentFileIds = new Set();
/**
* Set tracking which files have been added to specific tool resource categories
* Format: "resourceType:fileId" (e.g., "execute_code:file123")
* Prevents the same file from being added multiple times to the same resource
*/
const processedResourceFiles = new Set();
/**
* The agent's tool resources object that will be updated with categorized files
* Create a shallow copy first to avoid mutating the original
*/
const tool_resources = Object.assign({}, (_tool_resources !== null && _tool_resources !== void 0 ? _tool_resources : {}));
// Deep copy each resource to avoid mutating nested objects/arrays
for (const [resourceType, resource] of Object.entries(tool_resources)) {
if (!resource) {
continue;
}
// Deep copy the resource to avoid mutations
tool_resources[resourceType] = Object.assign(Object.assign(Object.assign(Object.assign({}, resource), (resource.files && { files: [...resource.files] })), (resource.file_ids && { file_ids: [...resource.file_ids] })), (resource.vector_store_ids && { vector_store_ids: [...resource.vector_store_ids] }));
// Now track existing files
if (resource.files && Array.isArray(resource.files)) {
for (const file of resource.files) {
if (file === null || file === void 0 ? void 0 : file.file_id) {
processedResourceFiles.add(`${resourceType}:${file.file_id}`);
// Files from non-context resources should not be added to attachments from _attachments
if (resourceType !== librechatDataProvider.EToolResources.context && resourceType !== librechatDataProvider.EToolResources.ocr) {
attachmentFileIds.add(file.file_id);
}
}
}
}
}
const isContextEnabled = ((_d = (_c = (_b = appConfig === null || appConfig === void 0 ? void 0 : appConfig.endpoints) === null || _b === void 0 ? void 0 : _b[librechatDataProvider.EModelEndpoint.agents]) === null || _c === void 0 ? void 0 : _c.capabilities) !== null && _d !== void 0 ? _d : []).includes(librechatDataProvider.AgentCapabilities.context);
const fileIds = (_f = (_e = tool_resources[librechatDataProvider.EToolResources.context]) === null || _e === void 0 ? void 0 : _e.file_ids) !== null && _f !== void 0 ? _f : [];
const ocrFileIds = (_g = tool_resources[librechatDataProvider.EToolResources.ocr]) === null || _g === void 0 ? void 0 : _g.file_ids;
if (ocrFileIds != null) {
fileIds.push(...ocrFileIds);
delete tool_resources[librechatDataProvider.EToolResources.ocr];
}
if (fileIds.length > 0 && isContextEnabled) {
delete tool_resources[librechatDataProvider.EToolResources.context];
const context = yield getFiles({
file_id: { $in: fileIds },
}, {}, {}, { userId: (_h = req.user) === null || _h === void 0 ? void 0 : _h.id, agentId });
for (const file of context) {
if (!(file === null || file === void 0 ? void 0 : file.file_id)) {
continue;
}
// Clear from attachmentFileIds if it was pre-added
attachmentFileIds.delete(file.file_id);
// Add to attachments
attachments.push(file);
attachmentFileIds.add(file.file_id);
// Categorize for tool resources
categorizeFileForToolResources({
file,
tool_resources,
requestFileSet,
processedResourceFiles,
});
}
}
if (!_attachments) {
return { attachments: attachments.length > 0 ? attachments : undefined, tool_resources };
}
const files = yield _attachments;
for (const file of files) {
if (!file) {
continue;
}
categorizeFileForToolResources({
file,
tool_resources,
requestFileSet,
processedResourceFiles,
});
if (file.file_id && attachmentFileIds.has(file.file_id)) {
continue;
}
attachments.push(file);
if (file.file_id) {
attachmentFileIds.add(file.file_id);
}
}
return { attachments: attachments.length > 0 ? attachments : [], tool_resources };
}
catch (error) {
dataSchemas.logger.error('Error priming resources', error);
// Safely try to get attachments without rethrowing
let safeAttachments = [];
if (_attachments) {
try {
const attachmentFiles = yield _attachments;
safeAttachments = ((_j = attachmentFiles === null || attachmentFiles === void 0 ? void 0 : attachmentFiles.filter((file) => !!file)) !== null && _j !== void 0 ? _j : []);
}
catch (attachmentError) {
// If attachments promise is also rejected, just use empty array
dataSchemas.logger.error('Error resolving attachments in catch block', attachmentError);
safeAttachments = [];
}
}
return {
attachments: safeAttachments,
tool_resources: _tool_resources,
};
}
});
/**
* Initializes an agent for use in requests.
* Handles file processing, tool loading, provider configuration, and context token calculations.
*
* This function is exported from @hanzochat/api and replaces the CJS version from
* api/server/services/Endpoints/agents/agent.js
*
* @param params - Initialization parameters
* @param deps - Optional dependency injection for testing
* @returns Promise resolving to initialized agent with tools and configuration
* @throws Error if agent provider is not allowed or if required dependencies are missing
*/
function initializeAgent(params, db) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const { req, res, agent, loadTools, requestFiles = [], conversationId, endpointOption, parentMessageId, allowedProviders, isInitialAgent = false, } = params;
if (!db) {
throw new Error('initializeAgent requires db methods to be passed');
}
if (librechatDataProvider.isAgentsEndpoint(endpointOption === null || endpointOption === void 0 ? void 0 : endpointOption.endpoint) &&
allowedProviders.size > 0 &&
!allowedProviders.has(agent.provider)) {
throw new Error(`{ "type": "${librechatDataProvider.ErrorTypes.INVALID_AGENT_PROVIDER}", "info": "${agent.provider}" }`);
}
let currentFiles;
const _modelOptions = structuredClone(Object.assign({ model: agent.model }, (_a = agent.model_parameters) !== null && _a !== void 0 ? _a : { model: agent.model }, isInitialAgent === true ? endpointOption === null || endpointOption === void 0 ? void 0 : endpointOption.model_parameters : {}));
const { resendFiles, maxContextTokens, modelOptions } = extractLibreChatParams(_modelOptions);
const provider = agent.provider;
agent.endpoint = provider;
/**
* Load conversation files for ALL agents, not just the initial agent.
* This enables handoff agents to access files that were uploaded earlier
* in the conversation. Without this, file_search and execute_code tools
* on handoff agents would fail to find previously attached files.
*/
if (conversationId != null && resendFiles) {
const fileIds = (_b = (yield db.getConvoFiles(conversationId))) !== null && _b !== void 0 ? _b : [];
const toolResourceSet = new Set();
for (const tool of (_c = agent.tools) !== null && _c !== void 0 ? _c : []) {
if (librechatDataProvider.EToolResources[tool]) {
toolResourceSet.add(librechatDataProvider.EToolResources[tool]);
}
}
const toolFiles = (yield db.getToolFilesByIds(fileIds, toolResourceSet));
/**
* Retrieve execute_code files filtered to the current thread.
* This includes both code-generated files and user-uploaded execute_code files.
*/
let codeGeneratedFiles = [];
let userCodeFiles = [];
if (toolResourceSet.has(librechatDataProvider.EToolResources.execute_code)) {
let threadMessageIds;
let threadFileIds;
if (parentMessageId && parentMessageId !== librechatDataProvider.Constants.NO_PARENT && db.getMessages) {
/** Only select fields needed for thread traversal */
const messages = yield db.getMessages({ conversationId }, 'messageId parentMessageId files');
if (messages && messages.length > 0) {
/** Single O(n) pass: build Map, traverse thread, collect both IDs */
const threadData = getThreadData(messages, parentMessageId);
threadMessageIds = threadData.messageIds;
threadFileIds = threadData.fileIds;
}
}
/** Code-generated files (context: execute_code) filtered by messageId */
if (db.getCodeGeneratedFiles) {
codeGeneratedFiles = (yield db.getCodeGeneratedFiles(conversationId, threadMessageIds));
}
/** User-uploaded execute_code files (context: agents/message_attachment) from thread messages */
if (db.getUserCodeFiles && threadFileIds && threadFileIds.length > 0) {
userCodeFiles = (yield db.getUserCodeFiles(threadFileIds));
}
}
const allToolFiles = toolFiles.concat(codeGeneratedFiles, userCodeFiles);
if (requestFiles.length || allToolFiles.length) {
currentFiles = (yield db.updateFilesUsage(requestFiles.concat(allToolFiles)));
}
}
else if (requestFiles.length) {
currentFiles = (yield db.updateFilesUsage(requestFiles));
}
if (currentFiles && currentFiles.length) {
let endpointType;
if (!librechatDataProvider.paramEndpoints.has((_d = agent.endpoint) !== null && _d !== void 0 ? _d : '')) {
endpointType = librechatDataProvider.EModelEndpoint.custom;
}
currentFiles = filterFilesByEndpointConfig(req, {
files: currentFiles,
endpoint: (_e = agent.endpoint) !== null && _e !== void 0 ? _e : '',
endpointType,
});
}
const { attachments: primedAttachments, tool_resources } = yield primeResources({
req: req,
getFiles: db.getFiles,
appConfig: req.config,
agentId: agent.id,
attachments: currentFiles
? Promise.resolve(currentFiles)
: undefined,
tool_resources: agent.tool_resources,
requestFileSet: new Set(requestFiles === null || requestFiles === void 0 ? void 0 : requestFiles.map((file) => file.file_id)),
});
const { toolRegistry, toolContextMap, userMCPAuthMap, toolDefinitions, hasDeferredTools, tools: structuredTools, } = (_g = (yield (loadTools === null || loadTools === void 0 ? void 0 : loadTools({
req,
res,
provider,
agentId: agent.id,
tools: (_f = agent.tools) !== null && _f !== void 0 ? _f : [],
model: agent.model,
tool_options: agent.tool_options,
tool_resources,
})))) !== null && _g !== void 0 ? _g : {
tools: [],
toolContextMap: {},
userMCPAuthMap: undefined,
toolRegistry: undefined,
toolDefinitions: [],
hasDeferredTools: false,
};
const { getOptions, overrideProvider } = getProviderConfig({
provider,
appConfig: req.config,
});
if (overrideProvider !== agent.provider) {
agent.provider = overrideProvider;
}
const finalModelOptions = Object.assign(Object.assign({}, modelOptions), { model: agent.model });
const options = yield getOptions({
req,
endpoint: provider,
model_parameters: finalModelOptions,
db,
});
const llmConfig = options.llmConfig;
const tokensModel = agent.provider === librechatDataProvider.EModelEndpoint.azureOpenAI ? agent.model : llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.model;
const maxOutputTokens = optionalChainWithEmptyCheck(llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.maxOutputTokens, llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.maxTokens, 0);
const agentMaxContextTokens = optionalChainWithEmptyCheck(maxContextTokens, getModelMaxTokens(tokensModel !== null && tokensModel !== void 0 ? tokensModel : '', librechatDataProvider.providerEndpointMap[provider], options.endpointTokenConfig), 18000);
if (agent.endpoint === librechatDataProvider.EModelEndpoint.azureOpenAI &&
(llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.azureOpenAIApiInstanceName) == null) {
agent.provider = agents.Providers.OPENAI;
}
if (options.provider != null) {
agent.provider = options.provider;
}
/** Check for tool presence from either full instances or definitions (event-driven mode) */
const hasAgentTools = ((_h = structuredTools === null || structuredTools === void 0 ? void 0 : structuredTools.length) !== null && _h !== void 0 ? _h : 0) > 0 || ((_j = toolDefinitions === null || toolDefinitions === void 0 ? void 0 : toolDefinitions.length) !== null && _j !== void 0 ? _j : 0) > 0;
let tools = ((_k = options.tools) === null || _k === void 0 ? void 0 : _k.length)
? options.tools
: (structuredTools !== null && structuredTools !== void 0 ? structuredTools : []);
if ((agent.provider === agents.Providers.GOOGLE || agent.provider === agents.Providers.VERTEXAI) &&
((_l = options.tools) === null || _l === void 0 ? void 0 : _l.length) &&
hasAgentTools) {
throw new Error(`{ "type": "${librechatDataProvider.ErrorTypes.GOOGLE_TOOL_CONFLICT}"}`);
}
else if ((agent.provider === agents.Providers.OPENAI ||
agent.provider === agents.Providers.AZURE ||
agent.provider === agents.Providers.ANTHROPIC) &&
((_m = options.tools) === null || _m === void 0 ? void 0 : _m.length) &&
(structuredTools === null || structuredTools === void 0 ? void 0 : structuredTools.length)) {
tools = structuredTools.concat(options.tools);
}
agent.model_parameters = Object.assign({}, options.llmConfig);
if (options.configOptions) {
agent.model_parameters.configuration = options.configOptions;
}
if (agent.instructions && agent.instructions !== '') {
agent.instructions = librechatDataProvider.replaceSpecialVars({
text: agent.instructions,
user: req.user ? req.user : null,
});
}
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
const artifactsPromptResult = generateArtifactsPrompt({
endpoint: agent.provider,
artifacts: agent.artifacts,
});
agent.additional_instructions = artifactsPromptResult !== null && artifactsPromptResult !== void 0 ? artifactsPromptResult : undefined;
}
const agentMaxContextNum = Number(agentMaxContextTokens) || 18000;
const maxOutputTokensNum = Number(maxOutputTokens) || 0;
const finalAttachments = (primedAttachments !== null && primedAttachments !== void 0 ? primedAttachments : [])
.filter((a) => a != null)
.map((a) => a);
const initializedAgent = Object.assign(Object.assign({}, agent), { resendFiles,
toolRegistry,
tool_resources,
userMCPAuthMap,
toolDefinitions,
hasDeferredTools, attachments: finalAttachments, toolContextMap: toolContextMap !== null && toolContextMap !== void 0 ? toolContextMap : {}, useLegacyContent: !!options.useLegacyContent, tools: (tools !== null && tools !== void 0 ? tools : []), maxContextTokens: maxContextTokens != null && maxContextTokens > 0
? maxContextTokens
: Math.round((agentMaxContextNum - maxOutputTokensNum) * 0.9) });
return initializedAgent;
});
}
/**
* Converts OCR tool resource to context tool resource in place.
* This modifies the input object directly (used for updateData in the handler).
*
* @param data - Object containing tool_resources and/or tools to convert
* @returns void - modifies the input object directly
*/
function convertOcrToContextInPlace(data) {
var _a, _b, _c, _d, _e, _f;
// Convert OCR to context in tool_resources
if ((_a = data.tool_resources) === null || _a === void 0 ? void 0 : _a.ocr) {
if (!data.tool_resources.context) {
data.tool_resources.context = data.tool_resources.ocr;
}
else {
// Merge OCR into existing context
if ((_c = (_b = data.tool_resources.ocr) === null || _b === void 0 ? void 0 : _b.file_ids) === null || _c === void 0 ? void 0 : _c.length) {
const existingFileIds = data.tool_resources.context.file_ids || [];
const ocrFileIds = data.tool_resources.ocr.file_ids || [];
data.tool_resources.context.file_ids = [...new Set([...existingFileIds, ...ocrFileIds])];
}
if ((_e = (_d = data.tool_resources.ocr) === null || _d === void 0 ? void 0 : _d.files) === null || _e === void 0 ? void 0 : _e.length) {
const existingFiles = data.tool_resources.context.files || [];
const ocrFiles = data.tool_resources.ocr.files || [];
const filesMap = new Map();
[...existingFiles, ...ocrFiles].forEach((file) => {
if (file === null || file === void 0 ? void 0 : file.file_id) {
filesMap.set(file.file_id, file);
}
});
data.tool_resources.context.files = Array.from(filesMap.values());
}
}
delete data.tool_resources.ocr;
}
// Convert OCR to context in tools array
if ((_f = data.tools) === null || _f === void 0 ? void 0 : _f.includes(librechatDataProvider.EToolResources.ocr)) {
data.tools = data.tools.map((tool) => tool === librechatDataProvider.EToolResources.ocr ? librechatDataProvider.EToolResources.context : tool);
data.tools = [...new Set(data.tools)];
}
}
/**
* Merges tool resources from existing agent with incoming update data,
* converting OCR to context and handling deduplication.
* Used when existing agent has OCR that needs to be converted and merged with updateData.
*
* @param existingAgent - The existing agent data
* @param updateData - The incoming update data
* @returns Object with merged tool_resources and tools
*/
function mergeAgentOcrConversion(existingAgent, updateData) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
if (!((_a = existingAgent.tool_resources) === null || _a === void 0 ? void 0 : _a.ocr)) {
return {};
}
const result = {};
// Convert existing agent's OCR to context
result.tool_resources = Object.assign({}, existingAgent.tool_resources);
if (!result.tool_resources.context) {
// Simple case: no context exists, just move ocr to context
result.tool_resources.context = result.tool_resources.ocr;
}
else {
// Merge case: context already exists, merge both file_ids and files arrays
// Merge file_ids if they exist
if ((_c = (_b = result.tool_resources.ocr) === null || _b === void 0 ? void 0 : _b.file_ids) === null || _c === void 0 ? void 0 : _c.length) {
const existingFileIds = result.tool_resources.context.file_ids || [];
const ocrFileIds = result.tool_resources.ocr.file_ids || [];
result.tool_resources.context.file_ids = [...new Set([...existingFileIds, ...ocrFileIds])];
}
// Merge files array if it exists (already fetched files)
if ((_e = (_d = result.tool_resources.ocr) === null || _d === void 0 ? void 0 : _d.files) === null || _e === void 0 ? void 0 : _e.length) {
const existingFiles = result.tool_resources.context.files || [];
const ocrFiles = ((_f = result.tool_resources.ocr) === null || _f === void 0 ? void 0 : _f.files) || [];
// Merge and deduplicate by file_id
const filesMap = new Map();
[...existingFiles, ...ocrFiles].forEach((file) => {
if (file === null || file === void 0 ? void 0 : file.file_id) {
filesMap.set(file.file_id, file);
}
});
result.tool_resources.context.files = Array.from(filesMap.values());
}
}
// Remove the deprecated ocr resource
delete result.tool_resources.ocr;
// Update tools array: replace 'ocr' with 'context'
if ((_g = existingAgent.tools) === null || _g === void 0 ? void 0 : _g.includes(librechatDataProvider.EToolResources.ocr)) {
result.tools = existingAgent.tools.map((tool) => tool === librechatDataProvider.EToolResources.ocr ? librechatDataProvider.EToolResources.context : tool);
// Remove duplicates if context already existed
result.tools = [...new Set(result.tools)];
}
// Merge with any context that might already be in updateData (from incoming OCR conversion)
if (((_h = updateData.tool_resources) === null || _h === void 0 ? void 0 : _h.context) && result.tool_resources.context) {
// Merge the contexts
const mergedContext = Object.assign({}, result.tool_resources.context);
// Merge file_ids
if ((_j = updateData.tool_resources.context.file_ids) === null || _j === void 0 ? void 0 : _j.length) {
const existingIds = mergedContext.file_ids || [];
const newIds = updateData.tool_resources.context.file_ids || [];
mergedContext.file_ids = [...new Set([...existingIds, ...newIds])];
}
// Merge files
if ((_k = updateData.tool_resources.context.files) === null || _k === void 0 ? void 0 : _k.length) {
const existingFiles = mergedContext.files || [];
const newFiles = updateData.tool_resources.context.files || [];
const filesMap = new Map();
[...existingFiles, ...newFiles].forEach((file) => {
if (file === null || file === void 0 ? void 0 : file.file_id) {
filesMap.set(file.file_id, file);
}
});
mergedContext.files = Array.from(filesMap.values());
}
result.tool_resources.context = mergedContext;
}
return result;
}
/**
* In-memory event transport using Node.js EventEmitter.
* For horizontal scaling, replace with RedisEventTransport.
*/
class InMemoryEventTransport {
constructor() {
this.streams = new Map();
}
getOrCreateStream(streamId) {
let state = this.streams.get(streamId);
if (!state) {
const emitter = new events.EventEmitter();
emitter.setMaxListeners(100);
state = { emitter };
this.streams.set(streamId, state);
}
return state;
}
subscribe(streamId, handlers) {
const state = this.getOrCreateStream(streamId);
const chunkHandler = (event) => handlers.onChunk(event);
const doneHandler = (event) => { var _a; return (_a = handlers.onDone) === null || _a === void 0 ? void 0 : _a.call(handlers, event); };
const errorHandler = (error) => { var _a; return (_a = handlers.onError) === null || _a === void 0 ? void 0 : _a.call(handlers, error); };
state.emitter.on('chunk', chunkHandler);
state.emitter.on('done', doneHandler);
state.emitter.on('error', errorHandler);
dataSchemas.logger.debug(`[InMemoryEventTransport] subscribe ${streamId}: listeners=${state.emitter.listenerCount('chunk')}`);
return {
unsubscribe: () => {
var _a;
const currentState = this.streams.get(streamId);
if (currentState) {
currentState.emitter.off('chunk', chunkHandler);
currentState.emitter.off('done', doneHandler);
currentState.emitter.off('error', errorHandler);
// Check if all subscribers left - cleanup and notify
if (currentState.emitter.listenerCount('chunk') === 0) {
(_a = currentState.allSubscribersLeftCallback) === null || _a === void 0 ? void 0 : _a.call(currentState);
/* Remove all EventEmitter listeners but preserve stream state
* (including allSubscribersLeftCallback) for reconnection.
* State is fully cleaned up by cleanup() when the job completes.
*/
currentState.emitter.removeAllListeners();
}
}
},
};
}
emitChunk(streamId, event) {
const state = this.streams.get(streamId);
state === null || state === void 0 ? void 0 : state.emitter.emit('chunk', event);
}
emitDone(streamId, event) {
const state = this.streams.get(streamId);
state === null || state === void 0 ? void 0 : state.emitter.emit('done', event);
}
emitError(streamId, error) {
var _a;
const state = this.streams.get(streamId);
// Only emit if there are listeners - Node.js throws on unhandled 'error' events
// This is intentional for the race condition where error occurs before client connects
if ((_a = state === null || state === void 0 ? void 0 : state.emitter.listenerCount('error')) !== null && _a !== void 0 ? _a : 0 > 0) {
state === null || state === void 0 ? void 0 : state.emitter.emit('error', error);
}
}
getSubscriberCount(streamId) {
var _a;
const state = this.streams.get(streamId);
return (_a = state === null || state === void 0 ? void 0 : state.emitter.listenerCount('chunk')) !== null && _a !== void 0 ? _a : 0;
}
onAllSubscribersLeft(streamId, callback) {
const state = this.getOrCreateStream(streamId);
state.allSubscribersLeftCallback = callback;
}
/**
* Check if this is the first subscriber (for ready signaling)
*/
isFirstSubscriber(streamId) {
var _a;
const state = this.streams.get(streamId);
const count = (_a = state === null || state === void 0 ? void 0 : state.emitter.listenerCount('chunk')) !== null && _a !== void 0 ? _a : 0;
dataSchemas.logger.debug(`[InMemoryEventTransport] isFirstSubscriber ${streamId}: count=${count}`);
return count === 1;
}
/**
* Cleanup a stream's event emitter
*/
cleanup(streamId) {
const state = this.streams.get(streamId);
if (state) {
state.emitter.removeAllListeners();
this.streams.delete(streamId);
}
}
/**
* Get count of tracked streams (for monitoring)
*/
getStreamCount() {
return this.streams.size;
}
/**
* Get all tracked stream IDs (for orphan cleanup)
*/
getTrackedStreamIds() {
return Array.from(this.streams.keys());
}
destroy() {
for (const state of this.streams.values()) {
state.emitter.removeAllListeners();
}
this.streams.clear();
dataSchemas.logger.debug('[InMemoryEventTransport] Destroyed');
}
}
/**
* In-memory implementation of IJobStore.
* Suitable for single-instance deployments.
* For horizontal scaling, use RedisJobStore.
*
* Content state is tied to jobs:
* - Uses WeakRef to graph for live access to contentParts and contentData (run steps)
* - No chunk persistence needed - same instance handles generation and reconnects
*/
class InMemoryJobStore {
constructor(options) {
this.jobs = new Map();
this.contentState = new Map();
this.cleanupInterval = null;
/** Maps userId -> Set of streamIds (conversationIds) for active jobs */
this.userJobMap = new Map();
/** Time to keep completed jobs before cleanup (0 = immediate) */
this.ttlAfterComplete = 0;
/** Maximum number of concurrent jobs */
this.maxJobs = 1000;
if (options === null || options === void 0 ? void 0 : options.ttlAfterComplete) {
this.ttlAfterComplete = options.ttlAfterComplete;
}
if (options === null || options === void 0 ? void 0 : options.maxJobs) {
this.maxJobs = options.maxJobs;
}
}
initialize() {
return __awaiter(this, void 0, void 0, function* () {
if (this.cleanupInterval) {
return;
}
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 60000);
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
dataSchemas.logger.debug('[InMemoryJobStore] Initialized with cleanup interval');
});
}
createJob(streamId, userId, conversationId) {
return __awaiter(this, void 0, void 0, function* () {
if (this.jobs.size >= this.maxJobs) {
yield this.evictOldest();
}
const job = {
streamId,
userId,
status: 'running',
createdAt: Date.now(),
conversationId,
syncSent: false,
};
this.jobs.set(streamId, job);
// Track job by userId for efficient user-scoped queries
let userJobs = this.userJobMap.get(userId);
if (!userJobs) {
userJobs = new Set();
this.userJobMap.set(userId, userJobs);
}
userJobs.add(streamId);
dataSchemas.logger.debug(`[InMemoryJobStore] Created job: ${streamId}`);
return job;
});
}
getJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
return (_a = this.jobs.get(streamId)) !== null && _a !== void 0 ? _a : null;
});
}
updateJob(streamId, updates) {
return __awaiter(this, void 0, void 0, function* () {
const job = this.jobs.get(streamId);
if (!job) {
return;
}
Object.assign(job, updates);
});
}
deleteJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
this.jobs.delete(streamId);
this.contentState.delete(streamId);
dataSchemas.logger.debug(`[InMemoryJobStore] Deleted job: ${streamId}`);
});
}
hasJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
return this.jobs.has(streamId);
});
}
getRunningJobs() {
return __awaiter(this, void 0, void 0, function* () {
const running = [];
for (const job of this.jobs.values()) {
if (job.status === 'running') {
running.push(job);
}
}
return running;
});
}
cleanup() {
return __awaiter(this, void 0, void 0, function* () {
const now = Date.now();
const toDelete = [];
for (const [streamId, job] of this.jobs) {
const isFinished = ['complete', 'error', 'aborted'].includes(job.status);
if (isFinished && job.completedAt) {
// TTL of 0 means immediate cleanup, otherwise wait for TTL to expire
if (this.ttlAfterComplete === 0 || now - job.completedAt > this.ttlAfterComplete) {
toDelete.push(streamId);
}
}
}
for (const id of toDelete) {
yield this.deleteJob(id);
}
if (toDelete.length > 0) {
dataSchemas.logger.debug(`[InMemoryJobStore] Cleaned up ${toDelete.length} expired jobs`);
}
return toDelete.length;
});
}
evictOldest() {
return __awaiter(this, void 0, void 0, function* () {
let oldestId = null;
let oldestTime = Infinity;
for (const [streamId, job] of this.jobs) {
if (job.createdAt < oldestTime) {
oldestTime = job.createdAt;
oldestId = streamId;
}
}
if (oldestId) {
dataSchemas.logger.warn(`[InMemoryJobStore] Evicting oldest job: ${oldestId}`);
yield this.deleteJob(oldestId);
}
});
}
/** Get job count (for monitoring) */
getJobCount() {
return __awaiter(this, void 0, void 0, function* () {
return this.jobs.size;
});
}
/** Get job count by status (for monitoring) */
getJobCountByStatus(status) {
return __awaiter(this, void 0, void 0, function* () {
let count = 0;
for (const job of this.jobs.values()) {
if (job.status === status) {
count++;
}
}
return count;
});
}
destroy() {
return __awaiter(this, void 0, void 0, function* () {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.jobs.clear();
this.contentState.clear();
this.userJobMap.clear();
dataSchemas.logger.debug('[InMemoryJobStore] Destroyed');
});
}
/**
* Get active job IDs for a user.
* Returns conversation IDs of running jobs belonging to the user.
* Also performs self-healing cleanup: removes stale entries for jobs that no longer exist.
*/
getActiveJobIdsByUser(userId) {
return __awaiter(this, void 0, void 0, function* () {
const trackedIds = this.userJobMap.get(userId);
if (!trackedIds || trackedIds.size === 0) {
return [];
}
const activeIds = [];
for (const streamId of trackedIds) {
const job = this.jobs.get(streamId);
// Only include if job exists AND is still running
if (job && job.status === 'running') {
activeIds.push(streamId);
}
else {
// Self-healing: job completed/deleted but mapping wasn't cleaned - fix it now
trackedIds.delete(streamId);
}
}
// Clean up empty set
if (trackedIds.size === 0) {
this.userJobMap.delete(userId);
}
return activeIds;
});
}
// ===== Content State Methods =====
/**
* Set the graph reference for a job.
* Uses WeakRef to allow garbage collection when graph is no longer needed.
*/
setGraph(streamId, graph) {
const existing = this.contentState.get(streamId);
if (existing) {
existing.graphRef = new WeakRef(graph);
}
else {
this.contentState.set(streamId, {
contentParts: [],
graphRef: new WeakRef(graph),
collectedUsage: [],
});
}
}
/**
* Set content parts reference for a job.
*/
setContentParts(streamId, contentParts) {
const existing = this.contentState.get(streamId);
if (existing) {
existing.contentParts = contentParts;
}
else {
this.contentState.set(streamId, { contentParts, graphRef: null, collectedUsage: [] });
}
}
/**
* Set collected usage reference for a job.
*/
setCollectedUsage(streamId, collectedUsage) {
const existing = this.contentState.get(streamId);
if (existing) {
existing.collectedUsage = collectedUsage;
}
else {
this.contentState.set(streamId, { contentParts: [], graphRef: null, collectedUsage });
}
}
/**
* Get collected usage for a job.
*/
getCollectedUsage(streamId) {
var _a;
const state = this.contentState.get(streamId);
return (_a = state === null || state === void 0 ? void 0 : state.collectedUsage) !== null && _a !== void 0 ? _a : [];
}
/**
* Get content parts for a job.
* Returns live content from stored reference.
*/
getContentParts(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const state = this.contentState.get(streamId);
if (!(state === null || state === void 0 ? void 0 : state.contentParts)) {
return null;
}
return {
content: state.contentParts,
};
});
}
/**
* Get run steps for a job from graph.contentData.
* Uses WeakRef - may return empty if graph has been GC'd.
*/
getRunSteps(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const state = this.contentState.get(streamId);
if (!(state === null || state === void 0 ? void 0 : state.graphRef)) {
return [];
}
// Dereference WeakRef - may return undefined if GC'd
const graph = state.graphRef.deref();
return (_a = graph === null || graph === void 0 ? void 0 : graph.contentData) !== null && _a !== void 0 ? _a : [];
});
}
/**
* No-op for in-memory - content available via graph reference.
*/
appendChunk() {
return __awaiter(this, void 0, void 0, function* () {
// No-op: content available via graph reference
});
}
/**
* Clear content state for a job.
*/
clearContentState(streamId) {
this.contentState.delete(streamId);
}
}
/**
* Manages generation jobs for resumable LLM streams.
*
* Architecture: Composes two pluggable services via dependency injection:
* - jobStore: Job metadata + content state (InMemory → Redis for horizontal scaling)
* - eventTransport: Pub/sub events (InMemory → Redis Pub/Sub for horizontal scaling)
*
* Content state is tied to jobs:
* - In-memory: jobStore holds WeakRef to graph for live content/run steps access
* - Redis: jobStore persists chunks, reconstructs content on demand
*
* All storage methods are async to support both in-memory and external stores (Redis, etc.).
*
* @example Redis injection:
* ```ts
* const manager = new GenerationJobManagerClass({
* jobStore: new RedisJobStore(redisClient),
* eventTransport: new RedisPubSubTransport(redisClient),
* });
* ```
*/
class GenerationJobManagerClass {
constructor(options) {
var _a, _b, _c;
/** Runtime state - always in-memory, not serializable */
this.runtimeState = new Map();
this.cleanupInterval = null;
/** Whether we're using Redis stores */
this._isRedis = false;
/** Whether to cleanup event transport immediately on job completion */
this._cleanupOnComplete = true;
/**
* Accumulate run steps for a stream (Redis mode only).
* Uses a simple in-memory buffer that gets flushed to Redis.
* Not used in in-memory mode - run steps come from live graph via WeakRef.
*/
this.runStepBuffers = null;
this.jobStore =
(_a = options === null || options === void 0 ? void 0 : options.jobStore) !== null && _a !== void 0 ? _a : new InMemoryJobStore({ ttlAfterComplete: 0, maxJobs: 1000 });
this.eventTransport = (_b = options === null || options === void 0 ? void 0 : options.eventTransport) !== null && _b !== void 0 ? _b : new InMemoryEventTransport();
this._cleanupOnComplete = (_c = options === null || options === void 0 ? void 0 : options.cleanupOnComplete) !== null && _c !== void 0 ? _c : true;
}
/**
* Initialize the job manager with periodic cleanup.
* Call this once at application startup.
*/
initialize() {
if (this.cleanupInterval) {
return;
}
this.jobStore.initialize();
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 60000);
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
dataSchemas.logger.debug('[GenerationJobManager] Initialized');
}
/**
* Configure the manager with custom stores.
* Call this BEFORE initialize() to use Redis or other stores.
*
* @example Using Redis
* ```ts
* import { createStreamServicesFromCache } from '~/stream/createStreamServices';
* import { cacheConfig, ioredisClient } from '~/cache';
*
* const services = createStreamServicesFromCache({ cacheConfig, ioredisClient });
* GenerationJobManager.configure(services);
* GenerationJobManager.initialize();
* ```
*/
configure(services) {
var _a, _b;
if (this.cleanupInterval) {
dataSchemas.logger.warn('[GenerationJobManager] Reconfiguring after initialization - destroying existing services');
this.destroy();
}
this.jobStore = services.jobStore;
this.eventTransport = services.eventTransport;
this._isRedis = (_a = services.isRedis) !== null && _a !== void 0 ? _a : false;
this._cleanupOnComplete = (_b = services.cleanupOnComplete) !== null && _b !== void 0 ? _b : true;
dataSchemas.logger.info(`[GenerationJobManager] Configured with ${this._isRedis ? 'Redis' : 'in-memory'} stores`);
}
/**
* Check if using Redis stores.
*/
get isRedis() {
return this._isRedis;
}
/**
* Get the job store instance (for advanced use cases).
*/
getJobStore() {
return this.jobStore;
}
/**
* Create a new generation job.
*
* This sets up:
* 1. Serializable job data in the job store
* 2. Runtime state including readyPromise (resolves when first SSE client connects)
* 3. allSubscribersLeft callback for handling client disconnections
*
* The readyPromise mechanism ensures generation doesn't start before the client
* is ready to receive events. The controller awaits this promise (with a short timeout)
* before starting LLM generation.
*
* @param streamId - Unique identifier for this stream
* @param userId - User who initiated the request
* @param conversationId - Optional conversation ID for lookup
* @returns A facade object for the GenerationJob
*/
createJob(streamId, userId, conversationId) {
return __awaiter(this, void 0, void 0, function* () {
const jobData = yield this.jobStore.createJob(streamId, userId, conversationId);
/**
* Create runtime state with readyPromise.
*
* With the resumable stream architecture, we no longer need to wait for the
* first subscriber before starting generation:
* - Redis mode: Events are persisted and can be replayed via sync
* - In-memory mode: Content is aggregated and sent via sync on connect
*
* We resolve readyPromise immediately to eliminate startup latency.
* The sync mechanism handles late-connecting clients.
*/
let resolveReady;
const readyPromise = new Promise((resolve) => {
resolveReady = resolve;
});
const runtime = {
abortController: new AbortController(),
readyPromise,
resolveReady: resolveReady,
syncSent: false,
earlyEventBuffer: [],
hasSubscriber: false,
};
this.runtimeState.set(streamId, runtime);
// Resolve immediately - early event buffer handles late subscribers
resolveReady();
/**
* Set up all-subscribers-left callback.
* When all SSE clients disconnect, this:
* 1. Resets syncSent so reconnecting clients get sync event (persisted to Redis)
* 2. Calls any registered allSubscribersLeft handlers (e.g., to save partial responses)
*/
this.eventTransport.onAllSubscribersLeft(streamId, () => {
const currentRuntime = this.runtimeState.get(streamId);
if (currentRuntime) {
currentRuntime.syncSent = false;
currentRuntime.hasSubscriber = false;
// Persist syncSent=false to Redis for cross-replica consistency
this.jobStore.updateJob(streamId, { syncSent: false }).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to persist syncSent=false:`, err);
});
// Call registered handlers (from job.emitter.on('allSubscribersLeft', ...))
if (currentRuntime.allSubscribersLeftHandlers) {
this.jobStore
.getContentParts(streamId)
.then((result) => {
var _a, _b;
const parts = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
for (const handler of (_b = currentRuntime.allSubscribersLeftHandlers) !== null && _b !== void 0 ? _b : []) {
try {
handler(parts);
}
catch (err) {
dataSchemas.logger.error(`[GenerationJobManager] Error in allSubscribersLeft handler:`, err);
}
}
})
.catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to get content parts for allSubscribersLeft handlers:`, err);
});
}
}
});
/**
* Set up cross-replica abort listener (Redis mode only).
* When abort is triggered on ANY replica, this replica receives the signal
* and aborts its local AbortController (if it's the one running generation).
*/
if (this.eventTransport.onAbort) {
this.eventTransport.onAbort(streamId, () => {
const currentRuntime = this.runtimeState.get(streamId);
if (currentRuntime && !currentRuntime.abortController.signal.aborted) {
dataSchemas.logger.debug(`[GenerationJobManager] Received cross-replica abort for ${streamId}`);
currentRuntime.abortController.abort();
}
});
}
dataSchemas.logger.debug(`[GenerationJobManager] Created job: ${streamId}`);
// Return facade for backwards compatibility
return this.buildJobFacade(streamId, jobData, runtime);
});
}
/**
* Build a GenerationJob facade from composed services.
*
* This facade provides a unified API (job.emitter, job.abortController, etc.)
* while internally delegating to the injected services (jobStore, eventTransport,
* contentState). This allows swapping implementations (e.g., Redis) without
* changing consumer code.
*
* IMPORTANT: The emitterProxy.on('allSubscribersLeft') handler registration
* does NOT use eventTransport.subscribe(). This is intentional:
*
* If we used subscribe() for internal handlers, those handlers would count
* as subscribers. When the real SSE client connects, isFirstSubscriber()
* would return false (because internal handler was "first"), and readyPromise
* would never resolve - causing a 5-second timeout delay before generation starts.
*
* Instead, allSubscribersLeft handlers are stored in runtime.allSubscribersLeftHandlers
* and called directly from the onAllSubscribersLeft callback in createJob().
*
* @param streamId - The stream identifier
* @param jobData - Serializable job metadata from job store
* @param runtime - Non-serializable runtime state (abort controller, promises, etc.)
* @returns A GenerationJob facade object
*/
buildJobFacade(streamId, jobData, runtime) {
/**
* Proxy emitter that delegates to eventTransport for most operations.
* Exception: allSubscribersLeft handlers are stored separately to avoid
* incrementing subscriber count (see class JSDoc above).
*/
const emitterProxy = {
on: (event, handler) => {
if (event === 'allSubscribersLeft') {
// Store handler for internal callback - don't use subscribe() to avoid counting as a subscriber
if (!runtime.allSubscribersLeftHandlers) {
runtime.allSubscribersLeftHandlers = [];
}
runtime.allSubscribersLeftHandlers.push(handler);
}
},
emit: () => {
/* handled via eventTransport */
},
listenerCount: () => this.eventTransport.getSubscriberCount(streamId),
setMaxListeners: () => {
/* no-op for proxy */
},
removeAllListeners: () => this.eventTransport.cleanup(streamId),
off: () => {
/* handled via unsubscribe */
},
};
return {
streamId,
emitter: emitterProxy,
status: jobData.status,
createdAt: jobData.createdAt,
completedAt: jobData.completedAt,
abortController: runtime.abortController,
error: jobData.error,
metadata: {
userId: jobData.userId,
conversationId: jobData.conversationId,
userMessage: jobData.userMessage,
responseMessageId: jobData.responseMessageId,
sender: jobData.sender,
},
readyPromise: runtime.readyPromise,
resolveReady: runtime.resolveReady,
finalEvent: runtime.finalEvent,
syncSent: runtime.syncSent,
};
}
/**
* Get or create runtime state for a job.
*
* This enables cross-replica support in Redis mode:
* - If runtime exists locally (same replica), return it
* - If job exists in Redis but not locally (cross-replica), create minimal runtime
*
* The lazily-created runtime state is sufficient for:
* - Subscribing to events (via Redis pub/sub)
* - Getting resume state
* - Handling reconnections
* - Receiving cross-replica abort signals (via Redis pub/sub)
*
* @param streamId - The stream identifier
* @returns Runtime state or null if job doesn't exist anywhere
*/
getOrCreateRuntimeState(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const existingRuntime = this.runtimeState.get(streamId);
if (existingRuntime) {
return existingRuntime;
}
// Job doesn't exist locally - check Redis
const jobData = yield this.jobStore.getJob(streamId);
if (!jobData) {
return null;
}
// Cross-replica scenario: job exists in Redis but not locally
// Create minimal runtime state for handling reconnection/subscription
dataSchemas.logger.debug(`[GenerationJobManager] Creating cross-replica runtime for ${streamId}`);
let resolveReady;
const readyPromise = new Promise((resolve) => {
resolveReady = resolve;
});
// For jobs created on other replicas, readyPromise should be pre-resolved
// since generation has already started
resolveReady();
// Parse finalEvent from Redis if available
let finalEvent;
if (jobData.finalEvent) {
try {
finalEvent = JSON.parse(jobData.finalEvent);
}
catch (_b) {
// Ignore parse errors
}
}
const runtime = {
abortController: new AbortController(),
readyPromise,
resolveReady: resolveReady,
syncSent: (_a = jobData.syncSent) !== null && _a !== void 0 ? _a : false,
earlyEventBuffer: [],
hasSubscriber: false,
finalEvent,
errorEvent: jobData.error,
};
this.runtimeState.set(streamId, runtime);
// Set up all-subscribers-left callback for this replica
this.eventTransport.onAllSubscribersLeft(streamId, () => {
const currentRuntime = this.runtimeState.get(streamId);
if (currentRuntime) {
currentRuntime.syncSent = false;
currentRuntime.hasSubscriber = false;
// Persist syncSent=false to Redis
this.jobStore.updateJob(streamId, { syncSent: false }).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to persist syncSent=false:`, err);
});
// Call registered handlers
if (currentRuntime.allSubscribersLeftHandlers) {
this.jobStore
.getContentParts(streamId)
.then((result) => {
var _a, _b;
const parts = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
for (const handler of (_b = currentRuntime.allSubscribersLeftHandlers) !== null && _b !== void 0 ? _b : []) {
try {
handler(parts);
}
catch (err) {
dataSchemas.logger.error(`[GenerationJobManager] Error in allSubscribersLeft handler:`, err);
}
}
})
.catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to get content parts for allSubscribersLeft handlers:`, err);
});
}
}
});
// Set up cross-replica abort listener (Redis mode only)
// This ensures lazily-initialized jobs can receive abort signals
if (this.eventTransport.onAbort) {
this.eventTransport.onAbort(streamId, () => {
const currentRuntime = this.runtimeState.get(streamId);
if (currentRuntime && !currentRuntime.abortController.signal.aborted) {
dataSchemas.logger.debug(`[GenerationJobManager] Received cross-replica abort for lazily-init job ${streamId}`);
currentRuntime.abortController.abort();
}
});
}
return runtime;
});
}
/**
* Get a job by streamId.
*/
getJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const jobData = yield this.jobStore.getJob(streamId);
if (!jobData) {
return undefined;
}
const runtime = yield this.getOrCreateRuntimeState(streamId);
if (!runtime) {
return undefined;
}
return this.buildJobFacade(streamId, jobData, runtime);
});
}
/**
* Check if a job exists.
*/
hasJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
return this.jobStore.hasJob(streamId);
});
}
/**
* Get job status.
*/
getJobStatus(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const jobData = yield this.jobStore.getJob(streamId);
return jobData === null || jobData === void 0 ? void 0 : jobData.status;
});
}
/**
* Mark job as complete.
* If cleanupOnComplete is true (default), immediately cleans up job resources.
* Exception: Jobs with errors are NOT immediately deleted to allow late-connecting
* clients to receive the error (race condition where error occurs before client connects).
* Note: eventTransport is NOT cleaned up here to allow the final event to be
* fully transmitted. It will be cleaned up when subscribers disconnect or
* by the periodic cleanup job.
*/
completeJob(streamId, error) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const runtime = this.runtimeState.get(streamId);
// Abort the controller to signal all pending operations (e.g., OAuth flow monitors)
// that the job is done and they should clean up
if (runtime) {
runtime.abortController.abort();
}
// Clear content state and run step buffer (Redis only)
this.jobStore.clearContentState(streamId);
(_a = this.runStepBuffers) === null || _a === void 0 ? void 0 : _a.delete(streamId);
// For error jobs, DON'T delete immediately - keep around so late-connecting
// clients can receive the error. This handles the race condition where error
// occurs before client connects to SSE stream.
//
// Cleanup strategy: Error jobs are cleaned up by periodic cleanup (every 60s)
// via jobStore.cleanup() which checks for jobs with status 'error' and
// completedAt set. The TTL is configurable via jobStore options (default: 0,
// meaning cleanup on next interval). This gives clients ~60s to connect and
// receive the error before the job is removed.
if (error) {
yield this.jobStore.updateJob(streamId, {
status: 'error',
completedAt: Date.now(),
error,
});
// Keep runtime state so subscribe() can access errorEvent
dataSchemas.logger.debug(`[GenerationJobManager] Job completed with error (keeping for late subscribers): ${streamId}`);
return;
}
// Immediate cleanup if configured (default: true) - only for successful completions
if (this._cleanupOnComplete) {
this.runtimeState.delete(streamId);
// Don't cleanup eventTransport here - let the done event fully transmit first.
// EventTransport will be cleaned up when subscribers disconnect or by periodic cleanup.
yield this.jobStore.deleteJob(streamId);
}
else {
// Only update status if keeping the job around
yield this.jobStore.updateJob(streamId, {
status: 'complete',
completedAt: Date.now(),
});
}
dataSchemas.logger.debug(`[GenerationJobManager] Job completed: ${streamId}`);
});
}
/**
* Abort a job (user-initiated).
* Returns all data needed for token spending and message saving.
*
* Cross-replica support (Redis mode):
* - Emits abort signal via Redis pub/sub
* - The replica running generation receives signal and aborts its AbortController
*/
abortJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
const jobData = yield this.jobStore.getJob(streamId);
const runtime = this.runtimeState.get(streamId);
if (!jobData) {
dataSchemas.logger.warn(`[GenerationJobManager] Cannot abort - job not found: ${streamId}`);
return {
text: '',
content: [],
jobData: null,
success: false,
finalEvent: null,
collectedUsage: [],
};
}
// Emit abort signal for cross-replica support (Redis mode)
// This ensures the generating replica receives the abort signal
if (this.eventTransport.emitAbort) {
this.eventTransport.emitAbort(streamId);
}
// Also abort local controller if we have it (same-replica abort)
if (runtime) {
runtime.abortController.abort();
}
/** Content before clearing state */
const result = yield this.jobStore.getContentParts(streamId);
const content = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
/** Collected usage for all models */
const collectedUsage = this.jobStore.getCollectedUsage(streamId);
/** Text from content parts for fallback token counting */
const text = librechatDataProvider.parseTextParts(content);
/** Detect "early abort" - aborted before any generation happened (e.g., during tool loading)
In this case, no messages were saved to DB, so frontend shouldn't navigate to conversation */
const isEarlyAbort = content.length === 0 && !jobData.responseMessageId;
/** Final event for abort */
const userMessageId = (_b = jobData.userMessage) === null || _b === void 0 ? void 0 : _b.messageId;
const abortFinalEvent = {
final: true,
// Don't include conversation for early aborts - it doesn't exist in DB
conversation: isEarlyAbort ? null : { conversationId: jobData.conversationId },
title: 'New Chat',
requestMessage: jobData.userMessage
? {
messageId: userMessageId,
parentMessageId: jobData.userMessage.parentMessageId,
conversationId: jobData.conversationId,
text: (_c = jobData.userMessage.text) !== null && _c !== void 0 ? _c : '',
isCreatedByUser: true,
}
: null,
responseMessage: isEarlyAbort
? null
: {
messageId: (_d = jobData.responseMessageId) !== null && _d !== void 0 ? _d : `${userMessageId !== null && userMessageId !== void 0 ? userMessageId : 'aborted'}_`,
parentMessageId: userMessageId,
conversationId: jobData.conversationId,
content,
sender: (_e = jobData.sender) !== null && _e !== void 0 ? _e : 'AI',
unfinished: true,
error: false,
isCreatedByUser: false,
},
aborted: true,
// Flag for early abort - no messages saved, frontend should go to new chat
earlyAbort: isEarlyAbort,
};
if (runtime) {
runtime.finalEvent = abortFinalEvent;
}
yield this.eventTransport.emitDone(streamId, abortFinalEvent);
this.jobStore.clearContentState(streamId);
(_f = this.runStepBuffers) === null || _f === void 0 ? void 0 : _f.delete(streamId);
// Immediate cleanup if configured (default: true)
if (this._cleanupOnComplete) {
this.runtimeState.delete(streamId);
// Don't cleanup eventTransport here - let the abort event fully transmit first.
yield this.jobStore.deleteJob(streamId);
}
else {
// Only update status if keeping the job around
yield this.jobStore.updateJob(streamId, {
status: 'aborted',
completedAt: Date.now(),
});
}
dataSchemas.logger.debug(`[GenerationJobManager] Job aborted: ${streamId}`);
return {
success: true,
jobData,
content,
finalEvent: abortFinalEvent,
text,
collectedUsage,
};
});
}
/**
* Subscribe to a job's event stream.
*
* This is called when an SSE client connects to /chat/stream/:streamId.
* On first subscription:
* - Resolves readyPromise (legacy, for API compatibility)
* - Replays any buffered early events (e.g., 'created' event)
*
* Supports cross-replica reconnection in Redis mode:
* - If job exists in Redis but not locally, creates minimal runtime state
* - Events are delivered via Redis pub/sub, not in-memory EventEmitter
*
* @param streamId - The stream to subscribe to
* @param onChunk - Handler for chunk events (streamed tokens, run steps, etc.)
* @param onDone - Handler for completion event (includes final message)
* @param onError - Handler for error events
* @returns Subscription object with unsubscribe function, or null if job not found
*/
subscribe(streamId, onChunk, onDone, onError) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
// Use lazy initialization to support cross-replica subscriptions
const runtime = yield this.getOrCreateRuntimeState(streamId);
if (!runtime) {
return null;
}
const jobData = yield this.jobStore.getJob(streamId);
// If job already complete/error, send final event or error
// Error status takes precedence to ensure errors aren't misreported as successes
setImmediate(() => {
var _a;
if (jobData && ['complete', 'error', 'aborted'].includes(jobData.status)) {
// Check for error status FIRST and prioritize error handling
if (jobData.status === 'error' && (runtime.errorEvent || jobData.error)) {
const errorToSend = (_a = runtime.errorEvent) !== null && _a !== void 0 ? _a : jobData.error;
if (errorToSend) {
dataSchemas.logger.debug(`[GenerationJobManager] Sending stored error to late subscriber: ${streamId}`);
onError === null || onError === void 0 ? void 0 : onError(errorToSend);
}
}
else if (runtime.finalEvent) {
onDone === null || onDone === void 0 ? void 0 : onDone(runtime.finalEvent);
}
}
});
const subscription = this.eventTransport.subscribe(streamId, {
onChunk: (event) => {
const e = event;
if (!e._internal) {
onChunk(e);
}
},
onDone: (event) => onDone === null || onDone === void 0 ? void 0 : onDone(event),
onError,
});
if (subscription.ready) {
yield subscription.ready;
}
const isFirst = this.eventTransport.isFirstSubscriber(streamId);
if (!runtime.hasSubscriber) {
runtime.hasSubscriber = true;
if (runtime.earlyEventBuffer.length > 0) {
dataSchemas.logger.debug(`[GenerationJobManager] Replaying ${runtime.earlyEventBuffer.length} buffered events for ${streamId}`);
for (const bufferedEvent of runtime.earlyEventBuffer) {
onChunk(bufferedEvent);
}
runtime.earlyEventBuffer = [];
}
(_b = (_a = this.eventTransport).syncReorderBuffer) === null || _b === void 0 ? void 0 : _b.call(_a, streamId);
}
if (isFirst) {
runtime.resolveReady();
dataSchemas.logger.debug(`[GenerationJobManager] First subscriber ready, resolving promise for ${streamId}`);
}
return subscription;
});
}
/**
* Emit a chunk event to all subscribers.
* Uses runtime state check for performance (avoids async job store lookup per token).
*
* If no subscriber has connected yet, buffers the event for replay when they do.
* This ensures early events (like 'created') aren't lost due to race conditions.
*
* In Redis mode, awaits the publish to guarantee event ordering.
* This is critical for streaming deltas (tool args, message content) to arrive in order.
*/
emitChunk(streamId, event) {
return __awaiter(this, void 0, void 0, function* () {
const runtime = this.runtimeState.get(streamId);
if (!runtime || runtime.abortController.signal.aborted) {
return;
}
// Track user message from created event
this.trackUserMessage(streamId, event);
// For Redis mode, persist chunk for later reconstruction (fire-and-forget for resumability)
if (this._isRedis) {
// The SSE event structure is { event: string, data: unknown, ... }
// The aggregator expects { event: string, data: unknown } where data is the payload
const eventObj = event;
const eventType = eventObj.event;
const eventData = eventObj.data;
if (eventType && eventData !== undefined) {
// Store in format expected by aggregateContent: { event, data }
this.jobStore.appendChunk(streamId, { event: eventType, data: eventData }).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to append chunk:`, err);
});
// For run step events, also save to run steps key for quick retrieval
if (eventType === 'on_run_step' || eventType === 'on_run_step_completed') {
this.saveRunStepFromEvent(streamId, eventData);
}
}
}
if (!runtime.hasSubscriber) {
runtime.earlyEventBuffer.push(event);
if (!this._isRedis) {
return;
}
}
yield this.eventTransport.emitChunk(streamId, event);
});
}
/**
* Extract and save run step from event data.
* The data is already the run step object from the event payload.
*/
saveRunStepFromEvent(streamId, data) {
// The data IS the run step object
const runStep = data;
if (!runStep.id) {
return;
}
// Fire and forget - accumulate run steps
this.accumulateRunStep(streamId, runStep);
}
accumulateRunStep(streamId, runStep) {
// Lazy initialization - only create map when first used (Redis mode)
if (!this.runStepBuffers) {
this.runStepBuffers = new Map();
}
let buffer = this.runStepBuffers.get(streamId);
if (!buffer) {
buffer = [];
this.runStepBuffers.set(streamId, buffer);
}
// Update or add run step
const existingIdx = buffer.findIndex((rs) => rs.id === runStep.id);
if (existingIdx >= 0) {
buffer[existingIdx] = runStep;
}
else {
buffer.push(runStep);
}
// Save to Redis
if (this.jobStore.saveRunSteps) {
this.jobStore.saveRunSteps(streamId, buffer).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to save run steps:`, err);
});
}
}
/**
* Track user message from created event.
*/
trackUserMessage(streamId, event) {
const data = event;
if (!data.created || !data.message) {
return;
}
const message = data.message;
const updates = {
userMessage: {
messageId: message.messageId,
parentMessageId: message.parentMessageId,
conversationId: message.conversationId,
text: message.text,
},
};
if (message.conversationId) {
updates.conversationId = message.conversationId;
}
this.jobStore.updateJob(streamId, updates);
}
/**
* Update job metadata.
*/
updateMetadata(streamId, metadata) {
return __awaiter(this, void 0, void 0, function* () {
const updates = {};
if (metadata.responseMessageId) {
updates.responseMessageId = metadata.responseMessageId;
}
if (metadata.sender) {
updates.sender = metadata.sender;
}
if (metadata.conversationId) {
updates.conversationId = metadata.conversationId;
}
if (metadata.userMessage) {
updates.userMessage = metadata.userMessage;
}
if (metadata.endpoint) {
updates.endpoint = metadata.endpoint;
}
if (metadata.iconURL) {
updates.iconURL = metadata.iconURL;
}
if (metadata.model) {
updates.model = metadata.model;
}
if (metadata.promptTokens !== undefined) {
updates.promptTokens = metadata.promptTokens;
}
yield this.jobStore.updateJob(streamId, updates);
});
}
/**
* Set reference to the graph's contentParts array.
*/
setContentParts(streamId, contentParts) {
// Use runtime state check for performance (sync check)
if (!this.runtimeState.has(streamId)) {
return;
}
this.jobStore.setContentParts(streamId, contentParts);
}
/**
* Set reference to the collectedUsage array.
* This array accumulates token usage from all models during generation.
*/
setCollectedUsage(streamId, collectedUsage) {
// Use runtime state check for performance (sync check)
if (!this.runtimeState.has(streamId)) {
return;
}
this.jobStore.setCollectedUsage(streamId, collectedUsage);
}
/**
* Set reference to the graph instance.
*/
setGraph(streamId, graph) {
// Use runtime state check for performance (sync check)
if (!this.runtimeState.has(streamId)) {
return;
}
this.jobStore.setGraph(streamId, graph);
}
/**
* Get resume state for reconnecting clients.
*/
getResumeState(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const jobData = yield this.jobStore.getJob(streamId);
if (!jobData) {
return null;
}
const result = yield this.jobStore.getContentParts(streamId);
const aggregatedContent = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
const runSteps = yield this.jobStore.getRunSteps(streamId);
dataSchemas.logger.debug(`[GenerationJobManager] getResumeState:`, {
streamId,
runStepsLength: runSteps.length,
aggregatedContentLength: aggregatedContent.length,
});
return {
runSteps,
aggregatedContent,
userMessage: jobData.userMessage,
responseMessageId: jobData.responseMessageId,
conversationId: jobData.conversationId,
sender: jobData.sender,
};
});
}
/**
* Mark that sync has been sent.
* Persists to Redis for cross-replica consistency.
*/
markSyncSent(streamId) {
const runtime = this.runtimeState.get(streamId);
if (runtime) {
runtime.syncSent = true;
}
// Persist to Redis for cross-replica consistency
this.jobStore.updateJob(streamId, { syncSent: true }).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to persist syncSent flag:`, err);
});
}
/**
* Check if sync has been sent.
* Checks local runtime first, then falls back to Redis for cross-replica scenarios.
*/
wasSyncSent(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const localSyncSent = (_a = this.runtimeState.get(streamId)) === null || _a === void 0 ? void 0 : _a.syncSent;
if (localSyncSent !== undefined) {
return localSyncSent;
}
// Cross-replica: check Redis
const jobData = yield this.jobStore.getJob(streamId);
return (_b = jobData === null || jobData === void 0 ? void 0 : jobData.syncSent) !== null && _b !== void 0 ? _b : false;
});
}
/**
* Emit a done event.
* Persists finalEvent to Redis for cross-replica access.
*/
emitDone(streamId, event) {
return __awaiter(this, void 0, void 0, function* () {
const runtime = this.runtimeState.get(streamId);
if (runtime) {
runtime.finalEvent = event;
}
// Persist finalEvent to Redis for cross-replica consistency
this.jobStore.updateJob(streamId, { finalEvent: JSON.stringify(event) }).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to persist finalEvent:`, err);
});
yield this.eventTransport.emitDone(streamId, event);
});
}
/**
* Emit an error event.
* Stores the error for late-connecting subscribers (race condition where error
* occurs before client connects to SSE stream).
*/
emitError(streamId, error) {
return __awaiter(this, void 0, void 0, function* () {
const runtime = this.runtimeState.get(streamId);
if (runtime) {
runtime.errorEvent = error;
}
// Persist error to job store for cross-replica consistency
this.jobStore.updateJob(streamId, { error }).catch((err) => {
dataSchemas.logger.error(`[GenerationJobManager] Failed to persist error:`, err);
});
yield this.eventTransport.emitError(streamId, error);
});
}
/**
* Cleanup expired jobs.
* Also cleans up any orphaned runtime state, buffers, and event transport entries.
*/
cleanup() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const count = yield this.jobStore.cleanup();
// Cleanup runtime state for deleted jobs
for (const streamId of this.runtimeState.keys()) {
if (!(yield this.jobStore.hasJob(streamId))) {
this.runtimeState.delete(streamId);
(_a = this.runStepBuffers) === null || _a === void 0 ? void 0 : _a.delete(streamId);
this.jobStore.clearContentState(streamId);
this.eventTransport.cleanup(streamId);
}
}
// Also check runStepBuffers for any orphaned entries (Redis mode only)
if (this.runStepBuffers) {
for (const streamId of this.runStepBuffers.keys()) {
if (!(yield this.jobStore.hasJob(streamId))) {
this.runStepBuffers.delete(streamId);
}
}
}
// Check eventTransport for orphaned streams (e.g., connections dropped without clean close)
// These are streams that exist in eventTransport but have no corresponding job
for (const streamId of this.eventTransport.getTrackedStreamIds()) {
if (!(yield this.jobStore.hasJob(streamId)) && !this.runtimeState.has(streamId)) {
this.eventTransport.cleanup(streamId);
}
}
if (count > 0) {
dataSchemas.logger.debug(`[GenerationJobManager] Cleaned up ${count} expired jobs`);
}
});
}
/**
* Get stream info for status endpoint.
*/
getStreamInfo(streamId) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const jobData = yield this.jobStore.getJob(streamId);
if (!jobData) {
return null;
}
const result = yield this.jobStore.getContentParts(streamId);
const aggregatedContent = (_a = result === null || result === void 0 ? void 0 : result.content) !== null && _a !== void 0 ? _a : [];
return {
active: jobData.status === 'running',
status: jobData.status,
aggregatedContent,
createdAt: jobData.createdAt,
};
});
}
/**
* Get total job count.
*/
getJobCount() {
return __awaiter(this, void 0, void 0, function* () {
return this.jobStore.getJobCount();
});
}
/**
* Get job count by status.
*/
getJobCountByStatus() {
return __awaiter(this, void 0, void 0, function* () {
const [running, complete, error, aborted] = yield Promise.all([
this.jobStore.getJobCountByStatus('running'),
this.jobStore.getJobCountByStatus('complete'),
this.jobStore.getJobCountByStatus('error'),
this.jobStore.getJobCountByStatus('aborted'),
]);
return { running, complete, error, aborted };
});
}
/**
* Get active job IDs for a user.
* Returns conversation IDs of running jobs belonging to the user.
* Performs self-healing cleanup of stale entries.
*
* @param userId - The user ID to query
* @returns Array of conversation IDs with active jobs
*/
getActiveJobIdsForUser(userId) {
return __awaiter(this, void 0, void 0, function* () {
return this.jobStore.getActiveJobIdsByUser(userId);
});
}
/**
* Destroy the manager.
* Cleans up all resources including runtime state, buffers, and stores.
*/
destroy() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
yield this.jobStore.destroy();
this.eventTransport.destroy();
this.runtimeState.clear();
(_a = this.runStepBuffers) === null || _a === void 0 ? void 0 : _a.clear();
dataSchemas.logger.debug('[GenerationJobManager] Destroyed');
});
}
}
const GenerationJobManager = new GenerationJobManagerClass();
const memoryInstructions = 'The system automatically stores important user information and can update or delete memories based on user requests, enabling dynamic memory management.';
const getDefaultInstructions = (validKeys, tokenLimit) => `Use the \`set_memory\` tool to save important information about the user, but ONLY when the user has requested you to remember something.
The \`delete_memory\` tool should only be used in two scenarios:
1. When the user explicitly asks to forget or remove specific information
2. When updating existing memories, use the \`set_memory\` tool instead of deleting and re-adding the memory.
1. ONLY use memory tools when the user requests memory actions with phrases like:
- "Remember [that] [I]..."
- "Don't forget [that] [I]..."
- "Please remember..."
- "Store this..."
- "Forget [that] [I]..."
- "Delete the memory about..."
2. NEVER store information just because the user mentioned it in conversation.
3. NEVER use memory tools when the user asks you to use other tools or invoke tools in general.
4. Memory tools are ONLY for memory requests, not for general tool usage.
5. If the user doesn't ask you to remember or forget something, DO NOT use any memory tools.
${validKeys && validKeys.length > 0 ? `\nVALID KEYS: ${validKeys.join(', ')}` : ''}
${tokenLimit ? `\nTOKEN LIMIT: Maximum ${tokenLimit} tokens per memory value.` : ''}
When in doubt, and the user hasn't asked to remember or forget anything, END THE TURN IMMEDIATELY.`;
/**
* Creates a memory tool instance with user context
*/
const createMemoryTool = ({ userId, setMemory, validKeys, tokenLimit, totalTokens = 0, }) => {
const remainingTokens = tokenLimit ? tokenLimit - totalTokens : Infinity;
const isOverflowing = tokenLimit ? remainingTokens <= 0 : false;
return tools.tool((_a) => __awaiter(void 0, [_a], void 0, function* ({ key, value }) {
try {
if (validKeys && validKeys.length > 0 && !validKeys.includes(key)) {
dataSchemas.logger.warn(`Memory Agent failed to set memory: Invalid key "${key}". Must be one of: ${validKeys.join(', ')}`);
return [`Invalid key "${key}". Must be one of: ${validKeys.join(', ')}`, undefined];
}
const tokenCount = TokenizerSingleton.getTokenCount(value, 'o200k_base');
if (isOverflowing) {
const errorArtifact = {
[librechatDataProvider.Tools.memory]: {
key: 'system',
type: 'error',
value: JSON.stringify({
errorType: 'already_exceeded',
tokenCount: Math.abs(remainingTokens),
totalTokens: totalTokens,
tokenLimit: tokenLimit,
}),
tokenCount: totalTokens,
},
};
return [`Memory storage exceeded. Cannot save new memories.`, errorArtifact];
}
if (tokenLimit) {
const newTotalTokens = totalTokens + tokenCount;
const newRemainingTokens = tokenLimit - newTotalTokens;
if (newRemainingTokens < 0) {
const errorArtifact = {
[librechatDataProvider.Tools.memory]: {
key: 'system',
type: 'error',
value: JSON.stringify({
errorType: 'would_exceed',
tokenCount: Math.abs(newRemainingTokens),
totalTokens: newTotalTokens,
tokenLimit,
}),
tokenCount: totalTokens,
},
};
return [`Memory storage would exceed limit. Cannot save this memory.`, errorArtifact];
}
}
const artifact = {
[librechatDataProvider.Tools.memory]: {
key,
value,
tokenCount,
type: 'update',
},
};
const result = yield setMemory({ userId, key, value, tokenCount });
if (result.ok) {
dataSchemas.logger.debug(`Memory set for key "${key}" (${tokenCount} tokens) for user "${userId}"`);
return [`Memory set for key "${key}" (${tokenCount} tokens)`, artifact];
}
dataSchemas.logger.warn(`Failed to set memory for key "${key}" for user "${userId}"`);
return [`Failed to set memory for key "${key}"`, undefined];
}
catch (error) {
dataSchemas.logger.error('Memory Agent failed to set memory', error);
return [`Error setting memory for key "${key}"`, undefined];
}
}), {
name: 'set_memory',
description: 'Saves important information about the user into memory.',
responseFormat: 'content_and_artifact',
schema: z.z.object({
key: z.z
.string()
.describe(validKeys && validKeys.length > 0
? `The key of the memory value. Must be one of: ${validKeys.join(', ')}`
: 'The key identifier for this memory'),
value: z.z
.string()
.describe('Value MUST be a complete sentence that fully describes relevant user information.'),
}),
});
};
/**
* Creates a delete memory tool instance with user context
*/
const createDeleteMemoryTool = ({ userId, deleteMemory, validKeys, }) => {
return tools.tool((_a) => __awaiter(void 0, [_a], void 0, function* ({ key }) {
try {
if (validKeys && validKeys.length > 0 && !validKeys.includes(key)) {
dataSchemas.logger.warn(`Memory Agent failed to delete memory: Invalid key "${key}". Must be one of: ${validKeys.join(', ')}`);
return [`Invalid key "${key}". Must be one of: ${validKeys.join(', ')}`, undefined];
}
const artifact = {
[librechatDataProvider.Tools.memory]: {
key,
type: 'delete',
},
};
const result = yield deleteMemory({ userId, key });
if (result.ok) {
dataSchemas.logger.debug(`Memory deleted for key "${key}" for user "${userId}"`);
return [`Memory deleted for key "${key}"`, artifact];
}
dataSchemas.logger.warn(`Failed to delete memory for key "${key}" for user "${userId}"`);
return [`Failed to delete memory for key "${key}"`, undefined];
}
catch (error) {
dataSchemas.logger.error('Memory Agent failed to delete memory', error);
return [`Error deleting memory for key "${key}"`, undefined];
}
}), {
name: 'delete_memory',
description: 'Deletes specific memory data about the user using the provided key. For updating existing memories, use the `set_memory` tool instead',
responseFormat: 'content_and_artifact',
schema: z.z.object({
key: z.z
.string()
.describe(validKeys && validKeys.length > 0
? `The key of the memory to delete. Must be one of: ${validKeys.join(', ')}`
: 'The key identifier of the memory to delete'),
}),
});
};
class BasicToolEndHandler {
constructor(callback) {
this.callback = callback;
}
handle(event, data, metadata) {
var _a;
if (!metadata) {
console.warn(`Graph or metadata not found in ${event} event`);
return;
}
const toolEndData = data;
if (!(toolEndData === null || toolEndData === void 0 ? void 0 : toolEndData.output)) {
console.warn('No output found in tool_end event');
return;
}
(_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, toolEndData, metadata);
}
}
function processMemory(_a) {
return __awaiter(this, arguments, void 0, function* ({ res, userId, setMemory, deleteMemory, messages: messages$1, memory, messageId, conversationId, validKeys, instructions, llmConfig, tokenLimit, totalTokens = 0, streamId = null, user, }) {
var _b, _c, _d, _e, _f;
try {
const memoryTool = createMemoryTool({
userId,
tokenLimit,
setMemory,
validKeys,
totalTokens,
});
const deleteMemoryTool = createDeleteMemoryTool({
userId,
validKeys,
deleteMemory,
});
const currentMemoryTokens = totalTokens;
let memoryStatus = `# Existing memory:\n${memory !== null && memory !== void 0 ? memory : 'No existing memories'}`;
if (tokenLimit) {
const remainingTokens = tokenLimit - currentMemoryTokens;
memoryStatus = `# Memory Status:
Current memory usage: ${currentMemoryTokens} tokens
Token limit: ${tokenLimit} tokens
Remaining capacity: ${remainingTokens} tokens
# Existing memory:
${memory !== null && memory !== void 0 ? memory : 'No existing memories'}`;
}
const defaultLLMConfig = {
provider: agents.Providers.OPENAI,
model: 'gpt-4.1-mini',
temperature: 0.4,
streaming: false,
disableStreaming: true,
};
const finalLLMConfig = Object.assign(Object.assign(Object.assign({}, defaultLLMConfig), llmConfig), {
/**
* Ensure streaming is always disabled for memory processing
*/
streaming: false, disableStreaming: true });
// Handle GPT-5+ models
if ('model' in finalLLMConfig && /\bgpt-[5-9](?:\.\d+)?\b/i.test((_b = finalLLMConfig.model) !== null && _b !== void 0 ? _b : '')) {
// Remove temperature for GPT-5+ models
delete finalLLMConfig.temperature;
// Move maxTokens to modelKwargs for GPT-5+ models
if ('maxTokens' in finalLLMConfig && finalLLMConfig.maxTokens != null) {
const modelKwargs = (_c = finalLLMConfig.modelKwargs) !== null && _c !== void 0 ? _c : {};
const paramName = finalLLMConfig.useResponsesApi === true
? 'max_output_tokens'
: 'max_completion_tokens';
modelKwargs[paramName] = finalLLMConfig.maxTokens;
delete finalLLMConfig.maxTokens;
finalLLMConfig.modelKwargs = modelKwargs;
}
}
const bedrockConfig = finalLLMConfig;
if ((llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.provider) === agents.Providers.BEDROCK &&
((_d = bedrockConfig.additionalModelRequestFields) === null || _d === void 0 ? void 0 : _d.thinking) != null &&
bedrockConfig.temperature != null) {
finalLLMConfig.temperature = 1;
}
const anthropicConfig = finalLLMConfig;
if ((llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.provider) === agents.Providers.ANTHROPIC &&
((_e = anthropicConfig.thinking) === null || _e === void 0 ? void 0 : _e.type) === 'enabled' &&
anthropicConfig.temperature != null) {
delete finalLLMConfig.temperature;
}
const llmConfigWithHeaders = finalLLMConfig;
if (((_f = llmConfigWithHeaders === null || llmConfigWithHeaders === void 0 ? void 0 : llmConfigWithHeaders.configuration) === null || _f === void 0 ? void 0 : _f.defaultHeaders) != null) {
llmConfigWithHeaders.configuration.defaultHeaders = resolveHeaders({
headers: llmConfigWithHeaders.configuration.defaultHeaders,
user: user ? createSafeUser(user) : undefined,
});
}
const artifactPromises = [];
const memoryCallback = createMemoryCallback({ res, artifactPromises, streamId });
const customHandlers = {
[agents.GraphEvents.TOOL_END]: new BasicToolEndHandler(memoryCallback),
};
/**
* For Bedrock provider, include instructions in the user message instead of as a system prompt.
* Bedrock's Converse API requires conversations to start with a user message, not a system message.
* Other providers can use the standard system prompt approach.
*/
const isBedrock = (llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.provider) === agents.Providers.BEDROCK;
let graphInstructions = instructions;
let graphAdditionalInstructions = memoryStatus;
let processedMessages = messages$1;
if (isBedrock) {
const combinedInstructions = [instructions, memoryStatus].filter(Boolean).join('\n\n');
if (messages$1.length > 0) {
const firstMessage = messages$1[0];
const originalContent = typeof firstMessage.content === 'string' ? firstMessage.content : '';
if (typeof firstMessage.content !== 'string') {
dataSchemas.logger.warn('Bedrock memory processing: First message has non-string content, using empty string');
}
const bedrockUserMessage = new messages.HumanMessage(`${combinedInstructions}\n\n${originalContent}`);
processedMessages = [bedrockUserMessage, ...messages$1.slice(1)];
}
else {
processedMessages = [new messages.HumanMessage(combinedInstructions)];
}
graphInstructions = undefined;
graphAdditionalInstructions = undefined;
}
const run = yield agents.Run.create({
runId: messageId,
graphConfig: {
type: 'standard',
llmConfig: finalLLMConfig,
tools: [memoryTool, deleteMemoryTool],
instructions: graphInstructions,
additional_instructions: graphAdditionalInstructions,
toolEnd: true,
},
customHandlers,
returnContent: true,
});
const config = {
runName: 'MemoryRun',
configurable: {
user_id: userId,
thread_id: conversationId,
provider: llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.provider,
},
streamMode: 'values',
recursionLimit: 3,
version: 'v2',
};
const inputs = {
messages: processedMessages,
};
const content = yield run.processStream(inputs, config);
if (content) {
dataSchemas.logger.debug('Memory Agent processed memory successfully', content);
}
else {
dataSchemas.logger.warn('Memory Agent processed memory but returned no content');
}
return yield Promise.all(artifactPromises);
}
catch (error) {
dataSchemas.logger.error('Memory Agent failed to process memory', error);
}
});
}
function createMemoryProcessor(_a) {
return __awaiter(this, arguments, void 0, function* ({ res, userId, messageId, memoryMethods, conversationId, config = {}, streamId = null, user, }) {
const { validKeys, instructions, llmConfig, tokenLimit } = config;
const finalInstructions = instructions || getDefaultInstructions(validKeys, tokenLimit);
const { withKeys, withoutKeys, totalTokens } = yield memoryMethods.getFormattedMemories({
userId,
});
return [
withoutKeys,
function (messages) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield processMemory({
res,
userId,
messages,
validKeys,
llmConfig,
messageId,
tokenLimit,
streamId,
conversationId,
memory: withKeys,
totalTokens: totalTokens || 0,
instructions: finalInstructions,
setMemory: memoryMethods.setMemory,
deleteMemory: memoryMethods.deleteMemory,
user,
});
}
catch (error) {
dataSchemas.logger.error('Memory Agent failed to process memory', error);
}
});
},
];
});
}
function handleMemoryArtifact(_a) {
return __awaiter(this, arguments, void 0, function* ({ res, data, metadata, streamId = null, }) {
var _b, _c;
const output = data === null || data === void 0 ? void 0 : data.output;
if (!output) {
return null;
}
if (!output.artifact) {
return null;
}
const memoryArtifact = output.artifact[librechatDataProvider.Tools.memory];
if (!memoryArtifact) {
return null;
}
const attachment = {
type: librechatDataProvider.Tools.memory,
toolCallId: output.tool_call_id,
messageId: (_b = metadata === null || metadata === void 0 ? void 0 : metadata.run_id) !== null && _b !== void 0 ? _b : '',
conversationId: (_c = metadata === null || metadata === void 0 ? void 0 : metadata.thread_id) !== null && _c !== void 0 ? _c : '',
[librechatDataProvider.Tools.memory]: memoryArtifact,
};
if (!res.headersSent) {
return attachment;
}
if (streamId) {
GenerationJobManager.emitChunk(streamId, { event: 'attachment', data: attachment });
}
else {
res.write(`event: attachment\ndata: ${JSON.stringify(attachment)}\n\n`);
}
return attachment;
});
}
/**
* Creates a memory callback for handling memory artifacts
* @param params - The parameters object
* @param params.res - The server response object
* @param params.artifactPromises - Array to collect artifact promises
* @param params.streamId - The stream ID for resumable mode, or null for standard mode
* @returns The memory callback function
*/
function createMemoryCallback({ res, artifactPromises, streamId = null, }) {
return (data, metadata) => __awaiter(this, void 0, void 0, function* () {
var _a;
const output = data === null || data === void 0 ? void 0 : data.output;
const memoryArtifact = (_a = output === null || output === void 0 ? void 0 : output.artifact) === null || _a === void 0 ? void 0 : _a[librechatDataProvider.Tools.memory];
if (memoryArtifact == null) {
return;
}
artifactPromises.push(handleMemoryArtifact({ res, data, metadata, streamId }).catch((error) => {
dataSchemas.logger.error('Error processing memory artifact content:', error);
return null;
}));
});
}
const { GLOBAL_PROJECT_NAME } = librechatDataProvider.Constants;
/**
* Check if agents need to be migrated to the new permission system
* This performs a dry-run check similar to the migration script
*/
function checkAgentPermissionsMigration(_a) {
return __awaiter(this, arguments, void 0, function* ({ methods, mongoose, AgentModel, }) {
dataSchemas.logger.debug('Checking if agent permissions migration is needed');
try {
const db = mongoose.connection.db;
if (db) {
yield ensureRequiredCollectionsExist(db);
}
// Verify required roles exist
const ownerRole = yield methods.findRoleByIdentifier(librechatDataProvider.AccessRoleIds.AGENT_OWNER);
const viewerRole = yield methods.findRoleByIdentifier(librechatDataProvider.AccessRoleIds.AGENT_VIEWER);
const editorRole = yield methods.findRoleByIdentifier(librechatDataProvider.AccessRoleIds.AGENT_EDITOR);
if (!ownerRole || !viewerRole || !editorRole) {
dataSchemas.logger.warn('Required agent roles not found. Permission system may not be fully initialized.');
return {
totalToMigrate: 0,
globalEditAccess: 0,
globalViewAccess: 0,
privateAgents: 0,
};
}
// Get global project agent IDs
const globalProject = yield methods.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
const globalAgentIds = new Set((globalProject === null || globalProject === void 0 ? void 0 : globalProject.agentIds) || []);
// Find agents without ACL entries (no batching for efficiency on startup)
const agentsToMigrate = yield AgentModel.aggregate([
{
$lookup: {
from: 'aclentries',
localField: '_id',
foreignField: 'resourceId',
as: 'aclEntries',
},
},
{
$addFields: {
userAclEntries: {
$filter: {
input: '$aclEntries',
as: 'aclEntry',
cond: {
$and: [
{ $eq: ['$$aclEntry.resourceType', librechatDataProvider.ResourceType.AGENT] },
{ $eq: ['$$aclEntry.principalType', librechatDataProvider.PrincipalType.USER] },
],
},
},
},
},
},
{
$match: {
author: { $exists: true, $ne: null },
userAclEntries: { $size: 0 },
},
},
{
$project: {
_id: 1,
id: 1,
name: 1,
author: 1,
isCollaborative: 1,
},
},
]);
const categories = {
globalEditAccess: [],
globalViewAccess: [],
privateAgents: [],
};
agentsToMigrate.forEach((agent) => {
const isGlobal = globalAgentIds.has(agent.id);
const isCollab = agent.isCollaborative;
if (isGlobal && isCollab) {
categories.globalEditAccess.push(agent);
}
else if (isGlobal && !isCollab) {
categories.globalViewAccess.push(agent);
}
else {
categories.privateAgents.push(agent);
}
});
const result = {
totalToMigrate: agentsToMigrate.length,
globalEditAccess: categories.globalEditAccess.length,
globalViewAccess: categories.globalViewAccess.length,
privateAgents: categories.privateAgents.length,
};
// Add details for debugging
if (agentsToMigrate.length > 0) {
result.details = {
globalEditAccess: categories.globalEditAccess.map((a) => ({
name: a.name,
id: a.id,
})),
globalViewAccess: categories.globalViewAccess.map((a) => ({
name: a.name,
id: a.id,
})),
privateAgents: categories.privateAgents.map((a) => ({
name: a.name,
id: a.id,
})),
};
}
dataSchemas.logger.debug('Agent migration check completed', {
totalToMigrate: result.totalToMigrate,
globalEditAccess: result.globalEditAccess,
globalViewAccess: result.globalViewAccess,
privateAgents: result.privateAgents,
});
return result;
}
catch (error) {
dataSchemas.logger.error('Failed to check agent permissions migration', error);
// Return zero counts on error to avoid blocking startup
return {
totalToMigrate: 0,
globalEditAccess: 0,
globalViewAccess: 0,
privateAgents: 0,
};
}
});
}
/**
* Log migration warning to console if agents need migration
*/
function logAgentMigrationWarning(result) {
if (result.totalToMigrate === 0) {
return;
}
// Create a visible warning box
const border = '='.repeat(80);
const warning = [
'',
border,
' IMPORTANT: AGENT PERMISSIONS MIGRATION REQUIRED',
border,
'',
` Total agents to migrate: ${result.totalToMigrate}`,
` - Global Edit Access: ${result.globalEditAccess} agents`,
` - Global View Access: ${result.globalViewAccess} agents`,
` - Private Agents: ${result.privateAgents} agents`,
'',
' The new agent sharing system requires migrating existing agents.',
' Please run the following command to migrate your agents:',
'',
' npm run migrate:agent-permissions',
'',
' For a dry run (preview) of what will be migrated:',
'',
' npm run migrate:agent-permissions:dry-run',
'',
' This migration will:',
' 1. Grant owner permissions to agent authors',
' 2. Set appropriate public permissions based on global project status',
' 3. Preserve existing collaborative settings',
'',
border,
'',
];
// Use console methods directly for visibility
console.log('\n' + warning.join('\n') + '\n');
// Also log with logger for consistency
dataSchemas.logger.warn('Agent permissions migration required', {
totalToMigrate: result.totalToMigrate,
globalEditAccess: result.globalEditAccess,
globalViewAccess: result.globalViewAccess,
privateAgents: result.privateAgents,
});
}
/**
* Create a chat completion chunk in OpenAI format
*/
function createChunk(context, delta, finishReason = null, usage) {
return Object.assign({ id: context.requestId, object: 'chat.completion.chunk', created: context.created, model: context.model, choices: [
{
index: 0,
delta,
finish_reason: finishReason,
},
] }, (usage && { usage }));
}
/**
* Write an SSE event to the response
*/
function writeSSE(res, data) {
if (typeof data === 'string') {
res.write(`data: ${data}\n\n`);
}
else {
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
}
/**
* Create a lightweight stream tracker (doesn't store content)
*/
function createOpenAIStreamTracker() {
const tracker = {
hasText: false,
hasReasoning: false,
toolCalls: new Map(),
usage: {
promptTokens: 0,
completionTokens: 0,
reasoningTokens: 0,
},
addText: () => {
tracker.hasText = true;
},
addReasoning: () => {
tracker.hasReasoning = true;
},
};
return tracker;
}
/**
* Create a content aggregator for non-streaming responses
*/
function createOpenAIContentAggregator() {
const textChunks = [];
const reasoningChunks = [];
return {
textChunks,
reasoningChunks,
toolCalls: new Map(),
usage: {
promptTokens: 0,
completionTokens: 0,
reasoningTokens: 0,
},
getText: () => textChunks.join(''),
getReasoning: () => reasoningChunks.join(''),
addText: (text) => textChunks.push(text),
addReasoning: (text) => reasoningChunks.push(text),
};
}
/**
* Graph event types from @librechat/agents
*/
const GraphEvents = {
CHAT_MODEL_END: 'on_chat_model_end',
TOOL_END: 'on_tool_end',
CHAT_MODEL_STREAM: 'on_chat_model_stream',
ON_RUN_STEP: 'on_run_step',
ON_RUN_STEP_DELTA: 'on_run_step_delta',
ON_RUN_STEP_COMPLETED: 'on_run_step_completed',
ON_MESSAGE_DELTA: 'on_message_delta',
ON_REASONING_DELTA: 'on_reasoning_delta',
ON_TOOL_EXECUTE: 'on_tool_execute',
};
/**
* Step types from librechat-data-provider
*/
const StepTypes = {
MESSAGE_CREATION: 'message_creation',
TOOL_CALLS: 'tool_calls',
};
/**
* Handler for message delta events - streams text content
*/
class OpenAIMessageDeltaHandler {
constructor(config) {
this.config = config;
}
handle(_event, data) {
const content = data === null || data === void 0 ? void 0 : data.content;
if (!content || !Array.isArray(content)) {
return;
}
for (const part of content) {
if (part.type === 'text' && part.text) {
this.config.tracker.addText();
const chunk = createChunk(this.config.context, { content: part.text });
writeSSE(this.config.res, chunk);
}
}
}
}
/**
* Handler for run step delta events - streams tool calls
*/
class OpenAIRunStepDeltaHandler {
constructor(config) {
this.config = config;
}
handle(_event, data) {
var _a, _b;
const delta = data === null || data === void 0 ? void 0 : data.delta;
if (!delta || delta.type !== StepTypes.TOOL_CALLS) {
return;
}
const toolCalls = delta.tool_calls;
if (!toolCalls || !Array.isArray(toolCalls)) {
return;
}
for (const tc of toolCalls) {
if (tc.index === undefined) {
continue;
}
// Initialize tool call in tracker if needed
let trackedTc = this.config.tracker.toolCalls.get(tc.index);
if (!trackedTc && tc.id) {
trackedTc = {
id: tc.id,
type: 'function',
function: {
name: '',
arguments: '',
},
};
this.config.tracker.toolCalls.set(tc.index, trackedTc);
}
// Build the streaming delta
const streamDelta = {
tool_calls: [
Object.assign(Object.assign(Object.assign({ index: tc.index }, (tc.id && { id: tc.id })), (tc.type && { type: tc.type })), (tc.function && {
function: Object.assign(Object.assign({}, (tc.function.name && { name: tc.function.name })), (tc.function.arguments && { arguments: tc.function.arguments })),
})),
],
};
// Update tracked tool call
if (trackedTc) {
if ((_a = tc.function) === null || _a === void 0 ? void 0 : _a.name) {
trackedTc.function.name += tc.function.name;
}
if ((_b = tc.function) === null || _b === void 0 ? void 0 : _b.arguments) {
trackedTc.function.arguments += tc.function.arguments;
}
}
const chunk = createChunk(this.config.context, streamDelta);
writeSSE(this.config.res, chunk);
}
}
}
/**
* Handler for run step events - sends initial tool call info
*/
class OpenAIRunStepHandler {
constructor(config) {
this.config = config;
}
handle(_event, data) {
var _a;
// Run step events are primarily for LibreChat UI, we use deltas for streaming
// This handler is a no-op for OpenAI format
if (((_a = data === null || data === void 0 ? void 0 : data.stepDetails) === null || _a === void 0 ? void 0 : _a.type) === StepTypes.TOOL_CALLS) ;
}
}
/**
* Handler for model end events - captures usage
*/
class OpenAIModelEndHandler {
constructor(config) {
this.config = config;
}
handle(_event, data) {
var _a, _b, _c;
const usage = (_a = data === null || data === void 0 ? void 0 : data.output) === null || _a === void 0 ? void 0 : _a.usage_metadata;
if (!usage) {
return;
}
this.config.tracker.usage.promptTokens += (_b = usage.input_tokens) !== null && _b !== void 0 ? _b : 0;
this.config.tracker.usage.completionTokens += (_c = usage.output_tokens) !== null && _c !== void 0 ? _c : 0;
}
}
/**
* Handler for chat model stream events
*/
class OpenAIChatModelStreamHandler {
handle() {
// Handled by message delta handler
}
}
/**
* Handler for tool end events
*/
class OpenAIToolEndHandler {
handle() {
// Tool results don't need to be streamed in OpenAI format
// They're used internally by the agent
}
}
/**
* Handler for reasoning delta events.
* Streams reasoning/thinking content using the `delta.reasoning` field (OpenRouter convention).
*/
class OpenAIReasoningDeltaHandler {
constructor(config) {
this.config = config;
}
handle(_event, data) {
const content = data === null || data === void 0 ? void 0 : data.content;
if (!content || !Array.isArray(content)) {
return;
}
for (const part of content) {
if (part.type === 'text' && part.text) {
// Mark that reasoning was emitted
this.config.tracker.addReasoning();
// Stream as delta.reasoning (OpenRouter convention)
const chunk = createChunk(this.config.context, { reasoning: part.text });
writeSSE(this.config.res, chunk);
}
}
}
}
/**
* Create all handlers for OpenAI streaming format
*/
function createOpenAIHandlers(config, toolExecuteOptions) {
const handlers = {
[GraphEvents.ON_MESSAGE_DELTA]: new OpenAIMessageDeltaHandler(config),
[GraphEvents.ON_RUN_STEP_DELTA]: new OpenAIRunStepDeltaHandler(config),
[GraphEvents.ON_RUN_STEP]: new OpenAIRunStepHandler(config),
[GraphEvents.ON_RUN_STEP_COMPLETED]: new OpenAIRunStepHandler(config),
[GraphEvents.CHAT_MODEL_END]: new OpenAIModelEndHandler(config),
[GraphEvents.CHAT_MODEL_STREAM]: new OpenAIChatModelStreamHandler(),
[GraphEvents.TOOL_END]: new OpenAIToolEndHandler(),
[GraphEvents.ON_REASONING_DELTA]: new OpenAIReasoningDeltaHandler(config),
};
if (toolExecuteOptions) {
handlers[GraphEvents.ON_TOOL_EXECUTE] = createToolExecuteHandler(toolExecuteOptions);
}
return handlers;
}
/**
* Send the final chunk with finish_reason and optional usage
*/
function sendFinalChunk(config, finishReason = 'stop') {
const { res, context, tracker } = config;
// Determine finish reason based on content
let reason = finishReason;
if (tracker.toolCalls.size > 0 && !tracker.hasText) {
reason = 'tool_calls';
}
// Build usage object with reasoning token details (OpenRouter/OpenAI convention)
const usage = {
prompt_tokens: tracker.usage.promptTokens,
completion_tokens: tracker.usage.completionTokens,
total_tokens: tracker.usage.promptTokens + tracker.usage.completionTokens,
};
// Add reasoning token breakdown if there are reasoning tokens
if (tracker.usage.reasoningTokens > 0) {
usage.completion_tokens_details = {
reasoning_tokens: tracker.usage.reasoningTokens,
};
}
const finalChunk = createChunk(context, {}, reason, usage);
writeSSE(res, finalChunk);
// Send [DONE] marker
writeSSE(res, '[DONE]');
}
let urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
const POOL_SIZE_MULTIPLIER = 128;
let pool, poolOffset;
let fillPool = bytes => {
if (!pool || pool.length < bytes) {
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
crypto$2.randomFillSync(pool);
poolOffset = 0;
} else if (poolOffset + bytes > pool.length) {
crypto$2.randomFillSync(pool);
poolOffset = 0;
}
poolOffset += bytes;
};
let nanoid = (size = 21) => {
fillPool((size |= 0));
let id = '';
for (let i = poolOffset - size; i < poolOffset; i++) {
id += urlAlphabet[pool[i] & 63];
}
return id
};
/**
* Convert OpenAI messages to LibreChat format
*/
function convertMessages(messages) {
return messages.map((msg) => {
let content;
if (typeof msg.content === 'string') {
content = msg.content;
}
else if (msg.content) {
content = msg.content.map((part) => {
if (part.type === 'text') {
return { type: 'text', text: part.text };
}
if (part.type === 'image_url') {
return { type: 'image_url', image_url: part.image_url };
}
return part;
});
}
else {
content = '';
}
return Object.assign(Object.assign(Object.assign({ role: msg.role, content }, (msg.name && { name: msg.name })), (msg.tool_calls && { tool_calls: msg.tool_calls })), (msg.tool_call_id && { tool_call_id: msg.tool_call_id }));
});
}
/**
* Create an error response in OpenAI format
*/
function createErrorResponse(message, type = 'invalid_request_error', code = null) {
return {
error: {
message,
type,
param: null,
code,
},
};
}
/**
* Send an error response
*/
function sendErrorResponse(res, statusCode, message, type = 'invalid_request_error', code = null) {
res.status(statusCode).json(createErrorResponse(message, type, code));
}
/**
* Type guard for validation failure
*/
function isChatCompletionValidationFailure(result) {
return !result.valid;
}
/**
* Validate the chat completion request
*/
function validateRequest(body) {
if (!body || typeof body !== 'object') {
return { valid: false, error: 'Request body is required' };
}
const request = body;
if (!request.model || typeof request.model !== 'string') {
return { valid: false, error: 'model (agent_id) is required' };
}
if (!request.messages || !Array.isArray(request.messages)) {
return { valid: false, error: 'messages array is required' };
}
if (request.messages.length === 0) {
return { valid: false, error: 'messages array cannot be empty' };
}
// Validate each message has role and content
for (let i = 0; i < request.messages.length; i++) {
const msg = request.messages[i];
if (!msg.role || typeof msg.role !== 'string') {
return { valid: false, error: `messages[${i}].role is required` };
}
if (!['system', 'user', 'assistant', 'tool'].includes(msg.role)) {
return {
valid: false,
error: `messages[${i}].role must be one of: system, user, assistant, tool`,
};
}
}
return { valid: true, request: request };
}
/**
* Build a non-streaming response from aggregated content
*/
function buildNonStreamingResponse(context, text, reasoning, toolCalls, usage) {
const toolCallsArray = Array.from(toolCalls.values());
const finishReason = toolCallsArray.length > 0 && !text ? 'tool_calls' : 'stop';
return {
id: context.requestId,
object: 'chat.completion',
created: context.created,
model: context.model,
choices: [
{
index: 0,
message: Object.assign(Object.assign({ role: 'assistant', content: text || null }, (reasoning && { reasoning })), (toolCallsArray.length > 0 && { tool_calls: toolCallsArray })),
finish_reason: finishReason,
},
],
usage,
};
}
/**
* Main handler for OpenAI-compatible chat completions with agents.
*
* This function:
* 1. Validates the request
* 2. Looks up the agent by ID (model parameter)
* 3. Initializes the agent with tools
* 4. Runs the agent and streams/returns the response
*
* @param req - Express request object
* @param res - Express response object
* @param deps - Dependencies for the service
*/
function createAgentChatCompletion(req, res, deps) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e;
// Validate request
const validation = validateRequest(req.body);
if (isChatCompletionValidationFailure(validation)) {
sendErrorResponse(res, 400, validation.error);
return;
}
const request = validation.request;
const agentId = request.model;
const requestedStreaming = request.stream === true;
// Look up the agent
const agent = yield deps.getAgent({ id: agentId });
if (!agent) {
sendErrorResponse(res, 404, `Agent not found: ${agentId}`, 'invalid_request_error', 'model_not_found');
return;
}
// Generate IDs
const requestId = `chatcmpl-${nanoid()}`;
const conversationId = (_a = request.conversation_id) !== null && _a !== void 0 ? _a : nanoid();
const created = Math.floor(Date.now() / 1000);
// Build response context
const context = {
created,
requestId,
model: agentId,
};
// Set up abort controller
const abortController = new AbortController();
// Handle client disconnect
req.on('close', () => {
abortController.abort();
});
try {
// Build allowed providers set (empty = all allowed)
const allowedProviders = new Set();
// Initialize the agent first to check for disableStreaming
const initializedAgent = yield deps.initializeAgent({
req,
res,
agent,
conversationId,
parentMessageId: request.parent_message_id,
loadTools: deps.loadAgentTools,
endpointOption: {
endpoint: agent.provider,
model_parameters: (_b = agent.model_parameters) !== null && _b !== void 0 ? _b : {},
},
allowedProviders,
isInitialAgent: true,
});
// Determine if streaming is enabled (check both request and agent config)
const streamingDisabled = !!((_c = initializedAgent.model_parameters) === null || _c === void 0 ? void 0 : _c.disableStreaming);
const isStreaming = requestedStreaming && !streamingDisabled;
// Create tracker for streaming or aggregator for non-streaming
const tracker = isStreaming ? createOpenAIStreamTracker() : null;
const aggregator = isStreaming ? null : createOpenAIContentAggregator();
// Set up response headers for streaming
if (isStreaming) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
// Send initial chunk with role
const initialChunk = createChunk(context, { role: 'assistant' });
writeSSE(res, initialChunk);
}
// Create handler config (only used for streaming)
const handlerConfig = isStreaming && tracker
? {
res,
context,
tracker,
}
: null;
// Create event handlers
const eventHandlers = isStreaming && handlerConfig
? createOpenAIHandlers(handlerConfig, deps.toolExecuteOptions)
: {};
// Convert messages to internal format
const messages = convertMessages(request.messages);
// Create and run the agent
if (deps.createRun) {
const userId = (_e = (_d = req.user) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : 'api-user';
const run = yield deps.createRun({
agents: [initializedAgent],
messages,
runId: requestId,
signal: abortController.signal,
customHandlers: eventHandlers,
requestBody: {
messageId: requestId,
conversationId,
},
user: { id: userId },
});
if (run) {
yield run.processStream({ messages }, {
runName: 'AgentRun',
configurable: {
thread_id: conversationId,
user_id: userId,
},
signal: abortController.signal,
streamMode: 'values',
version: 'v2',
}, {});
}
}
// Finalize response
if (isStreaming && handlerConfig) {
sendFinalChunk(handlerConfig);
res.end();
}
else if (aggregator) {
// Build and send non-streaming response
const usage = Object.assign({ prompt_tokens: aggregator.usage.promptTokens, completion_tokens: aggregator.usage.completionTokens, total_tokens: aggregator.usage.promptTokens + aggregator.usage.completionTokens }, (aggregator.usage.reasoningTokens > 0 && {
completion_tokens_details: { reasoning_tokens: aggregator.usage.reasoningTokens },
}));
const response = buildNonStreamingResponse(context, aggregator.getText(), aggregator.getReasoning(), aggregator.toolCalls, usage);
res.json(response);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An error occurred';
// Check if we already started streaming (headers sent)
if (res.headersSent) {
// Headers already sent, try to send error in stream format
const errorChunk = createChunk(context, { content: `\n\nError: ${errorMessage}` }, 'stop');
writeSSE(res, errorChunk);
writeSSE(res, '[DONE]');
res.end();
}
else {
sendErrorResponse(res, 500, errorMessage, 'server_error');
}
}
});
}
/**
* List available agents/models
*
* This provides a /v1/models compatible endpoint that lists available agents.
*/
function listAgentModels(_req, res, deps) {
return __awaiter(this, void 0, void 0, function* () {
try {
const agents = yield deps.getAgents({});
const models = agents.map((agent) => ({
id: agent.id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: 'librechat',
permission: [],
root: agent.id,
parent: null,
// Extensions
name: agent.name,
provider: agent.provider,
}));
res.json({
object: 'list',
data: models,
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to list models';
sendErrorResponse(res, 500, errorMessage, 'server_error');
}
});
}
/**
* Records token usage for collected LLM calls and spends tokens against balance.
* This handles both sequential execution (tool calls) and parallel execution (multiple agents).
*/
function recordCollectedUsage(deps, params) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e;
const { user, model, balance, transactions, conversationId, collectedUsage, endpointTokenConfig, context = 'message', } = params;
const { spendTokens, spendStructuredTokens } = deps;
if (!collectedUsage || !collectedUsage.length) {
return;
}
const firstUsage = collectedUsage[0];
const input_tokens = ((firstUsage === null || firstUsage === void 0 ? void 0 : firstUsage.input_tokens) || 0) +
(Number((_a = firstUsage === null || firstUsage === void 0 ? void 0 : firstUsage.input_token_details) === null || _a === void 0 ? void 0 : _a.cache_creation) ||
Number(firstUsage === null || firstUsage === void 0 ? void 0 : firstUsage.cache_creation_input_tokens) ||
0) +
(Number((_b = firstUsage === null || firstUsage === void 0 ? void 0 : firstUsage.input_token_details) === null || _b === void 0 ? void 0 : _b.cache_read) ||
Number(firstUsage === null || firstUsage === void 0 ? void 0 : firstUsage.cache_read_input_tokens) ||
0);
let total_output_tokens = 0;
for (const usage of collectedUsage) {
if (!usage) {
continue;
}
const cache_creation = Number((_c = usage.input_token_details) === null || _c === void 0 ? void 0 : _c.cache_creation) ||
Number(usage.cache_creation_input_tokens) ||
0;
const cache_read = Number((_d = usage.input_token_details) === null || _d === void 0 ? void 0 : _d.cache_read) || Number(usage.cache_read_input_tokens) || 0;
total_output_tokens += Number(usage.output_tokens) || 0;
const txMetadata = {
context,
balance,
transactions,
conversationId,
user,
endpointTokenConfig,
model: (_e = usage.model) !== null && _e !== void 0 ? _e : model,
};
if (cache_creation > 0 || cache_read > 0) {
spendStructuredTokens(txMetadata, {
promptTokens: {
input: usage.input_tokens,
write: cache_creation,
read: cache_read,
},
completionTokens: usage.output_tokens,
}).catch((err) => {
dataSchemas.logger.error('[packages/api #recordCollectedUsage] Error spending structured tokens', err);
});
continue;
}
spendTokens(txMetadata, {
promptTokens: usage.input_tokens,
completionTokens: usage.output_tokens,
}).catch((err) => {
dataSchemas.logger.error('[packages/api #recordCollectedUsage] Error spending tokens', err);
});
}
// NOTE: Commerce is debited exactly once, by the cloud gateway (api.hanzo.ai),
// against the user's per-user billing subject when their hk- key is forwarded.
// Chat must NOT also record usage to Commerce here — that was a second debit
// (to a mis-keyed account) and breaks the single-debit money path. Local
// token accounting above remains for the in-app usage display.
return {
input_tokens,
output_tokens: total_output_tokens,
};
});
}
/**
* Create a new response tracker
*/
function createResponseTracker() {
const tracker = {
sequenceNumber: 0,
items: [],
currentMessage: null,
currentContentIndex: 0,
currentReasoning: null,
currentReasoningContentIndex: 0,
functionCalls: new Map(),
functionCallOutputs: new Map(),
accumulatedText: '',
accumulatedReasoningText: '',
accumulatedArguments: new Map(),
usage: {
inputTokens: 0,
outputTokens: 0,
reasoningTokens: 0,
cachedTokens: 0,
},
status: 'in_progress',
nextSequence: () => tracker.sequenceNumber++,
};
return tracker;
}
/* =============================================================================
* SSE EVENT WRITING
* ============================================================================= */
/**
* Write a semantic SSE event to the response.
* The `event:` field matches the `type` in the data payload.
*/
function writeEvent(res, event) {
res.write(`event: ${event.type}\n`);
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
/**
* Write the terminal [DONE] event
*/
function writeDone(res) {
res.write('data: [DONE]\n\n');
}
/* =============================================================================
* RESPONSE BUILDING
* ============================================================================= */
/**
* Build a Response object from context and tracker
* Includes all required fields per Open Responses spec
*/
function buildResponse(context, tracker, status = 'in_progress') {
var _a, _b;
const isCompleted = status === 'completed';
return {
// Required fields
id: context.responseId,
object: 'response',
created_at: context.createdAt,
completed_at: isCompleted ? Math.floor(Date.now() / 1000) : null,
status,
incomplete_details: null,
model: context.model,
previous_response_id: (_a = context.previousResponseId) !== null && _a !== void 0 ? _a : null,
instructions: (_b = context.instructions) !== null && _b !== void 0 ? _b : null,
output: tracker.items,
error: null,
tools: [],
tool_choice: 'auto',
truncation: 'disabled',
parallel_tool_calls: true,
text: { format: { type: 'text' } },
temperature: 1,
top_p: 1,
presence_penalty: 0,
frequency_penalty: 0,
top_logprobs: 0,
reasoning: null,
user: null,
usage: isCompleted
? {
input_tokens: tracker.usage.inputTokens,
output_tokens: tracker.usage.outputTokens,
total_tokens: tracker.usage.inputTokens + tracker.usage.outputTokens,
input_tokens_details: { cached_tokens: tracker.usage.cachedTokens },
output_tokens_details: { reasoning_tokens: tracker.usage.reasoningTokens },
}
: null,
max_output_tokens: null,
max_tool_calls: null,
store: false,
background: false,
service_tier: 'default',
metadata: {},
safety_identifier: null,
prompt_cache_key: null,
};
}
/* =============================================================================
* ITEM BUILDERS
* ============================================================================= */
let itemIdCounter = 0;
/**
* Generate a unique item ID
*/
function generateItemId(prefix) {
return `${prefix}_${Date.now().toString(36)}${(itemIdCounter++).toString(36)}`;
}
/**
* Create a new message item
*/
function createMessageItem(status = 'in_progress') {
return {
type: 'message',
id: generateItemId('msg'),
role: 'assistant',
status,
content: [],
};
}
/**
* Create a new function call item
*/
function createFunctionCallItem(callId, name, status = 'in_progress') {
return {
type: 'function_call',
id: generateItemId('fc'),
call_id: callId,
name,
arguments: '',
status,
};
}
/**
* Create a new function call output item
*/
function createFunctionCallOutputItem(callId, output, status = 'completed') {
return {
type: 'function_call_output',
id: generateItemId('fco'),
call_id: callId,
output,
status,
};
}
/**
* Create a new reasoning item
*/
function createReasoningItem(status = 'in_progress') {
return {
type: 'reasoning',
id: generateItemId('reason'),
status,
content: [],
summary: [],
};
}
/**
* Create output text content
*/
function createOutputTextContent(text = '') {
return {
type: 'output_text',
text,
annotations: [],
logprobs: [],
};
}
/**
* Create reasoning text content
*/
function createReasoningTextContent(text = '') {
return {
type: 'reasoning_text',
text,
};
}
/**
* Emit response.created event
* This is the first event emitted per the Open Responses spec
*/
function emitResponseCreated(config) {
const { res, context, tracker } = config;
const response = buildResponse(context, tracker, 'in_progress');
writeEvent(res, {
type: 'response.created',
sequence_number: tracker.nextSequence(),
response,
});
}
/**
* Emit response.in_progress event
*/
function emitResponseInProgress(config) {
const { res, context, tracker } = config;
const response = buildResponse(context, tracker, 'in_progress');
writeEvent(res, {
type: 'response.in_progress',
sequence_number: tracker.nextSequence(),
response,
});
}
/**
* Emit response.completed event
*/
function emitResponseCompleted(config) {
const { res, context, tracker } = config;
tracker.status = 'completed';
const response = buildResponse(context, tracker, 'completed');
writeEvent(res, {
type: 'response.completed',
sequence_number: tracker.nextSequence(),
response,
});
}
/**
* Emit response.failed event
*/
function emitResponseFailed(config, error) {
const { res, context, tracker } = config;
tracker.status = 'failed';
const response = buildResponse(context, tracker, 'failed');
response.error = {
type: error.type,
message: error.message,
code: error.code,
};
writeEvent(res, {
type: 'response.failed',
sequence_number: tracker.nextSequence(),
response,
});
}
/**
* Emit response.output_item.added event for a message
*/
function emitMessageItemAdded(config) {
const { res, tracker } = config;
const item = createMessageItem('in_progress');
tracker.currentMessage = item;
tracker.currentContentIndex = 0;
tracker.accumulatedText = '';
tracker.items.push(item);
writeEvent(res, {
type: 'response.output_item.added',
sequence_number: tracker.nextSequence(),
output_index: tracker.items.length - 1,
item,
});
return item;
}
/**
* Emit response.output_item.done event for a message
*/
function emitMessageItemDone(config) {
const { res, tracker } = config;
if (!tracker.currentMessage) {
return;
}
tracker.currentMessage.status = 'completed';
const outputIndex = tracker.items.indexOf(tracker.currentMessage);
writeEvent(res, {
type: 'response.output_item.done',
sequence_number: tracker.nextSequence(),
output_index: outputIndex,
item: tracker.currentMessage,
});
tracker.currentMessage = null;
}
/**
* Emit response.content_part.added for text content
*/
function emitTextContentPartAdded(config) {
const { res, tracker } = config;
if (!tracker.currentMessage) {
return;
}
const part = createOutputTextContent('');
tracker.currentMessage.content.push(part);
const outputIndex = tracker.items.indexOf(tracker.currentMessage);
writeEvent(res, {
type: 'response.content_part.added',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentMessage.id,
output_index: outputIndex,
content_index: tracker.currentContentIndex,
part,
});
}
/**
* Emit response.output_text.delta event
*/
function emitOutputTextDelta(config, delta) {
const { res, tracker } = config;
if (!tracker.currentMessage) {
return;
}
tracker.accumulatedText += delta;
const outputIndex = tracker.items.indexOf(tracker.currentMessage);
writeEvent(res, {
type: 'response.output_text.delta',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentMessage.id,
output_index: outputIndex,
content_index: tracker.currentContentIndex,
delta,
logprobs: [],
});
}
/**
* Emit response.output_text.done event
*/
function emitOutputTextDone(config) {
const { res, tracker } = config;
if (!tracker.currentMessage) {
return;
}
const outputIndex = tracker.items.indexOf(tracker.currentMessage);
const contentIndex = tracker.currentContentIndex;
// Update the content part with final text
if (tracker.currentMessage.content[contentIndex]) {
tracker.currentMessage.content[contentIndex].text =
tracker.accumulatedText;
}
writeEvent(res, {
type: 'response.output_text.done',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentMessage.id,
output_index: outputIndex,
content_index: contentIndex,
text: tracker.accumulatedText,
logprobs: [],
});
}
/**
* Emit response.content_part.done for text content
*/
function emitTextContentPartDone(config) {
const { res, tracker } = config;
if (!tracker.currentMessage) {
return;
}
const outputIndex = tracker.items.indexOf(tracker.currentMessage);
const contentIndex = tracker.currentContentIndex;
const part = tracker.currentMessage.content[contentIndex];
if (part) {
writeEvent(res, {
type: 'response.content_part.done',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentMessage.id,
output_index: outputIndex,
content_index: contentIndex,
part,
});
}
tracker.currentContentIndex++;
}
/* =============================================================================
* FUNCTION CALL EVENT EMITTERS
* ============================================================================= */
/**
* Emit response.output_item.added for a function call
*/
function emitFunctionCallItemAdded(config, callId, name) {
const { res, tracker } = config;
const item = createFunctionCallItem(callId, name, 'in_progress');
tracker.functionCalls.set(callId, item);
tracker.accumulatedArguments.set(callId, '');
tracker.items.push(item);
writeEvent(res, {
type: 'response.output_item.added',
sequence_number: tracker.nextSequence(),
output_index: tracker.items.length - 1,
item,
});
return item;
}
/**
* Emit response.function_call_arguments.delta event
*/
function emitFunctionCallArgumentsDelta(config, callId, delta) {
var _a;
const { res, tracker } = config;
const item = tracker.functionCalls.get(callId);
if (!item) {
return;
}
const accumulated = ((_a = tracker.accumulatedArguments.get(callId)) !== null && _a !== void 0 ? _a : '') + delta;
tracker.accumulatedArguments.set(callId, accumulated);
item.arguments = accumulated;
const outputIndex = tracker.items.indexOf(item);
writeEvent(res, {
type: 'response.function_call_arguments.delta',
sequence_number: tracker.nextSequence(),
item_id: item.id,
output_index: outputIndex,
call_id: callId,
delta,
});
}
/**
* Emit response.function_call_arguments.done event
*/
function emitFunctionCallArgumentsDone(config, callId) {
var _a;
const { res, tracker } = config;
const item = tracker.functionCalls.get(callId);
if (!item) {
return;
}
const outputIndex = tracker.items.indexOf(item);
const args = (_a = tracker.accumulatedArguments.get(callId)) !== null && _a !== void 0 ? _a : '';
writeEvent(res, {
type: 'response.function_call_arguments.done',
sequence_number: tracker.nextSequence(),
item_id: item.id,
output_index: outputIndex,
call_id: callId,
arguments: args,
});
}
/**
* Emit response.output_item.done for a function call
*/
function emitFunctionCallItemDone(config, callId) {
const { res, tracker } = config;
const item = tracker.functionCalls.get(callId);
if (!item) {
return;
}
item.status = 'completed';
const outputIndex = tracker.items.indexOf(item);
writeEvent(res, {
type: 'response.output_item.done',
sequence_number: tracker.nextSequence(),
output_index: outputIndex,
item,
});
}
/**
* Emit function call output item (internal tool result)
*/
function emitFunctionCallOutputItem(config, callId, output) {
const { res, tracker } = config;
const item = createFunctionCallOutputItem(callId, output, 'completed');
tracker.functionCallOutputs.set(callId, item);
tracker.items.push(item);
// Emit added
writeEvent(res, {
type: 'response.output_item.added',
sequence_number: tracker.nextSequence(),
output_index: tracker.items.length - 1,
item,
});
// Immediately emit done since it's already complete
writeEvent(res, {
type: 'response.output_item.done',
sequence_number: tracker.nextSequence(),
output_index: tracker.items.length - 1,
item,
});
}
/* =============================================================================
* REASONING EVENT EMITTERS
* ============================================================================= */
/**
* Emit response.output_item.added for reasoning
*/
function emitReasoningItemAdded(config) {
const { res, tracker } = config;
const item = createReasoningItem('in_progress');
tracker.currentReasoning = item;
tracker.currentReasoningContentIndex = 0;
tracker.accumulatedReasoningText = '';
tracker.items.push(item);
writeEvent(res, {
type: 'response.output_item.added',
sequence_number: tracker.nextSequence(),
output_index: tracker.items.length - 1,
item,
});
return item;
}
/**
* Emit response.content_part.added for reasoning
*/
function emitReasoningContentPartAdded(config) {
const { res, tracker } = config;
if (!tracker.currentReasoning) {
return;
}
const part = createReasoningTextContent('');
if (!tracker.currentReasoning.content) {
tracker.currentReasoning.content = [];
}
tracker.currentReasoning.content.push(part);
const outputIndex = tracker.items.indexOf(tracker.currentReasoning);
writeEvent(res, {
type: 'response.content_part.added',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentReasoning.id,
output_index: outputIndex,
content_index: tracker.currentReasoningContentIndex,
part,
});
}
/**
* Emit response.reasoning.delta event
*/
function emitReasoningDelta(config, delta) {
const { res, tracker } = config;
if (!tracker.currentReasoning) {
return;
}
tracker.accumulatedReasoningText += delta;
const outputIndex = tracker.items.indexOf(tracker.currentReasoning);
writeEvent(res, {
type: 'response.reasoning.delta',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentReasoning.id,
output_index: outputIndex,
content_index: tracker.currentReasoningContentIndex,
delta,
});
}
/**
* Emit response.reasoning.done event
*/
function emitReasoningDone(config) {
const { res, tracker } = config;
if (!tracker.currentReasoning || !tracker.currentReasoning.content) {
return;
}
const outputIndex = tracker.items.indexOf(tracker.currentReasoning);
const contentIndex = tracker.currentReasoningContentIndex;
// Update the content part with final text
if (tracker.currentReasoning.content[contentIndex]) {
tracker.currentReasoning.content[contentIndex].text =
tracker.accumulatedReasoningText;
}
writeEvent(res, {
type: 'response.reasoning.done',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentReasoning.id,
output_index: outputIndex,
content_index: contentIndex,
text: tracker.accumulatedReasoningText,
});
}
/**
* Emit response.content_part.done for reasoning
*/
function emitReasoningContentPartDone(config) {
const { res, tracker } = config;
if (!tracker.currentReasoning || !tracker.currentReasoning.content) {
return;
}
const outputIndex = tracker.items.indexOf(tracker.currentReasoning);
const contentIndex = tracker.currentReasoningContentIndex;
const part = tracker.currentReasoning.content[contentIndex];
if (part) {
writeEvent(res, {
type: 'response.content_part.done',
sequence_number: tracker.nextSequence(),
item_id: tracker.currentReasoning.id,
output_index: outputIndex,
content_index: contentIndex,
part,
});
}
tracker.currentReasoningContentIndex++;
}
/**
* Emit response.output_item.done for reasoning
*/
function emitReasoningItemDone(config) {
const { res, tracker } = config;
if (!tracker.currentReasoning) {
return;
}
tracker.currentReasoning.status = 'completed';
const outputIndex = tracker.items.indexOf(tracker.currentReasoning);
writeEvent(res, {
type: 'response.output_item.done',
sequence_number: tracker.nextSequence(),
output_index: outputIndex,
item: tracker.currentReasoning,
});
tracker.currentReasoning = null;
}
/* =============================================================================
* ERROR HANDLING
* ============================================================================= */
/**
* Emit error event
*/
function emitError(config, error) {
const { res, tracker } = config;
writeEvent(res, {
type: 'error',
sequence_number: tracker.nextSequence(),
error: {
type: error.type,
message: error.message,
code: error.code,
},
});
}
/**
* Emit librechat:attachment event for file/image attachments
* This is a LibreChat extension to the Open Responses streaming protocol.
* External clients can safely ignore these events.
*/
function emitAttachment(config, attachment, options) {
const { res, tracker } = config;
writeEvent(res, {
type: 'librechat:attachment',
sequence_number: tracker.nextSequence(),
attachment,
message_id: options === null || options === void 0 ? void 0 : options.messageId,
conversation_id: options === null || options === void 0 ? void 0 : options.conversationId,
});
}
/**
* Write attachment event directly to response (for use outside streaming context)
* Useful when attachment processing happens asynchronously
*/
function writeAttachmentEvent(res, sequenceNumber, attachment, options) {
writeEvent(res, {
type: 'librechat:attachment',
sequence_number: sequenceNumber,
attachment,
message_id: options === null || options === void 0 ? void 0 : options.messageId,
conversation_id: options === null || options === void 0 ? void 0 : options.conversationId,
});
}
/* =============================================================================
* NON-STREAMING RESPONSE BUILDER
* ============================================================================= */
/**
* Build a complete non-streaming response
*/
function buildResponsesNonStreamingResponse(context, tracker) {
return buildResponse(context, tracker, 'completed');
}
/**
* Update tracker usage from collected data
*/
function updateTrackerUsage(tracker, usage) {
if (usage.promptTokens != null) {
tracker.usage.inputTokens = usage.promptTokens;
}
if (usage.completionTokens != null) {
tracker.usage.outputTokens = usage.completionTokens;
}
if (usage.reasoningTokens != null) {
tracker.usage.reasoningTokens = usage.reasoningTokens;
}
if (usage.cachedTokens != null) {
tracker.usage.cachedTokens = usage.cachedTokens;
}
}
/* =============================================================================
* REQUEST VALIDATION
* ============================================================================= */
/**
* Validate a request body
*/
function validateResponseRequest(body) {
if (!body || typeof body !== 'object') {
return { valid: false, error: 'Request body is required' };
}
const request = body;
// Required: model
if (!request.model || typeof request.model !== 'string') {
return { valid: false, error: 'model is required and must be a string' };
}
// Required: input (string or array)
if (request.input === undefined || request.input === null) {
return { valid: false, error: 'input is required' };
}
if (typeof request.input !== 'string' && !Array.isArray(request.input)) {
return { valid: false, error: 'input must be a string or array of items' };
}
// Optional validations
if (request.stream !== undefined && typeof request.stream !== 'boolean') {
return { valid: false, error: 'stream must be a boolean' };
}
if (request.temperature !== undefined) {
const temp = request.temperature;
if (typeof temp !== 'number' || temp < 0 || temp > 2) {
return { valid: false, error: 'temperature must be a number between 0 and 2' };
}
}
if (request.max_output_tokens !== undefined) {
if (typeof request.max_output_tokens !== 'number' || request.max_output_tokens < 1) {
return { valid: false, error: 'max_output_tokens must be a positive number' };
}
}
return { valid: true, request: request };
}
/**
* Check if validation failed
*/
function isValidationFailure(result) {
return !result.valid;
}
/**
* Convert Open Responses input to internal message format.
* Handles both string input and array of items.
*/
function convertInputToMessages(input) {
// Simple string input becomes a user message
if (typeof input === 'string') {
return [{ role: 'user', content: input }];
}
const messages = [];
for (const item of input) {
if (item.type === 'item_reference') {
// Skip item references - they're handled by previous_response_id
continue;
}
if (item.type === 'message') {
const messageItem = item;
let content;
if (typeof messageItem.content === 'string') {
content = messageItem.content;
}
else if (Array.isArray(messageItem.content)) {
content = messageItem.content
.filter((part) => part != null)
.map((part) => {
var _a, _b, _c;
if (part.type === 'input_text' || part.type === 'output_text') {
return { type: 'text', text: (_a = part.text) !== null && _a !== void 0 ? _a : '' };
}
if (part.type === 'refusal') {
return { type: 'text', text: (_b = part.refusal) !== null && _b !== void 0 ? _b : '' };
}
if (part.type === 'input_image') {
return {
type: 'image_url',
image_url: {
url: part.image_url,
detail: part.detail,
},
};
}
if (part.type === 'input_file') {
const filePart = part;
return { type: 'text', text: `[File: ${(_c = filePart.filename) !== null && _c !== void 0 ? _c : 'unknown'}]` };
}
return null;
})
.filter((part) => part != null);
}
else {
content = '';
}
// Map developer role to system (LibreChat convention)
let role;
if (messageItem.role === 'developer') {
role = 'system';
}
else if (messageItem.role === 'user') {
role = 'user';
}
else if (messageItem.role === 'assistant') {
role = 'assistant';
}
else if (messageItem.role === 'system') {
role = 'system';
}
else {
role = 'user';
}
messages.push({ role, content });
}
if (item.type === 'function_call') {
// Function call items represent prior tool calls from assistant
const fcItem = item;
// Add as assistant message with tool_calls
messages.push({
role: 'assistant',
content: '',
tool_calls: [
{
id: fcItem.call_id,
type: 'function',
function: { name: fcItem.name, arguments: fcItem.arguments },
},
],
});
}
if (item.type === 'function_call_output') {
// Function call output items represent tool results
const fcoItem = item;
messages.push({
role: 'tool',
content: fcoItem.output,
tool_call_id: fcoItem.call_id,
});
}
// Reasoning items are typically not passed back as input
// They're model-generated and may be encrypted
}
return messages;
}
/**
* Merge previous conversation messages with new input
*/
function mergeMessagesWithInput(previousMessages, newInput) {
return [...previousMessages, ...newInput];
}
/* =============================================================================
* ERROR RESPONSE
* ============================================================================= */
/**
* Send an error response in Open Responses format
*/
function sendResponsesErrorResponse(res, statusCode, message, type = 'invalid_request', code) {
res.status(statusCode).json({
error: {
type,
message,
code: code !== null && code !== void 0 ? code : null,
param: null,
},
});
}
/* =============================================================================
* RESPONSE CONTEXT
* ============================================================================= */
/**
* Generate a unique response ID
*/
function generateResponseId() {
return `resp_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 8)}`;
}
/**
* Create a response context from request
*/
function createResponseContext(request, responseId) {
return {
responseId: responseId !== null && responseId !== void 0 ? responseId : generateResponseId(),
model: request.model,
createdAt: Math.floor(Date.now() / 1000),
previousResponseId: request.previous_response_id,
instructions: request.instructions,
};
}
/* =============================================================================
* STREAMING SETUP
* ============================================================================= */
/**
* Set up streaming response headers
*/
function setupStreamingResponse(res) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
}
/**
* Create LibreChat event handlers that emit Open Responses events
*/
function createResponsesEventHandlers(config) {
const state = {
messageStarted: false,
messageContentStarted: false,
reasoningStarted: false,
reasoningContentStarted: false,
activeToolCalls: new Set(),
completedToolCalls: new Set(),
};
/**
* Ensure message item is started
*/
const ensureMessageStarted = () => {
if (!state.messageStarted) {
emitMessageItemAdded(config);
state.messageStarted = true;
}
};
/**
* Ensure message content part is started
*/
const ensureMessageContentStarted = () => {
ensureMessageStarted();
if (!state.messageContentStarted) {
emitTextContentPartAdded(config);
state.messageContentStarted = true;
}
};
/**
* Ensure reasoning item is started
*/
const ensureReasoningStarted = () => {
if (!state.reasoningStarted) {
emitReasoningItemAdded(config);
state.reasoningStarted = true;
}
};
/**
* Ensure reasoning content part is started
*/
const ensureReasoningContentStarted = () => {
ensureReasoningStarted();
if (!state.reasoningContentStarted) {
emitReasoningContentPartAdded(config);
state.reasoningContentStarted = true;
}
};
/**
* Close any open content streams
*/
const closeOpenStreams = () => {
// Close message content if open
if (state.messageContentStarted) {
emitOutputTextDone(config);
emitTextContentPartDone(config);
state.messageContentStarted = false;
}
// Close message item if open
if (state.messageStarted) {
emitMessageItemDone(config);
state.messageStarted = false;
}
// Close reasoning content if open
if (state.reasoningContentStarted) {
emitReasoningDone(config);
emitReasoningContentPartDone(config);
state.reasoningContentStarted = false;
}
// Close reasoning item if open
if (state.reasoningStarted) {
emitReasoningItemDone(config);
state.reasoningStarted = false;
}
};
const handlers = {
/**
* Handle text message deltas
*/
on_message_delta: {
handle: (_event, data) => {
var _a;
const deltaData = data;
const content = (_a = deltaData === null || deltaData === void 0 ? void 0 : deltaData.delta) === null || _a === void 0 ? void 0 : _a.content;
if (Array.isArray(content)) {
for (const part of content) {
if (part.type === 'text' && part.text) {
ensureMessageContentStarted();
emitOutputTextDelta(config, part.text);
}
}
}
},
},
/**
* Handle reasoning deltas
*/
on_reasoning_delta: {
handle: (_event, data) => {
var _a;
const deltaData = data;
const content = (_a = deltaData === null || deltaData === void 0 ? void 0 : deltaData.delta) === null || _a === void 0 ? void 0 : _a.content;
if (Array.isArray(content)) {
for (const part of content) {
const text = part.think || part.text;
if (text) {
ensureReasoningContentStarted();
emitReasoningDelta(config, text);
}
}
}
},
},
/**
* Handle run step (tool call initiation)
*/
on_run_step: {
handle: (_event, data) => {
var _a, _b;
const stepData = data;
const stepDetails = stepData === null || stepData === void 0 ? void 0 : stepData.stepDetails;
if ((stepDetails === null || stepDetails === void 0 ? void 0 : stepDetails.type) === 'tool_calls' && stepDetails.tool_calls) {
// Close any open message/reasoning before tool calls
closeOpenStreams();
for (const tc of stepDetails.tool_calls) {
const callId = (_a = tc.id) !== null && _a !== void 0 ? _a : '';
const name = (_b = tc.name) !== null && _b !== void 0 ? _b : '';
if (callId && !state.activeToolCalls.has(callId)) {
state.activeToolCalls.add(callId);
emitFunctionCallItemAdded(config, callId, name);
}
}
}
},
},
/**
* Handle run step delta (tool call argument streaming)
*/
on_run_step_delta: {
handle: (_event, data) => {
var _a, _b;
const deltaData = data;
const delta = deltaData === null || deltaData === void 0 ? void 0 : deltaData.delta;
if ((delta === null || delta === void 0 ? void 0 : delta.type) === 'tool_calls' && delta.tool_calls) {
for (const tc of delta.tool_calls) {
const args = (_a = tc.args) !== null && _a !== void 0 ? _a : '';
if (!args) {
continue;
}
// Find the call_id for this tool call by index
const toolCallsArray = Array.from(state.activeToolCalls);
const callId = toolCallsArray[(_b = tc.index) !== null && _b !== void 0 ? _b : 0];
if (callId) {
emitFunctionCallArgumentsDelta(config, callId, args);
}
}
}
},
},
/**
* Handle tool end (tool execution complete)
*/
on_tool_end: {
handle: (_event, data) => {
var _a;
const toolData = data;
const callId = toolData === null || toolData === void 0 ? void 0 : toolData.tool_call_id;
const output = (_a = toolData === null || toolData === void 0 ? void 0 : toolData.output) !== null && _a !== void 0 ? _a : '';
if (callId && state.activeToolCalls.has(callId) && !state.completedToolCalls.has(callId)) {
state.completedToolCalls.add(callId);
// Complete the function call item
emitFunctionCallArgumentsDone(config, callId);
emitFunctionCallItemDone(config, callId);
// Emit the function call output (internal tool result)
emitFunctionCallOutputItem(config, callId, output);
}
},
},
/**
* Handle chat model end (usage collection)
*/
on_chat_model_end: {
handle: (_event, data) => {
var _a, _b, _c, _d;
const endData = data;
const usage = (_a = endData === null || endData === void 0 ? void 0 : endData.output) === null || _a === void 0 ? void 0 : _a.usage_metadata;
if (usage) {
// Extract cached tokens from either OpenAI or Anthropic format
const cachedTokens = ((_c = (_b = usage.input_token_details) === null || _b === void 0 ? void 0 : _b.cache_read) !== null && _c !== void 0 ? _c : 0) + ((_d = usage.cache_read_input_tokens) !== null && _d !== void 0 ? _d : 0);
updateTrackerUsage(config.tracker, {
promptTokens: usage.input_tokens,
completionTokens: usage.output_tokens,
cachedTokens,
});
}
},
},
};
/**
* Finalize the stream - close open items and emit completed
*/
const finalizeStream = () => {
closeOpenStreams();
emitResponseCompleted(config);
writeDone(config.res);
};
return { handlers, state, finalizeStream };
}
/**
* Create an aggregator for non-streaming responses
*/
function createResponseAggregator() {
const aggregator = {
textChunks: [],
reasoningChunks: [],
toolCalls: new Map(),
toolOutputs: new Map(),
usage: {
inputTokens: 0,
outputTokens: 0,
reasoningTokens: 0,
cachedTokens: 0,
},
addText: (text) => {
aggregator.textChunks.push(text);
},
addReasoning: (text) => {
aggregator.reasoningChunks.push(text);
},
getText: () => aggregator.textChunks.join(''),
getReasoning: () => aggregator.reasoningChunks.join(''),
};
return aggregator;
}
/**
* Build a non-streaming response from aggregator
* Includes all required fields per Open Responses spec
*/
function buildAggregatedResponse(context, aggregator) {
var _a, _b;
const output = [];
// Add reasoning item if present
const reasoningText = aggregator.getReasoning();
if (reasoningText) {
output.push({
type: 'reasoning',
id: `reason_${Date.now().toString(36)}`,
status: 'completed',
content: [{ type: 'reasoning_text', text: reasoningText }],
summary: [],
});
}
// Add function calls and outputs
for (const [callId, tc] of aggregator.toolCalls) {
output.push({
type: 'function_call',
id: `fc_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 6)}`,
call_id: callId,
name: tc.name,
arguments: tc.arguments,
status: 'completed',
});
const toolOutput = aggregator.toolOutputs.get(callId);
if (toolOutput) {
output.push({
type: 'function_call_output',
id: `fco_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 6)}`,
call_id: callId,
output: toolOutput,
status: 'completed',
});
}
}
// Add message item if there's text (or always add one if no other output)
const text = aggregator.getText();
if (text || output.length === 0) {
output.push({
type: 'message',
id: `msg_${Date.now().toString(36)}`,
role: 'assistant',
status: 'completed',
content: text ? [{ type: 'output_text', text, annotations: [], logprobs: [] }] : [],
});
}
return {
// Required fields per Open Responses spec
id: context.responseId,
object: 'response',
created_at: context.createdAt,
completed_at: Math.floor(Date.now() / 1000),
status: 'completed',
incomplete_details: null,
model: context.model,
previous_response_id: (_a = context.previousResponseId) !== null && _a !== void 0 ? _a : null,
instructions: (_b = context.instructions) !== null && _b !== void 0 ? _b : null,
output,
error: null,
tools: [],
tool_choice: 'auto',
truncation: 'disabled',
parallel_tool_calls: true,
text: { format: { type: 'text' } },
temperature: 1,
top_p: 1,
presence_penalty: 0,
frequency_penalty: 0,
top_logprobs: 0,
reasoning: null,
user: null,
usage: {
input_tokens: aggregator.usage.inputTokens,
output_tokens: aggregator.usage.outputTokens,
total_tokens: aggregator.usage.inputTokens + aggregator.usage.outputTokens,
input_tokens_details: { cached_tokens: aggregator.usage.cachedTokens },
output_tokens_details: { reasoning_tokens: aggregator.usage.reasoningTokens },
},
max_output_tokens: null,
max_tool_calls: null,
store: false,
background: false,
service_tier: 'default',
metadata: {},
safety_identifier: null,
prompt_cache_key: null,
};
}
/**
* Create event handlers for non-streaming aggregation
*/
function createAggregatorEventHandlers(aggregator) {
const activeToolCalls = new Set();
return {
on_message_delta: {
handle: (_event, data) => {
var _a;
const deltaData = data;
const content = (_a = deltaData === null || deltaData === void 0 ? void 0 : deltaData.delta) === null || _a === void 0 ? void 0 : _a.content;
if (Array.isArray(content)) {
for (const part of content) {
if (part.type === 'text' && part.text) {
aggregator.addText(part.text);
}
}
}
},
},
on_reasoning_delta: {
handle: (_event, data) => {
var _a;
const deltaData = data;
const content = (_a = deltaData === null || deltaData === void 0 ? void 0 : deltaData.delta) === null || _a === void 0 ? void 0 : _a.content;
if (Array.isArray(content)) {
for (const part of content) {
const text = part.think || part.text;
if (text) {
aggregator.addReasoning(text);
}
}
}
},
},
on_run_step: {
handle: (_event, data) => {
var _a, _b;
const stepData = data;
const stepDetails = stepData === null || stepData === void 0 ? void 0 : stepData.stepDetails;
if ((stepDetails === null || stepDetails === void 0 ? void 0 : stepDetails.type) === 'tool_calls' && stepDetails.tool_calls) {
for (const tc of stepDetails.tool_calls) {
const callId = (_a = tc.id) !== null && _a !== void 0 ? _a : '';
const name = (_b = tc.name) !== null && _b !== void 0 ? _b : '';
if (callId && !activeToolCalls.has(callId)) {
activeToolCalls.add(callId);
aggregator.toolCalls.set(callId, { id: callId, name, arguments: '' });
}
}
}
},
},
on_run_step_delta: {
handle: (_event, data) => {
var _a, _b;
const deltaData = data;
const delta = deltaData === null || deltaData === void 0 ? void 0 : deltaData.delta;
if ((delta === null || delta === void 0 ? void 0 : delta.type) === 'tool_calls' && delta.tool_calls) {
for (const tc of delta.tool_calls) {
const args = (_a = tc.args) !== null && _a !== void 0 ? _a : '';
if (!args) {
continue;
}
const toolCallsArray = Array.from(activeToolCalls);
const callId = toolCallsArray[(_b = tc.index) !== null && _b !== void 0 ? _b : 0];
if (callId) {
const existing = aggregator.toolCalls.get(callId);
if (existing) {
existing.arguments += args;
}
}
}
}
},
},
on_tool_end: {
handle: (_event, data) => {
var _a;
const toolData = data;
const callId = toolData === null || toolData === void 0 ? void 0 : toolData.tool_call_id;
const output = (_a = toolData === null || toolData === void 0 ? void 0 : toolData.output) !== null && _a !== void 0 ? _a : '';
if (callId) {
aggregator.toolOutputs.set(callId, output);
}
},
},
on_chat_model_end: {
handle: (_event, data) => {
var _a, _b, _c, _d, _e, _f;
const endData = data;
const usage = (_a = endData === null || endData === void 0 ? void 0 : endData.output) === null || _a === void 0 ? void 0 : _a.usage_metadata;
if (usage) {
aggregator.usage.inputTokens = (_b = usage.input_tokens) !== null && _b !== void 0 ? _b : 0;
aggregator.usage.outputTokens = (_c = usage.output_tokens) !== null && _c !== void 0 ? _c : 0;
// Extract cached tokens from either OpenAI or Anthropic format
aggregator.usage.cachedTokens =
((_e = (_d = usage.input_token_details) === null || _d === void 0 ? void 0 : _d.cache_read) !== null && _e !== void 0 ? _e : 0) + ((_f = usage.cache_read_input_tokens) !== null && _f !== void 0 ? _f : 0);
}
},
},
};
}
/**
* Parses tool names from JSON-formatted tool_search output.
* Format: { "found": N, "tools": [{ "name": "tool_name", ... }], ... }
*
* @param content - The JSON string content
* @param discoveredTools - Set to add discovered tool names to
* @returns true if parsing succeeded, false otherwise
*/
function parseToolSearchJson(content, discoveredTools) {
try {
const parsed = JSON.parse(content);
if (!parsed.tools || !Array.isArray(parsed.tools)) {
return false;
}
for (const tool of parsed.tools) {
if (tool.name && typeof tool.name === 'string') {
discoveredTools.add(tool.name);
}
}
return parsed.tools.length > 0;
}
catch (_a) {
return false;
}
}
/**
* Parses tool names from legacy text-formatted tool_search output.
* Format: "- tool_name (score: X.XX)"
*
* @param content - The text content
* @param discoveredTools - Set to add discovered tool names to
*/
function parseToolSearchLegacy(content, discoveredTools) {
const toolNameRegex = /^- ([^\s(]+)\s*\(score:/gm;
let match;
while ((match = toolNameRegex.exec(content)) !== null) {
const toolName = match[1];
if (toolName) {
discoveredTools.add(toolName);
}
}
}
/**
* Extracts discovered tool names from message history by parsing tool_search results.
* When the LLM calls tool_search, the result contains tool names that were discovered.
* These tools should have defer_loading overridden to false on subsequent turns.
*
* Supports both:
* - New JSON format: { "tools": [{ "name": "tool_name" }] }
* - Legacy text format: "- tool_name (score: X.XX)"
*
* @param messages - The conversation message history
* @returns Set of tool names that were discovered via tool_search
*/
function extractDiscoveredToolsFromHistory(messages) {
var _a, _b, _c, _d;
const discoveredTools = new Set();
for (const message of messages) {
const msgType = (_d = (_b = (_a = message._getType) === null || _a === void 0 ? void 0 : _a.call(message)) !== null && _b !== void 0 ? _b : (_c = message.constructor) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '';
if (msgType !== 'tool') {
continue;
}
const name = message.name;
if (name !== agents.Constants.TOOL_SEARCH) {
continue;
}
const content = message.content;
if (typeof content !== 'string') {
continue;
}
/** Try JSON format first (new), fall back to regex (legacy) */
if (!parseToolSearchJson(content, discoveredTools)) {
parseToolSearchLegacy(content, discoveredTools);
}
}
return discoveredTools;
}
/**
* Overrides defer_loading to false for tools that were already discovered via tool_search.
* This prevents the LLM from having to re-discover tools on every turn.
*
* @param toolRegistry - The tool registry to modify (mutated in place)
* @param discoveredTools - Set of tool names that were previously discovered
* @returns Number of tools that had defer_loading overridden
*/
function overrideDeferLoadingForDiscoveredTools(toolRegistry, discoveredTools) {
let overrideCount = 0;
for (const toolName of discoveredTools) {
const toolDef = toolRegistry.get(toolName);
if (toolDef && toolDef.defer_loading === true) {
toolDef.defer_loading = false;
overrideCount++;
}
}
return overrideCount;
}
const customProviders = new Set([
agents.Providers.XAI,
agents.Providers.DEEPSEEK,
agents.Providers.MOONSHOT,
agents.Providers.OPENROUTER,
librechatDataProvider.KnownEndpoints.ollama,
]);
function getReasoningKey(provider, llmConfig, agentEndpoint) {
var _a, _b;
let reasoningKey = 'reasoning_content';
if (provider === agents.Providers.GOOGLE) {
reasoningKey = 'reasoning';
}
else if (((_b = (_a = llmConfig.configuration) === null || _a === void 0 ? void 0 : _a.baseURL) === null || _b === void 0 ? void 0 : _b.includes(librechatDataProvider.KnownEndpoints.openrouter)) ||
(agentEndpoint && agentEndpoint.toLowerCase().includes(librechatDataProvider.KnownEndpoints.openrouter))) {
reasoningKey = 'reasoning';
}
else if (llmConfig.useResponsesApi === true &&
(provider === agents.Providers.OPENAI || provider === agents.Providers.AZURE)) {
reasoningKey = 'reasoning';
}
return reasoningKey;
}
/**
* Creates a new Run instance with custom handlers and configuration.
*
* @param options - The options for creating the Run instance.
* @param options.agents - The agents for this run.
* @param options.signal - The signal for this run.
* @param options.runId - Optional run ID; otherwise, a new run ID will be generated.
* @param options.customHandlers - Custom event handlers.
* @param options.streaming - Whether to use streaming.
* @param options.streamUsage - Whether to stream usage information.
* @param options.messages - Optional message history to extract discovered tools from.
* When provided, tools that were previously discovered via tool_search will have
* their defer_loading overridden to false, preventing redundant re-discovery.
* @returns {Promise<Run<IState>>} A promise that resolves to a new Run instance.
*/
function createRun(_a) {
return __awaiter(this, arguments, void 0, function* ({ runId, signal, agents: agents$1, messages, requestBody, user, tokenCounter, customHandlers, indexTokenCountMap, streaming = true, streamUsage = true, }) {
var _b, _c;
/**
* Only extract discovered tools if:
* 1. We have message history to parse
* 2. At least one agent has deferred tools (using precomputed flag)
*
* This optimization avoids iterating through messages in the ~95% of cases
* where no agent uses deferred tool loading.
*/
const hasAnyDeferredTools = agents$1.some((agent) => agent.hasDeferredTools === true);
const discoveredTools = hasAnyDeferredTools && (messages === null || messages === void 0 ? void 0 : messages.length)
? extractDiscoveredToolsFromHistory(messages)
: new Set();
const agentInputs = [];
const buildAgentContext = (agent) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
const provider = (_a = librechatDataProvider.providerEndpointMap[agent.provider]) !== null && _a !== void 0 ? _a : agent.provider;
const llmConfig = Object.assign({
provider,
streaming,
streamUsage,
}, agent.model_parameters);
const systemMessage = Object.values((_b = agent.toolContextMap) !== null && _b !== void 0 ? _b : {})
.join('\n')
.trim();
const systemContent = [
systemMessage,
(_c = agent.instructions) !== null && _c !== void 0 ? _c : '',
(_d = agent.additional_instructions) !== null && _d !== void 0 ? _d : '',
]
.join('\n')
.trim();
/**
* Resolve request-based headers for Custom Endpoints. Note: if this is added to
* non-custom endpoints, needs consideration of varying provider header configs.
* This is done at this step because the request body may contain dynamic values
* that need to be resolved after agent initialization.
*/
if (((_e = llmConfig === null || llmConfig === void 0 ? void 0 : llmConfig.configuration) === null || _e === void 0 ? void 0 : _e.defaultHeaders) != null) {
llmConfig.configuration.defaultHeaders = resolveHeaders({
headers: llmConfig.configuration.defaultHeaders,
user: createSafeUser(user),
body: requestBody,
});
}
/** Resolves issues with new OpenAI usage field */
if (customProviders.has(agent.provider) ||
(agent.provider === agents.Providers.OPENAI && agent.endpoint !== agent.provider)) {
llmConfig.streamUsage = false;
llmConfig.usage = true;
}
/**
* Override defer_loading for tools that were discovered in previous turns.
* This prevents the LLM from having to re-discover tools via tool_search.
* Also add the discovered tools' definitions so the LLM has their schemas.
*/
let toolDefinitions = (_f = agent.toolDefinitions) !== null && _f !== void 0 ? _f : [];
if (discoveredTools.size > 0 && agent.toolRegistry) {
overrideDeferLoadingForDiscoveredTools(agent.toolRegistry, discoveredTools);
/** Add discovered tools' definitions so the LLM can see their schemas */
const existingToolNames = new Set(toolDefinitions.map((d) => d.name));
for (const toolName of discoveredTools) {
if (existingToolNames.has(toolName)) {
continue;
}
const toolDef = agent.toolRegistry.get(toolName);
if (toolDef) {
toolDefinitions = [...toolDefinitions, toolDef];
}
}
}
const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint);
const agentInput = {
provider,
reasoningKey,
toolDefinitions,
agentId: agent.id,
tools: agent.tools,
clientOptions: llmConfig,
instructions: systemContent,
name: (_g = agent.name) !== null && _g !== void 0 ? _g : undefined,
toolRegistry: agent.toolRegistry,
maxContextTokens: agent.maxContextTokens,
useLegacyContent: (_h = agent.useLegacyContent) !== null && _h !== void 0 ? _h : false,
discoveredTools: discoveredTools.size > 0 ? Array.from(discoveredTools) : undefined,
};
agentInputs.push(agentInput);
};
for (const agent of agents$1) {
buildAgentContext(agent);
}
const graphConfig = {
signal,
agents: agentInputs,
edges: agents$1[0].edges,
};
if (agentInputs.length > 1 || ((_c = (_b = graphConfig.edges) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0) {
graphConfig.type = 'multi-agent';
}
else {
graphConfig.type = 'standard';
}
return agents.Run.create({
runId,
graphConfig,
tokenCounter,
customHandlers,
indexTokenCountMap,
});
});
}
/**
* Builds a Set of tool names for use with formatAgentMessages.
*
* In event-driven mode, tools are defined via toolDefinitions (which includes
* deferred tools like tool_search). In legacy mode, tools come from loaded
* tool instances.
*
* This ensures tool_search and other deferred tools are included in the toolSet,
* allowing their ToolMessages to be preserved in conversation history.
*/
function buildToolSet(agentConfig) {
if (!agentConfig) {
return new Set();
}
const { toolDefinitions, tools } = agentConfig;
const toolNames = toolDefinitions && toolDefinitions.length > 0
? toolDefinitions.map((def) => def.name)
: (tools !== null && tools !== void 0 ? tools : []).map((tool) => tool === null || tool === void 0 ? void 0 : tool.name);
return new Set(toolNames.filter((name) => Boolean(name)));
}
/** Avatar schema shared between create and update */
const agentAvatarSchema = z.z.object({
filepath: z.z.string(),
source: z.z.string(),
});
/** Base resource schema for tool resources */
const agentBaseResourceSchema = z.z.object({
file_ids: z.z.array(z.z.string()).optional(),
files: z.z.array(z.z.any()).optional(), // Files are populated at runtime, not from user input
});
/** File resource schema extends base with vector_store_ids */
const agentFileResourceSchema = agentBaseResourceSchema.extend({
vector_store_ids: z.z.array(z.z.string()).optional(),
});
/** Tool resources schema matching AgentToolResources interface */
const agentToolResourcesSchema = z.z
.object({
image_edit: agentBaseResourceSchema.optional(),
execute_code: agentBaseResourceSchema.optional(),
file_search: agentFileResourceSchema.optional(),
context: agentBaseResourceSchema.optional(),
/** @deprecated Use context instead */
ocr: agentBaseResourceSchema.optional(),
})
.optional();
/** Support contact schema for agent */
const agentSupportContactSchema = z.z
.object({
name: z.z.string().optional(),
email: z.z.union([z.z.literal(''), z.z.string().email()]).optional(),
})
.optional();
/** Graph edge schema for agent handoffs */
const graphEdgeSchema = z.z.object({
from: z.z.union([z.z.string(), z.z.array(z.z.string())]),
to: z.z.union([z.z.string(), z.z.array(z.z.string())]),
description: z.z.string().optional(),
edgeType: z.z.enum(['handoff', 'direct']).optional(),
prompt: z.z.union([z.z.string(), z.z.function()]).optional(),
excludeResults: z.z.boolean().optional(),
promptKey: z.z.string().optional(),
});
/** Per-tool options schema (defer_loading, allowed_callers) */
const toolOptionsSchema = z.z.object({
defer_loading: z.z.boolean().optional(),
allowed_callers: z.z.array(z.z.enum(['direct', 'code_execution'])).optional(),
});
/** Agent tool options - map of tool_id to tool options */
const agentToolOptionsSchema = z.z.record(z.z.string(), toolOptionsSchema).optional();
/** Base agent schema with all common fields */
const agentBaseSchema = z.z.object({
name: z.z.string().nullable().optional(),
description: z.z.string().nullable().optional(),
instructions: z.z.string().nullable().optional(),
avatar: agentAvatarSchema.nullable().optional(),
model_parameters: z.z.record(z.z.unknown()).optional(),
tools: z.z.array(z.z.string()).optional(),
/** @deprecated Use edges instead */
agent_ids: z.z.array(z.z.string()).optional(),
edges: z.z.array(graphEdgeSchema).optional(),
end_after_tools: z.z.boolean().optional(),
hide_sequential_outputs: z.z.boolean().optional(),
artifacts: z.z.string().optional(),
recursion_limit: z.z.number().optional(),
conversation_starters: z.z.array(z.z.string()).optional(),
tool_resources: agentToolResourcesSchema,
tool_options: agentToolOptionsSchema,
support_contact: agentSupportContactSchema,
category: z.z.string().optional(),
});
/** Create schema extends base with required fields for creation */
const agentCreateSchema = agentBaseSchema.extend({
provider: z.z.string(),
model: z.z.string().nullable(),
tools: z.z.array(z.z.string()).optional().default([]),
});
/** Update schema extends base with all fields optional and additional update-only fields */
const agentUpdateSchema = agentBaseSchema.extend({
avatar: z.z.union([agentAvatarSchema, z.z.null()]).optional(),
provider: z.z.string().optional(),
model: z.z.string().nullable().optional(),
projectIds: z.z.array(z.z.string()).optional(),
removeProjectIds: z.z.array(z.z.string()).optional(),
isCollaborative: z.z.boolean().optional(),
});
/**
* Validates an agent's model against the available models configuration.
* This is a non-middleware version of validateModel that can be used
* in service initialization flows.
*
* @param params - Validation parameters
* @returns Object indicating whether the model is valid and any error details
*/
function validateAgentModel(params) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const { req, res, agent, modelsConfig, logViolation } = params;
const { model, provider: endpoint } = agent;
if (!model) {
return {
isValid: false,
error: {
message: `{ "type": "${librechatDataProvider.ErrorTypes.MISSING_MODEL}", "info": "${endpoint}" }`,
},
};
}
if (!modelsConfig) {
return {
isValid: false,
error: {
message: `{ "type": "${librechatDataProvider.ErrorTypes.MODELS_NOT_LOADED}" }`,
},
};
}
const availableModels = modelsConfig[endpoint];
if (!availableModels) {
return {
isValid: false,
error: {
message: `{ "type": "${librechatDataProvider.ErrorTypes.ENDPOINT_MODELS_NOT_LOADED}", "info": "${endpoint}" }`,
},
};
}
const validModel = !!availableModels.find((availableModel) => availableModel === model);
if (validModel) {
return { isValid: true };
}
const { ILLEGAL_MODEL_REQ_SCORE: score = 1 } = (_a = process.env) !== null && _a !== void 0 ? _a : {};
const type = librechatDataProvider.ViolationTypes.ILLEGAL_MODEL_REQUEST;
const errorMessage = {
type,
model,
endpoint,
};
yield logViolation(req, res, type, errorMessage, score);
return {
isValid: false,
error: {
message: `{ "type": "${librechatDataProvider.ViolationTypes.ILLEGAL_MODEL_REQUEST}", "info": "${endpoint}|${model}" }`,
},
};
});
}
/**
* Filters out duplicate plugins from the list of plugins.
*
* @param plugins The list of plugins to filter.
* @returns The list of plugins with duplicates removed.
*/
const filterUniquePlugins = (plugins) => {
const seen = new Set();
return ((plugins === null || plugins === void 0 ? void 0 : plugins.filter((plugin) => {
const duplicate = seen.has(plugin.pluginKey);
seen.add(plugin.pluginKey);
return !duplicate;
})) || []);
};
/**
* Determines if a plugin is authenticated by checking if all required authentication fields have non-empty values.
* Supports alternate authentication fields, allowing validation against multiple possible environment variables.
*
* @param plugin The plugin object containing the authentication configuration.
* @returns True if the plugin is authenticated for all required fields, false otherwise.
*/
const checkPluginAuth = (plugin) => {
if (!(plugin === null || plugin === void 0 ? void 0 : plugin.authConfig) || plugin.authConfig.length === 0) {
return false;
}
return plugin.authConfig.every((authFieldObj) => {
const authFieldOptions = authFieldObj.authField.split('||');
let isFieldAuthenticated = false;
for (const fieldOption of authFieldOptions) {
const envValue = process.env[fieldOption];
if (envValue && envValue.trim() !== '' && envValue !== librechatDataProvider.AuthType.USER_PROVIDED) {
isFieldAuthenticated = true;
break;
}
}
return isFieldAuthenticated;
});
};
/**
* @param toolkits
* @param toolName
* @returns toolKey
*/
function getToolkitKey({ toolkits, toolName, }) {
let toolkitKey;
if (!toolName) {
return toolkitKey;
}
for (const toolkit of toolkits) {
if (toolName.startsWith(librechatDataProvider.EToolResources.image_edit)) {
const splitMatches = toolkit.pluginKey.split('_');
const suffix = splitMatches[splitMatches.length - 1];
if (toolName.endsWith(suffix)) {
toolkitKey = toolkit.pluginKey;
break;
}
}
if (toolName.startsWith(toolkit.pluginKey)) {
toolkitKey = toolkit.pluginKey;
break;
}
}
return toolkitKey;
}
/** Google Search tool JSON schema */
const googleSearchSchema = {
type: 'object',
properties: {
query: {
type: 'string',
minLength: 1,
description: 'The search query string.',
},
max_results: {
type: 'integer',
minimum: 1,
maximum: 10,
description: 'The maximum number of search results to return. Defaults to 5.',
},
},
required: ['query'],
};
/** DALL-E 3 tool JSON schema */
const dalle3Schema = {
type: 'object',
properties: {
prompt: {
type: 'string',
maxLength: 4000,
description: 'A text description of the desired image, following the rules, up to 4000 characters.',
},
style: {
type: 'string',
enum: ['vivid', 'natural'],
description: 'Must be one of `vivid` or `natural`. `vivid` generates hyper-real and dramatic images, `natural` produces more natural, less hyper-real looking images',
},
quality: {
type: 'string',
enum: ['hd', 'standard'],
description: 'The quality of the generated image. Only `hd` and `standard` are supported.',
},
size: {
type: 'string',
enum: ['1024x1024', '1792x1024', '1024x1792'],
description: 'The size of the requested image. Use 1024x1024 (square) as the default, 1792x1024 if the user requests a wide image, and 1024x1792 for full-body portraits. Always include this parameter in the request.',
},
},
required: ['prompt', 'style', 'quality', 'size'],
};
/** Flux API tool JSON schema */
const fluxApiSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['generate', 'list_finetunes', 'generate_finetuned'],
description: 'Action to perform: "generate" for image generation, "generate_finetuned" for finetuned model generation, "list_finetunes" to get available custom models',
},
prompt: {
type: 'string',
description: 'Text prompt for image generation. Required when action is "generate". Not used for list_finetunes.',
},
width: {
type: 'number',
description: 'Width of the generated image in pixels. Must be a multiple of 32. Default is 1024.',
},
height: {
type: 'number',
description: 'Height of the generated image in pixels. Must be a multiple of 32. Default is 768.',
},
prompt_upsampling: {
type: 'boolean',
description: 'Whether to perform upsampling on the prompt.',
},
steps: {
type: 'integer',
description: 'Number of steps to run the model for, a number from 1 to 50. Default is 40.',
},
seed: {
type: 'number',
description: 'Optional seed for reproducibility.',
},
safety_tolerance: {
type: 'number',
description: 'Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.',
},
endpoint: {
type: 'string',
enum: [
'/v1/flux-pro-1.1',
'/v1/flux-pro',
'/v1/flux-dev',
'/v1/flux-pro-1.1-ultra',
'/v1/flux-pro-finetuned',
'/v1/flux-pro-1.1-ultra-finetuned',
],
description: 'Endpoint to use for image generation.',
},
raw: {
type: 'boolean',
description: 'Generate less processed, more natural-looking images. Only works for /v1/flux-pro-1.1-ultra.',
},
finetune_id: {
type: 'string',
description: 'ID of the finetuned model to use',
},
finetune_strength: {
type: 'number',
description: 'Strength of the finetuning effect (typically between 0.1 and 1.2)',
},
guidance: {
type: 'number',
description: 'Guidance scale for finetuned models',
},
aspect_ratio: {
type: 'string',
description: 'Aspect ratio for ultra models (e.g., "16:9")',
},
},
required: [],
};
/** OpenWeather tool JSON schema */
const openWeatherSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['help', 'current_forecast', 'timestamp', 'daily_aggregation', 'overview'],
description: 'The action to perform',
},
city: {
type: 'string',
description: 'City name for geocoding if lat/lon not provided',
},
lat: {
type: 'number',
description: 'Latitude coordinate',
},
lon: {
type: 'number',
description: 'Longitude coordinate',
},
exclude: {
type: 'string',
description: 'Parts to exclude from the response',
},
units: {
type: 'string',
enum: ['Celsius', 'Kelvin', 'Fahrenheit'],
description: 'Temperature units',
},
lang: {
type: 'string',
description: 'Language code',
},
date: {
type: 'string',
description: 'Date in YYYY-MM-DD format for timestamp and daily_aggregation',
},
tz: {
type: 'string',
description: 'Timezone',
},
},
required: ['action'],
};
/** Wolfram Alpha tool JSON schema */
const wolframSchema = {
type: 'object',
properties: {
input: {
type: 'string',
description: 'Natural language query to WolframAlpha following the guidelines',
},
},
required: ['input'],
};
/** Stable Diffusion tool JSON schema */
const stableDiffusionSchema = {
type: 'object',
properties: {
prompt: {
type: 'string',
description: 'Detailed keywords to describe the subject, using at least 7 keywords to accurately describe the image, separated by comma',
},
negative_prompt: {
type: 'string',
description: 'Keywords we want to exclude from the final image, using at least 7 keywords to accurately describe the image, separated by comma',
},
},
required: ['prompt', 'negative_prompt'],
};
/** Azure AI Search tool JSON schema */
const azureAISearchSchema = {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search word or phrase to Azure AI Search',
},
},
required: ['query'],
};
/** Traversaal Search tool JSON schema */
const traversaalSearchSchema = {
type: 'object',
properties: {
query: {
type: 'string',
description: "A properly written sentence to be interpreted by an AI to search the web according to the user's request.",
},
},
required: ['query'],
};
/** Tavily Search Results tool JSON schema */
const tavilySearchSchema = {
type: 'object',
properties: {
query: {
type: 'string',
minLength: 1,
description: 'The search query string.',
},
max_results: {
type: 'number',
minimum: 1,
maximum: 10,
description: 'The maximum number of search results to return. Defaults to 5.',
},
search_depth: {
type: 'string',
enum: ['basic', 'advanced'],
description: 'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.',
},
include_images: {
type: 'boolean',
description: 'Whether to include a list of query-related images in the response. Default is False.',
},
include_answer: {
type: 'boolean',
description: 'Whether to include answers in the search results. Default is False.',
},
include_raw_content: {
type: 'boolean',
description: 'Whether to include raw content in the search results. Default is False.',
},
include_domains: {
type: 'array',
items: { type: 'string' },
description: 'A list of domains to specifically include in the search results.',
},
exclude_domains: {
type: 'array',
items: { type: 'string' },
description: 'A list of domains to specifically exclude from the search results.',
},
topic: {
type: 'string',
enum: ['general', 'news', 'finance'],
description: 'The category of the search. Use news ONLY if query SPECIFCALLY mentions the word "news".',
},
time_range: {
type: 'string',
enum: ['day', 'week', 'month', 'year', 'd', 'w', 'm', 'y'],
description: 'The time range back from the current date to filter results.',
},
days: {
type: 'number',
minimum: 1,
description: 'Number of days back from the current date to include. Only if topic is news.',
},
include_image_descriptions: {
type: 'boolean',
description: 'When include_images is true, also add a descriptive text for each image. Default is false.',
},
},
required: ['query'],
};
/** File Search tool JSON schema */
const fileSearchSchema = {
type: 'object',
properties: {
query: {
type: 'string',
description: "A natural language query to search for relevant information in the files. Be specific and use keywords related to the information you're looking for. The query will be used for semantic similarity matching against the file contents.",
},
},
required: ['query'],
};
/** OpenAI Image Generation tool JSON schema */
const imageGenOaiSchema = {
type: 'object',
properties: {
prompt: {
type: 'string',
maxLength: 32000,
description: `Describe the image you want in detail.
Be highly specific—break your idea into layers:
(1) main concept and subject,
(2) composition and position,
(3) lighting and mood,
(4) style, medium, or camera details,
(5) important features (age, expression, clothing, etc.),
(6) background.
Use positive, descriptive language and specify what should be included, not what to avoid.
List number and characteristics of people/objects, and mention style/technical requirements (e.g., "DSLR photo, 85mm lens, golden hour").
Do not reference any uploaded images—use for new image creation from text only.`,
},
background: {
type: 'string',
enum: ['transparent', 'opaque', 'auto'],
description: 'Sets transparency for the background. Must be one of transparent, opaque or auto (default). When transparent, the output format should be png or webp.',
},
quality: {
type: 'string',
enum: ['auto', 'high', 'medium', 'low'],
description: 'The quality of the image. One of auto (default), high, medium, or low.',
},
size: {
type: 'string',
enum: ['auto', '1024x1024', '1536x1024', '1024x1536'],
description: 'The size of the generated image. One of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or auto (default).',
},
},
required: ['prompt'],
};
/** OpenAI Image Edit tool JSON schema */
const imageEditOaiSchema = {
type: 'object',
properties: {
image_ids: {
type: 'array',
items: { type: 'string' },
minItems: 1,
description: `IDs (image ID strings) of previously generated or uploaded images that should guide the edit.
Guidelines:
- If the user's request depends on any prior image(s), copy their image IDs into the \`image_ids\` array (in the same order the user refers to them).
- Never invent or hallucinate IDs; only use IDs that are still visible in the conversation context.
- If no earlier image is relevant, omit the field entirely.`,
},
prompt: {
type: 'string',
maxLength: 32000,
description: `Describe the changes, enhancements, or new ideas to apply to the uploaded image(s).
Be highly specific—break your request into layers:
(1) main concept or transformation,
(2) specific edits/replacements or composition guidance,
(3) desired style, mood, or technique,
(4) features/items to keep, change, or add (such as objects, people, clothing, lighting, etc.).
Use positive, descriptive language and clarify what should be included or changed, not what to avoid.
Always base this prompt on the most recently uploaded reference images.`,
},
quality: {
type: 'string',
enum: ['auto', 'high', 'medium', 'low'],
description: 'The quality of the image. One of auto (default), high, medium, or low. High/medium/low only supported for gpt-image-1.',
},
size: {
type: 'string',
enum: ['auto', '1024x1024', '1536x1024', '1024x1536', '256x256', '512x512'],
description: 'The size of the generated images. For gpt-image-1: auto (default), 1024x1024, 1536x1024, 1024x1536. For dall-e-2: 256x256, 512x512, 1024x1024.',
},
},
required: ['image_ids', 'prompt'],
};
/** Gemini Image Generation tool JSON schema */
const geminiImageGenSchema = {
type: 'object',
properties: {
prompt: {
type: 'string',
maxLength: 32000,
description: 'A detailed text description of the desired image, up to 32000 characters. For "editing" requests, describe the changes you want to make to the referenced image. Be specific about composition, style, lighting, and subject matter.',
},
image_ids: {
type: 'array',
items: { type: 'string' },
description: `Optional array of image IDs to use as visual context for generation.
Guidelines:
- For "editing" requests: ALWAYS include the image ID being "edited"
- For new generation with context: Include any relevant reference image IDs
- If the user's request references any prior images, include their image IDs in this array
- These images will be used as visual context/inspiration for the new generation
- Never invent or hallucinate IDs; only use IDs that are visible in the conversation
- If no images are relevant, omit this field entirely`,
},
aspectRatio: {
type: 'string',
enum: ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'],
description: 'The aspect ratio of the generated image. Use 16:9 or 3:2 for landscape, 9:16 or 2:3 for portrait, 21:9 for ultra-wide/cinematic, 1:1 for square. Defaults to 1:1 if not specified.',
},
imageSize: {
type: 'string',
enum: ['1K', '2K', '4K'],
description: 'The resolution of the generated image. Use 1K for standard, 2K for high, 4K for maximum quality. Defaults to 1K if not specified.',
},
},
required: ['prompt'],
};
/** Tool definitions registry - maps tool names to their definitions */
const toolDefinitions = {
google: {
name: 'google',
description: 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.',
schema: googleSearchSchema,
toolType: 'builtin',
},
dalle: {
name: 'dalle',
description: `Use DALLE to create images from text descriptions.
- It requires prompts to be in English, detailed, and to specify image type and human features for diversity.
- Create only one image, without repeating or listing descriptions outside the "prompts" field.
- Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`,
schema: dalle3Schema,
toolType: 'builtin',
},
flux: {
name: 'flux',
description: 'Use Flux to generate images from text descriptions. This tool can generate images and list available finetunes. Each generate call creates one image. For multiple images, make multiple consecutive calls.',
schema: fluxApiSchema,
toolType: 'builtin',
},
open_weather: {
name: 'open_weather',
description: 'Provides weather data from OpenWeather One Call API 3.0. Actions: help, current_forecast, timestamp, daily_aggregation, overview. If lat/lon not provided, specify "city" for geocoding. Units: "Celsius", "Kelvin", or "Fahrenheit" (default: Celsius). For timestamp action, use "date" in YYYY-MM-DD format.',
schema: openWeatherSchema,
toolType: 'builtin',
},
wolfram: {
name: 'wolfram',
description: 'WolframAlpha offers computation, math, curated knowledge, and real-time data. It handles natural language queries and performs complex calculations. Follow the guidelines to get the best results.',
schema: wolframSchema,
toolType: 'builtin',
},
'stable-diffusion': {
name: 'stable-diffusion',
description: "You can generate images using text with 'stable-diffusion'. This tool is exclusively for visual content.",
schema: stableDiffusionSchema,
toolType: 'builtin',
},
'azure-ai-search': {
name: 'azure-ai-search',
description: "Use the 'azure-ai-search' tool to retrieve search results relevant to your input",
schema: azureAISearchSchema,
toolType: 'builtin',
},
traversaal_search: {
name: 'traversaal_search',
description: 'An AI search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events. Input should be a search query.',
schema: traversaalSearchSchema,
toolType: 'builtin',
},
tavily_search_results_json: {
name: 'tavily_search_results_json',
description: 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.',
schema: tavilySearchSchema,
toolType: 'builtin',
},
file_search: {
name: 'file_search',
description: 'Performs semantic search across attached "file_search" documents using natural language queries. This tool analyzes the content of uploaded files to find relevant information, quotes, and passages that best match your query.',
schema: fileSearchSchema,
toolType: 'builtin',
responseFormat: 'content_and_artifact',
},
image_gen_oai: {
name: 'image_gen_oai',
description: `Generates high-quality, original images based solely on text, not using any uploaded reference images.
When to use \`image_gen_oai\`:
- To create entirely new images from detailed text descriptions that do NOT reference any image files.
When NOT to use \`image_gen_oai\`:
- If the user has uploaded any images and requests modifications, enhancements, or remixing based on those uploads → use \`image_edit_oai\` instead.
Generated image IDs will be returned in the response, so you can refer to them in future requests made to \`image_edit_oai\`.`,
schema: imageGenOaiSchema,
toolType: 'builtin',
responseFormat: 'content_and_artifact',
},
image_edit_oai: {
name: 'image_edit_oai',
description: `Generates high-quality, original images based on text and one or more uploaded/referenced images.
When to use \`image_edit_oai\`:
- The user wants to modify, extend, or remix one **or more** uploaded images, either:
- Previously generated, or in the current request (both to be included in the \`image_ids\` array).
- Always when the user refers to uploaded images for editing, enhancement, remixing, style transfer, or combining elements.
- Any current or existing images are to be used as visual guides.
- If there are any files in the current request, they are more likely than not expected as references for image edit requests.
When NOT to use \`image_edit_oai\`:
- Brand-new generations that do not rely on an existing image → use \`image_gen_oai\` instead.
Both generated and referenced image IDs will be returned in the response, so you can refer to them in future requests made to \`image_edit_oai\`.`,
schema: imageEditOaiSchema,
toolType: 'builtin',
responseFormat: 'content_and_artifact',
},
gemini_image_gen: {
name: 'gemini_image_gen',
description: `Generates high-quality, original images based on text prompts, with optional image context.
When to use \`gemini_image_gen\`:
- To create entirely new images from detailed text descriptions
- To generate images using existing images as context or inspiration
- When the user requests image generation, creation, or asks to "generate an image"
- When the user asks to "edit", "modify", "change", or "swap" elements in an image (generates new image with changes)
When NOT to use \`gemini_image_gen\`:
- For uploading or saving existing images without modification
Generated image IDs will be returned in the response, so you can refer to them in future requests.`,
schema: geminiImageGenSchema,
toolType: 'builtin',
responseFormat: 'content_and_artifact',
},
};
/** Tool definitions from @librechat/agents */
const agentToolDefinitions = {
[agents.CalculatorToolDefinition.name]: {
name: agents.CalculatorToolDefinition.name,
description: agents.CalculatorToolDefinition.description,
schema: agents.CalculatorToolDefinition.schema,
toolType: 'builtin',
},
[agents.CodeExecutionToolDefinition.name]: {
name: agents.CodeExecutionToolDefinition.name,
description: agents.CodeExecutionToolDefinition.description,
schema: agents.CodeExecutionToolDefinition.schema,
toolType: 'builtin',
},
[agents.WebSearchToolDefinition.name]: {
name: agents.WebSearchToolDefinition.name,
description: agents.WebSearchToolDefinition.description,
schema: agents.WebSearchToolDefinition.schema,
toolType: 'builtin',
},
};
function getToolDefinition(toolName) {
var _a;
return (_a = toolDefinitions[toolName]) !== null && _a !== void 0 ? _a : agentToolDefinitions[toolName];
}
function getAllToolDefinitions() {
return [...Object.values(toolDefinitions), ...Object.values(agentToolDefinitions)];
}
function getToolSchema(toolName) {
var _a;
return (_a = getToolDefinition(toolName)) === null || _a === void 0 ? void 0 : _a.schema;
}
/** Default description for Gemini image generation tool */
const DEFAULT_GEMINI_IMAGE_GEN_DESCRIPTION = `Generates high-quality, original images based on text prompts, with optional image context.
When to use \`gemini_image_gen\`:
- To create entirely new images from detailed text descriptions
- To generate images using existing images as context or inspiration
- When the user requests image generation, creation, or asks to "generate an image"
- When the user asks to "edit", "modify", "change", or "swap" elements in an image (generates new image with changes)
When NOT to use \`gemini_image_gen\`:
- For uploading or saving existing images without modification
Generated image IDs will be returned in the response, so you can refer to them in future requests.`;
const getGeminiImageGenDescription = () => {
return process.env.GEMINI_IMAGE_GEN_DESCRIPTION || DEFAULT_GEMINI_IMAGE_GEN_DESCRIPTION;
};
/** Default prompt description for Gemini image generation */
const DEFAULT_GEMINI_IMAGE_GEN_PROMPT_DESCRIPTION = `A detailed text description of the desired image, up to 32000 characters. For "editing" requests, describe the changes you want to make to the referenced image. Be specific about composition, style, lighting, and subject matter.`;
const getGeminiImageGenPromptDescription = () => {
return (process.env.GEMINI_IMAGE_GEN_PROMPT_DESCRIPTION || DEFAULT_GEMINI_IMAGE_GEN_PROMPT_DESCRIPTION);
};
/** Default image IDs description */
const DEFAULT_GEMINI_IMAGE_IDS_DESCRIPTION = `
Optional array of image IDs to use as visual context for generation.
Guidelines:
- For "editing" requests: ALWAYS include the image ID being "edited"
- For new generation with context: Include any relevant reference image IDs
- If the user's request references any prior images, include their image IDs in this array
- These images will be used as visual context/inspiration for the new generation
- Never invent or hallucinate IDs; only use IDs that are visible in the conversation
- If no images are relevant, omit this field entirely
`.trim();
const getGeminiImageIdsDescription = () => {
return process.env.GEMINI_IMAGE_IDS_DESCRIPTION || DEFAULT_GEMINI_IMAGE_IDS_DESCRIPTION;
};
const geminiImageGenJsonSchema = {
type: 'object',
properties: {
prompt: {
type: 'string',
maxLength: 32000,
description: getGeminiImageGenPromptDescription(),
},
image_ids: {
type: 'array',
items: { type: 'string' },
description: getGeminiImageIdsDescription(),
},
aspectRatio: {
type: 'string',
enum: ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'],
description: 'The aspect ratio of the generated image. Use 16:9 or 3:2 for landscape, 9:16 or 2:3 for portrait, 21:9 for ultra-wide/cinematic, 1:1 for square. Defaults to 1:1 if not specified.',
},
imageSize: {
type: 'string',
enum: ['1K', '2K', '4K'],
description: 'The resolution of the generated image. Use 1K for standard, 2K for high, 4K for maximum quality. Defaults to 1K if not specified.',
},
},
required: ['prompt'],
};
const geminiToolkit = {
gemini_image_gen: {
name: 'gemini_image_gen',
description: getGeminiImageGenDescription(),
description_for_model: `Use this tool to generate images from text descriptions using Vertex AI Gemini.
1. Prompts should be detailed and specific for best results.
2. One image per function call. Create only 1 image per request.
3. IMPORTANT: When user asks to "edit", "modify", "change", or "swap" elements in an existing image:
- ALWAYS include the original image ID in the image_ids array
- Describe the desired changes clearly in the prompt
- The tool will generate a new image based on the original image context + your prompt
4. IMPORTANT: For editing requests, use DIRECT editing instructions:
- User says "remove the gun" → prompt should be "remove the gun from this image"
- User says "make it blue" → prompt should be "make this image blue"
- User says "add sunglasses" → prompt should be "add sunglasses to this image"
- DO NOT reconstruct or modify the original prompt - use the user's editing instruction directly
- ALWAYS include the image being edited in image_ids array
5. OPTIONAL: Use image_ids to provide context images that will influence the generation:
- Include any relevant image IDs from the conversation in the image_ids array
- These images will be used as visual context/inspiration for the new generation
- For "editing" requests, always include the image being "edited"
6. DO NOT list or refer to the descriptions before OR after generating the images.
7. Always mention the image type (photo, oil painting, watercolor painting, illustration, cartoon, drawing, vector, render, etc.) at the beginning of the prompt.
8. Use aspectRatio to control the shape of the image:
- 16:9 or 3:2 for landscape/wide images
- 9:16 or 2:3 for portrait/tall images
- 21:9 for ultra-wide/cinematic images
- 1:1 for square images (default)
9. Use imageSize to control the resolution: 1K (standard), 2K (high), 4K (maximum quality).
The prompt should be a detailed paragraph describing every part of the image in concrete, objective detail.`,
schema: geminiImageGenJsonSchema,
responseFormat: 'content_and_artifact',
},
};
/**
* Builds tool context string for image generation tools based on available image files.
* @param params - The parameters for building image context
* @param params.imageFiles - Array of image file objects with file_id property
* @param params.toolName - The name of the tool (e.g., 'gemini_image_gen', 'image_edit_oai')
* @param params.contextDescription - Description of what the images are for (e.g., 'image context', 'image editing')
* @returns The tool context string or empty string if no images
*/
function buildImageToolContext({ imageFiles, toolName, contextDescription = 'image context', }) {
if (!imageFiles || imageFiles.length === 0) {
return '';
}
let toolContext = '';
for (let i = 0; i < imageFiles.length; i++) {
const file = imageFiles[i];
if (!file) {
continue;
}
if (i === 0) {
toolContext = `Image files provided in this request (their image IDs listed in order of appearance) available for ${contextDescription}:`;
}
toolContext += `\n\t- ${file.file_id}`;
if (i === imageFiles.length - 1) {
toolContext += `\n\nInclude any you need in the \`image_ids\` array when calling \`${toolName}\` to use them as visual context for generation. You may also include previously referenced or generated image IDs.`;
}
}
return toolContext;
}
/** Default descriptions for image generation tool */
const DEFAULT_IMAGE_GEN_DESCRIPTION = `Generates high-quality, original images based solely on text, not using any uploaded reference images.
When to use \`image_gen_oai\`:
- To create entirely new images from detailed text descriptions that do NOT reference any image files.
When NOT to use \`image_gen_oai\`:
- If the user has uploaded any images and requests modifications, enhancements, or remixing based on those uploads → use \`image_edit_oai\` instead.
Generated image IDs will be returned in the response, so you can refer to them in future requests made to \`image_edit_oai\`.`;
const getImageGenDescription = () => {
return process.env.IMAGE_GEN_OAI_DESCRIPTION || DEFAULT_IMAGE_GEN_DESCRIPTION;
};
/** Default prompt descriptions */
const DEFAULT_IMAGE_GEN_PROMPT_DESCRIPTION = `Describe the image you want in detail.
Be highly specific—break your idea into layers:
(1) main concept and subject,
(2) composition and position,
(3) lighting and mood,
(4) style, medium, or camera details,
(5) important features (age, expression, clothing, etc.),
(6) background.
Use positive, descriptive language and specify what should be included, not what to avoid.
List number and characteristics of people/objects, and mention style/technical requirements (e.g., "DSLR photo, 85mm lens, golden hour").
Do not reference any uploaded images—use for new image creation from text only.`;
const getImageGenPromptDescription = () => {
return process.env.IMAGE_GEN_OAI_PROMPT_DESCRIPTION || DEFAULT_IMAGE_GEN_PROMPT_DESCRIPTION;
};
/** Default description for image editing tool */
const DEFAULT_IMAGE_EDIT_DESCRIPTION = `Generates high-quality, original images based on text and one or more uploaded/referenced images.
When to use \`image_edit_oai\`:
- The user wants to modify, extend, or remix one **or more** uploaded images, either:
- Previously generated, or in the current request (both to be included in the \`image_ids\` array).
- Always when the user refers to uploaded images for editing, enhancement, remixing, style transfer, or combining elements.
- Any current or existing images are to be used as visual guides.
- If there are any files in the current request, they are more likely than not expected as references for image edit requests.
When NOT to use \`image_edit_oai\`:
- Brand-new generations that do not rely on an existing image → use \`image_gen_oai\` instead.
Both generated and referenced image IDs will be returned in the response, so you can refer to them in future requests made to \`image_edit_oai\`.
`.trim();
const getImageEditDescription = () => {
return process.env.IMAGE_EDIT_OAI_DESCRIPTION || DEFAULT_IMAGE_EDIT_DESCRIPTION;
};
const DEFAULT_IMAGE_EDIT_PROMPT_DESCRIPTION = `Describe the changes, enhancements, or new ideas to apply to the uploaded image(s).
Be highly specific—break your request into layers:
(1) main concept or transformation,
(2) specific edits/replacements or composition guidance,
(3) desired style, mood, or technique,
(4) features/items to keep, change, or add (such as objects, people, clothing, lighting, etc.).
Use positive, descriptive language and clarify what should be included or changed, not what to avoid.
Always base this prompt on the most recently uploaded reference images.`;
const getImageEditPromptDescription = () => {
return process.env.IMAGE_EDIT_OAI_PROMPT_DESCRIPTION || DEFAULT_IMAGE_EDIT_PROMPT_DESCRIPTION;
};
const imageGenOaiJsonSchema = {
type: 'object',
properties: {
prompt: {
type: 'string',
maxLength: 32000,
description: getImageGenPromptDescription(),
},
background: {
type: 'string',
enum: ['transparent', 'opaque', 'auto'],
description: 'Sets transparency for the background. Must be one of transparent, opaque or auto (default). When transparent, the output format should be png or webp.',
},
quality: {
type: 'string',
enum: ['auto', 'high', 'medium', 'low'],
description: 'The quality of the image. One of auto (default), high, medium, or low.',
},
size: {
type: 'string',
enum: ['auto', '1024x1024', '1536x1024', '1024x1536'],
description: 'The size of the generated image. One of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or auto (default).',
},
},
required: ['prompt'],
};
const imageEditOaiJsonSchema = {
type: 'object',
properties: {
image_ids: {
type: 'array',
items: { type: 'string' },
minItems: 1,
description: `IDs (image ID strings) of previously generated or uploaded images that should guide the edit.
Guidelines:
- If the user's request depends on any prior image(s), copy their image IDs into the \`image_ids\` array (in the same order the user refers to them).
- Never invent or hallucinate IDs; only use IDs that are still visible in the conversation context.
- If no earlier image is relevant, omit the field entirely.`,
},
prompt: {
type: 'string',
maxLength: 32000,
description: getImageEditPromptDescription(),
},
quality: {
type: 'string',
enum: ['auto', 'high', 'medium', 'low'],
description: 'The quality of the image. One of auto (default), high, medium, or low. High/medium/low only supported for gpt-image-1.',
},
size: {
type: 'string',
enum: ['auto', '1024x1024', '1536x1024', '1024x1536', '256x256', '512x512'],
description: 'The size of the generated images. For gpt-image-1: auto (default), 1024x1024, 1536x1024, 1024x1536. For dall-e-2: 256x256, 512x512, 1024x1024.',
},
},
required: ['image_ids', 'prompt'],
};
const oaiToolkit = {
image_gen_oai: {
name: 'image_gen_oai',
description: getImageGenDescription(),
schema: imageGenOaiJsonSchema,
responseFormat: 'content_and_artifact',
},
image_edit_oai: {
name: 'image_edit_oai',
description: getImageEditDescription(),
schema: imageEditOaiJsonSchema,
responseFormat: 'content_and_artifact',
},
};
/** Builds the web search tool context with citation format instructions. */
function buildWebSearchContext() {
return `# \`${librechatDataProvider.Tools.web_search}\`:
Current Date & Time: ${librechatDataProvider.replaceSpecialVars({ text: '{{iso_datetime}}' })}
**Execute immediately without preface.** After search, provide a brief summary addressing the query directly, then structure your response with clear Markdown formatting (## headers, lists, tables). Cite sources properly, tailor tone to query type, and provide comprehensive details.
**CITATION FORMAT - UNICODE ESCAPE SEQUENCES ONLY:**
Use these EXACT escape sequences (copy verbatim): \\ue202 (before each anchor), \\ue200 (group start), \\ue201 (group end), \\ue203 (highlight start), \\ue204 (highlight end)
Anchor pattern: \\ue202turn{N}{type}{index} where N=turn number, type=search|news|image|ref, index=0,1,2...
**Examples (copy these exactly):**
- Single: "Statement.\\ue202turn0search0"
- Multiple: "Statement.\\ue202turn0search0\\ue202turn0news1"
- Group: "Statement. \\ue200\\ue202turn0search0\\ue202turn0news1\\ue201"
- Highlight: "\\ue203Cited text.\\ue204\\ue202turn0search0"
- Image: "See photo\\ue202turn0image0."
**CRITICAL:** Output escape sequences EXACTLY as shown. Do NOT substitute with † or other symbols. Place anchors AFTER punctuation. Cite every non-obvious fact/quote. NEVER use markdown links, [1], footnotes, or HTML tags.`.trim();
}
/**
* @fileoverview Utility functions for building tool registries from agent tool_options.
* Tool classification (deferred_tools, allowed_callers) is configured via the agent UI.
*
* @module packages/api/src/tools/classification
*/
/**
* Extracts the MCP server name from a tool name.
* Tool names follow the pattern: toolName_mcp_ServerName
* @param toolName - The full tool name
* @returns The server name or undefined if not an MCP tool
*/
function getServerNameFromTool(toolName) {
const parts = toolName.split(librechatDataProvider.Constants.mcp_delimiter);
if (parts.length >= 2) {
return parts[parts.length - 1];
}
return undefined;
}
/**
* Builds a tool registry from agent-level tool_options.
*
* @param tools - Array of tool definitions
* @param agentToolOptions - Per-tool configuration from the agent
* @returns Map of tool name to tool definition with classification
*/
function buildToolRegistryFromAgentOptions(tools, agentToolOptions) {
const registry = new Map();
for (const tool of tools) {
const { name, description, parameters } = tool;
const agentOptions = agentToolOptions[name];
const allowed_callers = (agentOptions === null || agentOptions === void 0 ? void 0 : agentOptions.allowed_callers) && agentOptions.allowed_callers.length > 0
? agentOptions.allowed_callers
: ['direct'];
const defer_loading = (agentOptions === null || agentOptions === void 0 ? void 0 : agentOptions.defer_loading) === true;
const toolDef = {
name,
allowed_callers,
defer_loading,
toolType: 'mcp',
};
if (description) {
toolDef.description = description;
}
if (parameters) {
toolDef.parameters = parameters;
}
if (tool.serverName) {
toolDef.serverName = tool.serverName;
}
registry.set(name, toolDef);
}
return registry;
}
/**
* Extracts MCP tool definition from a loaded tool instance.
* MCP tools have the original JSON schema attached as `mcpJsonSchema` property.
*
* @param tool - The loaded tool instance
* @returns Tool definition
*/
function extractMCPToolDefinition(tool) {
const def = { name: tool.name };
if (tool.description) {
def.description = tool.description;
}
if (tool.mcpJsonSchema) {
def.parameters = tool.mcpJsonSchema;
}
const serverName = getServerNameFromTool(tool.name);
if (serverName) {
def.serverName = serverName;
}
return def;
}
/**
* Checks if a tool is an MCP tool based on its properties.
* @param tool - The tool to check (can be any object with potential mcp property)
* @returns Whether the tool is an MCP tool
*/
function isMCPTool(tool) {
return typeof tool === 'object' && tool !== null && tool.mcp === true;
}
/**
* Cleans up the temporary mcpJsonSchema property from MCP tools after registry is populated.
* This property is only needed during registry building and can be safely removed afterward.
*
* @param tools - Array of tools to clean up
*/
function cleanupMCPToolSchemas(tools) {
for (const tool of tools) {
if (tool.mcpJsonSchema !== undefined) {
delete tool.mcpJsonSchema;
}
}
}
/** Builds tool registry from MCP tool definitions. */
function buildToolRegistry(mcpToolDefs, agentToolOptions) {
if (agentToolOptions && Object.keys(agentToolOptions).length > 0) {
return buildToolRegistryFromAgentOptions(mcpToolDefs, agentToolOptions);
}
/** No agent options - build basic definitions for event-driven mode */
const registry = new Map();
for (const toolDef of mcpToolDefs) {
registry.set(toolDef.name, {
name: toolDef.name,
description: toolDef.description,
parameters: toolDef.parameters,
serverName: toolDef.serverName,
toolType: 'mcp',
});
}
return registry;
}
/**
* Checks if an agent's tools have any that match PTC patterns (programmatic only or dual context).
* @param toolRegistry - The tool registry to check
* @returns Whether any tools are configured for programmatic calling
*/
function agentHasProgrammaticTools(toolRegistry) {
var _a;
for (const toolDef of toolRegistry.values()) {
if ((_a = toolDef.allowed_callers) === null || _a === void 0 ? void 0 : _a.includes('code_execution')) {
return true;
}
}
return false;
}
/**
* Checks if an agent's tools have any that are deferred.
* @param toolRegistry - The tool registry to check
* @returns Whether any tools are configured as deferred
*/
function agentHasDeferredTools(toolRegistry) {
for (const toolDef of toolRegistry.values()) {
if (toolDef.defer_loading === true) {
return true;
}
}
return false;
}
/**
* Builds the tool registry from MCP tools and conditionally creates PTC and tool search tools.
*
* This function:
* 1. Filters loaded tools for MCP tools
* 2. Extracts tool definitions and builds the registry from agent's tool_options
* 3. Cleans up temporary mcpJsonSchema properties
* 4. Creates PTC tool only if agent has tools configured for programmatic calling
* 5. Creates tool search tool only if agent has deferred tools
*
* @param params - Parameters including loaded tools, userId, agentId, agentToolOptions, and dependencies
* @returns Tool registry and any additional tools created
*/
function buildToolClassification(params) {
return __awaiter(this, void 0, void 0, function* () {
const { userId, agentId, loadedTools, agentToolOptions, definitionsOnly = false, deferredToolsEnabled = true, loadAuthValues, } = params;
const additionalTools = [];
const mcpTools = loadedTools.filter(isMCPTool);
if (mcpTools.length === 0) {
return {
additionalTools,
toolDefinitions: [],
toolRegistry: undefined,
hasDeferredTools: false,
};
}
const mcpToolDefs = mcpTools.map(extractMCPToolDefinition);
const toolRegistry = buildToolRegistry(mcpToolDefs, agentToolOptions);
/** Clean up temporary mcpJsonSchema property from tools now that registry is populated */
cleanupMCPToolSchemas(mcpTools);
/**
* Check if this agent actually has tools configured for these features.
* Only enable PTC if the agent has programmatic tools.
* Only enable tool search if the agent has deferred tools AND the capability is enabled.
*/
const hasProgrammaticTools = agentHasProgrammaticTools(toolRegistry);
const hasDeferredTools = deferredToolsEnabled && agentHasDeferredTools(toolRegistry);
/** Clear defer_loading if capability disabled */
if (!deferredToolsEnabled) {
for (const toolDef of toolRegistry.values()) {
if (toolDef.defer_loading !== true) {
continue;
}
toolDef.defer_loading = false;
}
}
/** Build toolDefinitions array from registry (single pass, reused) */
const toolDefinitions = Array.from(toolRegistry.values());
/** No programmatic or deferred tools - skip PTC/ToolSearch */
if (!hasProgrammaticTools && !hasDeferredTools) {
dataSchemas.logger.debug(`[buildToolClassification] Agent ${agentId} has no programmatic or deferred tools, skipping PTC/ToolSearch`);
return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools: false };
}
/** Tool search uses local mode (no API key needed) */
if (hasDeferredTools) {
if (!definitionsOnly) {
const toolSearchTool = agents.createToolSearch({
mode: 'local',
toolRegistry,
});
additionalTools.push(toolSearchTool);
}
/** Add ToolSearch definition for event-driven mode */
toolDefinitions.push({
name: agents.ToolSearchToolDefinition.name,
description: agents.ToolSearchToolDefinition.description,
parameters: agents.ToolSearchToolDefinition.schema,
});
toolRegistry.set(agents.ToolSearchToolDefinition.name, {
name: agents.ToolSearchToolDefinition.name,
allowed_callers: ['direct'],
});
dataSchemas.logger.debug(`[buildToolClassification] Tool Search enabled for agent ${agentId}`);
}
/** PTC requires CODE_API_KEY for sandbox execution */
if (!hasProgrammaticTools) {
return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools };
}
/** In definitions-only mode, add PTC definition without creating the tool instance */
if (definitionsOnly) {
toolDefinitions.push({
name: agents.ProgrammaticToolCallingDefinition.name,
description: agents.ProgrammaticToolCallingDefinition.description,
parameters: agents.ProgrammaticToolCallingDefinition.schema,
});
toolRegistry.set(agents.ProgrammaticToolCallingDefinition.name, {
name: agents.ProgrammaticToolCallingDefinition.name,
allowed_callers: ['direct'],
});
dataSchemas.logger.debug(`[buildToolClassification] PTC definition added for agent ${agentId} (definitions only)`);
return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools };
}
try {
const authValues = yield loadAuthValues({
userId,
authFields: [agents.EnvVar.CODE_API_KEY],
});
const codeApiKey = authValues[agents.EnvVar.CODE_API_KEY];
if (!codeApiKey) {
dataSchemas.logger.warn('[buildToolClassification] PTC configured but CODE_API_KEY not available');
return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools };
}
const ptcTool = agents.createProgrammaticToolCallingTool({ apiKey: codeApiKey });
additionalTools.push(ptcTool);
/** Add PTC definition for event-driven mode */
toolDefinitions.push({
name: agents.ProgrammaticToolCallingDefinition.name,
description: agents.ProgrammaticToolCallingDefinition.description,
parameters: agents.ProgrammaticToolCallingDefinition.schema,
});
toolRegistry.set(agents.ProgrammaticToolCallingDefinition.name, {
name: agents.ProgrammaticToolCallingDefinition.name,
allowed_callers: ['direct'],
});
dataSchemas.logger.debug(`[buildToolClassification] PTC tool enabled for agent ${agentId}`);
}
catch (error) {
dataSchemas.logger.error('[buildToolClassification] Error creating PTC tool:', error);
}
return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools };
});
}
/**
* @fileoverview Tool definitions loader for event-driven mode.
* Loads tool definitions without creating tool instances for efficient initialization.
*
* @module packages/api/src/tools/definitions
*/
const mcpToolPattern = /_mcp_/;
/**
* Loads tool definitions without creating tool instances.
* This is the efficient path for event-driven mode where tools are loaded on-demand.
*/
function loadToolDefinitions(params, deps) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const { userId, agentId, tools, toolOptions = {}, deferredToolsEnabled = false } = params;
const { getOrFetchMCPServerTools, isBuiltInTool, loadAuthValues, getActionToolDefinitions } = deps;
const emptyResult = {
toolDefinitions: [],
toolRegistry: new Map(),
hasDeferredTools: false,
};
if (!tools || tools.length === 0) {
return emptyResult;
}
const mcpServerToolsCache = new Map();
const mcpToolDefs = [];
const builtInToolDefs = [];
let actionToolDefs = [];
const actionToolNames = [];
const mcpAllPattern = `${librechatDataProvider.Constants.mcp_all}${librechatDataProvider.Constants.mcp_delimiter}`;
for (const toolName of tools) {
if (toolName.includes(librechatDataProvider.actionDelimiter)) {
actionToolNames.push(toolName);
continue;
}
if (!mcpToolPattern.test(toolName)) {
if (!isBuiltInTool(toolName)) {
continue;
}
const registryDef = getToolDefinition(toolName);
if (!registryDef) {
continue;
}
builtInToolDefs.push({
name: toolName,
description: registryDef.description,
parameters: registryDef.schema,
});
continue;
}
const parts = toolName.split(librechatDataProvider.Constants.mcp_delimiter);
const serverName = parts[parts.length - 1];
if (!mcpServerToolsCache.has(serverName)) {
const serverTools = yield getOrFetchMCPServerTools(userId, serverName);
mcpServerToolsCache.set(serverName, serverTools || {});
}
const serverTools = mcpServerToolsCache.get(serverName);
if (!serverTools) {
continue;
}
if (toolName.startsWith(mcpAllPattern)) {
for (const [actualToolName, toolDef] of Object.entries(serverTools)) {
if (toolDef === null || toolDef === void 0 ? void 0 : toolDef.function) {
mcpToolDefs.push({
name: actualToolName,
description: toolDef.function.description,
parameters: toolDef.function.parameters
? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters))
: undefined,
serverName,
});
}
}
continue;
}
const toolDef = serverTools[toolName];
if (toolDef === null || toolDef === void 0 ? void 0 : toolDef.function) {
mcpToolDefs.push({
name: toolName,
description: toolDef.function.description,
parameters: toolDef.function.parameters
? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters))
: undefined,
serverName,
});
}
}
if (actionToolNames.length > 0 && getActionToolDefinitions) {
const fetchedActionDefs = yield getActionToolDefinitions(agentId, actionToolNames);
actionToolDefs = fetchedActionDefs.map((def) => ({
name: def.name,
description: def.description,
parameters: def.parameters,
}));
}
const loadedTools = mcpToolDefs.map((def) => ({
name: def.name,
description: def.description,
mcp: true,
mcpJsonSchema: def.parameters,
}));
const classificationResult = yield buildToolClassification({
userId,
agentId,
loadedTools,
loadAuthValues,
deferredToolsEnabled,
definitionsOnly: true,
agentToolOptions: toolOptions,
});
const { toolDefinitions, hasDeferredTools } = classificationResult;
const toolRegistry = (_a = classificationResult.toolRegistry) !== null && _a !== void 0 ? _a : new Map();
for (const actionDef of actionToolDefs) {
if (!toolRegistry.has(actionDef.name)) {
toolRegistry.set(actionDef.name, {
name: actionDef.name,
description: actionDef.description,
parameters: actionDef.parameters,
allowed_callers: ['direct'],
});
}
}
for (const builtInDef of builtInToolDefs) {
if (!toolRegistry.has(builtInDef.name)) {
toolRegistry.set(builtInDef.name, {
name: builtInDef.name,
description: builtInDef.description,
parameters: builtInDef.parameters,
allowed_callers: ['direct'],
});
}
}
const allDefinitions = [
...toolDefinitions,
...actionToolDefs.filter((d) => !toolDefinitions.some((td) => td.name === d.name)),
...builtInToolDefs.filter((d) => !toolDefinitions.some((td) => td.name === d.name)),
];
return {
toolDefinitions: allDefinitions,
toolRegistry,
hasDeferredTools,
};
});
}
function extractWebSearchEnvVars({ keys, config, }) {
if (!config) {
return [];
}
const authFields = [];
const relevantKeys = keys.filter((k) => k in config);
for (const key of relevantKeys) {
const value = config[key];
if (typeof value === 'string') {
const varName = librechatDataProvider.extractVariableName(value);
if (varName) {
authFields.push(varName);
}
}
}
return authFields;
}
/**
* Loads and verifies web search authentication values
* @param params - Authentication parameters
* @returns Authentication result
*/
function loadWebSearchAuth(_a) {
return __awaiter(this, arguments, void 0, function* ({ userId, webSearchConfig, loadAuthValues, throwError = true, }) {
var _b, _c, _d, _e;
let authenticated = true;
const authResult = {};
/** Type-safe iterator for the category-service combinations */
function checkAuth(category) {
return __awaiter(this, void 0, void 0, function* () {
let isUserProvided = false;
// Check if a specific service is specified in the config
let specificService;
if (category === librechatDataProvider.SearchCategories.PROVIDERS && (webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.searchProvider)) {
specificService = webSearchConfig.searchProvider;
}
else if (category === librechatDataProvider.SearchCategories.SCRAPERS && (webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.scraperProvider)) {
specificService = webSearchConfig.scraperProvider;
}
else if (category === librechatDataProvider.SearchCategories.RERANKERS && (webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.rerankerType)) {
specificService = webSearchConfig.rerankerType;
}
// If a specific service is specified, only check that one
const services = specificService
? [specificService]
: Object.keys(dataSchemas.webSearchAuth[category]);
for (const service of services) {
// Skip if the service doesn't exist in the webSearchAuth config
if (!dataSchemas.webSearchAuth[category][service]) {
continue;
}
const serviceConfig = dataSchemas.webSearchAuth[category][service];
// Split keys into required and optional
const requiredKeys = [];
const optionalKeys = [];
for (const key in serviceConfig) {
const typedKey = key;
if (serviceConfig[typedKey] === 1) {
requiredKeys.push(typedKey);
}
else if (serviceConfig[typedKey] === 0) {
optionalKeys.push(typedKey);
}
}
if (requiredKeys.length === 0)
continue;
const requiredAuthFields = extractWebSearchEnvVars({
keys: requiredKeys,
config: webSearchConfig,
});
const optionalAuthFields = extractWebSearchEnvVars({
keys: optionalKeys,
config: webSearchConfig,
});
if (requiredAuthFields.length !== requiredKeys.length)
continue;
const allKeys = [...requiredKeys, ...optionalKeys];
const allAuthFields = [...requiredAuthFields, ...optionalAuthFields];
const optionalSet = new Set(optionalAuthFields);
try {
const authValues = yield loadAuthValues({
userId,
authFields: allAuthFields,
optional: optionalSet,
throwError,
});
let allFieldsAuthenticated = true;
for (let j = 0; j < allAuthFields.length; j++) {
const field = allAuthFields[j];
const value = authValues[field];
const originalKey = allKeys[j];
if (originalKey)
authResult[originalKey] = value;
if (!optionalSet.has(field) && !value) {
allFieldsAuthenticated = false;
break;
}
if (!isUserProvided && process.env[field] !== value) {
isUserProvided = true;
}
}
if (!allFieldsAuthenticated) {
continue;
}
if (category === librechatDataProvider.SearchCategories.PROVIDERS) {
authResult.searchProvider = service;
}
else if (category === librechatDataProvider.SearchCategories.SCRAPERS) {
authResult.scraperProvider = service;
}
else if (category === librechatDataProvider.SearchCategories.RERANKERS) {
authResult.rerankerType = service;
}
return [true, isUserProvided];
}
catch (_a) {
continue;
}
}
return [false, isUserProvided];
});
}
const categories = [
librechatDataProvider.SearchCategories.PROVIDERS,
librechatDataProvider.SearchCategories.SCRAPERS,
librechatDataProvider.SearchCategories.RERANKERS,
];
const authTypes = [];
for (const category of categories) {
const [isCategoryAuthenticated, isUserProvided] = yield checkAuth(category);
if (!isCategoryAuthenticated) {
authenticated = false;
authTypes.push([category, librechatDataProvider.AuthType.USER_PROVIDED]);
continue;
}
authTypes.push([category, isUserProvided ? librechatDataProvider.AuthType.USER_PROVIDED : librechatDataProvider.AuthType.SYSTEM_DEFINED]);
}
authResult.safeSearch = (_b = webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.safeSearch) !== null && _b !== void 0 ? _b : librechatDataProvider.SafeSearchTypes.MODERATE;
authResult.scraperTimeout =
(_e = (_c = webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.scraperTimeout) !== null && _c !== void 0 ? _c : (_d = webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.firecrawlOptions) === null || _d === void 0 ? void 0 : _d.timeout) !== null && _e !== void 0 ? _e : 7500;
authResult.firecrawlOptions = webSearchConfig === null || webSearchConfig === void 0 ? void 0 : webSearchConfig.firecrawlOptions;
return {
authTypes,
authResult,
authenticated,
};
});
}
/**
* Key prefixes for Redis storage.
* All keys include the streamId for easy cleanup.
* Note: streamId === conversationId, so no separate mapping needed.
*
* IMPORTANT: Uses hash tags {streamId} for Redis Cluster compatibility.
* All keys for the same stream hash to the same slot, enabling:
* - Pipeline operations across related keys
* - Atomic multi-key operations
*/
const KEYS = {
/** Job metadata: stream:{streamId}:job */
job: (streamId) => `stream:{${streamId}}:job`,
/** Chunk stream (Redis Streams): stream:{streamId}:chunks */
chunks: (streamId) => `stream:{${streamId}}:chunks`,
/** Run steps: stream:{streamId}:runsteps */
runSteps: (streamId) => `stream:{${streamId}}:runsteps`,
/** Running jobs set for cleanup (global set - single slot) */
runningJobs: 'stream:running',
/** User's active jobs set: stream:user:{userId}:jobs */
userJobs: (userId) => `stream:user:{${userId}}:jobs`,
};
/**
* Default TTL values in seconds.
* Can be overridden via constructor options.
*/
const DEFAULT_TTL = {
/** TTL for completed jobs (5 minutes) */
completed: 300,
/** TTL for running jobs/chunks (20 minutes - failsafe for crashed jobs) */
running: 1200,
/** TTL for chunks after completion (0 = delete immediately) */
chunksAfterComplete: 0,
/** TTL for run steps after completion (0 = delete immediately) */
runStepsAfterComplete: 0,
};
class RedisJobStore {
constructor(redis, options) {
var _a, _b, _c, _d;
this.cleanupInterval = null;
/**
* Local cache for graph references on THIS instance.
* Enables fast reconnects when client returns to the same server.
* Uses WeakRef to allow garbage collection when graph is no longer needed.
*/
this.localGraphCache = new Map();
/**
* Local cache for collectedUsage arrays.
* Generation happens on a single instance, so collectedUsage is only available locally.
* For cross-replica abort, the abort handler falls back to text-based token counting.
*/
this.localCollectedUsageCache = new Map();
/** Cleanup interval in ms (1 minute) */
this.cleanupIntervalMs = 60000;
this.redis = redis;
this.ttl = {
completed: (_a = options === null || options === void 0 ? void 0 : options.completedTtl) !== null && _a !== void 0 ? _a : DEFAULT_TTL.completed,
running: (_b = options === null || options === void 0 ? void 0 : options.runningTtl) !== null && _b !== void 0 ? _b : DEFAULT_TTL.running,
chunksAfterComplete: (_c = options === null || options === void 0 ? void 0 : options.chunksAfterCompleteTtl) !== null && _c !== void 0 ? _c : DEFAULT_TTL.chunksAfterComplete,
runStepsAfterComplete: (_d = options === null || options === void 0 ? void 0 : options.runStepsAfterCompleteTtl) !== null && _d !== void 0 ? _d : DEFAULT_TTL.runStepsAfterComplete,
};
// Detect cluster mode using ioredis's isCluster property
this.isCluster = redis.isCluster === true;
}
initialize() {
return __awaiter(this, void 0, void 0, function* () {
if (this.cleanupInterval) {
return;
}
// Start periodic cleanup
this.cleanupInterval = setInterval(() => {
this.cleanup().catch((err) => {
dataSchemas.logger.error('[RedisJobStore] Cleanup error:', err);
});
}, this.cleanupIntervalMs);
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
dataSchemas.logger.info('[RedisJobStore] Initialized with cleanup interval');
});
}
createJob(streamId, userId, conversationId) {
return __awaiter(this, void 0, void 0, function* () {
const job = {
streamId,
userId,
status: 'running',
createdAt: Date.now(),
conversationId,
syncSent: false,
};
const key = KEYS.job(streamId);
const userJobsKey = KEYS.userJobs(userId);
// For cluster mode, we can't pipeline keys on different slots
// The job key uses hash tag {streamId}, runningJobs and userJobs are on different slots
if (this.isCluster) {
yield this.redis.hset(key, this.serializeJob(job));
yield this.redis.expire(key, this.ttl.running);
yield this.redis.sadd(KEYS.runningJobs, streamId);
yield this.redis.sadd(userJobsKey, streamId);
}
else {
const pipeline = this.redis.pipeline();
pipeline.hset(key, this.serializeJob(job));
pipeline.expire(key, this.ttl.running);
pipeline.sadd(KEYS.runningJobs, streamId);
pipeline.sadd(userJobsKey, streamId);
yield pipeline.exec();
}
dataSchemas.logger.debug(`[RedisJobStore] Created job: ${streamId}`);
return job;
});
}
getJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const data = yield this.redis.hgetall(KEYS.job(streamId));
if (!data || Object.keys(data).length === 0) {
return null;
}
return this.deserializeJob(data);
});
}
updateJob(streamId, updates) {
return __awaiter(this, void 0, void 0, function* () {
const key = KEYS.job(streamId);
const serialized = this.serializeJob(updates);
if (Object.keys(serialized).length === 0) {
return;
}
const fields = Object.entries(serialized).flat();
const updated = yield this.redis.eval('if redis.call("EXISTS", KEYS[1]) == 1 then redis.call("HSET", KEYS[1], unpack(ARGV)) return 1 else return 0 end', 1, key, ...fields);
if (updated === 0) {
return;
}
// If status changed to complete/error/aborted, update TTL and remove from running set
// Note: userJobs cleanup is handled lazily via self-healing in getActiveJobIdsByUser
if (updates.status && ['complete', 'error', 'aborted'].includes(updates.status)) {
// In cluster mode, separate runningJobs (global) from stream-specific keys
if (this.isCluster) {
yield this.redis.expire(key, this.ttl.completed);
yield this.redis.srem(KEYS.runningJobs, streamId);
if (this.ttl.chunksAfterComplete === 0) {
yield this.redis.del(KEYS.chunks(streamId));
}
else {
yield this.redis.expire(KEYS.chunks(streamId), this.ttl.chunksAfterComplete);
}
if (this.ttl.runStepsAfterComplete === 0) {
yield this.redis.del(KEYS.runSteps(streamId));
}
else {
yield this.redis.expire(KEYS.runSteps(streamId), this.ttl.runStepsAfterComplete);
}
}
else {
const pipeline = this.redis.pipeline();
pipeline.expire(key, this.ttl.completed);
pipeline.srem(KEYS.runningJobs, streamId);
if (this.ttl.chunksAfterComplete === 0) {
pipeline.del(KEYS.chunks(streamId));
}
else {
pipeline.expire(KEYS.chunks(streamId), this.ttl.chunksAfterComplete);
}
if (this.ttl.runStepsAfterComplete === 0) {
pipeline.del(KEYS.runSteps(streamId));
}
else {
pipeline.expire(KEYS.runSteps(streamId), this.ttl.runStepsAfterComplete);
}
yield pipeline.exec();
}
}
});
}
deleteJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
// Clear local caches
this.localGraphCache.delete(streamId);
this.localCollectedUsageCache.delete(streamId);
// Note: userJobs cleanup is handled lazily via self-healing in getActiveJobIdsByUser
// In cluster mode, separate runningJobs (global) from stream-specific keys (same slot)
if (this.isCluster) {
// Stream-specific keys all hash to same slot due to {streamId}
const pipeline = this.redis.pipeline();
pipeline.del(KEYS.job(streamId));
pipeline.del(KEYS.chunks(streamId));
pipeline.del(KEYS.runSteps(streamId));
yield pipeline.exec();
// Global set is on different slot - execute separately
yield this.redis.srem(KEYS.runningJobs, streamId);
}
else {
const pipeline = this.redis.pipeline();
pipeline.del(KEYS.job(streamId));
pipeline.del(KEYS.chunks(streamId));
pipeline.del(KEYS.runSteps(streamId));
pipeline.srem(KEYS.runningJobs, streamId);
yield pipeline.exec();
}
dataSchemas.logger.debug(`[RedisJobStore] Deleted job: ${streamId}`);
});
}
hasJob(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const exists = yield this.redis.exists(KEYS.job(streamId));
return exists === 1;
});
}
getRunningJobs() {
return __awaiter(this, void 0, void 0, function* () {
const streamIds = yield this.redis.smembers(KEYS.runningJobs);
if (streamIds.length === 0) {
return [];
}
const jobs = [];
for (const streamId of streamIds) {
const job = yield this.getJob(streamId);
if (job && job.status === 'running') {
jobs.push(job);
}
}
return jobs;
});
}
cleanup() {
return __awaiter(this, void 0, void 0, function* () {
const now = Date.now();
const streamIds = yield this.redis.smembers(KEYS.runningJobs);
let cleaned = 0;
// Clean up stale local graph cache entries (WeakRefs that were collected)
for (const [streamId, graphRef] of this.localGraphCache) {
if (!graphRef.deref()) {
this.localGraphCache.delete(streamId);
}
}
// Process in batches of 50 to avoid sequential per-job round-trips
const BATCH_SIZE = 50;
for (let i = 0; i < streamIds.length; i += BATCH_SIZE) {
const batch = streamIds.slice(i, i + BATCH_SIZE);
const results = yield Promise.allSettled(batch.map((streamId) => __awaiter(this, void 0, void 0, function* () {
const job = yield this.getJob(streamId);
// Job no longer exists (TTL expired) - remove from set
if (!job) {
yield this.redis.srem(KEYS.runningJobs, streamId);
this.localGraphCache.delete(streamId);
this.localCollectedUsageCache.delete(streamId);
return 1;
}
// Job completed but still in running set (shouldn't happen, but handle it)
if (job.status !== 'running') {
yield this.redis.srem(KEYS.runningJobs, streamId);
this.localGraphCache.delete(streamId);
this.localCollectedUsageCache.delete(streamId);
return 1;
}
// Stale running job (failsafe - running for > configured TTL)
if (now - job.createdAt > this.ttl.running * 1000) {
dataSchemas.logger.warn(`[RedisJobStore] Cleaning up stale job: ${streamId}`);
yield this.deleteJob(streamId);
return 1;
}
return 0;
})));
for (const result of results) {
if (result.status === 'fulfilled') {
cleaned += result.value;
}
else {
dataSchemas.logger.warn(`[RedisJobStore] Cleanup failed for a job:`, result.reason);
}
}
}
if (cleaned > 0) {
dataSchemas.logger.debug(`[RedisJobStore] Cleaned up ${cleaned} jobs`);
}
return cleaned;
});
}
getJobCount() {
return __awaiter(this, void 0, void 0, function* () {
// This is approximate - counts jobs in running set + scans for job keys
// For exact count, would need to scan all job:* keys
const runningCount = yield this.redis.scard(KEYS.runningJobs);
return runningCount;
});
}
getJobCountByStatus(status) {
return __awaiter(this, void 0, void 0, function* () {
if (status === 'running') {
return this.redis.scard(KEYS.runningJobs);
}
// For other statuses, we'd need to scan - return 0 for now
// In production, consider maintaining separate sets per status if needed
return 0;
});
}
/**
* Get active job IDs for a user.
* Returns conversation IDs of running jobs belonging to the user.
* Also performs self-healing cleanup: removes stale entries for jobs that no longer exist.
*
* @param userId - The user ID to query
* @returns Array of conversation IDs with active jobs
*/
getActiveJobIdsByUser(userId) {
return __awaiter(this, void 0, void 0, function* () {
const userJobsKey = KEYS.userJobs(userId);
const trackedIds = yield this.redis.smembers(userJobsKey);
if (trackedIds.length === 0) {
return [];
}
const activeIds = [];
const staleIds = [];
for (const streamId of trackedIds) {
const job = yield this.getJob(streamId);
// Only include if job exists AND is still running
if (job && job.status === 'running') {
activeIds.push(streamId);
}
else {
// Self-healing: job completed/deleted but mapping wasn't cleaned - mark for removal
staleIds.push(streamId);
}
}
// Clean up stale entries
if (staleIds.length > 0) {
yield this.redis.srem(userJobsKey, ...staleIds);
dataSchemas.logger.debug(`[RedisJobStore] Self-healed ${staleIds.length} stale job entries for user ${userId}`);
}
return activeIds;
});
}
destroy() {
return __awaiter(this, void 0, void 0, function* () {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
// Clear local caches
this.localGraphCache.clear();
this.localCollectedUsageCache.clear();
// Don't close the Redis connection - it's shared
dataSchemas.logger.info('[RedisJobStore] Destroyed');
});
}
// ===== Content State Methods =====
// For Redis, content is primarily reconstructed from chunks.
// However, we keep a LOCAL graph cache for fast same-instance reconnects.
/**
* Store graph reference in local cache.
* This enables fast reconnects when client returns to the same instance.
* Falls back to Redis chunk reconstruction for cross-instance reconnects.
*
* @param streamId - The stream identifier
* @param graph - The graph instance (stored as WeakRef)
*/
setGraph(streamId, graph) {
this.localGraphCache.set(streamId, new WeakRef(graph));
}
/**
* No-op for Redis - content parts are reconstructed from chunks.
* Metadata (agentId, groupId) is embedded directly on content parts by the agent runtime.
*/
setContentParts() {
// Content parts are reconstructed from chunks during getContentParts
// No separate storage needed
}
/**
* Store collectedUsage reference in local cache.
* This is used for abort handling to spend tokens for all models.
* Note: Only available on the generating instance; cross-replica abort uses fallback.
*/
setCollectedUsage(streamId, collectedUsage) {
this.localCollectedUsageCache.set(streamId, collectedUsage);
}
/**
* Get collected usage for a job.
* Only available if this is the generating instance.
*/
getCollectedUsage(streamId) {
var _a;
return (_a = this.localCollectedUsageCache.get(streamId)) !== null && _a !== void 0 ? _a : [];
}
/**
* Get aggregated content - tries local cache first, falls back to Redis reconstruction.
*
* Optimization: If this instance has the live graph (same-instance reconnect),
* we return the content directly without Redis round-trip.
* For cross-instance reconnects, we reconstruct from Redis Streams.
*
* @param streamId - The stream identifier
* @returns Content parts array or null if not found
*/
getContentParts(streamId) {
return __awaiter(this, void 0, void 0, function* () {
// 1. Try local graph cache first (fast path for same-instance reconnect)
const graphRef = this.localGraphCache.get(streamId);
if (graphRef) {
const graph = graphRef.deref();
if (graph) {
const localParts = graph.getContentParts();
if (localParts && localParts.length > 0) {
return {
content: localParts,
};
}
}
else {
// WeakRef was collected, remove from cache
this.localGraphCache.delete(streamId);
}
}
// 2. Fall back to Redis chunk reconstruction (cross-instance reconnect)
const chunks = yield this.getChunks(streamId);
if (chunks.length === 0) {
return null;
}
// Use the same content aggregator as live streaming
const { contentParts, aggregateContent } = agents.createContentAggregator();
// Valid event types for content aggregation
const validEvents = new Set([
'on_run_step',
'on_message_delta',
'on_reasoning_delta',
'on_run_step_delta',
'on_run_step_completed',
'on_agent_update',
]);
for (const chunk of chunks) {
const event = chunk;
if (!event.event || !event.data || !validEvents.has(event.event)) {
continue;
}
// Pass event string directly - GraphEvents values are lowercase strings
// eslint-disable-next-line @typescript-eslint/no-explicit-any
aggregateContent({ event: event.event, data: event.data });
}
// Filter out undefined entries
const filtered = [];
for (const part of contentParts) {
if (part !== undefined) {
filtered.push(part);
}
}
return {
content: filtered,
};
});
}
/**
* Get run steps - tries local cache first, falls back to Redis.
*
* Optimization: If this instance has the live graph, we get run steps
* directly without Redis round-trip.
*
* @param streamId - The stream identifier
* @returns Run steps array
*/
getRunSteps(streamId) {
return __awaiter(this, void 0, void 0, function* () {
// 1. Try local graph cache first (fast path for same-instance reconnect)
const graphRef = this.localGraphCache.get(streamId);
if (graphRef) {
const graph = graphRef.deref();
if (graph) {
const localSteps = graph.getRunSteps();
if (localSteps && localSteps.length > 0) {
return localSteps;
}
}
// Note: Don't delete from cache here - graph may still be valid
// but just not have run steps yet
}
// 2. Fall back to Redis (cross-instance reconnect)
const key = KEYS.runSteps(streamId);
const data = yield this.redis.get(key);
if (!data) {
return [];
}
try {
return JSON.parse(data);
}
catch (_a) {
return [];
}
});
}
/**
* Clear content state for a job.
* Removes both local cache and Redis data.
*/
clearContentState(streamId) {
// Clear local caches immediately
this.localGraphCache.delete(streamId);
this.localCollectedUsageCache.delete(streamId);
// Fire and forget - async cleanup for Redis
this.clearContentStateAsync(streamId).catch((err) => {
dataSchemas.logger.error(`[RedisJobStore] Failed to clear content state for ${streamId}:`, err);
});
}
/**
* Clear content state async.
*/
clearContentStateAsync(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const pipeline = this.redis.pipeline();
pipeline.del(KEYS.chunks(streamId));
pipeline.del(KEYS.runSteps(streamId));
yield pipeline.exec();
});
}
/**
* Append a streaming chunk to Redis Stream.
* Uses XADD for efficient append-only storage.
* Sets TTL on first chunk to ensure cleanup if job crashes.
*/
appendChunk(streamId, event) {
return __awaiter(this, void 0, void 0, function* () {
const key = KEYS.chunks(streamId);
// Pipeline XADD + EXPIRE in a single round-trip.
// EXPIRE is O(1) and idempotent — refreshing TTL on every chunk is better than
// only setting it once, since the original approach could let the TTL expire
// during long-running streams.
const pipeline = this.redis.pipeline();
pipeline.xadd(key, '*', 'event', JSON.stringify(event));
pipeline.expire(key, this.ttl.running);
yield pipeline.exec();
});
}
/**
* Get all chunks from Redis Stream.
*/
getChunks(streamId) {
return __awaiter(this, void 0, void 0, function* () {
const key = KEYS.chunks(streamId);
const entries = yield this.redis.xrange(key, '-', '+');
return entries
.map(([, fields]) => {
const eventIdx = fields.indexOf('event');
if (eventIdx >= 0 && eventIdx + 1 < fields.length) {
try {
return JSON.parse(fields[eventIdx + 1]);
}
catch (_a) {
return null;
}
}
return null;
})
.filter(Boolean);
});
}
/**
* Save run steps for resume state.
*/
saveRunSteps(streamId, runSteps) {
return __awaiter(this, void 0, void 0, function* () {
const key = KEYS.runSteps(streamId);
yield this.redis.set(key, JSON.stringify(runSteps), 'EX', this.ttl.running);
});
}
// ===== Consumer Group Methods =====
// These enable tracking which chunks each client has seen.
// Based on https://upstash.com/blog/resumable-llm-streams
/**
* Create a consumer group for a stream.
* Used to track which chunks a client has already received.
*
* @param streamId - The stream identifier
* @param groupName - Unique name for the consumer group (e.g., session ID)
* @param startFrom - Where to start reading ('0' = from beginning, '$' = only new)
*/
createConsumerGroup(streamId_1, groupName_1) {
return __awaiter(this, arguments, void 0, function* (streamId, groupName, startFrom = '0') {
var _a;
const key = KEYS.chunks(streamId);
try {
yield this.redis.xgroup('CREATE', key, groupName, startFrom, 'MKSTREAM');
dataSchemas.logger.debug(`[RedisJobStore] Created consumer group ${groupName} for ${streamId}`);
}
catch (err) {
// BUSYGROUP error means group already exists - that's fine
const error = err;
if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('BUSYGROUP'))) {
throw err;
}
}
});
}
/**
* Read chunks from a consumer group (only unseen chunks).
* This is the key to the resumable stream pattern.
*
* @param streamId - The stream identifier
* @param groupName - Consumer group name
* @param consumerName - Name of the consumer within the group
* @param count - Maximum number of chunks to read (default: all available)
* @returns Array of { id, event } where id is the Redis stream entry ID
*/
readChunksFromGroup(streamId_1, groupName_1) {
return __awaiter(this, arguments, void 0, function* (streamId, groupName, consumerName = 'consumer-1', count) {
var _a;
const key = KEYS.chunks(streamId);
try {
// XREADGROUP GROUP groupName consumerName [COUNT count] STREAMS key >
// The '>' means only read new messages not yet delivered to this consumer
let result;
if (count) {
result = yield this.redis.xreadgroup('GROUP', groupName, consumerName, 'COUNT', count, 'STREAMS', key, '>');
}
else {
result = yield this.redis.xreadgroup('GROUP', groupName, consumerName, 'STREAMS', key, '>');
}
if (!result || result.length === 0) {
return [];
}
// Result format: [[streamKey, [[id, [field, value, ...]], ...]]]
const [, messages] = result[0];
const chunks = [];
for (const [id, fields] of messages) {
const eventIdx = fields.indexOf('event');
if (eventIdx >= 0 && eventIdx + 1 < fields.length) {
try {
chunks.push({
id,
event: JSON.parse(fields[eventIdx + 1]),
});
}
catch (_b) {
// Skip malformed entries
}
}
}
return chunks;
}
catch (err) {
const error = err;
// NOGROUP error means the group doesn't exist yet
if ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('NOGROUP')) {
return [];
}
throw err;
}
});
}
/**
* Acknowledge that chunks have been processed.
* This tells Redis we've successfully delivered these chunks to the client.
*
* @param streamId - The stream identifier
* @param groupName - Consumer group name
* @param messageIds - Array of Redis stream entry IDs to acknowledge
*/
acknowledgeChunks(streamId, groupName, messageIds) {
return __awaiter(this, void 0, void 0, function* () {
if (messageIds.length === 0) {
return;
}
const key = KEYS.chunks(streamId);
yield this.redis.xack(key, groupName, ...messageIds);
});
}
/**
* Delete a consumer group.
* Called when a client disconnects and won't reconnect.
*
* @param streamId - The stream identifier
* @param groupName - Consumer group name to delete
*/
deleteConsumerGroup(streamId, groupName) {
return __awaiter(this, void 0, void 0, function* () {
const key = KEYS.chunks(streamId);
try {
yield this.redis.xgroup('DESTROY', key, groupName);
dataSchemas.logger.debug(`[RedisJobStore] Deleted consumer group ${groupName} for ${streamId}`);
}
catch (_a) {
// Ignore errors - group may not exist
}
});
}
/**
* Get pending chunks for a consumer (chunks delivered but not acknowledged).
* Useful for recovering from crashes.
*
* @param streamId - The stream identifier
* @param groupName - Consumer group name
* @param consumerName - Consumer name
*/
getPendingChunks(streamId_1, groupName_1) {
return __awaiter(this, arguments, void 0, function* (streamId, groupName, consumerName = 'consumer-1') {
const key = KEYS.chunks(streamId);
try {
// Read pending messages (delivered but not acked) by using '0' instead of '>'
const result = yield this.redis.xreadgroup('GROUP', groupName, consumerName, 'STREAMS', key, '0');
if (!result || result.length === 0) {
return [];
}
const [, messages] = result[0];
const chunks = [];
for (const [id, fields] of messages) {
const eventIdx = fields.indexOf('event');
if (eventIdx >= 0 && eventIdx + 1 < fields.length) {
try {
chunks.push({
id,
event: JSON.parse(fields[eventIdx + 1]),
});
}
catch (_a) {
// Skip malformed entries
}
}
}
return chunks;
}
catch (_b) {
return [];
}
});
}
/**
* Serialize job data for Redis hash storage.
* Converts complex types to strings.
*/
serializeJob(job) {
const result = {};
for (const [key, value] of Object.entries(job)) {
if (value === undefined) {
continue;
}
if (typeof value === 'object') {
result[key] = JSON.stringify(value);
}
else if (typeof value === 'boolean') {
result[key] = value ? '1' : '0';
}
else {
result[key] = String(value);
}
}
return result;
}
/**
* Deserialize job data from Redis hash.
*/
deserializeJob(data) {
return {
streamId: data.streamId,
userId: data.userId,
status: data.status,
createdAt: parseInt(data.createdAt, 10),
completedAt: data.completedAt ? parseInt(data.completedAt, 10) : undefined,
conversationId: data.conversationId || undefined,
error: data.error || undefined,
userMessage: data.userMessage ? JSON.parse(data.userMessage) : undefined,
responseMessageId: data.responseMessageId || undefined,
sender: data.sender || undefined,
syncSent: data.syncSent === '1',
finalEvent: data.finalEvent || undefined,
endpoint: data.endpoint || undefined,
iconURL: data.iconURL || undefined,
model: data.model || undefined,
promptTokens: data.promptTokens ? parseInt(data.promptTokens, 10) : undefined,
};
}
}
/**
* Redis key prefixes for pub/sub channels
*/
const CHANNELS = {
/** Main event channel: stream:{streamId}:events (hash tag for cluster compatibility) */
events: (streamId) => `stream:{${streamId}}:events`,
};
/**
* Event types for pub/sub messages
*/
const EventTypes = {
CHUNK: 'chunk',
DONE: 'done',
ERROR: 'error',
ABORT: 'abort',
};
/** Max time (ms) to wait for out-of-order messages before force-flushing */
const REORDER_TIMEOUT_MS = 500;
/** Max messages to buffer before force-flushing (prevents memory issues) */
const MAX_BUFFER_SIZE = 100;
/**
* Redis Pub/Sub implementation of IEventTransport.
* Enables real-time event delivery across multiple instances.
*
* Architecture (inspired by https://upstash.com/blog/resumable-llm-streams):
* - Publisher: Emits events to Redis channel when chunks arrive
* - Subscriber: Listens to Redis channel and forwards to SSE clients
* - Decoupled: Generator and consumer don't need direct connection
*
* Note: Requires TWO Redis connections - one for publishing, one for subscribing.
* This is a Redis limitation: a client in subscribe mode can't publish.
*
* @example
* ```ts
* const transport = new RedisEventTransport(publisherClient, subscriberClient);
* transport.subscribe(streamId, { onChunk: (e) => res.write(e) });
* transport.emitChunk(streamId, { text: 'Hello' });
* ```
*/
class RedisEventTransport {
/**
* Create a new Redis event transport.
*
* @param publisher - Redis client for publishing (can be shared)
* @param subscriber - Redis client for subscribing (must be dedicated)
*/
constructor(publisher, subscriber) {
/** Track subscribers per stream */
this.streams = new Map();
/** Track channel subscription state: resolved promise = active, pending = in-flight */
this.channelSubscriptions = new Map();
/** Counter for generating unique subscriber IDs */
this.subscriberIdCounter = 0;
/** Sequence counters per stream for publishing (ensures ordered delivery in cluster mode) */
this.sequenceCounters = new Map();
this.publisher = publisher;
this.subscriber = subscriber;
// Set up message handler for all subscriptions
this.subscriber.on('message', (channel, message) => {
this.handleMessage(channel, message);
});
}
/** Get next sequence number for a stream (0-indexed) */
getNextSequence(streamId) {
var _a;
const current = (_a = this.sequenceCounters.get(streamId)) !== null && _a !== void 0 ? _a : 0;
this.sequenceCounters.set(streamId, current + 1);
return current;
}
/** Reset publish sequence counter and subscriber reorder state for a stream (full cleanup only) */
resetSequence(streamId) {
this.sequenceCounters.delete(streamId);
const state = this.streams.get(streamId);
if (state) {
if (state.reorderBuffer.flushTimeout) {
clearTimeout(state.reorderBuffer.flushTimeout);
state.reorderBuffer.flushTimeout = null;
}
state.reorderBuffer.nextSeq = 0;
state.reorderBuffer.pending.clear();
}
}
/** Advance subscriber reorder buffer to current publisher sequence without resetting publisher (cross-replica safe) */
syncReorderBuffer(streamId) {
var _a;
const currentSeq = (_a = this.sequenceCounters.get(streamId)) !== null && _a !== void 0 ? _a : 0;
const state = this.streams.get(streamId);
if (state) {
if (state.reorderBuffer.flushTimeout) {
clearTimeout(state.reorderBuffer.flushTimeout);
state.reorderBuffer.flushTimeout = null;
}
state.reorderBuffer.nextSeq = currentSeq;
state.reorderBuffer.pending.clear();
}
}
/**
* Handle incoming pub/sub message with reordering support for Redis Cluster
*/
handleMessage(channel, message) {
const match = channel.match(/^stream:\{([^}]+)\}:events$/);
if (!match) {
return;
}
const streamId = match[1];
const streamState = this.streams.get(streamId);
if (!streamState) {
return;
}
try {
const parsed = JSON.parse(message);
if (parsed.type === EventTypes.CHUNK && parsed.seq != null) {
this.handleOrderedChunk(streamId, streamState, parsed);
}
else if ((parsed.type === EventTypes.DONE || parsed.type === EventTypes.ERROR) &&
parsed.seq != null) {
this.handleTerminalEvent(streamId, streamState, parsed);
}
else {
this.deliverMessage(streamState, parsed);
}
}
catch (err) {
dataSchemas.logger.error(`[RedisEventTransport] Failed to parse message:`, err);
}
}
/**
* Handle terminal events (done/error) with sequence-based ordering.
* Buffers the terminal event and delivers after all preceding chunks arrive.
*/
handleTerminalEvent(streamId, streamState, message) {
const buffer = streamState.reorderBuffer;
const seq = message.seq;
if (seq < buffer.nextSeq) {
dataSchemas.logger.debug(`[RedisEventTransport] Dropping duplicate terminal event for stream ${streamId}: seq=${seq}, expected=${buffer.nextSeq}`);
return;
}
if (seq === buffer.nextSeq) {
this.deliverMessage(streamState, message);
buffer.nextSeq++;
this.flushPendingMessages(streamId, streamState);
}
else {
buffer.pending.set(seq, message);
this.scheduleFlushTimeout(streamId, streamState);
}
}
/**
* Handle chunk messages with sequence-based reordering.
* Buffers out-of-order messages and delivers them in sequence.
*/
handleOrderedChunk(streamId, streamState, message) {
const buffer = streamState.reorderBuffer;
const seq = message.seq;
if (seq === buffer.nextSeq) {
this.deliverMessage(streamState, message);
buffer.nextSeq++;
this.flushPendingMessages(streamId, streamState);
}
else if (seq > buffer.nextSeq) {
buffer.pending.set(seq, message);
if (buffer.pending.size >= MAX_BUFFER_SIZE) {
dataSchemas.logger.warn(`[RedisEventTransport] Buffer overflow for stream ${streamId}, force-flushing`);
this.forceFlushBuffer(streamId, streamState);
}
else {
this.scheduleFlushTimeout(streamId, streamState);
}
}
else {
dataSchemas.logger.debug(`[RedisEventTransport] Dropping duplicate/old message for stream ${streamId}: seq=${seq}, expected=${buffer.nextSeq}`);
}
}
/** Deliver consecutive pending messages */
flushPendingMessages(streamId, streamState) {
const buffer = streamState.reorderBuffer;
while (buffer.pending.has(buffer.nextSeq)) {
const message = buffer.pending.get(buffer.nextSeq);
buffer.pending.delete(buffer.nextSeq);
this.deliverMessage(streamState, message);
buffer.nextSeq++;
}
if (buffer.pending.size === 0 && buffer.flushTimeout) {
clearTimeout(buffer.flushTimeout);
buffer.flushTimeout = null;
}
}
/** Force-flush all pending messages in order (used on timeout or overflow) */
forceFlushBuffer(streamId, streamState) {
const buffer = streamState.reorderBuffer;
if (buffer.flushTimeout) {
clearTimeout(buffer.flushTimeout);
buffer.flushTimeout = null;
}
if (buffer.pending.size === 0) {
return;
}
const sortedSeqs = [...buffer.pending.keys()].sort((a, b) => a - b);
const skipped = sortedSeqs[0] - buffer.nextSeq;
if (skipped > 0) {
dataSchemas.logger.warn(`[RedisEventTransport] Stream ${streamId}: skipping ${skipped} missing messages (seq ${buffer.nextSeq}-${sortedSeqs[0] - 1})`);
}
for (const seq of sortedSeqs) {
const message = buffer.pending.get(seq);
buffer.pending.delete(seq);
this.deliverMessage(streamState, message);
}
buffer.nextSeq = sortedSeqs[sortedSeqs.length - 1] + 1;
}
/** Schedule a timeout to force-flush if gaps aren't filled */
scheduleFlushTimeout(streamId, streamState) {
const buffer = streamState.reorderBuffer;
if (buffer.flushTimeout) {
return;
}
buffer.flushTimeout = setTimeout(() => {
buffer.flushTimeout = null;
if (buffer.pending.size > 0) {
dataSchemas.logger.warn(`[RedisEventTransport] Stream ${streamId}: timeout waiting for seq ${buffer.nextSeq}, force-flushing ${buffer.pending.size} messages`);
this.forceFlushBuffer(streamId, streamState);
}
}, REORDER_TIMEOUT_MS);
}
/** Deliver a message to all handlers */
deliverMessage(streamState, message) {
var _a, _b, _c;
for (const [, handlers] of streamState.handlers) {
switch (message.type) {
case EventTypes.CHUNK:
handlers.onChunk(message.data);
break;
case EventTypes.DONE:
(_a = handlers.onDone) === null || _a === void 0 ? void 0 : _a.call(handlers, message.data);
break;
case EventTypes.ERROR:
(_b = handlers.onError) === null || _b === void 0 ? void 0 : _b.call(handlers, (_c = message.error) !== null && _c !== void 0 ? _c : 'Unknown error');
break;
}
}
if (message.type === EventTypes.ABORT) {
for (const callback of streamState.abortCallbacks) {
try {
callback();
}
catch (err) {
dataSchemas.logger.error(`[RedisEventTransport] Error in abort callback:`, err);
}
}
}
}
/**
* Subscribe to events for a stream.
*
* On first subscriber for a stream, subscribes to the Redis channel.
* Returns unsubscribe function that cleans up when last subscriber leaves.
*/
subscribe(streamId, handlers) {
const channel = CHANNELS.events(streamId);
const subscriberId = `sub_${++this.subscriberIdCounter}`;
// Initialize stream state if needed
if (!this.streams.has(streamId)) {
this.streams.set(streamId, {
count: 0,
handlers: new Map(),
allSubscribersLeftCallbacks: [],
abortCallbacks: [],
reorderBuffer: {
nextSeq: 0,
pending: new Map(),
flushTimeout: null,
},
});
}
const streamState = this.streams.get(streamId);
streamState.count++;
streamState.handlers.set(subscriberId, handlers);
let readyPromise = this.channelSubscriptions.get(channel);
if (!readyPromise) {
readyPromise = this.subscriber
.subscribe(channel)
.then(() => {
dataSchemas.logger.debug(`[RedisEventTransport] Subscription active for channel ${channel}`);
})
.catch((err) => {
this.channelSubscriptions.delete(channel);
dataSchemas.logger.error(`[RedisEventTransport] Failed to subscribe to ${channel}:`, err);
});
this.channelSubscriptions.set(channel, readyPromise);
}
return {
ready: readyPromise,
unsubscribe: () => {
const state = this.streams.get(streamId);
if (!state) {
return;
}
state.handlers.delete(subscriberId);
state.count--;
// If last subscriber left, unsubscribe from Redis and notify
if (state.count === 0) {
// Clear any pending flush timeout and buffered messages
if (state.reorderBuffer.flushTimeout) {
clearTimeout(state.reorderBuffer.flushTimeout);
state.reorderBuffer.flushTimeout = null;
}
state.reorderBuffer.pending.clear();
this.subscriber.unsubscribe(channel).catch((err) => {
dataSchemas.logger.error(`[RedisEventTransport] Failed to unsubscribe from ${channel}:`, err);
});
this.channelSubscriptions.delete(channel);
// Call all-subscribers-left callbacks
for (const callback of state.allSubscribersLeftCallbacks) {
try {
callback();
}
catch (err) {
dataSchemas.logger.error(`[RedisEventTransport] Error in allSubscribersLeft callback:`, err);
}
}
/**
* Preserve stream state (callbacks, abort handlers) for reconnection.
* Previously this deleted the entire state, which lost the
* allSubscribersLeftCallbacks and abortCallbacks registered by
* GenerationJobManager.createJob(). On the next subscribe() call,
* fresh state was created without those callbacks, causing
* hasSubscriber to never reset and syncReorderBuffer to be skipped.
* State is fully cleaned up by cleanup() when the job completes.
*/
}
},
};
}
/**
* Publish a chunk event to all subscribers across all instances.
* Includes sequence number for ordered delivery in Redis Cluster mode.
*/
emitChunk(streamId, event) {
return __awaiter(this, void 0, void 0, function* () {
const channel = CHANNELS.events(streamId);
const seq = this.getNextSequence(streamId);
const message = { type: EventTypes.CHUNK, seq, data: event };
try {
yield this.publisher.publish(channel, JSON.stringify(message));
}
catch (err) {
dataSchemas.logger.error(`[RedisEventTransport] Failed to publish chunk:`, err);
}
});
}
/**
* Publish a done event to all subscribers.
* Includes sequence number to ensure delivery after all chunks.
*/
emitDone(streamId, event) {
return __awaiter(this, void 0, void 0, function* () {
const channel = CHANNELS.events(streamId);
const seq = this.getNextSequence(streamId);
const message = { type: EventTypes.DONE, seq, data: event };
try {
yield this.publisher.publish(channel, JSON.stringify(message));
}
catch (err) {
dataSchemas.logger.error(`[RedisEventTransport] Failed to publish done:`, err);
throw err;
}
});
}
/**
* Publish an error event to all subscribers.
* Includes sequence number to ensure delivery after all chunks.
*/
emitError(streamId, error) {
return __awaiter(this, void 0, void 0, function* () {
const channel = CHANNELS.events(streamId);
const seq = this.getNextSequence(streamId);
const message = { type: EventTypes.ERROR, seq, error };
try {
yield this.publisher.publish(channel, JSON.stringify(message));
}
catch (err) {
dataSchemas.logger.error(`[RedisEventTransport] Failed to publish error:`, err);
throw err;
}
});
}
/**
* Get subscriber count for a stream (local instance only).
*
* Note: In a multi-instance setup, this only returns local subscriber count.
* For global count, would need to track in Redis (e.g., with a counter key).
*/
getSubscriberCount(streamId) {
var _a, _b;
return (_b = (_a = this.streams.get(streamId)) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0;
}
/**
* Check if this is the first subscriber (local instance only).
*/
isFirstSubscriber(streamId) {
return this.getSubscriberCount(streamId) === 1;
}
/**
* Register callback for when all subscribers leave.
*/
onAllSubscribersLeft(streamId, callback) {
const state = this.streams.get(streamId);
if (state) {
state.allSubscribersLeftCallbacks.push(callback);
}
else {
// Create state just for the callback
this.streams.set(streamId, {
count: 0,
handlers: new Map(),
allSubscribersLeftCallbacks: [callback],
abortCallbacks: [],
reorderBuffer: {
nextSeq: 0,
pending: new Map(),
flushTimeout: null,
},
});
}
}
/**
* Publish an abort signal to all replicas.
* This enables cross-replica abort: when a user aborts on Replica B,
* the generating Replica A receives the signal and stops.
*/
emitAbort(streamId) {
const channel = CHANNELS.events(streamId);
const message = { type: EventTypes.ABORT };
this.publisher.publish(channel, JSON.stringify(message)).catch((err) => {
dataSchemas.logger.error(`[RedisEventTransport] Failed to publish abort:`, err);
});
}
/**
* Register callback for abort signals from any replica.
* Called when abort is triggered on any replica (including this one).
*
* @param streamId - The stream identifier
* @param callback - Called when abort signal is received
*/
onAbort(streamId, callback) {
const channel = CHANNELS.events(streamId);
let state = this.streams.get(streamId);
if (!state) {
state = {
count: 0,
handlers: new Map(),
allSubscribersLeftCallbacks: [],
abortCallbacks: [],
reorderBuffer: {
nextSeq: 0,
pending: new Map(),
flushTimeout: null,
},
};
this.streams.set(streamId, state);
}
state.abortCallbacks.push(callback);
if (!this.channelSubscriptions.has(channel)) {
const ready = this.subscriber
.subscribe(channel)
.then(() => { })
.catch((err) => {
this.channelSubscriptions.delete(channel);
dataSchemas.logger.error(`[RedisEventTransport] Failed to subscribe to ${channel}:`, err);
});
this.channelSubscriptions.set(channel, ready);
}
}
/**
* Get all tracked stream IDs (for orphan cleanup)
*/
getTrackedStreamIds() {
return Array.from(this.streams.keys());
}
/**
* Cleanup resources for a specific stream.
*/
cleanup(streamId) {
const channel = CHANNELS.events(streamId);
const state = this.streams.get(streamId);
if (state) {
// Clear flush timeout
if (state.reorderBuffer.flushTimeout) {
clearTimeout(state.reorderBuffer.flushTimeout);
state.reorderBuffer.flushTimeout = null;
}
// Clear all handlers and callbacks
state.handlers.clear();
state.allSubscribersLeftCallbacks = [];
state.abortCallbacks = [];
state.reorderBuffer.pending.clear();
}
// Reset sequence counter for this stream
this.resetSequence(streamId);
if (this.channelSubscriptions.has(channel)) {
this.subscriber.unsubscribe(channel).catch((err) => {
dataSchemas.logger.error(`[RedisEventTransport] Failed to cleanup ${channel}:`, err);
});
this.channelSubscriptions.delete(channel);
}
this.streams.delete(streamId);
}
/**
* Destroy all resources.
*/
destroy() {
// Clear all flush timeouts and buffered messages
for (const [, state] of this.streams) {
if (state.reorderBuffer.flushTimeout) {
clearTimeout(state.reorderBuffer.flushTimeout);
state.reorderBuffer.flushTimeout = null;
}
state.reorderBuffer.pending.clear();
}
for (const channel of this.channelSubscriptions.keys()) {
this.subscriber.unsubscribe(channel).catch(() => { });
}
this.channelSubscriptions.clear();
this.streams.clear();
this.sequenceCounters.clear();
try {
this.subscriber.disconnect();
}
catch (_a) {
/* ignore */
}
dataSchemas.logger.info('[RedisEventTransport] Destroyed');
}
}
/**
* Create stream services (job store + event transport).
*
* Automatically detects Redis from cacheConfig.USE_REDIS_STREAMS and uses
* the existing ioredisClient. Falls back to in-memory if Redis
* is not configured or not available.
*
* USE_REDIS_STREAMS defaults to USE_REDIS if not explicitly set,
* allowing users to disable Redis for streams while keeping it for other caches.
*
* @example Auto-detect (uses cacheConfig)
* ```ts
* const services = createStreamServices();
* // Uses Redis if USE_REDIS_STREAMS=true (defaults to USE_REDIS), otherwise in-memory
* ```
*
* @example Force in-memory
* ```ts
* const services = createStreamServices({ useRedis: false });
* ```
*/
function createStreamServices(config = {}) {
var _a, _b;
// Use provided config or fall back to cache config (USE_REDIS_STREAMS for stream-specific override)
const useRedis = (_a = config.useRedis) !== null && _a !== void 0 ? _a : cacheConfig.USE_REDIS_STREAMS;
const redisClient = (_b = config.redisClient) !== null && _b !== void 0 ? _b : exports.ioredisClient;
const { redisSubscriber, inMemoryOptions } = config;
// Check if we should and can use Redis
if (useRedis && redisClient) {
try {
// For subscribing, we need a dedicated connection
// If subscriber not provided, duplicate the main client
let subscriber = redisSubscriber;
if (!subscriber && 'duplicate' in redisClient) {
subscriber = redisClient.duplicate();
dataSchemas.logger.info('[StreamServices] Duplicated Redis client for subscriber');
}
if (!subscriber) {
dataSchemas.logger.warn('[StreamServices] No subscriber client available, falling back to in-memory');
return createInMemoryServices(inMemoryOptions);
}
const jobStore = new RedisJobStore(redisClient);
const eventTransport = new RedisEventTransport(redisClient, subscriber);
dataSchemas.logger.info('[StreamServices] Created Redis-backed stream services');
return {
jobStore,
eventTransport,
isRedis: true,
};
}
catch (err) {
dataSchemas.logger.error('[StreamServices] Failed to create Redis services, falling back to in-memory:', err);
return createInMemoryServices(inMemoryOptions);
}
}
return createInMemoryServices(inMemoryOptions);
}
/**
* Create in-memory stream services
*/
function createInMemoryServices(options) {
var _a, _b;
const jobStore = new InMemoryJobStore({
ttlAfterComplete: (_a = options === null || options === void 0 ? void 0 : options.ttlAfterComplete) !== null && _a !== void 0 ? _a : 300000, // 5 minutes
maxJobs: (_b = options === null || options === void 0 ? void 0 : options.maxJobs) !== null && _b !== void 0 ? _b : 1000,
});
const eventTransport = new InMemoryEventTransport();
dataSchemas.logger.info('[StreamServices] Created in-memory stream services');
return {
jobStore,
eventTransport,
isRedis: false,
};
}
Object.defineProperty(exports, "decrypt", {
enumerable: true,
get: function () { return dataSchemas.decrypt; }
});
Object.defineProperty(exports, "decryptV2", {
enumerable: true,
get: function () { return dataSchemas.decryptV2; }
});
Object.defineProperty(exports, "decryptV3", {
enumerable: true,
get: function () { return dataSchemas.decryptV3; }
});
Object.defineProperty(exports, "encrypt", {
enumerable: true,
get: function () { return dataSchemas.encrypt; }
});
Object.defineProperty(exports, "encryptV2", {
enumerable: true,
get: function () { return dataSchemas.encryptV2; }
});
Object.defineProperty(exports, "encryptV3", {
enumerable: true,
get: function () { return dataSchemas.encryptV3; }
});
Object.defineProperty(exports, "getRandomValues", {
enumerable: true,
get: function () { return dataSchemas.getRandomValues; }
});
Object.defineProperty(exports, "hashBackupCode", {
enumerable: true,
get: function () { return dataSchemas.hashBackupCode; }
});
Object.defineProperty(exports, "supportsAdaptiveThinking", {
enumerable: true,
get: function () { return librechatDataProvider.supportsAdaptiveThinking; }
});
exports.AVATAR_REFRESH_BATCH_SIZE = AVATAR_REFRESH_BATCH_SIZE;
exports.AgentApiKeyService = AgentApiKeyService;
exports.BasicToolEndHandler = BasicToolEndHandler;
exports.DEFAULT_GRAPH_SCOPES = DEFAULT_GRAPH_SCOPES;
exports.DEFAULT_RETENTION_HOURS = DEFAULT_RETENTION_HOURS;
exports.ErrorController = ErrorController;
exports.FlowStateManager = FlowStateManager;
exports.GRAPH_TOKEN_PLACEHOLDER = GRAPH_TOKEN_PLACEHOLDER;
exports.GenerationJobManager = GenerationJobManager;
exports.GenerationJobManagerClass = GenerationJobManagerClass;
exports.GraphEvents = GraphEvents;
exports.InMemoryEventTransport = InMemoryEventTransport;
exports.InMemoryJobStore = InMemoryJobStore;
exports.MAX_AVATAR_REFRESH_AGENTS = MAX_AVATAR_REFRESH_AGENTS;
exports.MAX_RETENTION_HOURS = MAX_RETENTION_HOURS;
exports.MCPConnection = MCPConnection;
exports.MCPDomainNotAllowedError = MCPDomainNotAllowedError;
exports.MCPErrorCodes = MCPErrorCodes;
exports.MCPInspectionFailedError = MCPInspectionFailedError;
exports.MCPManager = MCPManager;
exports.MCPOAuthHandler = MCPOAuthHandler;
exports.MCPServersRegistry = MCPServersRegistry;
exports.MCPTokenStorage = MCPTokenStorage;
exports.MIN_RETENTION_HOURS = MIN_RETENTION_HOURS;
exports.OAUTH_CSRF_COOKIE = OAUTH_CSRF_COOKIE;
exports.OAUTH_CSRF_MAX_AGE = OAUTH_CSRF_MAX_AGE;
exports.OAUTH_SESSION_COOKIE = OAUTH_SESSION_COOKIE;
exports.OAUTH_SESSION_COOKIE_PATH = OAUTH_SESSION_COOKIE_PATH;
exports.OAUTH_SESSION_MAX_AGE = OAUTH_SESSION_MAX_AGE;
exports.OAuthReconnectionManager = OAuthReconnectionManager;
exports.OpenAIChatModelStreamHandler = OpenAIChatModelStreamHandler;
exports.OpenAIMessageDeltaHandler = OpenAIMessageDeltaHandler;
exports.OpenAIModelEndHandler = OpenAIModelEndHandler;
exports.OpenAIReasoningDeltaHandler = OpenAIReasoningDeltaHandler;
exports.OpenAIRunStepDeltaHandler = OpenAIRunStepDeltaHandler;
exports.OpenAIRunStepHandler = OpenAIRunStepHandler;
exports.OpenAIToolEndHandler = OpenAIToolEndHandler;
exports.RedisEventTransport = RedisEventTransport;
exports.RedisJobStore = RedisJobStore;
exports.StepTypes = StepTypes;
exports.Tokenizer = TokenizerSingleton;
exports._clearHanzoKeyCache = _clearHanzoKeyCache;
exports.agentAvatarSchema = agentAvatarSchema;
exports.agentBaseResourceSchema = agentBaseResourceSchema;
exports.agentBaseSchema = agentBaseSchema;
exports.agentCreateSchema = agentCreateSchema;
exports.agentFileResourceSchema = agentFileResourceSchema;
exports.agentHasDeferredTools = agentHasDeferredTools;
exports.agentHasProgrammaticTools = agentHasProgrammaticTools;
exports.agentSupportContactSchema = agentSupportContactSchema;
exports.agentToolOptionsSchema = agentToolOptionsSchema;
exports.agentToolResourcesSchema = agentToolResourcesSchema;
exports.agentUpdateSchema = agentUpdateSchema;
exports.applyContextToAgent = applyContextToAgent;
exports.applyDefaultParams = applyDefaultParams$1;
exports.azureAISearchSchema = azureAISearchSchema;
exports.backfillRemoteAgentPermissions = backfillRemoteAgentPermissions;
exports.batchDeleteKeys = batchDeleteKeys;
exports.billingSubject = billingSubject;
exports.buildAgentInstructions = buildAgentInstructions;
exports.buildAggregatedResponse = buildAggregatedResponse;
exports.buildImageToolContext = buildImageToolContext;
exports.buildNonStreamingResponse = buildNonStreamingResponse;
exports.buildPromptGroupFilter = buildPromptGroupFilter;
exports.buildResponse = buildResponse;
exports.buildResponsesNonStreamingResponse = buildResponsesNonStreamingResponse;
exports.buildToolClassification = buildToolClassification;
exports.buildToolRegistryFromAgentOptions = buildToolRegistryFromAgentOptions;
exports.buildToolSet = buildToolSet;
exports.buildWebSearchContext = buildWebSearchContext;
exports.cacheConfig = cacheConfig;
exports.checkAccess = checkAccess;
exports.checkAgentPermissionsMigration = checkAgentPermissionsMigration;
exports.checkAndIncrementPendingRequest = checkAndIncrementPendingRequest;
exports.checkConfig = checkConfig;
exports.checkEmailConfig = checkEmailConfig;
exports.checkHealth = checkHealth;
exports.checkInterfaceConfig = checkInterfaceConfig;
exports.checkPluginAuth = checkPluginAuth;
exports.checkPromptCacheSupport = checkPromptCacheSupport;
exports.checkPromptPermissionsMigration = checkPromptPermissionsMigration;
exports.checkRemoteAgentAccess = checkRemoteAgentAccess;
exports.checkUserKeyExpiry = checkUserKeyExpiry;
exports.checkVariables = checkVariables;
exports.checkWebSearchConfig = checkWebSearchConfig;
exports.cleanupMCPToolSchemas = cleanupMCPToolSchemas;
exports.configureReasoning = configureReasoning;
exports.conflictingAzureVariables = conflictingAzureVariables;
exports.constructAzureURL = constructAzureURL;
exports.containsGraphTokenPlaceholder = containsGraphTokenPlaceholder;
exports.convertInputToMessages = convertInputToMessages;
exports.convertJsonSchemaToZod = convertJsonSchemaToZod;
exports.convertMessages = convertMessages;
exports.convertOcrToContextInPlace = convertOcrToContextInPlace;
exports.convertWithResolvedRefs = convertWithResolvedRefs;
exports.countTokens = countTokens;
exports.createAgentChatCompletion = createAgentChatCompletion;
exports.createAggregatorEventHandlers = createAggregatorEventHandlers;
exports.createAnthropicVertexClient = createAnthropicVertexClient;
exports.createApiKeyHandlers = createApiKeyHandlers;
exports.createApiKeyServiceDependencies = createApiKeyServiceDependencies;
exports.createAxiosInstance = createAxiosInstance;
exports.createBearerAuthHeader = createBearerAuthHeader;
exports.createCheckRemoteAgentAccess = createCheckRemoteAgentAccess;
exports.createChunk = createChunk;
exports.createEdgeCollector = createEdgeCollector;
exports.createEmptyPromptGroupsResponse = createEmptyPromptGroupsResponse;
exports.createErrorResponse = createErrorResponse;
exports.createFetch = createFetch;
exports.createFunctionCallItem = createFunctionCallItem;
exports.createFunctionCallOutputItem = createFunctionCallOutputItem;
exports.createHandleLLMNewToken = createHandleLLMNewToken;
exports.createHandleOAuthToken = createHandleOAuthToken;
exports.createMemoryCallback = createMemoryCallback;
exports.createMemoryProcessor = createMemoryProcessor;
exports.createMemoryTool = createMemoryTool;
exports.createMessageItem = createMessageItem;
exports.createMultiAgentMapper = createMultiAgentMapper;
exports.createOpenAIContentAggregator = createOpenAIContentAggregator;
exports.createOpenAIHandlers = createOpenAIHandlers;
exports.createOpenAIStreamTracker = createOpenAIStreamTracker;
exports.createOutputTextContent = createOutputTextContent;
exports.createReasoningItem = createReasoningItem;
exports.createReasoningTextContent = createReasoningTextContent;
exports.createRequireApiKeyAuth = createRequireApiKeyAuth;
exports.createResponseAggregator = createResponseAggregator;
exports.createResponseContext = createResponseContext;
exports.createResponseTracker = createResponseTracker;
exports.createResponsesEventHandlers = createResponsesEventHandlers;
exports.createRun = createRun;
exports.createSSRFSafeAgents = createSSRFSafeAgents;
exports.createSSRFSafeUndiciConnect = createSSRFSafeUndiciConnect;
exports.createSafeUser = createSafeUser;
exports.createSequentialChainEdges = createSequentialChainEdges;
exports.createSetBalanceConfig = createSetBalanceConfig;
exports.createStreamEventHandlers = createStreamEventHandlers;
exports.createStreamServices = createStreamServices;
exports.createTempChatExpirationDate = createTempChatExpirationDate;
exports.createTokenCounter = createTokenCounter;
exports.createToolExecuteHandler = createToolExecuteHandler;
exports.createToolExecuteHandlers = createToolExecuteHandlers;
exports.dalle3Schema = dalle3Schema;
exports.decrementPendingRequest = decrementPendingRequest;
exports.deleteMistralFile = deleteMistralFile;
exports.deleteRagFile = deleteRagFile;
exports.deprecatedAzureVariables = deprecatedAzureVariables;
exports.deriveBaseURL = deriveBaseURL;
exports.detectOAuthRequirement = detectOAuthRequirement;
exports.emitAttachment = emitAttachment;
exports.emitError = emitError;
exports.emitFunctionCallArgumentsDelta = emitFunctionCallArgumentsDelta;
exports.emitFunctionCallArgumentsDone = emitFunctionCallArgumentsDone;
exports.emitFunctionCallItemAdded = emitFunctionCallItemAdded;
exports.emitFunctionCallItemDone = emitFunctionCallItemDone;
exports.emitFunctionCallOutputItem = emitFunctionCallOutputItem;
exports.emitMessageItemAdded = emitMessageItemAdded;
exports.emitMessageItemDone = emitMessageItemDone;
exports.emitOutputTextDelta = emitOutputTextDelta;
exports.emitOutputTextDone = emitOutputTextDone;
exports.emitReasoningContentPartAdded = emitReasoningContentPartAdded;
exports.emitReasoningContentPartDone = emitReasoningContentPartDone;
exports.emitReasoningDelta = emitReasoningDelta;
exports.emitReasoningDone = emitReasoningDone;
exports.emitReasoningItemAdded = emitReasoningItemAdded;
exports.emitReasoningItemDone = emitReasoningItemDone;
exports.emitResponseCompleted = emitResponseCompleted;
exports.emitResponseCreated = emitResponseCreated;
exports.emitResponseFailed = emitResponseFailed;
exports.emitResponseInProgress = emitResponseInProgress;
exports.emitTextContentPartAdded = emitTextContentPartAdded;
exports.emitTextContentPartDone = emitTextContentPartDone;
exports.encodeAndFormatAudios = encodeAndFormatAudios;
exports.encodeAndFormatDocuments = encodeAndFormatDocuments;
exports.encodeAndFormatVideos = encodeAndFormatVideos;
exports.encodeHeaderValue = encodeHeaderValue;
exports.enrichRemoteAgentPrincipals = enrichRemoteAgentPrincipals;
exports.ensureCollectionExists = ensureCollectionExists;
exports.ensureRequiredCollectionsExist = ensureRequiredCollectionsExist;
exports.escapeRegExp = escapeRegExp;
exports.escapeRegex = escapeRegex;
exports.exchangeAdminCode = exchangeAdminCode;
exports.extractBaseURL = extractBaseURL;
exports.extractDefaultParams = extractDefaultParams;
exports.extractDiscoveredToolsFromHistory = extractDiscoveredToolsFromHistory;
exports.extractFileContext = extractFileContext;
exports.extractLibreChatParams = extractLibreChatParams;
exports.extractMCPServerDomain = extractMCPServerDomain;
exports.extractMCPServers = extractMCPServers;
exports.extractMCPToolDefinition = extractMCPToolDefinition;
exports.extractOpenIDTokenInfo = extractOpenIDTokenInfo;
exports.extractWebSearchEnvVars = extractWebSearchEnvVars;
exports.fetchAnthropicModels = fetchAnthropicModels;
exports.fetchModels = fetchModels;
exports.fetchOpenAIModels = fetchOpenAIModels;
exports.fileSearchSchema = fileSearchSchema;
exports.filterAccessibleIdsBySharedLogic = filterAccessibleIdsBySharedLogic;
exports.filterFilesByEndpointConfig = filterFilesByEndpointConfig;
exports.filterMalformedContentParts = filterMalformedContentParts;
exports.filterOrphanedEdges = filterOrphanedEdges;
exports.filterUniquePlugins = filterUniquePlugins;
exports.findMatchingPattern = findMatchingPattern;
exports.findOpenIDUser = findOpenIDUser;
exports.findPrimaryAgentId = findPrimaryAgentId;
exports.fluxApiSchema = fluxApiSchema;
exports.formatPromptGroupsResponse = formatPromptGroupsResponse;
exports.geminiImageGenSchema = geminiImageGenSchema;
exports.geminiToolkit = geminiToolkit;
exports.genAzureChatCompletion = genAzureChatCompletion;
exports.genAzureEndpoint = genAzureEndpoint;
exports.generateAdminExchangeCode = generateAdminExchangeCode;
exports.generateArtifactsPrompt = generateArtifactsPrompt;
exports.generateCheckAccess = generateCheckAccess;
exports.generateItemId = generateItemId;
exports.generateOAuthCsrfToken = generateOAuthCsrfToken;
exports.generateResponseId = generateResponseId;
exports.generateServerNameFromTitle = generateServerNameFromTitle;
exports.generateShortLivedToken = generateShortLivedToken;
exports.getAccessToken = getAccessToken;
exports.getAdminPanelUrl = getAdminPanelUrl;
exports.getAllToolDefinitions = getAllToolDefinitions;
exports.getAnthropicModels = getAnthropicModels;
exports.getAzureContainerClient = getAzureContainerClient;
exports.getAzureCredentials = getAzureCredentials;
exports.getBalanceConfig = getBalanceConfig;
exports.getBasePath = getBasePath;
exports.getBedrockModels = getBedrockModels;
exports.getClaudeHeaders = getClaudeHeaders;
exports.getCustomEndpointConfig = getCustomEndpointConfig;
exports.getEdgeKey = getEdgeKey;
exports.getEdgeParticipants = getEdgeParticipants;
exports.getFileBasename = getFileBasename;
exports.getFirebaseStorage = getFirebaseStorage;
exports.getGoogleConfig = getGoogleConfig;
exports.getGoogleModels = getGoogleModels;
exports.getImageBasename = getImageBasename;
exports.getLLMConfig = getLLMConfig;
exports.getMCPInstructionsForServers = getMCPInstructionsForServers;
exports.getModelMaxOutputTokens = getModelMaxOutputTokens;
exports.getModelMaxTokens = getModelMaxTokens;
exports.getModelTokenValue = getModelTokenValue;
exports.getOpenAIConfig = getOpenAIConfig;
exports.getOpenAILLMConfig = getOpenAILLMConfig;
exports.getOpenAIModels = getOpenAIModels;
exports.getProviderConfig = getProviderConfig;
exports.getReasoningKey = getReasoningKey;
exports.getRemoteAgentPermissions = getRemoteAgentPermissions;
exports.getSafetySettings = getSafetySettings;
exports.getServerNameFromTool = getServerNameFromTool;
exports.getSignedUrl = getSignedUrl;
exports.getTempChatRetentionHours = getTempChatRetentionHours;
exports.getThreadData = getThreadData;
exports.getToolDefinition = getToolDefinition;
exports.getToolSchema = getToolSchema;
exports.getToolkitKey = getToolkitKey;
exports.getTransactionsConfig = getTransactionsConfig;
exports.getUserMCPAuthMap = getUserMCPAuthMap;
exports.getVertexCredentialOptions = getVertexCredentialOptions;
exports.getVertexDeploymentName = getVertexDeploymentName;
exports.getViolationInfo = getViolationInfo;
exports.googleSearchSchema = googleSearchSchema;
exports.graphEdgeSchema = graphEdgeSchema;
exports.handleError = handleError;
exports.handleJsonParseError = handleJsonParseError;
exports.hasCustomUserVars = hasCustomUserVars;
exports.imageEditOaiSchema = imageEditOaiSchema;
exports.imageGenOaiSchema = imageGenOaiSchema;
exports.initializeAgent = initializeAgent;
exports.initializeAnthropic = initializeAnthropic;
exports.initializeAzureBlobService = initializeAzureBlobService;
exports.initializeBedrock = initializeBedrock;
exports.initializeCustom = initializeCustom;
exports.initializeFileStorage = initializeFileStorage;
exports.initializeFirebase = initializeFirebase;
exports.initializeGoogle = initializeGoogle;
exports.initializeOpenAI = initializeOpenAI;
exports.initializeS3 = initializeS3;
exports.inputSchema = inputSchema;
exports.isActionDomainAllowed = isActionDomainAllowed;
exports.isAdminPanelRedirect = isAdminPanelRedirect;
exports.isAnthropicVertexCredentials = isAnthropicVertexCredentials;
exports.isChatCompletionValidationFailure = isChatCompletionValidationFailure;
exports.isConcurrentLimitEnabled = isConcurrentLimitEnabled;
exports.isEmailDomainAllowed = isEmailDomainAllowed;
exports.isEnabled = isEnabled;
exports.isHanzoPerUserKeyEnabled = isHanzoPerUserKeyEnabled;
exports.isKnownCustomProvider = isKnownCustomProvider;
exports.isMCPDomainAllowed = isMCPDomainAllowed;
exports.isMCPDomainNotAllowedError = isMCPDomainNotAllowedError;
exports.isMCPInspectionFailedError = isMCPInspectionFailedError;
exports.isMCPTool = isMCPTool;
exports.isMemoryEnabled = isMemoryEnabled;
exports.isOpenIDAvailable = isOpenIDAvailable;
exports.isOpenIDTokenValid = isOpenIDTokenValid;
exports.isPrivateIP = isPrivateIP;
exports.isSSRFTarget = isSSRFTarget;
exports.isUserProvided = isUserProvided;
exports.isValidationFailure = isValidationFailure;
exports.keyvMongo = keyvMongo;
exports.knownAnthropicParams = knownAnthropicParams;
exports.knownGoogleParams = knownGoogleParams;
exports.knownOpenAIParams = knownOpenAIParams;
exports.limiterCache = limiterCache;
exports.listAgentModels = listAgentModels;
exports.loadAnthropicVertexCredentials = loadAnthropicVertexCredentials;
exports.loadCustomEndpointsConfig = loadCustomEndpointsConfig;
exports.loadMemoryConfig = loadMemoryConfig;
exports.loadOCRConfig = loadOCRConfig;
exports.loadServiceKey = loadServiceKey;
exports.loadToolDefinitions = loadToolDefinitions;
exports.loadWebSearchAuth = loadWebSearchAuth;
exports.loadYaml = loadYaml;
exports.logAgentMigrationWarning = logAgentMigrationWarning;
exports.logAxiosError = logAxiosError;
exports.logFile = logFile;
exports.logHeaders = logHeaders;
exports.logPromptMigrationWarning = logPromptMigrationWarning;
exports.logToolError = logToolError;
exports.markPublicPromptGroups = markPublicPromptGroups;
exports.matchModelName = matchModelName;
exports.math = math;
exports.maxOutputTokensMap = maxOutputTokensMap;
exports.maxTokensMap = maxTokensMap;
exports.mcpOptionsContainGraphTokenPlaceholder = mcpOptionsContainGraphTokenPlaceholder;
exports.mcpToolPattern = mcpToolPattern$1;
exports.memoryInstructions = memoryInstructions;
exports.mergeAgentOcrConversion = mergeAgentOcrConversion;
exports.mergeMessagesWithInput = mergeMessagesWithInput;
exports.modelMaxOutputs = modelMaxOutputs;
exports.modelSchema = modelSchema;
exports.normalizeHttpError = normalizeHttpError;
exports.normalizeJsonSchema = normalizeJsonSchema;
exports.normalizeServerName = normalizeServerName;
exports.oaiToolkit = oaiToolkit;
exports.omitTitleOptions = omitTitleOptions;
exports.openWeatherSchema = openWeatherSchema;
exports.optionalChainWithEmptyCheck = optionalChainWithEmptyCheck;
exports.overrideDeferLoadingForDiscoveredTools = overrideDeferLoadingForDiscoveredTools;
exports.parseText = parseText;
exports.parseTextNative = parseTextNative;
exports.payloadParser = payloadParser;
exports.performOCR = performOCR;
exports.performStartupChecks = performStartupChecks;
exports.preProcessGraphTokens = preProcessGraphTokens;
exports.primeResources = primeResources;
exports.processAudioFile = processAudioFile;
exports.processMCPEnv = processMCPEnv;
exports.processMemory = processMemory;
exports.processModelData = processModelData;
exports.processOpenIDPlaceholders = processOpenIDPlaceholders;
exports.processTextWithTokenLimit = processTextWithTokenLimit;
exports.providerConfigMap = providerConfigMap;
exports.readFileAsBuffer = readFileAsBuffer;
exports.readFileAsString = readFileAsString;
exports.readJsonFile = readJsonFile;
exports.recordCollectedUsage = recordCollectedUsage;
exports.recordContainsGraphTokenPlaceholder = recordContainsGraphTokenPlaceholder;
exports.refreshAccessToken = refreshAccessToken;
exports.refreshListAvatars = refreshListAvatars;
exports.requireAdmin = requireAdmin;
exports.resolveGraphTokenPlaceholder = resolveGraphTokenPlaceholder;
exports.resolveGraphTokensInRecord = resolveGraphTokensInRecord;
exports.resolveHanzoCloudKey = resolveHanzoCloudKey;
exports.resolveHeaders = resolveHeaders;
exports.resolveHostnameSSRF = resolveHostnameSSRF;
exports.resolveJsonSchemaRefs = resolveJsonSchemaRefs;
exports.resolveNestedObject = resolveNestedObject;
exports.safeStringify = safeStringify;
exports.safeValidatePromptGroupUpdate = safeValidatePromptGroupUpdate;
exports.sanitizeFileForTransmit = sanitizeFileForTransmit;
exports.sanitizeFilename = sanitizeFilename;
exports.sanitizeMessageForTransmit = sanitizeMessageForTransmit;
exports.sanitizeModelName = sanitizeModelName;
exports.sanitizeTitle = sanitizeTitle;
exports.sanitizeUrlForLogging = sanitizeUrlForLogging;
exports.scanKeys = scanKeys;
exports.sendErrorResponse = sendErrorResponse;
exports.sendEvent = sendEvent;
exports.sendFinalChunk = sendFinalChunk;
exports.sendResponsesErrorResponse = sendResponsesErrorResponse;
exports.serializeUserForExchange = serializeUserForExchange;
exports.sessionCache = sessionCache;
exports.setOAuthCsrfCookie = setOAuthCsrfCookie;
exports.setOAuthSession = setOAuthSession;
exports.setOAuthSessionCookie = setOAuthSessionCookie;
exports.setupStreamingResponse = setupStreamingResponse;
exports.shouldUseSecureCookie = shouldUseSecureCookie;
exports.skipAgentCheck = skipAgentCheck;
exports.splitAndTrim = splitAndTrim;
exports.stableDiffusionSchema = stableDiffusionSchema;
exports.standardCache = standardCache;
exports.tavilySearchSchema = tavilySearchSchema;
exports.tiktokenModels = tiktokenModels;
exports.toolDefinitions = toolDefinitions;
exports.toolOptionsSchema = toolOptionsSchema;
exports.traversaalSearchSchema = traversaalSearchSchema;
exports.unescapeLaTeX = unescapeLaTeX;
exports.updateInterfacePermissions = updateInterfacePermissions;
exports.updatePromptGroupSchema = updatePromptGroupSchema;
exports.updateTrackerUsage = updateTrackerUsage;
exports.uploadAzureMistralOCR = uploadAzureMistralOCR;
exports.uploadDocumentToMistral = uploadDocumentToMistral;
exports.uploadGoogleVertexMistralOCR = uploadGoogleVertexMistralOCR;
exports.uploadMistralOCR = uploadMistralOCR;
exports.validateAgentModel = validateAgentModel;
exports.validateAudio = validateAudio;
exports.validateImage = validateImage;
exports.validateOAuthCsrf = validateOAuthCsrf;
exports.validateOAuthSession = validateOAuthSession;
exports.validatePdf = validatePdf;
exports.validatePromptGroupUpdate = validatePromptGroupUpdate;
exports.validateRequest = validateRequest;
exports.validateResponseRequest = validateResponseRequest;
exports.validateVideo = validateVideo;
exports.violationCache = violationCache;
exports.violationFile = violationFile;
exports.withTimeout = withTimeout;
exports.wolframSchema = wolframSchema;
exports.writeAttachmentEvent = writeAttachmentEvent;
exports.writeDone = writeDone;
exports.writeEvent = writeEvent;
exports.writeSSE = writeSSE;
//# sourceMappingURL=index.js.map