Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ ALLOW_SHARED_LINKS_PUBLIC=false
#===================================================#

APP_TITLE=LibreChat
# APP_DESCRIPTION=
# CUSTOM_FOOTER="My custom footer"
HELP_AND_FAQ_URL=https://librechat.ai

Expand Down
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ bower_components/
.clineignore
.cursor
.aider*
.bg-shell/

# Floobits
.floo
Expand Down Expand Up @@ -130,7 +129,6 @@ helm/**/charts/
helm/**/.values.yaml

!/client/src/@types/i18next.d.ts
!/client/src/@types/react.d.ts

# SAML Idp cert
*.cert
Expand All @@ -145,6 +143,7 @@ helm/**/.values.yaml
/.codeium
*.local.md


# Removed Windows wrapper files per user request
hive-mind-prompt-*.txt

Expand Down Expand Up @@ -176,4 +175,3 @@ claude-flow
# Removed Windows wrapper files per user request
hive-mind-prompt-*.txt
CLAUDE.md
.gsd
34 changes: 10 additions & 24 deletions api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,7 @@ class BaseClient {
}
delete userMessage.image_urls;
}
userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user).catch(
(err) => {
logger.error('[BaseClient] Failed to save user message:', err);
return {};
},
);
userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user);
this.savedMessageIds.add(userMessage.messageId);
if (typeof opts?.getReqData === 'function') {
opts.getReqData({
Expand Down Expand Up @@ -732,30 +727,21 @@ class BaseClient {
* @param {string | null} user
*/
async saveMessageToDatabase(message, endpointOptions, user = null) {
// Snapshot options before any await; disposeClient may set client.options = null
// while this method is suspended at an I/O boundary, but the local reference
// remains valid (disposeClient nulls the property, not the object itself).
const options = this.options;
if (!options) {
logger.error('[BaseClient] saveMessageToDatabase: client disposed before save, skipping');
return {};
}

if (this.user && user !== this.user) {
throw new Error('User mismatch.');
}

const hasAddedConvo = options?.req?.body?.addedConvo != null;
const hasAddedConvo = this.options?.req?.body?.addedConvo != null;
const reqCtx = {
userId: options?.req?.user?.id,
isTemporary: options?.req?.body?.isTemporary,
interfaceConfig: options?.req?.config?.interfaceConfig,
userId: this.options?.req?.user?.id,
isTemporary: this.options?.req?.body?.isTemporary,
interfaceConfig: this.options?.req?.config?.interfaceConfig,
};
const savedMessage = await db.saveMessage(
reqCtx,
{
...message,
endpoint: options.endpoint,
endpoint: this.options.endpoint,
unfinished: false,
user,
...(hasAddedConvo && { addedConvo: true }),
Expand All @@ -769,20 +755,20 @@ class BaseClient {

const fieldsToKeep = {
conversationId: message.conversationId,
endpoint: options.endpoint,
endpointType: options.endpointType,
endpoint: this.options.endpoint,
endpointType: this.options.endpointType,
...endpointOptions,
};

const existingConvo =
this.fetchedConvo === true
? null
: await db.getConvo(options?.req?.user?.id, message.conversationId);
: await db.getConvo(this.options?.req?.user?.id, message.conversationId);

const unsetFields = {};
const exceptions = new Set(['spec', 'iconURL']);
const hasNonEphemeralAgent =
isAgentsEndpoint(options.endpoint) &&
isAgentsEndpoint(this.options.endpoint) &&
endpointOptions?.agent_id &&
!isEphemeralAgentId(endpointOptions.agent_id);
if (hasNonEphemeralAgent) {
Expand Down
48 changes: 1 addition & 47 deletions api/app/clients/specs/BaseClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jest.mock('~/models', () => ({
updateFileUsage: jest.fn(),
}));

const { getConvo, saveConvo, saveMessage } = require('~/models');
const { getConvo, saveConvo } = require('~/models');

jest.mock('@librechat/agents', () => {
const actual = jest.requireActual('@librechat/agents');
Expand Down Expand Up @@ -906,52 +906,6 @@ describe('BaseClient', () => {
);
});

test('saveMessageToDatabase returns early when this.options is null (client disposed)', async () => {
const savedOptions = TestClient.options;
TestClient.options = null;
saveMessage.mockClear();

const result = await TestClient.saveMessageToDatabase(
{ messageId: 'msg-1', conversationId: 'conv-1', isCreatedByUser: true, text: 'hi' },
{},
null,
);

expect(result).toEqual({});
expect(saveMessage).not.toHaveBeenCalled();

TestClient.options = savedOptions;
});

test('saveMessageToDatabase uses snapshot of options, immune to mid-await disposal', async () => {
const savedOptions = TestClient.options;
saveMessage.mockClear();
saveConvo.mockClear();

// Make db.saveMessage yield, simulating I/O suspension during which disposal occurs
saveMessage.mockImplementation(async (_reqCtx, msgData) => {
// Simulate disposeClient nullifying client.options while awaiting
TestClient.options = null;
return msgData;
});
saveConvo.mockResolvedValue({ conversationId: 'conv-1' });

const result = await TestClient.saveMessageToDatabase(
{ messageId: 'msg-1', conversationId: 'conv-1', isCreatedByUser: true, text: 'hi' },
{ endpoint: 'openAI' },
null,
);

// Should complete without TypeError, using the snapshotted options
expect(result).toHaveProperty('message');
expect(result).toHaveProperty('conversation');
expect(saveMessage).toHaveBeenCalled();

TestClient.options = savedOptions;
saveMessage.mockReset();
saveConvo.mockReset();
});

test('userMessagePromise is awaited before saving response message', async () => {
// Mock the saveMessageToDatabase method
TestClient.saveMessageToDatabase = jest.fn().mockImplementation(() => {
Expand Down
7 changes: 1 addition & 6 deletions api/db/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const { connectDb } = require('./connect');
const indexSync = require('./indexSync');

// createModels MUST run before requiring indexSync.
// indexSync.js captures mongoose.models.Message and mongoose.models.Conversation
// at module load time. If those models are not registered first, all MeiliSearch
// sync operations will silently fail on every startup.
createModels(mongoose);

const indexSync = require('./indexSync');

module.exports = { connectDb, indexSync };
26 changes: 0 additions & 26 deletions api/db/index.spec.js

This file was deleted.

18 changes: 3 additions & 15 deletions api/db/indexSync.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const { isEnabled, FlowStateManager } = require('@librechat/api');
const { getLogStores } = require('~/cache');
const { batchResetMeiliFlags } = require('./utils');

const Conversation = mongoose.models.Conversation;
const Message = mongoose.models.Message;

const searchEnabled = isEnabled(process.env.SEARCH);
const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC);
let currentTimeout = null;
Expand Down Expand Up @@ -197,14 +200,6 @@ async function performSync(flowManager, flowId, flowType) {
return { messagesSync: false, convosSync: false };
}

const Message = mongoose.models.Message;
const Conversation = mongoose.models.Conversation;
if (!Message || !Conversation) {
throw new Error(
'[indexSync] Models not registered. Ensure createModels() has been called before indexSync.',
);
}

const client = MeiliSearchClient.getInstance();

const { status } = await client.health();
Expand Down Expand Up @@ -354,13 +349,6 @@ async function indexSync() {
logger.debug('[indexSync] Creating indices...');
currentTimeout = setTimeout(async () => {
try {
const Message = mongoose.models.Message;
const Conversation = mongoose.models.Conversation;
if (!Message || !Conversation) {
throw new Error(
'[indexSync] Models not registered. Ensure createModels() has been called before indexSync.',
);
}
await Message.syncWithMeili();
await Conversation.syncWithMeili();
} catch (err) {
Expand Down
19 changes: 19 additions & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const mongoSanitize = require('express-mongo-sanitize');
const {
isEnabled,
apiNotFound,
getAppMetadata,
ErrorController,
transformManifest,
transformIndexHtml,
memoryDiagnostics,
performStartupChecks,
handleJsonParseError,
Expand Down Expand Up @@ -81,6 +84,9 @@ const startServer = async () => {
}
}

const appMetadata = getAppMetadata();
indexHTML = transformIndexHtml(indexHTML, appMetadata);

app.get('/health', (_req, res) => res.status(200).send('OK'));

/* Middleware */
Expand Down Expand Up @@ -112,6 +118,19 @@ const startServer = async () => {
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
}

const manifestPath = path.join(appConfig.paths.dist, 'manifest.webmanifest');
if (fs.existsSync(manifestPath)) {
const manifestJSON = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const manifestContent = JSON.stringify(transformManifest(manifestJSON, appMetadata));
app.get('/manifest.webmanifest', (_req, res) => {
res.set({
'Content-Type': 'application/manifest+json',
'Cache-Control': 'no-store, no-cache, must-revalidate',
});
res.send(manifestContent);
});
}

app.use(staticCache(appConfig.paths.dist));
app.use(staticCache(appConfig.paths.fonts));
app.use(staticCache(appConfig.paths.assets));
Expand Down
1 change: 1 addition & 0 deletions api/server/routes/__tests__/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ app.use('/api/config', configRoute);

afterEach(() => {
delete process.env.APP_TITLE;
delete process.env.APP_DESCRIPTION;
delete process.env.GOOGLE_CLIENT_ID;
delete process.env.GOOGLE_CLIENT_SECRET;
delete process.env.FACEBOOK_CLIENT_ID;
Expand Down
10 changes: 8 additions & 2 deletions api/server/routes/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { isEnabled, getBalanceConfig } = require('@librechat/api');
const { CacheKeys, defaultSocialLogins } = require('librechat-data-provider');
const {
CacheKeys,
defaultAppTitle,
defaultSocialLogins,
defaultAppDescription,
} = require('librechat-data-provider');
const { getLdapConfig } = require('~/server/services/Config/ldap');
const { getAppConfig } = require('~/server/services/Config/app');
const { getLogStores } = require('~/cache');
Expand Down Expand Up @@ -55,7 +60,8 @@ router.get('/', async function (req, res) {

/** @type {TStartupConfig} */
const payload = {
appTitle: process.env.APP_TITLE || 'LibreChat',
appTitle: process.env.APP_TITLE || defaultAppTitle,
appDescription: process.env.APP_DESCRIPTION || defaultAppDescription,
socialLogins: appConfig?.registration?.socialLogins ?? defaultSocialLogins,
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
facebookLoginEnabled:
Expand Down
7 changes: 1 addition & 6 deletions api/server/services/Files/Citations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,7 @@ async function processFileCitations({ user, appConfig, toolArtifact, toolCallId,
logger.error(
`[processFileCitations] Permission check failed for FILE_CITATIONS: ${error.message}`,
);
logger.warn(
'[processFileCitations] Returning null citations due to permission check error — citations will not be shown for this message',
);
return null;
logger.debug(`[processFileCitations] Proceeding with citations due to permission error`);
}
}

Expand Down Expand Up @@ -148,8 +145,6 @@ async function enhanceSourcesWithMetadata(sources, appConfig) {
metadata: {
...source.metadata,
storageType: configuredStorageType,
fileType: fileRecord.type || undefined,
fileBytes: fileRecord.bytes || undefined,
},
};
});
Expand Down
4 changes: 1 addition & 3 deletions client/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'jest-file-loader',
},
transformIgnorePatterns: [
'/node_modules/(?!(@zattoo/use-double-click|@dicebear|@react-dnd|react-dnd.*|dnd-core|filenamify|filename-reserved-regex|heic-to|lowlight|highlight\\.js|fault|react-markdown|unified|bail|trough|devlop|is-.*|parse-entities|stringify-entities|character-.*|trim-lines|style-to-object|inline-style-parser|html-url-attributes|escape-string-regexp|longest-streak|zwitch|ccount|markdown-table|comma-separated-tokens|space-separated-tokens|web-namespaces|property-information|remark-.*|rehype-.*|recma-.*|hast.*|mdast-.*|unist-.*|vfile.*|micromark.*|estree-util-.*|decode-named-character-reference)/)/',
],
transformIgnorePatterns: ['node_modules/?!@zattoo/use-double-click'],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/test/setupTests.js'],
clearMocks: true,
};
6 changes: 0 additions & 6 deletions client/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,9 @@ server {

# location /api {
# proxy_pass http://api:3080/api;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header Host $host;
# }

# location / {
# proxy_pass http://api:3080;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header Host $host;
# }
#}
8 changes: 0 additions & 8 deletions client/src/@types/react.d.ts

This file was deleted.

Loading
Loading