mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-21 23:55:23 +00:00
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: Use SSE to upload files in order to avoid idle timeouts. Idle timeouts can occur for example from gateways and other services like cloudfare when uploading large files. For example during rag processing the file is uploaded to librechat which then sends it to rag. While librechat is waiting for the embeddings to come back from rag the file upload is sitting idle. Gateways tend to want to cancel the upload with an http 408 , 504, or 524. This change uses SSE to perform the upload so that while librechat is sending the file to rag, it consistently sends back a heartbeat event to the client to keep the connection alive. This is especially useful when utilizing EMBEDDING_BATCH_SIZE in librechat rag which will allow rag to process signifigantly larger files without running out of memory. * added tests to packages\api\src\files\sse.spec.ts in order to test the new sse.ts * fix: Harden SSE file upload lifecycle * style: Sort data provider imports --------- Co-authored-by: Marc Amick <MarcAmick@jhu.edu> Co-authored-by: Danny Avila <danny@librechat.ai>
316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
const express = require('express');
|
|
const {
|
|
isEnabled,
|
|
getBalanceConfig,
|
|
getCloudFrontConfig,
|
|
getAppConfigOptionsFromUser,
|
|
resolveBuildInfo,
|
|
resolveTitleTiming,
|
|
sanitizeModelSpecs,
|
|
excludeHiddenModelSpecs,
|
|
isFileSnapshotEnabled,
|
|
} = require('@librechat/api');
|
|
const { EModelEndpoint, defaultSocialLogins } = require('librechat-data-provider');
|
|
const { logger, getTenantId, SystemCapabilities } = require('@librechat/data-schemas');
|
|
const { hasCapability } = require('~/server/middleware/roles/capabilities');
|
|
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
|
const { getRumConfig } = require('~/server/services/Config/rum');
|
|
const { getAppConfig } = require('~/server/services/Config/app');
|
|
|
|
const router = express.Router();
|
|
const emailLoginEnabled =
|
|
process.env.ALLOW_EMAIL_LOGIN === undefined || isEnabled(process.env.ALLOW_EMAIL_LOGIN);
|
|
const passwordResetEnabled = isEnabled(process.env.ALLOW_PASSWORD_RESET);
|
|
|
|
const sharedLinksEnabled =
|
|
process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS);
|
|
|
|
const publicSharedLinksEnabled =
|
|
sharedLinksEnabled && isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC);
|
|
|
|
const sharePointFilePickerEnabled = isEnabled(process.env.ENABLE_SHAREPOINT_FILEPICKER);
|
|
const openidReuseTokens = isEnabled(process.env.OPENID_REUSE_TOKENS);
|
|
|
|
/**
|
|
* Resolve build metadata eagerly at module load so the first `/api/config`
|
|
* request does not pay the cost of `execFileSync('git', ...)` on the hot path.
|
|
* The resolver caches its result after the first call.
|
|
*/
|
|
resolveBuildInfo();
|
|
|
|
function isBirthday() {
|
|
const today = new Date();
|
|
return today.getMonth() === 1 && today.getDate() === 11;
|
|
}
|
|
|
|
/**
|
|
* Pre-login fields rendered by the unauthenticated login, registration, password-reset,
|
|
* and email-verification pages. Any field added here is readable by anonymous callers
|
|
* of `GET /api/config`, so keep this set strictly to what those pages need.
|
|
*
|
|
* See client consumers under `client/src/components/Auth/` and `client/src/routes/Layouts/Startup.tsx`.
|
|
*/
|
|
function buildPreLoginPayload() {
|
|
const isOpenIdEnabled =
|
|
!!process.env.OPENID_CLIENT_ID &&
|
|
(isEnabled(process.env.OPENID_USE_PKCE) || !!process.env.OPENID_CLIENT_SECRET?.trim()) &&
|
|
!!process.env.OPENID_ISSUER &&
|
|
!!process.env.OPENID_SESSION_SECRET;
|
|
|
|
const isSamlEnabled =
|
|
!!process.env.SAML_ENTRY_POINT &&
|
|
!!process.env.SAML_ISSUER &&
|
|
!!process.env.SAML_CERT &&
|
|
!!process.env.SAML_SESSION_SECRET;
|
|
|
|
const ldap = getLdapConfig();
|
|
|
|
/** @type {Partial<TStartupConfig>} */
|
|
const payload = {
|
|
appTitle: process.env.APP_TITLE || 'LibreChat',
|
|
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
|
|
facebookLoginEnabled: !!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
|
|
githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
|
|
googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
|
|
appleLoginEnabled:
|
|
!!process.env.APPLE_CLIENT_ID &&
|
|
!!process.env.APPLE_TEAM_ID &&
|
|
!!process.env.APPLE_KEY_ID &&
|
|
!!process.env.APPLE_PRIVATE_KEY_PATH,
|
|
openidLoginEnabled: isOpenIdEnabled,
|
|
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
|
|
openidImageUrl: process.env.OPENID_IMAGE_URL,
|
|
openidAutoRedirect: isEnabled(process.env.OPENID_AUTO_REDIRECT),
|
|
samlLoginEnabled: !isOpenIdEnabled && isSamlEnabled,
|
|
samlLabel: process.env.SAML_BUTTON_LABEL,
|
|
samlImageUrl: process.env.SAML_IMAGE_URL,
|
|
serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
|
|
emailLoginEnabled,
|
|
registrationEnabled: !ldap?.enabled && isEnabled(process.env.ALLOW_REGISTRATION),
|
|
socialLoginEnabled: isEnabled(process.env.ALLOW_SOCIAL_LOGIN),
|
|
emailEnabled:
|
|
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
|
!!process.env.EMAIL_USERNAME &&
|
|
!!process.env.EMAIL_PASSWORD &&
|
|
!!process.env.EMAIL_FROM,
|
|
passwordResetEnabled,
|
|
};
|
|
|
|
const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
|
|
if (minPasswordLength && !isNaN(minPasswordLength)) {
|
|
payload.minPasswordLength = minPasswordLength;
|
|
}
|
|
|
|
if (ldap) {
|
|
payload.ldap = ldap;
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* Fields shared by authenticated chat and share-view config. Anonymous share
|
|
* views receive these through `/api/share/:shareId/config` after share access
|
|
* checks, not through the generic startup config endpoint.
|
|
*/
|
|
function buildPublicSharePayload() {
|
|
/** @type {Partial<TStartupConfig>} */
|
|
const payload = {
|
|
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
|
};
|
|
|
|
if (typeof process.env.CUSTOM_FOOTER === 'string') {
|
|
payload.customFooter = process.env.CUSTOM_FOOTER;
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* Post-login fields appended only when `req.user` is present. These describe the
|
|
* authenticated UX (account-settings links, share-link feature flags, birthday icon,
|
|
* openid token-reuse marker) and are not needed on the pre-login screens, so they
|
|
* are not exposed to unauthenticated callers.
|
|
*/
|
|
function buildPostLoginPayload() {
|
|
/** @type {Partial<TStartupConfig>} */
|
|
const payload = {
|
|
showBirthdayIcon:
|
|
isBirthday() ||
|
|
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
|
process.env.SHOW_BIRTHDAY_ICON === '',
|
|
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
|
sharedLinksEnabled,
|
|
publicSharedLinksEnabled,
|
|
openidReuseTokens,
|
|
/** Read inline (not module-level) for per-request evaluation and test isolation */
|
|
allowAccountDeletion:
|
|
process.env.ALLOW_ACCOUNT_DELETION === undefined ||
|
|
isEnabled(process.env.ALLOW_ACCOUNT_DELETION),
|
|
};
|
|
|
|
return payload;
|
|
}
|
|
|
|
function buildBuildInfoPayload(interfaceConfig) {
|
|
if (interfaceConfig?.buildInfo === false) {
|
|
return undefined;
|
|
}
|
|
const info = resolveBuildInfo();
|
|
if (!info.commit && !info.branch && !info.buildDate) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
commit: info.commit,
|
|
commitShort: info.commitShort,
|
|
branch: info.branch,
|
|
buildDate: info.buildDate,
|
|
};
|
|
}
|
|
|
|
function buildWebSearchConfig(appConfig) {
|
|
const ws = appConfig?.webSearch;
|
|
if (!ws) {
|
|
return undefined;
|
|
}
|
|
const { searchProvider, scraperProvider, rerankerType } = ws;
|
|
if (!searchProvider && !scraperProvider && !rerankerType) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(searchProvider && { searchProvider }),
|
|
...(scraperProvider && { scraperProvider }),
|
|
...(rerankerType && { rerankerType }),
|
|
};
|
|
}
|
|
|
|
function buildCloudFrontStartupConfig() {
|
|
const config = getCloudFrontConfig();
|
|
if (
|
|
config?.imageSigning !== 'cookies' ||
|
|
!config.domain ||
|
|
!config.cookieDomain ||
|
|
!config.privateKey ||
|
|
!config.keyPairId
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
cookieRefresh: {
|
|
endpoint: '/api/auth/cloudfront/refresh',
|
|
domain: config.domain,
|
|
},
|
|
};
|
|
}
|
|
|
|
router.get('/', async function (req, res) {
|
|
try {
|
|
const preLoginPayload = buildPreLoginPayload();
|
|
const publicSharePayload = buildPublicSharePayload();
|
|
const rum = getRumConfig();
|
|
|
|
if (!req.user) {
|
|
const tenantId = getTenantId();
|
|
const baseConfig = await getAppConfig(tenantId ? { tenantId } : { baseOnly: true });
|
|
|
|
/** @type {Partial<TStartupConfig>} */
|
|
const payload = {
|
|
...preLoginPayload,
|
|
socialLogins: baseConfig?.registration?.socialLogins ?? defaultSocialLogins,
|
|
turnstile: baseConfig?.turnstileConfig,
|
|
...(rum ? { rum } : {}),
|
|
};
|
|
|
|
const interfaceConfig = baseConfig?.interfaceConfig;
|
|
const buildInfoDisabled = interfaceConfig?.buildInfo === false;
|
|
if (interfaceConfig?.privacyPolicy || interfaceConfig?.termsOfService || buildInfoDisabled) {
|
|
payload.interface = {};
|
|
if (interfaceConfig.privacyPolicy) {
|
|
payload.interface.privacyPolicy = interfaceConfig.privacyPolicy;
|
|
}
|
|
if (interfaceConfig.termsOfService) {
|
|
payload.interface.termsOfService = interfaceConfig.termsOfService;
|
|
}
|
|
if (buildInfoDisabled) {
|
|
payload.interface.buildInfo = false;
|
|
}
|
|
}
|
|
|
|
const unauthBuildInfo = buildBuildInfoPayload(interfaceConfig);
|
|
if (unauthBuildInfo) {
|
|
payload.buildInfo = unauthBuildInfo;
|
|
}
|
|
|
|
return res.status(200).send(payload);
|
|
}
|
|
|
|
const appConfig = await getAppConfig(getAppConfigOptionsFromUser(req.user));
|
|
|
|
const balanceConfig = getBalanceConfig(appConfig);
|
|
const cloudFront = buildCloudFrontStartupConfig();
|
|
|
|
/** @type {TStartupConfig} */
|
|
const payload = {
|
|
...preLoginPayload,
|
|
...publicSharePayload,
|
|
...buildPostLoginPayload(),
|
|
sharedLinksSnapshotFilesEnabled: sharedLinksEnabled && isFileSnapshotEnabled(appConfig),
|
|
socialLogins: appConfig?.registration?.socialLogins ?? defaultSocialLogins,
|
|
interface: appConfig?.interfaceConfig,
|
|
titleGenerationTiming: resolveTitleTiming({
|
|
appConfig,
|
|
endpoint: EModelEndpoint.agents,
|
|
}),
|
|
turnstile: appConfig?.turnstileConfig,
|
|
modelSpecs: sanitizeModelSpecs(excludeHiddenModelSpecs(appConfig?.modelSpecs)),
|
|
balance: balanceConfig,
|
|
bundlerURL: process.env.SANDPACK_BUNDLER_URL,
|
|
staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL,
|
|
sharePointFilePickerEnabled,
|
|
sharePointBaseUrl: process.env.SHAREPOINT_BASE_URL,
|
|
sharePointPickerGraphScope: process.env.SHAREPOINT_PICKER_GRAPH_SCOPE,
|
|
sharePointPickerSharePointScope: process.env.SHAREPOINT_PICKER_SHAREPOINT_SCOPE,
|
|
conversationImportMaxFileSize: process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES
|
|
? parseInt(process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES, 10)
|
|
: 0,
|
|
...(cloudFront ? { cloudFront } : {}),
|
|
...(rum ? { rum } : {}),
|
|
fileUploadSseEnabled: isEnabled(process.env.FILE_UPLOAD_SSE_ENABLED),
|
|
};
|
|
|
|
const webSearch = buildWebSearchConfig(appConfig);
|
|
if (webSearch) {
|
|
payload.webSearch = webSearch;
|
|
}
|
|
|
|
const buildInfo = buildBuildInfoPayload(appConfig?.interfaceConfig);
|
|
if (buildInfo) {
|
|
payload.buildInfo = buildInfo;
|
|
}
|
|
|
|
if (!payload.allowAccountDeletion) {
|
|
try {
|
|
const userId = req.user.id ?? req.user._id?.toString();
|
|
if (userId) {
|
|
const canDelete = await hasCapability(
|
|
{ id: userId, role: req.user.role ?? '', tenantId: req.user.tenantId },
|
|
SystemCapabilities.ACCESS_ADMIN,
|
|
);
|
|
if (canDelete) {
|
|
payload.allowAccountDeletion = true;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.warn(`[config] ACCESS_ADMIN capability check failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
return res.status(200).send(payload);
|
|
} catch (err) {
|
|
logger.error('Error in startup config', err);
|
|
return res.status(500).send({ error: err.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|