Merge remote-tracking branch 'upstream/main' into feature/entra-id-azure-integration

This commit is contained in:
victorbjorkgren 2025-11-17 10:40:42 +01:00
commit 6ba2d8e967
41 changed files with 2255 additions and 1361 deletions

View file

@ -61,30 +61,23 @@ jobs:
npm run build:data-schemas npm run build:data-schemas
npm run build:api npm run build:api
- name: Run cache integration tests - name: Run all cache integration tests (Single Redis Node)
working-directory: packages/api working-directory: packages/api
env: env:
NODE_ENV: test NODE_ENV: test
USE_REDIS: true USE_REDIS: true
USE_REDIS_CLUSTER: false
REDIS_URI: redis://127.0.0.1:6379 REDIS_URI: redis://127.0.0.1:6379
REDIS_CLUSTER_URI: redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003 run: npm run test:cache-integration
run: npm run test:cache-integration:core
- name: Run cluster integration tests - name: Run all cache integration tests (Redis Cluster)
working-directory: packages/api working-directory: packages/api
env: env:
NODE_ENV: test NODE_ENV: test
USE_REDIS: true USE_REDIS: true
REDIS_URI: redis://127.0.0.1:6379 USE_REDIS_CLUSTER: true
run: npm run test:cache-integration:cluster REDIS_URI: redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
run: npm run test:cache-integration
- name: Run mcp integration tests
working-directory: packages/api
env:
NODE_ENV: test
USE_REDIS: true
REDIS_URI: redis://127.0.0.1:6379
run: npm run test:cache-integration:mcp
- name: Stop Redis Cluster - name: Stop Redis Cluster
if: always() if: always()

View file

@ -76,7 +76,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jwks-rsa": "^3.2.0", "jwks-rsa": "^3.2.0",
"keyv": "^5.3.2", "keyv": "^5.3.2",
@ -117,7 +117,7 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.7.0", "jest": "^30.2.0",
"mongodb-memory-server": "^10.1.4", "mongodb-memory-server": "^10.1.4",
"nodemon": "^3.0.3", "nodemon": "^3.0.3",
"supertest": "^7.1.0" "supertest": "^7.1.0"

View file

@ -989,7 +989,7 @@ describe('AgentClient - titleConvo', () => {
}; };
// Simulate the getOptions logic that handles GPT-5+ models // Simulate the getOptions logic that handles GPT-5+ models
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
delete clientOptions.maxTokens; delete clientOptions.maxTokens;
@ -1009,7 +1009,7 @@ describe('AgentClient - titleConvo', () => {
useResponsesApi: true, useResponsesApi: true,
}; };
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
const paramName = const paramName =
clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens'; clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
@ -1034,7 +1034,7 @@ describe('AgentClient - titleConvo', () => {
}; };
// Simulate the getOptions logic // Simulate the getOptions logic
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
delete clientOptions.maxTokens; delete clientOptions.maxTokens;
@ -1055,7 +1055,7 @@ describe('AgentClient - titleConvo', () => {
}; };
// Simulate the getOptions logic // Simulate the getOptions logic
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
delete clientOptions.maxTokens; delete clientOptions.maxTokens;
@ -1068,6 +1068,9 @@ describe('AgentClient - titleConvo', () => {
it('should handle various GPT-5+ model formats', () => { it('should handle various GPT-5+ model formats', () => {
const testCases = [ const testCases = [
{ model: 'gpt-5.1', shouldTransform: true },
{ model: 'gpt-5.1-chat-latest', shouldTransform: true },
{ model: 'gpt-5.1-codex', shouldTransform: true },
{ model: 'gpt-5', shouldTransform: true }, { model: 'gpt-5', shouldTransform: true },
{ model: 'gpt-5-turbo', shouldTransform: true }, { model: 'gpt-5-turbo', shouldTransform: true },
{ model: 'gpt-6', shouldTransform: true }, { model: 'gpt-6', shouldTransform: true },
@ -1087,7 +1090,10 @@ describe('AgentClient - titleConvo', () => {
}; };
// Simulate the getOptions logic // Simulate the getOptions logic
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (
/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) &&
clientOptions.maxTokens != null
) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
delete clientOptions.maxTokens; delete clientOptions.maxTokens;
@ -1105,6 +1111,9 @@ describe('AgentClient - titleConvo', () => {
it('should not swap max token param for older models when using useResponsesApi', () => { it('should not swap max token param for older models when using useResponsesApi', () => {
const testCases = [ const testCases = [
{ model: 'gpt-5.1', shouldTransform: true },
{ model: 'gpt-5.1-chat-latest', shouldTransform: true },
{ model: 'gpt-5.1-codex', shouldTransform: true },
{ model: 'gpt-5', shouldTransform: true }, { model: 'gpt-5', shouldTransform: true },
{ model: 'gpt-5-turbo', shouldTransform: true }, { model: 'gpt-5-turbo', shouldTransform: true },
{ model: 'gpt-6', shouldTransform: true }, { model: 'gpt-6', shouldTransform: true },
@ -1124,7 +1133,10 @@ describe('AgentClient - titleConvo', () => {
useResponsesApi: true, useResponsesApi: true,
}; };
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (
/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) &&
clientOptions.maxTokens != null
) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
const paramName = const paramName =
clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens'; clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
@ -1157,7 +1169,10 @@ describe('AgentClient - titleConvo', () => {
}; };
// Simulate the getOptions logic // Simulate the getOptions logic
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { if (
/\bgpt-[5-9](?:\.\d+)?\b/i.test(clientOptions.model) &&
clientOptions.maxTokens != null
) {
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
delete clientOptions.maxTokens; delete clientOptions.maxTokens;

View file

@ -10,8 +10,8 @@ const {
ResourceType, ResourceType,
EModelEndpoint, EModelEndpoint,
PermissionBits, PermissionBits,
isAgentsEndpoint,
checkOpenAIStorage, checkOpenAIStorage,
isAssistantsEndpoint,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { const {
filterFile, filterFile,
@ -376,11 +376,11 @@ router.post('/', async (req, res) => {
metadata.temp_file_id = metadata.file_id; metadata.temp_file_id = metadata.file_id;
metadata.file_id = req.file_id; metadata.file_id = req.file_id;
if (isAgentsEndpoint(metadata.endpoint)) { if (isAssistantsEndpoint(metadata.endpoint)) {
return await processAgentFileUpload({ req, res, metadata }); return await processFileUpload({ req, res, metadata });
} }
await processFileUpload({ req, res, metadata }); return await processAgentFileUpload({ req, res, metadata });
} catch (error) { } catch (error) {
let message = 'Error processing file'; let message = 'Error processing file';
logger.error('[/files] Error processing file:', error); logger.error('[/files] Error processing file:', error);

View file

@ -135,10 +135,10 @@
"babel-plugin-root-import": "^6.6.0", "babel-plugin-root-import": "^6.6.0",
"babel-plugin-transform-import-meta": "^2.3.2", "babel-plugin-transform-import-meta": "^2.3.2",
"babel-plugin-transform-vite-meta-env": "^1.0.3", "babel-plugin-transform-vite-meta-env": "^1.0.3",
"eslint-plugin-jest": "^28.11.0", "eslint-plugin-jest": "^29.1.0",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^30.2.0",
"jest-canvas-mock": "^2.5.2", "jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-file-loader": "^1.0.3", "jest-file-loader": "^1.0.3",
@ -147,7 +147,7 @@
"postcss-loader": "^7.1.0", "postcss-loader": "^7.1.0",
"postcss-preset-env": "^8.2.0", "postcss-preset-env": "^8.2.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5", "ts-jest": "^29.4.5",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^6.4.1", "vite": "^6.4.1",
"vite-plugin-compression2": "^2.2.1", "vite-plugin-compression2": "^2.2.1",

View file

@ -179,9 +179,10 @@ export const ArtifactCodeEditor = function ({
bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL, bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL,
}; };
}, [config, template, fileKey]); }, [config, template, fileKey]);
const [readOnly, setReadOnly] = useState(externalReadOnly ?? isSubmitting ?? false); const initialReadOnly = (externalReadOnly ?? false) || (isSubmitting ?? false);
const [readOnly, setReadOnly] = useState(initialReadOnly);
useEffect(() => { useEffect(() => {
setReadOnly(externalReadOnly ?? isSubmitting ?? false); setReadOnly((externalReadOnly ?? false) || (isSubmitting ?? false));
}, [isSubmitting, externalReadOnly]); }, [isSubmitting, externalReadOnly]);
if (Object.keys(files).length === 0) { if (Object.keys(files).length === 0) {

View file

@ -69,7 +69,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
return ( return (
<div className="group/reasoning"> <div className="group/reasoning">
<div className="group/thinking-container"> <div className="group/thinking-container">
<div className="sticky top-0 z-10 mb-2 pb-2 pt-2"> <div className="sticky top-0 z-10 mb-2 bg-presentation pb-2 pt-2">
<ThinkingButton <ThinkingButton
isExpanded={isExpanded} isExpanded={isExpanded}
onClick={handleClick} onClick={handleClick}

View file

@ -148,7 +148,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
return ( return (
<div className="group/thinking-container"> <div className="group/thinking-container">
<div className="sticky top-0 z-10 mb-4 bg-surface-primary pb-2 pt-2"> <div className="sticky top-0 z-10 mb-4 bg-presentation pb-2 pt-2">
<ThinkingButton <ThinkingButton
isExpanded={isExpanded} isExpanded={isExpanded}
onClick={handleClick} onClick={handleClick}

View file

@ -27,7 +27,11 @@ export default function MinimalHoverButtons({ message, searchResults }: THoverBu
isCopied ? localize('com_ui_copied_to_clipboard') : localize('com_ui_copy_to_clipboard') isCopied ? localize('com_ui_copied_to_clipboard') : localize('com_ui_copy_to_clipboard')
} }
> >
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />} {isCopied ? (
<CheckMark className="h-[19px] w-[19px]" />
) : (
<Clipboard className="h-[19px] w-[19px]" />
)}
</button> </button>
</div> </div>
); );

View file

@ -25,7 +25,7 @@ type EndpointIcon = {
function getOpenAIColor(_model: string | null | undefined) { function getOpenAIColor(_model: string | null | undefined) {
const model = _model?.toLowerCase() ?? ''; const model = _model?.toLowerCase() ?? '';
if (model && (/\b(o\d)\b/i.test(model) || /\bgpt-[5-9]\b/i.test(model))) { if (model && (/\b(o\d)\b/i.test(model) || /\bgpt-[5-9](?:\.\d+)?\b/i.test(model))) {
return '#000000'; return '#000000';
} }
return model.includes('gpt-4') ? '#AB68FF' : '#19C37D'; return model.includes('gpt-4') ? '#AB68FF' : '#19C37D';

View file

@ -2,8 +2,6 @@ import { useRef, useEffect, memo } from 'react';
import { ResizableHandleAlt, ResizablePanel } from '@librechat/client'; import { ResizableHandleAlt, ResizablePanel } from '@librechat/client';
import type { ImperativePanelHandle } from 'react-resizable-panels'; import type { ImperativePanelHandle } from 'react-resizable-panels';
const ANIMATION_DURATION = 500;
interface ArtifactsPanelProps { interface ArtifactsPanelProps {
artifacts: React.ReactNode | null; artifacts: React.ReactNode | null;
currentLayout: number[]; currentLayout: number[];
@ -24,14 +22,9 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
onRenderChange, onRenderChange,
}: ArtifactsPanelProps) { }: ArtifactsPanelProps) {
const artifactsPanelRef = useRef<ImperativePanelHandle>(null); const artifactsPanelRef = useRef<ImperativePanelHandle>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
if (artifacts != null) { if (artifacts != null) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
onRenderChange(true); onRenderChange(true);
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -39,17 +32,8 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
}); });
}); });
} else if (shouldRender) { } else if (shouldRender) {
artifactsPanelRef.current?.collapse(); onRenderChange(false);
timeoutRef.current = setTimeout(() => {
onRenderChange(false);
}, ANIMATION_DURATION);
} }
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [artifacts, shouldRender, onRenderChange]); }, [artifacts, shouldRender, onRenderChange]);
if (!shouldRender) { if (!shouldRender) {

View file

@ -162,6 +162,9 @@ const SidePanel = ({
transition: 'width 0.2s ease, visibility 0s linear 0.2s', transition: 'width 0.2s ease, visibility 0s linear 0.2s',
}} }}
onExpand={() => { onExpand={() => {
if (isCollapsed && (fullCollapse || collapsedSize === 0)) {
return;
}
setIsCollapsed(false); setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false'); localStorage.setItem('react-resizable-panels:collapsed', 'false');
}} }}

View file

@ -138,9 +138,9 @@ const SidePanelGroup = memo(
setCollapsedSize={setCollapsedSize} setCollapsedSize={setCollapsedSize}
fullCollapse={fullCollapse} fullCollapse={fullCollapse}
setFullCollapse={setFullCollapse} setFullCollapse={setFullCollapse}
defaultSize={currentLayout[currentLayout.length - 1]}
hasArtifacts={artifacts != null}
interfaceConfig={interfaceConfig} interfaceConfig={interfaceConfig}
hasArtifacts={shouldRenderArtifacts}
defaultSize={currentLayout[currentLayout.length - 1]}
/> />
)} )}
</ResizablePanelGroup> </ResizablePanelGroup>

View file

@ -378,6 +378,7 @@
"com_error_moderation": "Šķiet, ka mūsu moderācijas sistēma ir atzīmējusi nosūtīto saturu kā neatbilstošu mūsu vadlīnijām. Mēs nevaram turpināt darbu ar šo konkrēto tēmu. Ja jums ir vēl kādi jautājumi vai tēmas, kuras vēlaties izpētīt, lūdzu, rediģējiet savu ziņu vai izveidojiet jaunu sarunu.", "com_error_moderation": "Šķiet, ka mūsu moderācijas sistēma ir atzīmējusi nosūtīto saturu kā neatbilstošu mūsu vadlīnijām. Mēs nevaram turpināt darbu ar šo konkrēto tēmu. Ja jums ir vēl kādi jautājumi vai tēmas, kuras vēlaties izpētīt, lūdzu, rediģējiet savu ziņu vai izveidojiet jaunu sarunu.",
"com_error_no_base_url": "Nav atrasts bāzes URL. Lūdzu, norādiet to un mēģiniet vēlreiz.", "com_error_no_base_url": "Nav atrasts bāzes URL. Lūdzu, norādiet to un mēģiniet vēlreiz.",
"com_error_no_user_key": "Atslēga nav atrasta. Lūdzu, norādiet atslēgu un mēģiniet vēlreiz.", "com_error_no_user_key": "Atslēga nav atrasta. Lūdzu, norādiet atslēgu un mēģiniet vēlreiz.",
"com_error_refusal": "Drošības filtri noraidīja atbildi. Pārrakstiet savu ziņojumu un mēģiniet vēlreiz. Ja, lietojot Claude Sonnet 4.5 vai Opus 4.1, ar šo problēmu bieži saskaraties, varat izmēģināt Sonnet 4, kuram ir atšķirīgi lietošanas ierobežojumi.",
"com_file_pages": "Lapas: {{pages}}", "com_file_pages": "Lapas: {{pages}}",
"com_file_source": "Fails", "com_file_source": "Fails",
"com_file_unknown": "Nezināms fails", "com_file_unknown": "Nezināms fails",
@ -768,10 +769,12 @@
"com_ui_cancel": "Atcelt", "com_ui_cancel": "Atcelt",
"com_ui_cancelled": "Atcelts", "com_ui_cancelled": "Atcelts",
"com_ui_category": "Kategorija", "com_ui_category": "Kategorija",
"com_ui_change_version": "Mainīt versiju",
"com_ui_chat": "Saruna", "com_ui_chat": "Saruna",
"com_ui_chat_history": "Sarunu vēsture", "com_ui_chat_history": "Sarunu vēsture",
"com_ui_clear": "Notīrīt", "com_ui_clear": "Notīrīt",
"com_ui_clear_all": "Notīrīt visu", "com_ui_clear_all": "Notīrīt visu",
"com_ui_click_to_close": "Noklikšķiniet, lai aizvērtu",
"com_ui_client_id": "Klienta ID", "com_ui_client_id": "Klienta ID",
"com_ui_client_secret": "Klienta noslēpums", "com_ui_client_secret": "Klienta noslēpums",
"com_ui_close": "Aizvērt", "com_ui_close": "Aizvērt",

View file

@ -36,7 +36,7 @@ const shouldRebase = process.argv.includes('--rebase');
} }
console.purple('Removing previously made Docker container...'); console.purple('Removing previously made Docker container...');
const downCommand = 'sudo docker-compose -f ./deploy-compose.yml down'; const downCommand = 'sudo docker compose -f ./deploy-compose.yml down';
console.orange(downCommand); console.orange(downCommand);
execSync(downCommand, { stdio: 'inherit' }); execSync(downCommand, { stdio: 'inherit' });
@ -54,15 +54,15 @@ const shouldRebase = process.argv.includes('--rebase');
}); });
console.purple('Pulling latest LibreChat images...'); console.purple('Pulling latest LibreChat images...');
const pullCommand = 'sudo docker-compose -f ./deploy-compose.yml pull api'; const pullCommand = 'sudo docker compose -f ./deploy-compose.yml pull api';
console.orange(pullCommand); console.orange(pullCommand);
execSync(pullCommand, { stdio: 'inherit' }); execSync(pullCommand, { stdio: 'inherit' });
let startCommand = 'sudo docker-compose -f ./deploy-compose.yml up -d'; let startCommand = 'sudo docker compose -f ./deploy-compose.yml up -d';
console.green('Your LibreChat app is now up to date! Start the app with the following command:'); console.green('Your LibreChat app is now up to date! Start the app with the following command:');
console.purple(startCommand); console.purple(startCommand);
console.orange( console.orange(
'Note: it\'s also recommended to clear your browser cookies and localStorage for LibreChat to assure a fully clean installation.', "Note: it's also recommended to clear your browser cookies and localStorage for LibreChat to assure a fully clean installation.",
); );
console.orange('Also: Don\'t worry, your data is safe :)'); console.orange("Also: Don't worry, your data is safe :)");
})(); })();

View file

@ -0,0 +1,26 @@
{{- range $key, $value := .Values.additionalConfigMaps }}
{{- if or .data .binaryData }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "librechat.fullname" $ }}-{{ default "custom" $key }}
{{- with .labels }}
labels:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .data }}
data:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- with .binaryData }}
binaryData:
{{- toYaml . | nindent 2 }}
immutable: {{ default false .immutable }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -51,6 +51,15 @@ spec:
serviceAccountName: {{ include "librechat.serviceAccountName" . }} serviceAccountName: {{ include "librechat.serviceAccountName" . }}
securityContext: securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }} {{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.initContainers }}
initContainers:
{{- range $key, $value := .Values.initContainers }}
{{- if . }}
- name: {{ $key }}
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- end }}
containers: containers:
- name: {{ include "librechat.fullname" $ }} - name: {{ include "librechat.fullname" $ }}
securityContext: securityContext:

View file

@ -219,6 +219,13 @@ readinessProbe:
path: /health path: /health
port: 3080 port: 3080
# Additional init containers on the output Deployment definition.
initContainers: {}
# foo: # the name of the init container
# image: busybox
# command: ['sh', '-c', 'echo The app is starting! && sleep 5']
# # ... add more init containers as needed
# Additional volumes on the output Deployment definition. # Additional volumes on the output Deployment definition.
volumes: [] volumes: []
# - name: foo # - name: foo
@ -269,6 +276,16 @@ dnsConfig: {}
updateStrategy: updateStrategy:
type: RollingUpdate type: RollingUpdate
# Extra ConfigMaps to be created alongside the main ones
additionalConfigMaps: {}
# custom: # suffix of the ConfigMap name
# labels: {}
# annotations: {}
# data: {}
# binaryData: {}
# immutable: false
# # ... add more ConfigMaps as needed
# MongoDB Parameters # MongoDB Parameters
mongodb: mongodb:
enabled: true enabled: true

3194
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -97,7 +97,7 @@
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.10.1", "@axe-core/playwright": "^4.10.1",
"@eslint/compat": "^1.2.6", "@eslint/compat": "^1.2.6",
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.20.0", "@eslint/js": "^9.20.0",
"@microsoft/eslint-formatter-sarif": "^3.1.0", "@microsoft/eslint-formatter-sarif": "^3.1.0",
"@playwright/test": "^1.56.1", "@playwright/test": "^1.56.1",
@ -105,12 +105,12 @@
"caniuse-lite": "^1.0.30001741", "caniuse-lite": "^1.0.30001741",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"elliptic": "^6.6.1", "elliptic": "^6.6.1",
"eslint": "^9.20.1", "eslint": "^9.39.1",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-import-resolver-typescript": "^3.7.0", "eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-i18next": "^6.1.1", "eslint-plugin-i18next": "^6.1.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0", "eslint-plugin-jest": "^29.1.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
@ -118,10 +118,10 @@
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.14.0", "globals": "^15.14.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^29.7.0", "jest": "^30.2.0",
"lint-staged": "^15.4.3", "lint-staged": "^15.4.3",
"prettier": "^3.5.0", "prettier": "^3.5.0",
"prettier-eslint": "^16.3.0", "prettier-eslint": "^16.4.2",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"typescript-eslint": "^8.24.0" "typescript-eslint": "^8.24.0"
}, },

View file

@ -23,6 +23,7 @@
"test:cache-integration:core": "jest --testPathPattern=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", "test:cache-integration:core": "jest --testPathPattern=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
"test:cache-integration:cluster": "jest --testPathPattern=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand", "test:cache-integration:cluster": "jest --testPathPattern=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand",
"test:cache-integration:mcp": "jest --testPathPattern=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", "test:cache-integration:mcp": "jest --testPathPattern=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
"test:cache-integration": "npm run test:cache-integration:core && npm run test:cache-integration:cluster && npm run test:cache-integration:mcp",
"verify": "npm run test:ci", "verify": "npm run test:ci",
"b:clean": "bun run rimraf dist", "b:clean": "bun run rimraf dist",
"b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs", "b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs",
@ -63,7 +64,7 @@
"@types/node-fetch": "^2.6.13", "@types/node-fetch": "^2.6.13",
"@types/react": "^18.2.18", "@types/react": "^18.2.18",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"jest": "^29.5.0", "jest": "^30.2.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"librechat-data-provider": "*", "librechat-data-provider": "*",
"mongodb": "^6.14.2", "mongodb": "^6.14.2",
@ -95,7 +96,7 @@
"firebase": "^11.0.2", "firebase": "^11.0.2",
"form-data": "^4.0.4", "form-data": "^4.0.4",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"keyv": "^5.3.2", "keyv": "^5.3.2",
"keyv-file": "^5.1.2", "keyv-file": "^5.1.2",

View file

@ -345,7 +345,7 @@ ${memory ?? 'No existing memories'}`;
}; };
// Handle GPT-5+ models // Handle GPT-5+ models
if ('model' in finalLLMConfig && /\bgpt-[5-9]\b/i.test(finalLLMConfig.model ?? '')) { if ('model' in finalLLMConfig && /\bgpt-[5-9](?:\.\d+)?\b/i.test(finalLLMConfig.model ?? '')) {
// Remove temperature for GPT-5+ models // Remove temperature for GPT-5+ models
delete finalLLMConfig.temperature; delete finalLLMConfig.temperature;

View file

@ -7,17 +7,13 @@ describe('limiterCache', () => {
beforeEach(() => { beforeEach(() => {
originalEnv = { ...process.env }; originalEnv = { ...process.env };
// Clear cache-related env vars // Set test configuration with fallback defaults for local testing
delete process.env.USE_REDIS;
delete process.env.REDIS_URI;
delete process.env.USE_REDIS_CLUSTER;
delete process.env.REDIS_PING_INTERVAL;
delete process.env.REDIS_KEY_PREFIX;
// Set test configuration
process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_PING_INTERVAL = '0';
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
// Clear require cache to reload modules // Clear require cache to reload modules
jest.resetModules(); jest.resetModules();
@ -43,10 +39,6 @@ describe('limiterCache', () => {
}); });
test('should return RedisStore with sendCommand when USE_REDIS is true', async () => { test('should return RedisStore with sendCommand when USE_REDIS is true', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;

View file

@ -33,17 +33,13 @@ describe('sessionCache', () => {
beforeEach(() => { beforeEach(() => {
originalEnv = { ...process.env }; originalEnv = { ...process.env };
// Clear cache-related env vars // Set test configuration with fallback defaults for local testing
delete process.env.USE_REDIS;
delete process.env.REDIS_URI;
delete process.env.USE_REDIS_CLUSTER;
delete process.env.REDIS_PING_INTERVAL;
delete process.env.REDIS_KEY_PREFIX;
// Set test configuration
process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_PING_INTERVAL = '0';
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
// Clear require cache to reload modules // Clear require cache to reload modules
jest.resetModules(); jest.resetModules();
@ -55,10 +51,6 @@ describe('sessionCache', () => {
}); });
test('should return ConnectRedis store when USE_REDIS is true', async () => { test('should return ConnectRedis store when USE_REDIS is true', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;
@ -138,10 +130,6 @@ describe('sessionCache', () => {
}); });
test('should handle namespace with and without trailing colon', async () => { test('should handle namespace with and without trailing colon', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const store1 = cacheFactory.sessionCache('namespace1'); const store1 = cacheFactory.sessionCache('namespace1');
@ -152,10 +140,6 @@ describe('sessionCache', () => {
}); });
test('should register error handler for Redis connection', async () => { test('should register error handler for Redis connection', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;
@ -173,10 +157,6 @@ describe('sessionCache', () => {
}); });
test('should handle session expiration with TTL', async () => { test('should handle session expiration with TTL', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;

View file

@ -30,18 +30,13 @@ describe('standardCache', () => {
beforeEach(() => { beforeEach(() => {
originalEnv = { ...process.env }; originalEnv = { ...process.env };
// Clear cache-related env vars // Set test configuration with fallback defaults for local testing
delete process.env.USE_REDIS;
delete process.env.REDIS_URI;
delete process.env.USE_REDIS_CLUSTER;
delete process.env.REDIS_PING_INTERVAL;
delete process.env.REDIS_KEY_PREFIX;
delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES;
// Set test configuration
process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_PING_INTERVAL = '0';
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
// Clear require cache to reload modules // Clear require cache to reload modules
jest.resetModules(); jest.resetModules();
@ -119,10 +114,6 @@ describe('standardCache', () => {
describe('when connecting to a Redis server', () => { describe('when connecting to a Redis server', () => {
test('should handle different namespaces with correct prefixes', async () => { test('should handle different namespaces with correct prefixes', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const cache1 = cacheFactory.standardCache('namespace-one'); const cache1 = cacheFactory.standardCache('namespace-one');
@ -148,9 +139,6 @@ describe('standardCache', () => {
}); });
test('should respect FORCED_IN_MEMORY_CACHE_NAMESPACES', async () => { test('should respect FORCED_IN_MEMORY_CACHE_NAMESPACES', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'ROLES'; // Use a valid cache key process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'ROLES'; // Use a valid cache key
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
@ -167,10 +155,6 @@ describe('standardCache', () => {
}); });
test('should handle TTL correctly', async () => { test('should handle TTL correctly', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
testCache = cacheFactory.standardCache('ttl-test', 1000); // 1 second TTL testCache = cacheFactory.standardCache('ttl-test', 1000); // 1 second TTL

View file

@ -26,17 +26,13 @@ describe('violationCache', () => {
beforeEach(() => { beforeEach(() => {
originalEnv = { ...process.env }; originalEnv = { ...process.env };
// Clear cache-related env vars // Set test configuration with fallback defaults for local testing
delete process.env.USE_REDIS;
delete process.env.REDIS_URI;
delete process.env.USE_REDIS_CLUSTER;
delete process.env.REDIS_PING_INTERVAL;
delete process.env.REDIS_KEY_PREFIX;
// Set test configuration
process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_PING_INTERVAL = '0';
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
// Clear require cache to reload modules // Clear require cache to reload modules
jest.resetModules(); jest.resetModules();
@ -48,10 +44,6 @@ describe('violationCache', () => {
}); });
test('should create violation cache with Redis when USE_REDIS is true', async () => { test('should create violation cache with Redis when USE_REDIS is true', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;
@ -119,10 +111,6 @@ describe('violationCache', () => {
}); });
test('should respect namespace prefixing', async () => { test('should respect namespace prefixing', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;
@ -157,10 +145,6 @@ describe('violationCache', () => {
}); });
test('should respect TTL settings', async () => { test('should respect TTL settings', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;
@ -193,10 +177,6 @@ describe('violationCache', () => {
}); });
test('should handle complex violation data structures', async () => { test('should handle complex violation data structures', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const cacheFactory = await import('../../cacheFactory'); const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients'); const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients; const { ioredisClient } = redisClients;

View file

@ -9,9 +9,13 @@ describe('redisClients Integration Tests', () => {
let keyvRedisClient: RedisClientType | RedisClusterType | null = null; let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
// Helper function to test set/get/delete operations // Helper function to test set/get/delete operations
const testRedisOperations = async (client: RedisClient, keyPrefix: string): Promise<void> => { const testRedisOperations = async (
// Wait cluster to fully initialize client: RedisClient,
await new Promise((resolve) => setTimeout(resolve, 1000)); keyPrefix: string,
readyPromise?: Promise<void>,
): Promise<void> => {
// Wait for connection and topology discovery to complete
if (readyPromise) await readyPromise;
const testKey = `${keyPrefix}-test-key`; const testKey = `${keyPrefix}-test-key`;
const testValue = `${keyPrefix}-test-value`; const testValue = `${keyPrefix}-test-value`;
@ -35,18 +39,13 @@ describe('redisClients Integration Tests', () => {
beforeEach(() => { beforeEach(() => {
originalEnv = { ...process.env }; originalEnv = { ...process.env };
// Clear Redis-related env vars // Set common test configuration with fallback defaults for local testing
delete process.env.USE_REDIS; process.env.REDIS_PING_INTERVAL = '1000';
delete process.env.REDIS_URI;
delete process.env.USE_REDIS_CLUSTER;
delete process.env.REDIS_PING_INTERVAL;
delete process.env.REDIS_KEY_PREFIX;
// Set common test configuration
process.env.REDIS_PING_INTERVAL = '0';
process.env.REDIS_KEY_PREFIX = 'Redis-Integration-Test'; process.env.REDIS_KEY_PREFIX = 'Redis-Integration-Test';
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
process.env.REDIS_PING_INTERVAL = '1000'; process.env.USE_REDIS = process.env.USE_REDIS || 'true';
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
// Clear module cache to reload module // Clear module cache to reload module
jest.resetModules(); jest.resetModules();
@ -105,10 +104,6 @@ describe('redisClients Integration Tests', () => {
describe('when connecting to a Redis instance', () => { describe('when connecting to a Redis instance', () => {
test('should connect and perform set/get/delete operations', async () => { test('should connect and perform set/get/delete operations', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const clients = await import('../redisClients'); const clients = await import('../redisClients');
ioredisClient = clients.ioredisClient; ioredisClient = clients.ioredisClient;
await testRedisOperations(ioredisClient!, 'ioredis-single'); await testRedisOperations(ioredisClient!, 'ioredis-single');
@ -117,7 +112,6 @@ describe('redisClients Integration Tests', () => {
describe('when connecting to a Redis cluster', () => { describe('when connecting to a Redis cluster', () => {
test('should connect to cluster and perform set/get/delete operations', async () => { test('should connect to cluster and perform set/get/delete operations', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'true'; process.env.USE_REDIS_CLUSTER = 'true';
process.env.REDIS_URI = process.env.REDIS_URI =
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003'; 'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
@ -142,26 +136,21 @@ describe('redisClients Integration Tests', () => {
describe('when connecting to a Redis instance', () => { describe('when connecting to a Redis instance', () => {
test('should connect and perform set/get/delete operations', async () => { test('should connect and perform set/get/delete operations', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'false';
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
const clients = await import('../redisClients'); const clients = await import('../redisClients');
keyvRedisClient = clients.keyvRedisClient; keyvRedisClient = clients.keyvRedisClient;
await testRedisOperations(keyvRedisClient!, 'keyv-single'); await testRedisOperations(keyvRedisClient!, 'keyv-single', clients.keyvRedisClientReady!);
}); });
}); });
describe('when connecting to a Redis cluster', () => { describe('when connecting to a Redis cluster', () => {
test('should connect to cluster and perform set/get/delete operations', async () => { test('should connect to cluster and perform set/get/delete operations', async () => {
process.env.USE_REDIS = 'true';
process.env.USE_REDIS_CLUSTER = 'true'; process.env.USE_REDIS_CLUSTER = 'true';
process.env.REDIS_URI = process.env.REDIS_URI =
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003'; 'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
const clients = await import('../redisClients'); const clients = await import('../redisClients');
keyvRedisClient = clients.keyvRedisClient; keyvRedisClient = clients.keyvRedisClient;
await testRedisOperations(keyvRedisClient!, 'keyv-cluster'); await testRedisOperations(keyvRedisClient!, 'keyv-cluster', clients.keyvRedisClientReady!);
}); });
}); });
}); });

View file

@ -3,6 +3,7 @@ import type { Redis, Cluster } from 'ioredis';
import { logger } from '@librechat/data-schemas'; import { logger } from '@librechat/data-schemas';
import { createClient, createCluster } from '@keyv/redis'; import { createClient, createCluster } from '@keyv/redis';
import type { RedisClientType, RedisClusterType } from '@redis/client'; import type { RedisClientType, RedisClusterType } from '@redis/client';
import type { ScanCommandOptions } from '@redis/client/dist/lib/commands/SCAN';
import { cacheConfig } from './cacheConfig'; import { cacheConfig } from './cacheConfig';
const urls = cacheConfig.REDIS_URI?.split(',').map((uri) => new URL(uri)) || []; const urls = cacheConfig.REDIS_URI?.split(',').map((uri) => new URL(uri)) || [];
@ -121,6 +122,11 @@ if (cacheConfig.USE_REDIS) {
} }
let keyvRedisClient: RedisClientType | RedisClusterType | null = null; let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
let keyvRedisClientReady:
| Promise<void>
| Promise<RedisClientType<Record<string, never>, Record<string, never>, Record<string, never>>>
| null = null;
if (cacheConfig.USE_REDIS) { if (cacheConfig.USE_REDIS) {
/** /**
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above. * ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
@ -162,6 +168,22 @@ if (cacheConfig.USE_REDIS) {
defaults: redisOptions, defaults: redisOptions,
}); });
// Add scanIterator method to cluster client for API consistency with standalone client
if (!('scanIterator' in keyvRedisClient)) {
const clusterClient = keyvRedisClient as RedisClusterType;
(keyvRedisClient as unknown as RedisClientType).scanIterator = async function* (
options?: ScanCommandOptions,
) {
const masters = clusterClient.masters;
for (const master of masters) {
const nodeClient = await clusterClient.nodeClient(master);
for await (const key of nodeClient.scanIterator(options)) {
yield key;
}
}
};
}
keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS); keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
keyvRedisClient.on('error', (err) => { keyvRedisClient.on('error', (err) => {
@ -184,10 +206,13 @@ if (cacheConfig.USE_REDIS) {
logger.warn('@keyv/redis client disconnected'); logger.warn('@keyv/redis client disconnected');
}); });
keyvRedisClient.connect().catch((err) => { // Start connection immediately
keyvRedisClientReady = keyvRedisClient.connect();
keyvRedisClientReady.catch((err): void => {
logger.error('@keyv/redis initial connection failed:', err); logger.error('@keyv/redis initial connection failed:', err);
throw err; throw err;
}); });
} }
export { ioredisClient, keyvRedisClient }; export { ioredisClient, keyvRedisClient, keyvRedisClientReady };

View file

@ -25,10 +25,8 @@ describe('LeaderElection with Redis', () => {
throw new Error('Redis client is not initialized'); throw new Error('Redis client is not initialized');
} }
// Wait for Redis to be ready // Wait for connection and topology discovery to complete
if (!keyvRedisClient.isOpen) { await redisClients.keyvRedisClientReady;
await keyvRedisClient.connect();
}
// Increase max listeners to handle many instances in tests // Increase max listeners to handle many instances in tests
process.setMaxListeners(200); process.setMaxListeners(200);

View file

@ -940,6 +940,16 @@ describe('getOpenAIConfig', () => {
{ reasoning_effort: null, reasoning_summary: null, shouldHaveReasoning: false }, { reasoning_effort: null, reasoning_summary: null, shouldHaveReasoning: false },
{ reasoning_effort: undefined, reasoning_summary: undefined, shouldHaveReasoning: false }, { reasoning_effort: undefined, reasoning_summary: undefined, shouldHaveReasoning: false },
{ reasoning_effort: '', reasoning_summary: '', shouldHaveReasoning: false }, { reasoning_effort: '', reasoning_summary: '', shouldHaveReasoning: false },
{
reasoning_effort: ReasoningEffort.unset,
reasoning_summary: '',
shouldHaveReasoning: false,
},
{
reasoning_effort: ReasoningEffort.none,
reasoning_summary: null,
shouldHaveReasoning: true,
},
{ {
reasoning_effort: null, reasoning_effort: null,
reasoning_summary: ReasoningSummary.concise, reasoning_summary: ReasoningSummary.concise,

View file

@ -300,7 +300,11 @@ export function getOpenAILLMConfig({
delete modelKwargs.verbosity; delete modelKwargs.verbosity;
} }
if (llmConfig.model && /\bgpt-[5-9]\b/i.test(llmConfig.model) && llmConfig.maxTokens != null) { if (
llmConfig.model &&
/\bgpt-[5-9](?:\.\d+)?\b/i.test(llmConfig.model) &&
llmConfig.maxTokens != null
) {
const paramName = const paramName =
llmConfig.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens'; llmConfig.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
modelKwargs[paramName] = llmConfig.maxTokens; modelKwargs[paramName] = llmConfig.maxTokens;

View file

@ -121,8 +121,8 @@ describe('MCPServersInitializer Redis Integration Tests', () => {
// Ensure Redis is connected // Ensure Redis is connected
if (!keyvRedisClient) throw new Error('Redis client is not initialized'); if (!keyvRedisClient) throw new Error('Redis client is not initialized');
// Wait for Redis to be ready // Wait for connection and topology discovery to complete
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); await redisClients.keyvRedisClientReady;
// Become leader so we can perform write operations // Become leader so we can perform write operations
leaderInstance = new LeaderElection(); leaderInstance = new LeaderElection();

View file

@ -50,8 +50,8 @@ describe('MCPServersRegistry Redis Integration Tests', () => {
// Ensure Redis is connected // Ensure Redis is connected
if (!keyvRedisClient) throw new Error('Redis client is not initialized'); if (!keyvRedisClient) throw new Error('Redis client is not initialized');
// Wait for Redis to be ready // Wait for connection and topology discovery to complete
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); await redisClients.keyvRedisClientReady;
// Become leader so we can perform write operations // Become leader so we can perform write operations
leaderInstance = new LeaderElection(); leaderInstance = new LeaderElection();

View file

@ -73,6 +73,8 @@ export class ServerConfigsCacheRedis extends BaseRegistryCache {
entries.push([keyName, value as ParsedServerConfig]); entries.push([keyName, value as ParsedServerConfig]);
} }
} }
} else {
throw new Error('Redis client with scanIterator not available.');
} }
return fromPairs(entries); return fromPairs(entries);

View file

@ -25,8 +25,8 @@ describe('RegistryStatusCache Integration Tests', () => {
// Ensure Redis is connected // Ensure Redis is connected
if (!keyvRedisClient) throw new Error('Redis client is not initialized'); if (!keyvRedisClient) throw new Error('Redis client is not initialized');
// Wait for Redis to be ready // Wait for connection and topology discovery to complete
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); await redisClients.keyvRedisClientReady;
// Become leader so we can perform write operations // Become leader so we can perform write operations
leaderInstance = new LeaderElection(); leaderInstance = new LeaderElection();

View file

@ -31,7 +31,10 @@ describe('ServerConfigsCacheRedis Integration Tests', () => {
beforeAll(async () => { beforeAll(async () => {
// Set up environment variables for Redis (only if not already set) // Set up environment variables for Redis (only if not already set)
process.env.USE_REDIS = process.env.USE_REDIS ?? 'true'; process.env.USE_REDIS = process.env.USE_REDIS ?? 'true';
process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379'; process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER ?? 'true';
process.env.REDIS_URI =
process.env.REDIS_URI ??
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
process.env.REDIS_KEY_PREFIX = process.env.REDIS_KEY_PREFIX =
process.env.REDIS_KEY_PREFIX ?? 'ServerConfigsCacheRedis-IntegrationTest'; process.env.REDIS_KEY_PREFIX ?? 'ServerConfigsCacheRedis-IntegrationTest';
@ -49,8 +52,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => {
// Ensure Redis is connected // Ensure Redis is connected
if (!keyvRedisClient) throw new Error('Redis client is not initialized'); if (!keyvRedisClient) throw new Error('Redis client is not initialized');
// Wait for Redis to be ready // Wait for connection and topology discovery to complete
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); await redisClients.keyvRedisClientReady;
// Clear any existing leader key to ensure clean state // Clear any existing leader key to ensure clean state
await keyvRedisClient.del(LeaderElection.LEADER_KEY); await keyvRedisClient.del(LeaderElection.LEADER_KEY);

View file

@ -41,7 +41,7 @@
"dependencies": { "dependencies": {
"axios": "^1.12.1", "axios": "^1.12.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
@ -60,7 +60,7 @@
"@types/node": "^20.3.0", "@types/node": "^20.3.0",
"@types/react": "^18.2.18", "@types/react": "^18.2.18",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"jest": "^29.5.0", "jest": "^30.2.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",

View file

@ -927,7 +927,7 @@ export enum KnownEndpoints {
export enum FetchTokenConfig { export enum FetchTokenConfig {
openrouter = KnownEndpoints.openrouter, openrouter = KnownEndpoints.openrouter,
helicone = KnownEndpoints.helicone helicone = KnownEndpoints.helicone,
} }
export const defaultEndpoints: EModelEndpoint[] = [ export const defaultEndpoints: EModelEndpoint[] = [
@ -964,6 +964,10 @@ export const alternateName = {
}; };
const sharedOpenAIModels = [ const sharedOpenAIModels = [
'gpt-5.1',
'gpt-5.1-chat-latest',
'gpt-5.1-codex',
'gpt-5.1-codex-mini',
'gpt-5', 'gpt-5',
'gpt-5-mini', 'gpt-5-mini',
'gpt-5-nano', 'gpt-5-nano',

View file

@ -230,9 +230,10 @@ const openAIParams: Record<string, SettingDefinition> = {
description: 'com_endpoint_openai_reasoning_effort', description: 'com_endpoint_openai_reasoning_effort',
descriptionCode: true, descriptionCode: true,
type: 'enum', type: 'enum',
default: ReasoningEffort.none, default: ReasoningEffort.unset,
component: 'slider', component: 'slider',
options: [ options: [
ReasoningEffort.unset,
ReasoningEffort.none, ReasoningEffort.none,
ReasoningEffort.minimal, ReasoningEffort.minimal,
ReasoningEffort.low, ReasoningEffort.low,
@ -240,6 +241,7 @@ const openAIParams: Record<string, SettingDefinition> = {
ReasoningEffort.high, ReasoningEffort.high,
], ],
enumMappings: { enumMappings: {
[ReasoningEffort.unset]: 'com_ui_auto',
[ReasoningEffort.none]: 'com_ui_none', [ReasoningEffort.none]: 'com_ui_none',
[ReasoningEffort.minimal]: 'com_ui_minimal', [ReasoningEffort.minimal]: 'com_ui_minimal',
[ReasoningEffort.low]: 'com_ui_low', [ReasoningEffort.low]: 'com_ui_low',
@ -291,7 +293,7 @@ const openAIParams: Record<string, SettingDefinition> = {
ReasoningSummary.detailed, ReasoningSummary.detailed,
], ],
enumMappings: { enumMappings: {
[ReasoningSummary.none]: 'com_ui_none', [ReasoningSummary.none]: 'com_ui_unset',
[ReasoningSummary.auto]: 'com_ui_auto', [ReasoningSummary.auto]: 'com_ui_auto',
[ReasoningSummary.concise]: 'com_ui_concise', [ReasoningSummary.concise]: 'com_ui_concise',
[ReasoningSummary.detailed]: 'com_ui_detailed', [ReasoningSummary.detailed]: 'com_ui_detailed',

View file

@ -166,7 +166,8 @@ export enum ImageDetail {
} }
export enum ReasoningEffort { export enum ReasoningEffort {
none = '', unset = '',
none = 'none',
minimal = 'minimal', minimal = 'minimal',
low = 'low', low = 'low',
medium = 'medium', medium = 'medium',

View file

@ -48,7 +48,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.0", "@types/node": "^20.3.0",
"jest": "^29.5.0", "jest": "^30.2.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"mongodb-memory-server": "^10.1.4", "mongodb-memory-server": "^10.1.4",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",