mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-03 09:08:52 +01:00
Merge remote-tracking branch 'upstream/main' into feature/entra-id-azure-integration
This commit is contained in:
commit
6ba2d8e967
41 changed files with 2255 additions and 1361 deletions
21
.github/workflows/cache-integration-tests.yml
vendored
21
.github/workflows/cache-integration-tests.yml
vendored
|
|
@ -61,30 +61,23 @@ jobs:
|
|||
npm run build:data-schemas
|
||||
npm run build:api
|
||||
|
||||
- name: Run cache integration tests
|
||||
- name: Run all cache integration tests (Single Redis Node)
|
||||
working-directory: packages/api
|
||||
env:
|
||||
NODE_ENV: test
|
||||
USE_REDIS: true
|
||||
USE_REDIS_CLUSTER: false
|
||||
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:core
|
||||
run: npm run test:cache-integration
|
||||
|
||||
- name: Run cluster integration tests
|
||||
- name: Run all cache integration tests (Redis Cluster)
|
||||
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:cluster
|
||||
|
||||
- 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
|
||||
USE_REDIS_CLUSTER: true
|
||||
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: Stop Redis Cluster
|
||||
if: always()
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
"handlebars": "^4.7.7",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ioredis": "^5.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwks-rsa": "^3.2.0",
|
||||
"keyv": "^5.3.2",
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.2.0",
|
||||
"mongodb-memory-server": "^10.1.4",
|
||||
"nodemon": "^3.0.3",
|
||||
"supertest": "^7.1.0"
|
||||
|
|
|
|||
|
|
@ -989,7 +989,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
};
|
||||
|
||||
// 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.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
|
|
@ -1009,7 +1009,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
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 ?? {};
|
||||
const paramName =
|
||||
clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
|
||||
|
|
@ -1034,7 +1034,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
};
|
||||
|
||||
// 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.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
|
|
@ -1055,7 +1055,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
};
|
||||
|
||||
// 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.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
|
|
@ -1068,6 +1068,9 @@ describe('AgentClient - titleConvo', () => {
|
|||
|
||||
it('should handle various GPT-5+ model formats', () => {
|
||||
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-turbo', shouldTransform: true },
|
||||
{ model: 'gpt-6', shouldTransform: true },
|
||||
|
|
@ -1087,7 +1090,10 @@ describe('AgentClient - titleConvo', () => {
|
|||
};
|
||||
|
||||
// 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.max_completion_tokens = 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', () => {
|
||||
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-turbo', shouldTransform: true },
|
||||
{ model: 'gpt-6', shouldTransform: true },
|
||||
|
|
@ -1124,7 +1133,10 @@ describe('AgentClient - titleConvo', () => {
|
|||
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 ?? {};
|
||||
const paramName =
|
||||
clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
|
||||
|
|
@ -1157,7 +1169,10 @@ describe('AgentClient - titleConvo', () => {
|
|||
};
|
||||
|
||||
// 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.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ const {
|
|||
ResourceType,
|
||||
EModelEndpoint,
|
||||
PermissionBits,
|
||||
isAgentsEndpoint,
|
||||
checkOpenAIStorage,
|
||||
isAssistantsEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
filterFile,
|
||||
|
|
@ -376,11 +376,11 @@ router.post('/', async (req, res) => {
|
|||
metadata.temp_file_id = metadata.file_id;
|
||||
metadata.file_id = req.file_id;
|
||||
|
||||
if (isAgentsEndpoint(metadata.endpoint)) {
|
||||
return await processAgentFileUpload({ req, res, metadata });
|
||||
if (isAssistantsEndpoint(metadata.endpoint)) {
|
||||
return await processFileUpload({ req, res, metadata });
|
||||
}
|
||||
|
||||
await processFileUpload({ req, res, metadata });
|
||||
return await processAgentFileUpload({ req, res, metadata });
|
||||
} catch (error) {
|
||||
let message = 'Error processing file';
|
||||
logger.error('[/files] Error processing file:', error);
|
||||
|
|
|
|||
|
|
@ -135,10 +135,10 @@
|
|||
"babel-plugin-root-import": "^6.6.0",
|
||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||
"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",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-file-loader": "^1.0.3",
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.2.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-jest": "^29.4.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-compression2": "^2.2.1",
|
||||
|
|
|
|||
|
|
@ -179,9 +179,10 @@ export const ArtifactCodeEditor = function ({
|
|||
bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL,
|
||||
};
|
||||
}, [config, template, fileKey]);
|
||||
const [readOnly, setReadOnly] = useState(externalReadOnly ?? isSubmitting ?? false);
|
||||
const initialReadOnly = (externalReadOnly ?? false) || (isSubmitting ?? false);
|
||||
const [readOnly, setReadOnly] = useState(initialReadOnly);
|
||||
useEffect(() => {
|
||||
setReadOnly(externalReadOnly ?? isSubmitting ?? false);
|
||||
setReadOnly((externalReadOnly ?? false) || (isSubmitting ?? false));
|
||||
}, [isSubmitting, externalReadOnly]);
|
||||
|
||||
if (Object.keys(files).length === 0) {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
|
|||
return (
|
||||
<div className="group/reasoning">
|
||||
<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
|
||||
isExpanded={isExpanded}
|
||||
onClick={handleClick}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
|||
|
||||
return (
|
||||
<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
|
||||
isExpanded={isExpanded}
|
||||
onClick={handleClick}
|
||||
|
|
|
|||
|
|
@ -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 ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />}
|
||||
{isCopied ? (
|
||||
<CheckMark className="h-[19px] w-[19px]" />
|
||||
) : (
|
||||
<Clipboard className="h-[19px] w-[19px]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ type EndpointIcon = {
|
|||
|
||||
function getOpenAIColor(_model: string | null | undefined) {
|
||||
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 model.includes('gpt-4') ? '#AB68FF' : '#19C37D';
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { useRef, useEffect, memo } from 'react';
|
|||
import { ResizableHandleAlt, ResizablePanel } from '@librechat/client';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
interface ArtifactsPanelProps {
|
||||
artifacts: React.ReactNode | null;
|
||||
currentLayout: number[];
|
||||
|
|
@ -24,14 +22,9 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
|
|||
onRenderChange,
|
||||
}: ArtifactsPanelProps) {
|
||||
const artifactsPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (artifacts != null) {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
onRenderChange(true);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
|
|
@ -39,17 +32,8 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
|
|||
});
|
||||
});
|
||||
} else if (shouldRender) {
|
||||
artifactsPanelRef.current?.collapse();
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
onRenderChange(false);
|
||||
}, ANIMATION_DURATION);
|
||||
onRenderChange(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [artifacts, shouldRender, onRenderChange]);
|
||||
|
||||
if (!shouldRender) {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ const SidePanel = ({
|
|||
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
|
||||
}}
|
||||
onExpand={() => {
|
||||
if (isCollapsed && (fullCollapse || collapsedSize === 0)) {
|
||||
return;
|
||||
}
|
||||
setIsCollapsed(false);
|
||||
localStorage.setItem('react-resizable-panels:collapsed', 'false');
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -138,9 +138,9 @@ const SidePanelGroup = memo(
|
|||
setCollapsedSize={setCollapsedSize}
|
||||
fullCollapse={fullCollapse}
|
||||
setFullCollapse={setFullCollapse}
|
||||
defaultSize={currentLayout[currentLayout.length - 1]}
|
||||
hasArtifacts={artifacts != null}
|
||||
interfaceConfig={interfaceConfig}
|
||||
hasArtifacts={shouldRenderArtifacts}
|
||||
defaultSize={currentLayout[currentLayout.length - 1]}
|
||||
/>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
|
|
|
|||
|
|
@ -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_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_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_source": "Fails",
|
||||
"com_file_unknown": "Nezināms fails",
|
||||
|
|
@ -768,10 +769,12 @@
|
|||
"com_ui_cancel": "Atcelt",
|
||||
"com_ui_cancelled": "Atcelts",
|
||||
"com_ui_category": "Kategorija",
|
||||
"com_ui_change_version": "Mainīt versiju",
|
||||
"com_ui_chat": "Saruna",
|
||||
"com_ui_chat_history": "Sarunu vēsture",
|
||||
"com_ui_clear": "Notīrīt",
|
||||
"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_secret": "Klienta noslēpums",
|
||||
"com_ui_close": "Aizvērt",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const shouldRebase = process.argv.includes('--rebase');
|
|||
}
|
||||
|
||||
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);
|
||||
execSync(downCommand, { stdio: 'inherit' });
|
||||
|
||||
|
|
@ -54,15 +54,15 @@ const shouldRebase = process.argv.includes('--rebase');
|
|||
});
|
||||
|
||||
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);
|
||||
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.purple(startCommand);
|
||||
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 :)");
|
||||
})();
|
||||
|
|
|
|||
26
helm/librechat/templates/configmaps-additional.yaml
Executable file
26
helm/librechat/templates/configmaps-additional.yaml
Executable 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 }}
|
||||
|
|
@ -51,6 +51,15 @@ spec:
|
|||
serviceAccountName: {{ include "librechat.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
{{- if .Values.initContainers }}
|
||||
initContainers:
|
||||
{{- range $key, $value := .Values.initContainers }}
|
||||
{{- if . }}
|
||||
- name: {{ $key }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ include "librechat.fullname" $ }}
|
||||
securityContext:
|
||||
|
|
|
|||
|
|
@ -219,6 +219,13 @@ readinessProbe:
|
|||
path: /health
|
||||
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.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
|
|
@ -269,6 +276,16 @@ dnsConfig: {}
|
|||
updateStrategy:
|
||||
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:
|
||||
enabled: true
|
||||
|
|
|
|||
3194
package-lock.json
generated
3194
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -97,7 +97,7 @@
|
|||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.1",
|
||||
"@eslint/compat": "^1.2.6",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.20.0",
|
||||
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
||||
"@playwright/test": "^1.56.1",
|
||||
|
|
@ -105,12 +105,12 @@
|
|||
"caniuse-lite": "^1.0.30001741",
|
||||
"cross-env": "^7.0.3",
|
||||
"elliptic": "^6.6.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-i18next": "^6.1.1",
|
||||
"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-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
|
|
@ -118,10 +118,10 @@
|
|||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.14.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.2.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
"prettier": "^3.5.0",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"prettier-eslint": "^16.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"typescript-eslint": "^8.24.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"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: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",
|
||||
"b:clean": "bun run rimraf dist",
|
||||
"b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs",
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/react": "^18.2.18",
|
||||
"@types/winston": "^2.4.4",
|
||||
"jest": "^29.5.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"librechat-data-provider": "*",
|
||||
"mongodb": "^6.14.2",
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
"firebase": "^11.0.2",
|
||||
"form-data": "^4.0.4",
|
||||
"ioredis": "^5.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^5.3.2",
|
||||
"keyv-file": "^5.1.2",
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ ${memory ?? 'No existing memories'}`;
|
|||
};
|
||||
|
||||
// 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
|
||||
delete finalLLMConfig.temperature;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,13 @@ describe('limiterCache', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Clear cache-related env vars
|
||||
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
|
||||
// Set test configuration with fallback defaults for local testing
|
||||
process.env.REDIS_PING_INTERVAL = '0';
|
||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||
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
|
||||
jest.resetModules();
|
||||
|
|
@ -43,10 +39,6 @@ describe('limiterCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
|
|||
|
|
@ -33,17 +33,13 @@ describe('sessionCache', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Clear cache-related env vars
|
||||
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
|
||||
// Set test configuration with fallback defaults for local testing
|
||||
process.env.REDIS_PING_INTERVAL = '0';
|
||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||
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
|
||||
jest.resetModules();
|
||||
|
|
@ -55,10 +51,6 @@ describe('sessionCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
@ -138,10 +130,6 @@ describe('sessionCache', () => {
|
|||
});
|
||||
|
||||
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 store1 = cacheFactory.sessionCache('namespace1');
|
||||
|
|
@ -152,10 +140,6 @@ describe('sessionCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
@ -173,10 +157,6 @@ describe('sessionCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
|
|||
|
|
@ -30,18 +30,13 @@ describe('standardCache', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Clear cache-related env vars
|
||||
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
|
||||
// Set test configuration with fallback defaults for local testing
|
||||
process.env.REDIS_PING_INTERVAL = '0';
|
||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||
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
|
||||
jest.resetModules();
|
||||
|
|
@ -119,10 +114,6 @@ describe('standardCache', () => {
|
|||
|
||||
describe('when connecting to a Redis server', () => {
|
||||
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 cache1 = cacheFactory.standardCache('namespace-one');
|
||||
|
|
@ -148,9 +139,6 @@ describe('standardCache', () => {
|
|||
});
|
||||
|
||||
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
|
||||
|
||||
const cacheFactory = await import('../../cacheFactory');
|
||||
|
|
@ -167,10 +155,6 @@ describe('standardCache', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
testCache = cacheFactory.standardCache('ttl-test', 1000); // 1 second TTL
|
||||
|
||||
|
|
|
|||
|
|
@ -26,17 +26,13 @@ describe('violationCache', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Clear cache-related env vars
|
||||
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
|
||||
// Set test configuration with fallback defaults for local testing
|
||||
process.env.REDIS_PING_INTERVAL = '0';
|
||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||
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
|
||||
jest.resetModules();
|
||||
|
|
@ -48,10 +44,6 @@ describe('violationCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
@ -119,10 +111,6 @@ describe('violationCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
@ -157,10 +145,6 @@ describe('violationCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
@ -193,10 +177,6 @@ describe('violationCache', () => {
|
|||
});
|
||||
|
||||
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 redisClients = await import('../../redisClients');
|
||||
const { ioredisClient } = redisClients;
|
||||
|
|
|
|||
|
|
@ -9,9 +9,13 @@ describe('redisClients Integration Tests', () => {
|
|||
let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
|
||||
|
||||
// Helper function to test set/get/delete operations
|
||||
const testRedisOperations = async (client: RedisClient, keyPrefix: string): Promise<void> => {
|
||||
// Wait cluster to fully initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const testRedisOperations = async (
|
||||
client: RedisClient,
|
||||
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 testValue = `${keyPrefix}-test-value`;
|
||||
|
|
@ -35,18 +39,13 @@ describe('redisClients Integration Tests', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Clear Redis-related env vars
|
||||
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 common test configuration
|
||||
process.env.REDIS_PING_INTERVAL = '0';
|
||||
// Set common test configuration with fallback defaults for local testing
|
||||
process.env.REDIS_PING_INTERVAL = '1000';
|
||||
process.env.REDIS_KEY_PREFIX = 'Redis-Integration-Test';
|
||||
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
|
||||
jest.resetModules();
|
||||
|
|
@ -105,10 +104,6 @@ describe('redisClients Integration Tests', () => {
|
|||
|
||||
describe('when connecting to a Redis instance', () => {
|
||||
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');
|
||||
ioredisClient = clients.ioredisClient;
|
||||
await testRedisOperations(ioredisClient!, 'ioredis-single');
|
||||
|
|
@ -117,7 +112,6 @@ describe('redisClients Integration Tests', () => {
|
|||
|
||||
describe('when connecting to a Redis cluster', () => {
|
||||
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.REDIS_URI =
|
||||
'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', () => {
|
||||
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');
|
||||
keyvRedisClient = clients.keyvRedisClient;
|
||||
await testRedisOperations(keyvRedisClient!, 'keyv-single');
|
||||
await testRedisOperations(keyvRedisClient!, 'keyv-single', clients.keyvRedisClientReady!);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when connecting to a Redis cluster', () => {
|
||||
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.REDIS_URI =
|
||||
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
|
||||
|
||||
const clients = await import('../redisClients');
|
||||
keyvRedisClient = clients.keyvRedisClient;
|
||||
await testRedisOperations(keyvRedisClient!, 'keyv-cluster');
|
||||
await testRedisOperations(keyvRedisClient!, 'keyv-cluster', clients.keyvRedisClientReady!);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
29
packages/api/src/cache/redisClients.ts
vendored
29
packages/api/src/cache/redisClients.ts
vendored
|
|
@ -3,6 +3,7 @@ import type { Redis, Cluster } from 'ioredis';
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { createClient, createCluster } from '@keyv/redis';
|
||||
import type { RedisClientType, RedisClusterType } from '@redis/client';
|
||||
import type { ScanCommandOptions } from '@redis/client/dist/lib/commands/SCAN';
|
||||
import { cacheConfig } from './cacheConfig';
|
||||
|
||||
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 keyvRedisClientReady:
|
||||
| Promise<void>
|
||||
| Promise<RedisClientType<Record<string, never>, Record<string, never>, Record<string, never>>>
|
||||
| null = null;
|
||||
|
||||
if (cacheConfig.USE_REDIS) {
|
||||
/**
|
||||
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
|
||||
|
|
@ -162,6 +168,22 @@ if (cacheConfig.USE_REDIS) {
|
|||
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.on('error', (err) => {
|
||||
|
|
@ -184,10 +206,13 @@ if (cacheConfig.USE_REDIS) {
|
|||
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);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
export { ioredisClient, keyvRedisClient };
|
||||
export { ioredisClient, keyvRedisClient, keyvRedisClientReady };
|
||||
|
|
|
|||
|
|
@ -25,10 +25,8 @@ describe('LeaderElection with Redis', () => {
|
|||
throw new Error('Redis client is not initialized');
|
||||
}
|
||||
|
||||
// Wait for Redis to be ready
|
||||
if (!keyvRedisClient.isOpen) {
|
||||
await keyvRedisClient.connect();
|
||||
}
|
||||
// Wait for connection and topology discovery to complete
|
||||
await redisClients.keyvRedisClientReady;
|
||||
|
||||
// Increase max listeners to handle many instances in tests
|
||||
process.setMaxListeners(200);
|
||||
|
|
|
|||
|
|
@ -940,6 +940,16 @@ describe('getOpenAIConfig', () => {
|
|||
{ reasoning_effort: null, reasoning_summary: null, shouldHaveReasoning: false },
|
||||
{ reasoning_effort: undefined, reasoning_summary: undefined, 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_summary: ReasoningSummary.concise,
|
||||
|
|
|
|||
|
|
@ -300,7 +300,11 @@ export function getOpenAILLMConfig({
|
|||
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 =
|
||||
llmConfig.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
|
||||
modelKwargs[paramName] = llmConfig.maxTokens;
|
||||
|
|
|
|||
|
|
@ -121,8 +121,8 @@ describe('MCPServersInitializer Redis Integration Tests', () => {
|
|||
// Ensure Redis is connected
|
||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||
|
||||
// Wait for Redis to be ready
|
||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
||||
// Wait for connection and topology discovery to complete
|
||||
await redisClients.keyvRedisClientReady;
|
||||
|
||||
// Become leader so we can perform write operations
|
||||
leaderInstance = new LeaderElection();
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ describe('MCPServersRegistry Redis Integration Tests', () => {
|
|||
// Ensure Redis is connected
|
||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||
|
||||
// Wait for Redis to be ready
|
||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
||||
// Wait for connection and topology discovery to complete
|
||||
await redisClients.keyvRedisClientReady;
|
||||
|
||||
// Become leader so we can perform write operations
|
||||
leaderInstance = new LeaderElection();
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ export class ServerConfigsCacheRedis extends BaseRegistryCache {
|
|||
entries.push([keyName, value as ParsedServerConfig]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Redis client with scanIterator not available.');
|
||||
}
|
||||
|
||||
return fromPairs(entries);
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ describe('RegistryStatusCache Integration Tests', () => {
|
|||
// Ensure Redis is connected
|
||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||
|
||||
// Wait for Redis to be ready
|
||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
||||
// Wait for connection and topology discovery to complete
|
||||
await redisClients.keyvRedisClientReady;
|
||||
|
||||
// Become leader so we can perform write operations
|
||||
leaderInstance = new LeaderElection();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ describe('ServerConfigsCacheRedis Integration Tests', () => {
|
|||
beforeAll(async () => {
|
||||
// Set up environment variables for Redis (only if not already set)
|
||||
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 ?? 'ServerConfigsCacheRedis-IntegrationTest';
|
||||
|
||||
|
|
@ -49,8 +52,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => {
|
|||
// Ensure Redis is connected
|
||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||
|
||||
// Wait for Redis to be ready
|
||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
||||
// Wait for connection and topology discovery to complete
|
||||
await redisClients.keyvRedisClientReady;
|
||||
|
||||
// Clear any existing leader key to ensure clean state
|
||||
await keyvRedisClient.del(LeaderElection.LEADER_KEY);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.12.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
"@types/node": "^20.3.0",
|
||||
"@types/react": "^18.2.18",
|
||||
"@types/winston": "^2.4.4",
|
||||
"jest": "^29.5.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"rimraf": "^5.0.1",
|
||||
|
|
|
|||
|
|
@ -927,7 +927,7 @@ export enum KnownEndpoints {
|
|||
|
||||
export enum FetchTokenConfig {
|
||||
openrouter = KnownEndpoints.openrouter,
|
||||
helicone = KnownEndpoints.helicone
|
||||
helicone = KnownEndpoints.helicone,
|
||||
}
|
||||
|
||||
export const defaultEndpoints: EModelEndpoint[] = [
|
||||
|
|
@ -964,6 +964,10 @@ export const alternateName = {
|
|||
};
|
||||
|
||||
const sharedOpenAIModels = [
|
||||
'gpt-5.1',
|
||||
'gpt-5.1-chat-latest',
|
||||
'gpt-5.1-codex',
|
||||
'gpt-5.1-codex-mini',
|
||||
'gpt-5',
|
||||
'gpt-5-mini',
|
||||
'gpt-5-nano',
|
||||
|
|
|
|||
|
|
@ -230,9 +230,10 @@ const openAIParams: Record<string, SettingDefinition> = {
|
|||
description: 'com_endpoint_openai_reasoning_effort',
|
||||
descriptionCode: true,
|
||||
type: 'enum',
|
||||
default: ReasoningEffort.none,
|
||||
default: ReasoningEffort.unset,
|
||||
component: 'slider',
|
||||
options: [
|
||||
ReasoningEffort.unset,
|
||||
ReasoningEffort.none,
|
||||
ReasoningEffort.minimal,
|
||||
ReasoningEffort.low,
|
||||
|
|
@ -240,6 +241,7 @@ const openAIParams: Record<string, SettingDefinition> = {
|
|||
ReasoningEffort.high,
|
||||
],
|
||||
enumMappings: {
|
||||
[ReasoningEffort.unset]: 'com_ui_auto',
|
||||
[ReasoningEffort.none]: 'com_ui_none',
|
||||
[ReasoningEffort.minimal]: 'com_ui_minimal',
|
||||
[ReasoningEffort.low]: 'com_ui_low',
|
||||
|
|
@ -291,7 +293,7 @@ const openAIParams: Record<string, SettingDefinition> = {
|
|||
ReasoningSummary.detailed,
|
||||
],
|
||||
enumMappings: {
|
||||
[ReasoningSummary.none]: 'com_ui_none',
|
||||
[ReasoningSummary.none]: 'com_ui_unset',
|
||||
[ReasoningSummary.auto]: 'com_ui_auto',
|
||||
[ReasoningSummary.concise]: 'com_ui_concise',
|
||||
[ReasoningSummary.detailed]: 'com_ui_detailed',
|
||||
|
|
|
|||
|
|
@ -166,7 +166,8 @@ export enum ImageDetail {
|
|||
}
|
||||
|
||||
export enum ReasoningEffort {
|
||||
none = '',
|
||||
unset = '',
|
||||
none = 'none',
|
||||
minimal = 'minimal',
|
||||
low = 'low',
|
||||
medium = 'medium',
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"mongodb-memory-server": "^10.1.4",
|
||||
"rimraf": "^5.0.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue