Skip to content
Merged

aa #13

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
12b0818
🐛 fix: Memories Key Updates (#8302)
dustinhealy Jul 7, 2025
916cd46
🌍 i18n: Update translation.json with latest translations (#8288)
github-actions[bot] Jul 7, 2025
550610d
⚖️ feat: Add Violation Scores (#8304)
danny-avila Jul 7, 2025
e57fc83
🔧 fix: Import Path for Custom Configuration Loading (#8319)
nhtruong Jul 9, 2025
7e37211
🗝️ refactor: `loadServiceKey` to Support Stringified JSON and Env Var…
danny-avila Jul 9, 2025
4918899
🖨️ fix: Use Azure Serverless API Version for Responses API (#8316)
danny-avila Jul 9, 2025
4b32ec4
📝 fix: Resolve Markdown Rendering Issues (#8352)
danny-avila Jul 10, 2025
13a9bcd
🔧 fix: Omit 'additionalModelRequestFields' from Bedrock Titling (#8353)
danny-avila Jul 10, 2025
8ca4cf3
🔧 fix: Update Drag & Drop Logic with new File Option handling (#8354)
danny-avila Jul 10, 2025
4bbdc4c
🧩 fix: `additionalProperties` Handling and Ref Resolution in Zod Sche…
danny-avila Jul 10, 2025
e4531d6
🔃 refactor: Conslidate JSON Schema Conversion to Schema
danny-avila Jul 10, 2025
8523074
🔧 fix: Invalidate Tool Caching after MCP Initialization (#8384)
danny-avila Jul 11, 2025
19320f2
🔑 feat: Base64 Google Service Keys and Reliable Private Key Formats (…
danny-avila Jul 11, 2025
110984b
📦 chore: Bump `@librechat/agents` to v2.4.58 (#8386)
danny-avila Jul 11, 2025
4733f10
📦 chore: Bump `@librechat/agents` to v2.4.59 (#8392)
danny-avila Jul 11, 2025
1e4f1f7
🔑 feat: Grok 4 Pricing and Token Limits (#8395)
danny-avila Jul 11, 2025
929b433
🔧 fix: Plugin Method Undefined in Agent Tool Closure (#8413)
danny-avila Jul 11, 2025
2e1874e
🔧 fix: `handleError` import path to use '@librechat/api' (#8415)
danny-avila Jul 11, 2025
8e869f2
🧠 feat: Enforce Token Limit for Memory Usage (#8401)
samuelpath Jul 11, 2025
d2e1ca4
🖼️ fix: Permission Checks for Agent Avatar Uploads (#8412)
samuelpath Jul 11, 2025
9f44187
🗂️ fix: Disable `express-static-gzip` for Uploaded Images (#8307)
sbruel Jul 11, 2025
6aa4bb5
👟 fix: Edge Case of Azure Provider Assignment for Title Run (#8420)
danny-avila Jul 12, 2025
f1b29ff
🔒 feat: View/Delete Shared Agent Files (#8419)
danny-avila Jul 12, 2025
170cc34
✨ refactor: Imports to Prevent Circular Type Refs (#8423)
danny-avila Jul 12, 2025
e370a87
♻️ fix: Correct Message ID Assignment Logic (#8439)
danny-avila Jul 14, 2025
e11e716
🌍 i18n: Update translation.json with latest translations (#8422)
github-actions[bot] Jul 14, 2025
aeeb860
📦 chore: bump @librechat/agents to v2.4.60 (#8458)
danny-avila Jul 14, 2025
a9f01bb
📝 refactor: Memory Instructions for Improved Performance (#8463)
danny-avila Jul 14, 2025
418b5e9
♻️ fix: Resolve MCP Connection if Ping is Unsupported (#8483)
danny-avila Jul 15, 2025
01b012a
🏦 refactor: Centralize Caching & Redis Key Prefixing (#8457)
nhtruong Jul 15, 2025
62b4f3b
🛂 fix: Only Perform `allowedProviders` Validation for Agents (#8487)
danny-avila Jul 15, 2025
52bbac3
✨ feat: Add GitHub Actions workflow for publishing `@librechat/client…
danny-avila Jul 16, 2025
7f8c327
🌊 feat: Add Disable Streaming Toggle (#8177)
dustinhealy Jul 16, 2025
1dabe96
🕒 refactor: Use Legacy Content for Custom Endpoints and Azure Serverl…
danny-avila Jul 16, 2025
35d8ef5
🪙 fix: Use Fallback Token Transaction if No Collected Usage (#8503)
danny-avila Jul 16, 2025
4b12ea3
📦 chore: bump `@librechat/agents` to to v2.4.61 (#8504)
danny-avila Jul 16, 2025
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
35 changes: 32 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ REGISTRATION_VIOLATION_SCORE=1
CONCURRENT_VIOLATION_SCORE=1
MESSAGE_VIOLATION_SCORE=1
NON_BROWSER_VIOLATION_SCORE=20
TTS_VIOLATION_SCORE=0
STT_VIOLATION_SCORE=0
FORK_VIOLATION_SCORE=0
IMPORT_VIOLATION_SCORE=0
FILE_UPLOAD_VIOLATION_SCORE=0

LOGIN_MAX=7
LOGIN_WINDOW=5
Expand Down Expand Up @@ -575,6 +580,10 @@ ALLOW_SHARED_LINKS_PUBLIC=true
# If you have another service in front of your LibreChat doing compression, disable express based compression here
# DISABLE_COMPRESSION=true

# If you have gzipped version of uploaded image images in the same folder, this will enable gzip scan and serving of these images
# Note: The images folder will be scanned on startup and a ma kept in memory. Be careful for large number of images.
# ENABLE_IMAGE_OUTPUT_GZIP_SCAN=true

#===================================================#
# UI #
#===================================================#
Expand All @@ -592,11 +601,31 @@ HELP_AND_FAQ_URL=https://librechat.ai
# REDIS Options #
#===============#

# REDIS_URI=10.10.10.10:6379
# Enable Redis for caching and session storage
# USE_REDIS=true

# USE_REDIS_CLUSTER=true
# REDIS_CA=/path/to/ca.crt
# Single Redis instance
# REDIS_URI=redis://127.0.0.1:6379

# Redis cluster (multiple nodes)
# REDIS_URI=redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003

# Redis with TLS/SSL encryption and CA certificate
# REDIS_URI=rediss://127.0.0.1:6380
# REDIS_CA=/path/to/ca-cert.pem

# Redis authentication (if required)
# REDIS_USERNAME=your_redis_username
# REDIS_PASSWORD=your_redis_password

# Redis key prefix configuration
# Use environment variable name for dynamic prefix (recommended for cloud deployments)
# REDIS_KEY_PREFIX_VAR=K_REVISION
# Or use static prefix directly
# REDIS_KEY_PREFIX=librechat

# Redis connection limits
# REDIS_MAX_LISTENERS=40

#==================================================#
# Others #
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Publish `@librechat/client` to NPM

on:
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: false
default: 'Manual publish requested'

jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'

- name: Check if client package exists
run: |
if [ -d "packages/client" ]; then
echo "Client package directory found"
else
echo "Client package directory not found - workflow ready for future use"
exit 0
fi

- name: Placeholder for future publishing
run: echo "Client package publishing workflow is ready"
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,12 @@ helm/**/.values.yaml

# SAML Idp cert
*.cert

# AI Assistants
/.claude/
/.cursor/
/.copilot/
/.aider/
/.openai/
/.tabnine/
/.codeium
17 changes: 14 additions & 3 deletions api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,15 @@ class BaseClient {
/**
* Abstract method to record token usage. Subclasses must implement this method.
* If a correction to the token usage is needed, the method should return an object with the corrected token counts.
* Should only be used if `recordCollectedUsage` was not used instead.
* @param {string} [model]
* @param {number} promptTokens
* @param {number} completionTokens
* @returns {Promise<void>}
*/
async recordTokenUsage({ promptTokens, completionTokens }) {
async recordTokenUsage({ model, promptTokens, completionTokens }) {
logger.debug('[BaseClient] `recordTokenUsage` not implemented.', {
model,
promptTokens,
completionTokens,
});
Expand Down Expand Up @@ -197,6 +200,10 @@ class BaseClient {
this.currentMessages[this.currentMessages.length - 1].messageId = head;
}

if (opts.isRegenerate && responseMessageId.endsWith('_')) {
responseMessageId = crypto.randomUUID();
}

this.responseMessageId = responseMessageId;

return {
Expand Down Expand Up @@ -737,9 +744,13 @@ class BaseClient {
} else {
responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage);
completionTokens = responseMessage.tokenCount;
await this.recordTokenUsage({
usage,
promptTokens,
completionTokens,
model: responseMessage.model,
});
}

await this.recordTokenUsage({ promptTokens, completionTokens, usage });
}

if (userMessagePromise) {
Expand Down
40 changes: 40 additions & 0 deletions api/app/clients/specs/BaseClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,46 @@ describe('BaseClient', () => {
expect(response).toEqual(expectedResult);
});

test('should replace responseMessageId with new UUID when isRegenerate is true and messageId ends with underscore', async () => {
const mockCrypto = require('crypto');
const newUUID = 'new-uuid-1234';
jest.spyOn(mockCrypto, 'randomUUID').mockReturnValue(newUUID);

const opts = {
isRegenerate: true,
responseMessageId: 'existing-message-id_',
};

await TestClient.setMessageOptions(opts);

expect(TestClient.responseMessageId).toBe(newUUID);
expect(TestClient.responseMessageId).not.toBe('existing-message-id_');

mockCrypto.randomUUID.mockRestore();
});

test('should not replace responseMessageId when isRegenerate is false', async () => {
const opts = {
isRegenerate: false,
responseMessageId: 'existing-message-id_',
};

await TestClient.setMessageOptions(opts);

expect(TestClient.responseMessageId).toBe('existing-message-id_');
});

test('should not replace responseMessageId when it does not end with underscore', async () => {
const opts = {
isRegenerate: true,
responseMessageId: 'existing-message-id',
};

await TestClient.setMessageOptions(opts);

expect(TestClient.responseMessageId).toBe('existing-message-id');
});

test('sendMessage should work with provided conversationId and parentMessageId', async () => {
const userMessage = 'Second message in the conversation';
const opts = {
Expand Down
12 changes: 10 additions & 2 deletions api/app/clients/tools/util/fileSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,25 @@ const { getFiles } = require('~/models/File');
* @param {Object} options
* @param {ServerRequest} options.req
* @param {Agent['tool_resources']} options.tool_resources
* @param {string} [options.agentId] - The agent ID for file access control
* @returns {Promise<{
* files: Array<{ file_id: string; filename: string }>,
* toolContext: string
* }>}
*/
const primeFiles = async (options) => {
const { tool_resources } = options;
const { tool_resources, req, agentId } = options;
const file_ids = tool_resources?.[EToolResources.file_search]?.file_ids ?? [];
const agentResourceIds = new Set(file_ids);
const resourceFiles = tool_resources?.[EToolResources.file_search]?.files ?? [];
const dbFiles = ((await getFiles({ file_id: { $in: file_ids } })) ?? []).concat(resourceFiles);
const dbFiles = (
(await getFiles(
{ file_id: { $in: file_ids } },
null,
{ text: 0 },
{ userId: req?.user?.id, agentId },
)) ?? []
).concat(resourceFiles);

let toolContext = `- Note: Semantic search is available through the ${Tools.file_search} tool but no files are currently loaded. Request the user to upload documents to search through.`;

Expand Down
13 changes: 11 additions & 2 deletions api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,13 @@ const loadTools = async ({
authFields: [EnvVar.CODE_API_KEY],
});
const codeApiKey = authValues[EnvVar.CODE_API_KEY];
const { files, toolContext } = await primeCodeFiles(options, codeApiKey);
const { files, toolContext } = await primeCodeFiles(
{
...options,
agentId: agent?.id,
},
codeApiKey,
);
if (toolContext) {
toolContextMap[tool] = toolContext;
}
Expand All @@ -260,7 +266,10 @@ const loadTools = async ({
continue;
} else if (tool === Tools.file_search) {
requestedTools[tool] = async () => {
const { files, toolContext } = await primeSearchFiles(options);
const { files, toolContext } = await primeSearchFiles({
...options,
agentId: agent?.id,
});
if (toolContext) {
toolContextMap[tool] = toolContext;
}
Expand Down
33 changes: 33 additions & 0 deletions api/cache/cacheConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const fs = require('fs');
const { math, isEnabled } = require('@librechat/api');

// 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 = isEnabled(process.env.USE_REDIS);
if (USE_REDIS && !process.env.REDIS_URI) {
throw new Error('USE_REDIS is enabled but REDIS_URI is not set.');
}

const cacheConfig = {
USE_REDIS,
REDIS_URI: process.env.REDIS_URI,
REDIS_USERNAME: process.env.REDIS_USERNAME,
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CA: process.env.REDIS_CA ? fs.readFileSync(process.env.REDIS_CA, 'utf8') : null,
REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR] || REDIS_KEY_PREFIX || '',
REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40),

CI: isEnabled(process.env.CI),
DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE),

BAN_DURATION: math(process.env.BAN_DURATION, 7200000), // 2 hours
};

module.exports = { cacheConfig };
108 changes: 108 additions & 0 deletions api/cache/cacheConfig.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const fs = require('fs');

describe('cacheConfig', () => {
let originalEnv;
let originalReadFileSync;

beforeEach(() => {
originalEnv = { ...process.env };
originalReadFileSync = fs.readFileSync;

// Clear all related env vars first
delete process.env.REDIS_URI;
delete process.env.REDIS_CA;
delete process.env.REDIS_KEY_PREFIX_VAR;
delete process.env.REDIS_KEY_PREFIX;
delete process.env.USE_REDIS;

// Clear require cache
jest.resetModules();
});

afterEach(() => {
process.env = originalEnv;
fs.readFileSync = originalReadFileSync;
jest.resetModules();
});

describe('REDIS_KEY_PREFIX validation and resolution', () => {
test('should throw error when both REDIS_KEY_PREFIX_VAR and REDIS_KEY_PREFIX are set', () => {
process.env.REDIS_KEY_PREFIX_VAR = 'DEPLOYMENT_ID';
process.env.REDIS_KEY_PREFIX = 'manual-prefix';

expect(() => {
require('./cacheConfig');
}).toThrow('Only either REDIS_KEY_PREFIX_VAR or REDIS_KEY_PREFIX can be set.');
});

test('should resolve REDIS_KEY_PREFIX from variable reference', () => {
process.env.REDIS_KEY_PREFIX_VAR = 'DEPLOYMENT_ID';
process.env.DEPLOYMENT_ID = 'test-deployment-123';

const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('test-deployment-123');
});

test('should use direct REDIS_KEY_PREFIX value', () => {
process.env.REDIS_KEY_PREFIX = 'direct-prefix';

const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('direct-prefix');
});

test('should default to empty string when no prefix is configured', () => {
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('');
});

test('should handle empty variable reference', () => {
process.env.REDIS_KEY_PREFIX_VAR = 'EMPTY_VAR';
process.env.EMPTY_VAR = '';

const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('');
});

test('should handle undefined variable reference', () => {
process.env.REDIS_KEY_PREFIX_VAR = 'UNDEFINED_VAR';

const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('');
});
});

describe('USE_REDIS and REDIS_URI validation', () => {
test('should throw error when USE_REDIS is enabled but REDIS_URI is not set', () => {
process.env.USE_REDIS = 'true';

expect(() => {
require('./cacheConfig');
}).toThrow('USE_REDIS is enabled but REDIS_URI is not set.');
});

test('should not throw error when USE_REDIS is enabled and REDIS_URI is set', () => {
process.env.USE_REDIS = 'true';
process.env.REDIS_URI = 'redis://localhost:6379';

expect(() => {
require('./cacheConfig');
}).not.toThrow();
});

test('should handle empty REDIS_URI when USE_REDIS is enabled', () => {
process.env.USE_REDIS = 'true';
process.env.REDIS_URI = '';

expect(() => {
require('./cacheConfig');
}).toThrow('USE_REDIS is enabled but REDIS_URI is not set.');
});
});

describe('REDIS_CA file reading', () => {
test('should be null when REDIS_CA is not set', () => {
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_CA).toBeNull();
});
});
});
Loading
Loading