🧩 feat: Support Alternate API Keys for Plugins (#1760)

* refactor(DALL-E): retrieve env variables at runtime and not from memory

* feat(plugins): add alternate env variable handling to allow setting one api key for multiple plugins

* docs: update docs
This commit is contained in:
Danny Avila 2024-02-09 10:38:50 -05:00 committed by GitHub
parent 927ce5395b
commit 39caeb2027
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 328 additions and 113 deletions

View file

@ -8,15 +8,6 @@ const { processFileURL } = require('~/server/services/Files/process');
const extractBaseURL = require('~/utils/extractBaseURL'); const extractBaseURL = require('~/utils/extractBaseURL');
const { logger } = require('~/config'); const { logger } = require('~/config');
const {
DALLE2_SYSTEM_PROMPT,
DALLE_REVERSE_PROXY,
PROXY,
DALLE2_AZURE_API_VERSION,
DALLE2_BASEURL,
DALLE2_API_KEY,
DALLE_API_KEY,
} = process.env;
class OpenAICreateImage extends Tool { class OpenAICreateImage extends Tool {
constructor(fields = {}) { constructor(fields = {}) {
super(); super();
@ -26,19 +17,22 @@ class OpenAICreateImage extends Tool {
let apiKey = fields.DALLE2_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey(); let apiKey = fields.DALLE2_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
const config = { apiKey }; const config = { apiKey };
if (DALLE_REVERSE_PROXY) { if (process.env.DALLE_REVERSE_PROXY) {
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY); config.baseURL = extractBaseURL(process.env.DALLE_REVERSE_PROXY);
} }
if (DALLE2_AZURE_API_VERSION && DALLE2_BASEURL) { if (process.env.DALLE2_AZURE_API_VERSION && process.env.DALLE2_BASEURL) {
config.baseURL = DALLE2_BASEURL; config.baseURL = process.env.DALLE2_BASEURL;
config.defaultQuery = { 'api-version': DALLE2_AZURE_API_VERSION }; config.defaultQuery = { 'api-version': process.env.DALLE2_AZURE_API_VERSION };
config.defaultHeaders = { 'api-key': DALLE2_API_KEY, 'Content-Type': 'application/json' }; config.defaultHeaders = {
config.apiKey = DALLE2_API_KEY; 'api-key': process.env.DALLE2_API_KEY,
'Content-Type': 'application/json',
};
config.apiKey = process.env.DALLE2_API_KEY;
} }
if (PROXY) { if (process.env.PROXY) {
config.httpAgent = new HttpsProxyAgent(PROXY); config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
} }
this.openai = new OpenAI(config); this.openai = new OpenAI(config);
@ -51,7 +45,7 @@ Guidelines:
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]" "Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
- Generate images only once per human query unless explicitly requested by the user`; - Generate images only once per human query unless explicitly requested by the user`;
this.description_for_model = this.description_for_model =
DALLE2_SYSTEM_PROMPT ?? process.env.DALLE2_SYSTEM_PROMPT ??
`// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies: `// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies:
// 1. Prompts must be in English. Translate to English if needed. // 1. Prompts must be in English. Translate to English if needed.
// 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image. // 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image.
@ -67,7 +61,7 @@ Guidelines:
} }
getApiKey() { getApiKey() {
const apiKey = DALLE2_API_KEY ?? DALLE_API_KEY ?? ''; const apiKey = process.env.DALLE2_API_KEY ?? process.env.DALLE_API_KEY ?? '';
if (!apiKey) { if (!apiKey) {
throw new Error('Missing DALLE_API_KEY environment variable.'); throw new Error('Missing DALLE_API_KEY environment variable.');
} }

View file

@ -89,7 +89,7 @@
"icon": "https://i.imgur.com/u2TzXzH.png", "icon": "https://i.imgur.com/u2TzXzH.png",
"authConfig": [ "authConfig": [
{ {
"authField": "DALLE2_API_KEY", "authField": "DALLE2_API_KEY||DALLE_API_KEY",
"label": "OpenAI API Key", "label": "OpenAI API Key",
"description": "You can use DALL-E with your API Key from OpenAI." "description": "You can use DALL-E with your API Key from OpenAI."
} }
@ -102,7 +102,7 @@
"icon": "https://i.imgur.com/u2TzXzH.png", "icon": "https://i.imgur.com/u2TzXzH.png",
"authConfig": [ "authConfig": [
{ {
"authField": "DALLE3_API_KEY", "authField": "DALLE3_API_KEY||DALLE_API_KEY",
"label": "OpenAI API Key", "label": "OpenAI API Key",
"description": "You can use DALL-E with your API Key from OpenAI." "description": "You can use DALL-E with your API Key from OpenAI."
} }

View file

@ -9,14 +9,6 @@ const { processFileURL } = require('~/server/services/Files/process');
const extractBaseURL = require('~/utils/extractBaseURL'); const extractBaseURL = require('~/utils/extractBaseURL');
const { logger } = require('~/config'); const { logger } = require('~/config');
const {
DALLE3_SYSTEM_PROMPT,
DALLE_REVERSE_PROXY,
PROXY,
DALLE3_AZURE_API_VERSION,
DALLE3_BASEURL,
DALLE3_API_KEY,
} = process.env;
class DALLE3 extends Tool { class DALLE3 extends Tool {
constructor(fields = {}) { constructor(fields = {}) {
super(); super();
@ -25,19 +17,22 @@ class DALLE3 extends Tool {
this.fileStrategy = fields.fileStrategy; this.fileStrategy = fields.fileStrategy;
let apiKey = fields.DALLE3_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey(); let apiKey = fields.DALLE3_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
const config = { apiKey }; const config = { apiKey };
if (DALLE_REVERSE_PROXY) { if (process.env.DALLE_REVERSE_PROXY) {
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY); config.baseURL = extractBaseURL(process.env.DALLE_REVERSE_PROXY);
} }
if (DALLE3_AZURE_API_VERSION && DALLE3_BASEURL) { if (process.env.DALLE3_AZURE_API_VERSION && process.env.DALLE3_BASEURL) {
config.baseURL = DALLE3_BASEURL; config.baseURL = process.env.DALLE3_BASEURL;
config.defaultQuery = { 'api-version': DALLE3_AZURE_API_VERSION }; config.defaultQuery = { 'api-version': process.env.DALLE3_AZURE_API_VERSION };
config.defaultHeaders = { 'api-key': DALLE3_API_KEY, 'Content-Type': 'application/json' }; config.defaultHeaders = {
config.apiKey = DALLE3_API_KEY; 'api-key': process.env.DALLE3_API_KEY,
'Content-Type': 'application/json',
};
config.apiKey = process.env.DALLE3_API_KEY;
} }
if (PROXY) { if (process.env.PROXY) {
config.httpAgent = new HttpsProxyAgent(PROXY); config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
} }
this.openai = new OpenAI(config); this.openai = new OpenAI(config);
@ -47,7 +42,7 @@ class DALLE3 extends Tool {
- Create only one image, without repeating or listing descriptions outside the "prompts" field. - Create only one image, without repeating or listing descriptions outside the "prompts" field.
- Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`; - Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`;
this.description_for_model = this.description_for_model =
DALLE3_SYSTEM_PROMPT ?? process.env.DALLE3_SYSTEM_PROMPT ??
`// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies: `// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies:
// 1. Prompts must be in English. Translate to English if needed. // 1. Prompts must be in English. Translate to English if needed.
// 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image. // 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image.

View file

@ -30,6 +30,14 @@ const getOpenAIKey = async (options, user) => {
return openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY')); return openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY'));
}; };
/**
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
* Tools without required authentication or with valid authentication are considered valid.
*
* @param {Object} user The user object for whom to validate tool access.
* @param {Array<string>} tools An array of tool identifiers to validate. Defaults to an empty array.
* @returns {Promise<Array<string>>} A promise that resolves to an array of valid tool identifiers.
*/
const validateTools = async (user, tools = []) => { const validateTools = async (user, tools = []) => {
try { try {
const validToolsSet = new Set(tools); const validToolsSet = new Set(tools);
@ -37,16 +45,34 @@ const validateTools = async (user, tools = []) => {
validToolsSet.has(tool.pluginKey), validToolsSet.has(tool.pluginKey),
); );
/**
* Validates the credentials for a given auth field or set of alternate auth fields for a tool.
* If valid admin or user authentication is found, the function returns early. Otherwise, it removes the tool from the set of valid tools.
*
* @param {string} authField The authentication field or fields (separated by "||" for alternates) to validate.
* @param {string} toolName The identifier of the tool being validated.
*/
const validateCredentials = async (authField, toolName) => { const validateCredentials = async (authField, toolName) => {
const adminAuth = process.env[authField]; const fields = authField.split('||');
if (adminAuth && adminAuth.length > 0) { for (const field of fields) {
return; const adminAuth = process.env[field];
if (adminAuth && adminAuth.length > 0) {
return;
}
let userAuth = null;
try {
userAuth = await getUserPluginAuthValue(user, field);
} catch (err) {
if (field === fields[fields.length - 1] && !userAuth) {
throw err;
}
}
if (userAuth && userAuth.length > 0) {
return;
}
} }
const userAuth = await getUserPluginAuthValue(user, authField);
if (userAuth && userAuth.length > 0) {
return;
}
validToolsSet.delete(toolName); validToolsSet.delete(toolName);
}; };
@ -63,20 +89,55 @@ const validateTools = async (user, tools = []) => {
return Array.from(validToolsSet.values()); return Array.from(validToolsSet.values());
} catch (err) { } catch (err) {
logger.error('[validateTools] There was a problem validating tools', err); logger.error('[validateTools] There was a problem validating tools', err);
throw new Error(err); throw new Error('There was a problem validating tools');
} }
}; };
const loadToolWithAuth = async (userId, authFields, ToolConstructor, options = {}) => { /**
* Initializes a tool with authentication values for the given user, supporting alternate authentication fields.
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
*
* @param {string} userId The user ID for which the tool is being loaded.
* @param {Array<string>} authFields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
* @param {typeof import('langchain/tools').Tool} ToolConstructor The constructor function for the tool to be initialized.
* @param {Object} options Optional parameters to be passed to the tool constructor alongside authentication values.
* @returns {Function} An Async function that, when called, asynchronously initializes and returns an instance of the tool with authentication.
*/
const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => {
return async function () { return async function () {
let authValues = {}; let authValues = {};
for (const authField of authFields) { /**
let authValue = process.env[authField]; * Finds the first non-empty value for the given authentication field, supporting alternate fields.
if (!authValue) { * @param {string[]} fields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
authValue = await getUserPluginAuthValue(userId, authField); * @returns {Promise<{ authField: string, authValue: string} | null>} An object containing the authentication field and value, or null if not found.
*/
const findAuthValue = async (fields) => {
for (const field of fields) {
let value = process.env[field];
if (value) {
return { authField: field, authValue: value };
}
try {
value = await getUserPluginAuthValue(userId, field);
} catch (err) {
if (field === fields[fields.length - 1] && !value) {
throw err;
}
}
if (value) {
return { authField: field, authValue: value };
}
}
return null;
};
for (let authField of authFields) {
const fields = authField.split('||');
const result = await findAuthValue(fields);
if (result) {
authValues[result.authField] = result.authValue;
} }
authValues[authField] = authValue;
} }
return new ToolConstructor({ ...options, ...authValues, userId }); return new ToolConstructor({ ...options, ...authValues, userId });
@ -194,7 +255,7 @@ const loadTools = async ({
if (toolConstructors[tool]) { if (toolConstructors[tool]) {
const options = toolOptions[tool] || {}; const options = toolOptions[tool] || {};
const toolInstance = await loadToolWithAuth( const toolInstance = loadToolWithAuth(
user, user,
toolAuthFields[tool], toolAuthFields[tool],
toolConstructors[tool], toolConstructors[tool],
@ -250,6 +311,7 @@ const loadTools = async ({
}; };
module.exports = { module.exports = {
loadToolWithAuth,
validateTools, validateTools,
loadTools, loadTools,
}; };

View file

@ -4,26 +4,33 @@ const mockUser = {
findByIdAndDelete: jest.fn(), findByIdAndDelete: jest.fn(),
}; };
var mockPluginService = { const mockPluginService = {
updateUserPluginAuth: jest.fn(), updateUserPluginAuth: jest.fn(),
deleteUserPluginAuth: jest.fn(), deleteUserPluginAuth: jest.fn(),
getUserPluginAuthValue: jest.fn(), getUserPluginAuthValue: jest.fn(),
}; };
jest.mock('../../../../models/User', () => { jest.mock('~/models/User', () => {
return function () { return function () {
return mockUser; return mockUser;
}; };
}); });
jest.mock('../../../../server/services/PluginService', () => mockPluginService); jest.mock('~/server/services/PluginService', () => mockPluginService);
const User = require('../../../../models/User');
const { validateTools, loadTools } = require('./');
const PluginService = require('../../../../server/services/PluginService');
const { BaseChatModel } = require('langchain/chat_models/openai');
const { Calculator } = require('langchain/tools/calculator'); const { Calculator } = require('langchain/tools/calculator');
const { availableTools, OpenAICreateImage, GoogleSearchAPI, StructuredSD } = require('../'); const { BaseChatModel } = require('langchain/chat_models/openai');
const User = require('~/models/User');
const PluginService = require('~/server/services/PluginService');
const { validateTools, loadTools, loadToolWithAuth } = require('./handleTools');
const {
availableTools,
OpenAICreateImage,
GoogleSearchAPI,
StructuredSD,
WolframAlphaAPI,
} = require('../');
describe('Tool Handlers', () => { describe('Tool Handlers', () => {
let fakeUser; let fakeUser;
@ -44,7 +51,10 @@ describe('Tool Handlers', () => {
}); });
mockPluginService.updateUserPluginAuth.mockImplementation( mockPluginService.updateUserPluginAuth.mockImplementation(
(userId, authField, _pluginKey, credential) => { (userId, authField, _pluginKey, credential) => {
userAuthValues[`${userId}-${authField}`] = credential; const fields = authField.split('||');
fields.forEach((field) => {
userAuthValues[`${userId}-${field}`] = credential;
});
}, },
); );
@ -134,6 +144,18 @@ describe('Tool Handlers', () => {
loadTool2 = toolFunctions[sampleTools[1]]; loadTool2 = toolFunctions[sampleTools[1]];
loadTool3 = toolFunctions[sampleTools[2]]; loadTool3 = toolFunctions[sampleTools[2]];
}); });
let originalEnv;
beforeEach(() => {
originalEnv = process.env;
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
it('returns the expected load functions for requested tools', async () => { it('returns the expected load functions for requested tools', async () => {
expect(loadTool1).toBeDefined(); expect(loadTool1).toBeDefined();
expect(loadTool2).toBeDefined(); expect(loadTool2).toBeDefined();
@ -150,6 +172,86 @@ describe('Tool Handlers', () => {
expect(authTool).toBeInstanceOf(ToolClass); expect(authTool).toBeInstanceOf(ToolClass);
expect(tool).toBeInstanceOf(ToolClass2); expect(tool).toBeInstanceOf(ToolClass2);
}); });
it('should initialize an authenticated tool with primary auth field', async () => {
process.env.DALLE2_API_KEY = 'mocked_api_key';
const initToolFunction = loadToolWithAuth(
'userId',
['DALLE2_API_KEY||DALLE_API_KEY'],
ToolClass,
);
const authTool = await initToolFunction();
expect(authTool).toBeInstanceOf(ToolClass);
expect(mockPluginService.getUserPluginAuthValue).not.toHaveBeenCalled();
});
it('should initialize an authenticated tool with alternate auth field when primary is missing', async () => {
delete process.env.DALLE2_API_KEY; // Ensure the primary key is not set
process.env.DALLE_API_KEY = 'mocked_alternate_api_key';
const initToolFunction = loadToolWithAuth(
'userId',
['DALLE2_API_KEY||DALLE_API_KEY'],
ToolClass,
);
const authTool = await initToolFunction();
expect(authTool).toBeInstanceOf(ToolClass);
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledTimes(1);
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledWith(
'userId',
'DALLE2_API_KEY',
);
});
it('should fallback to getUserPluginAuthValue when env vars are missing', async () => {
mockPluginService.updateUserPluginAuth('userId', 'DALLE_API_KEY', 'dalle', 'mocked_api_key');
const initToolFunction = loadToolWithAuth(
'userId',
['DALLE2_API_KEY||DALLE_API_KEY'],
ToolClass,
);
const authTool = await initToolFunction();
expect(authTool).toBeInstanceOf(ToolClass);
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledTimes(2);
});
it('should initialize an authenticated tool with singular auth field', async () => {
process.env.WOLFRAM_APP_ID = 'mocked_app_id';
const initToolFunction = loadToolWithAuth('userId', ['WOLFRAM_APP_ID'], WolframAlphaAPI);
const authTool = await initToolFunction();
expect(authTool).toBeInstanceOf(WolframAlphaAPI);
expect(mockPluginService.getUserPluginAuthValue).not.toHaveBeenCalled();
});
it('should initialize an authenticated tool when env var is set', async () => {
process.env.WOLFRAM_APP_ID = 'mocked_app_id';
const initToolFunction = loadToolWithAuth('userId', ['WOLFRAM_APP_ID'], WolframAlphaAPI);
const authTool = await initToolFunction();
expect(authTool).toBeInstanceOf(WolframAlphaAPI);
expect(mockPluginService.getUserPluginAuthValue).not.toHaveBeenCalledWith(
'userId',
'WOLFRAM_APP_ID',
);
});
it('should fallback to getUserPluginAuthValue when singular env var is missing', async () => {
delete process.env.WOLFRAM_APP_ID; // Ensure the environment variable is not set
mockPluginService.getUserPluginAuthValue.mockResolvedValue('mocked_user_auth_value');
const initToolFunction = loadToolWithAuth('userId', ['WOLFRAM_APP_ID'], WolframAlphaAPI);
const authTool = await initToolFunction();
expect(authTool).toBeInstanceOf(WolframAlphaAPI);
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledTimes(1);
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledWith(
'userId',
'WOLFRAM_APP_ID',
);
});
it('should throw an error for an unauthenticated tool', async () => { it('should throw an error for an unauthenticated tool', async () => {
try { try {
await loadTool2(); await loadTool2();

View file

@ -1,17 +1,48 @@
const { getUserPluginAuthValue } = require('../../../../server/services/PluginService'); const { getUserPluginAuthValue } = require('~/server/services/PluginService');
const { availableTools } = require('../'); const { availableTools } = require('../');
const loadToolSuite = async ({ pluginKey, tools, user, options }) => { /**
* Loads a suite of tools with authentication values for a given user, supporting alternate authentication fields.
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
*
* @param {Object} params Parameters for loading the tool suite.
* @param {string} params.pluginKey Key identifying the plugin whose tools are to be loaded.
* @param {Array<Function>} params.tools Array of tool constructor functions.
* @param {Object} params.user User object for whom the tools are being loaded.
* @param {Object} [params.options={}] Optional parameters to be passed to each tool constructor.
* @returns {Promise<Array>} A promise that resolves to an array of instantiated tools.
*/
const loadToolSuite = async ({ pluginKey, tools, user, options = {} }) => {
const authConfig = availableTools.find((tool) => tool.pluginKey === pluginKey).authConfig; const authConfig = availableTools.find((tool) => tool.pluginKey === pluginKey).authConfig;
const suite = []; const suite = [];
const authValues = {}; const authValues = {};
for (const auth of authConfig) { const findAuthValue = async (authField) => {
let authValue = process.env[auth.authField]; const fields = authField.split('||');
if (!authValue) { for (const field of fields) {
authValue = await getUserPluginAuthValue(user, auth.authField); let value = process.env[field];
if (value) {
return value;
}
try {
value = await getUserPluginAuthValue(user, field);
if (value) {
return value;
}
} catch (err) {
console.error(`Error fetching plugin auth value for ${field}: ${err.message}`);
}
}
return null;
};
for (const auth of authConfig) {
const authValue = await findAuthValue(auth.authField);
if (authValue !== null) {
authValues[auth.authField] = authValue;
} else {
console.warn(`No auth value found for ${auth.authField}`);
} }
authValues[auth.authField] = authValue;
} }
for (const tool of tools) { for (const tool of tools) {

View file

@ -4,6 +4,12 @@ const { CacheKeys } = require('librechat-data-provider');
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs'); const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
const { getLogStores } = require('~/cache'); const { getLogStores } = require('~/cache');
/**
* Filters out duplicate plugins from the list of plugins.
*
* @param {TPlugin[]} plugins The list of plugins to filter.
* @returns {TPlugin[]} The list of plugins with duplicates removed.
*/
const filterUniquePlugins = (plugins) => { const filterUniquePlugins = (plugins) => {
const seen = new Set(); const seen = new Set();
return plugins.filter((plugin) => { return plugins.filter((plugin) => {
@ -13,17 +19,31 @@ const filterUniquePlugins = (plugins) => {
}); });
}; };
/**
* Determines if a plugin is authenticated by checking if all required authentication fields have non-empty values.
* Supports alternate authentication fields, allowing validation against multiple possible environment variables.
*
* @param {TPlugin} plugin The plugin object containing the authentication configuration.
* @returns {boolean} True if the plugin is authenticated for all required fields, false otherwise.
*/
const isPluginAuthenticated = (plugin) => { const isPluginAuthenticated = (plugin) => {
if (!plugin.authConfig || plugin.authConfig.length === 0) { if (!plugin.authConfig || plugin.authConfig.length === 0) {
return false; return false;
} }
return plugin.authConfig.every((authFieldObj) => { return plugin.authConfig.every((authFieldObj) => {
const envValue = process.env[authFieldObj.authField]; const authFieldOptions = authFieldObj.authField.split('||');
if (envValue === 'user_provided') { let isFieldAuthenticated = false;
return false;
for (const fieldOption of authFieldOptions) {
const envValue = process.env[fieldOption];
if (envValue && envValue.trim() !== '' && envValue !== 'user_provided') {
isFieldAuthenticated = true;
break;
}
} }
return envValue && envValue.trim() !== '';
return isFieldAuthenticated;
}); });
}; };

View file

@ -26,6 +26,12 @@
* @memberof typedefs * @memberof typedefs
*/ */
/**
* @exports TPlugin
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
* @memberof typedefs
*/
/** /**
* @exports TCustomConfig * @exports TCustomConfig
* @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig * @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig

View file

@ -26,44 +26,47 @@ function PluginAuthForm({ plugin, onSubmit }: TPluginAuthFormProps) {
onSubmit({ pluginKey: plugin?.pluginKey ?? '', action: 'install', auth }), onSubmit({ pluginKey: plugin?.pluginKey ?? '', action: 'install', auth }),
)} )}
> >
{plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => ( {plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => {
<div key={`${config.authField}-${i}`} className="flex w-full flex-col gap-1"> const authField = config.authField.split('||')[0];
<label return (
htmlFor={config.authField} <div key={`${authField}-${i}`} className="flex w-full flex-col gap-1">
className="mb-1 text-left text-sm font-medium text-slate-700/70 dark:text-slate-50/70" <label
> htmlFor={authField}
{config.label} className="mb-1 text-left text-sm font-medium text-slate-700/70 dark:text-slate-50/70"
</label> >
<HoverCard openDelay={300}> {config.label}
<HoverCardTrigger className="grid w-full items-center gap-2"> </label>
<input <HoverCard openDelay={300}>
type="text" <HoverCardTrigger className="grid w-full items-center gap-2">
autoComplete="off" <input
id={config.authField} type="text"
aria-invalid={!!errors[config.authField]} autoComplete="off"
aria-describedby={`${config.authField}-error`} id={authField}
aria-label={config.label} aria-invalid={!!errors[authField]}
aria-required="true" aria-describedby={`${authField}-error`}
{...register(config.authField, { aria-label={config.label}
required: `${config.label} is required.`, aria-required="true"
minLength: { {...register(authField, {
value: 10, required: `${config.label} is required.`,
message: `${config.label} must be at least 10 characters long`, minLength: {
}, value: 10,
})} message: `${config.label} must be at least 10 characters long`,
className="flex h-10 max-h-10 w-full resize-none rounded-md border border-gray-200 bg-transparent px-3 py-2 text-sm text-gray-700 shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:border-slate-400 focus:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-gray-400 focus:ring-opacity-0 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 focus:dark:bg-gray-600 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0" },
/> })}
</HoverCardTrigger> className="flex h-10 max-h-10 w-full resize-none rounded-md border border-gray-200 bg-transparent px-3 py-2 text-sm text-gray-700 shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:border-slate-400 focus:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-gray-400 focus:ring-opacity-0 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 focus:dark:bg-gray-600 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0"
<PluginTooltip content={config.description} position="right" /> />
</HoverCard> </HoverCardTrigger>
{errors[config.authField] && ( <PluginTooltip content={config.description} position="right" />
<span role="alert" className="mt-1 text-sm text-red-400"> </HoverCard>
{/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */} {errors[authField] && (
{errors[config.authField].message} <span role="alert" className="mt-1 text-sm text-red-400">
</span> {/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */}
)} {errors[authField].message}
</div> </span>
))} )}
</div>
);
})}
<button <button
disabled={!isDirty || !isValid || isSubmitting} disabled={!isDirty || !isValid || isSubmitting}
type="submit" type="submit"

View file

@ -407,6 +407,8 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
#### DALL-E: #### DALL-E:
**Note:** Make sure the `gptPlugins` endpoint is set in the [`ENDPOINTS`](#endpoints) environment variable if it was configured before.
**API Keys:** **API Keys:**
- `DALLE_API_KEY`: This environment variable is intended for storing the OpenAI API key that grants access to both DALL-E 2 and DALL-E 3 services. Typically, this key should be kept private. If you are distributing a plugin or software that integrates with DALL-E, you may choose to leave this commented out, requiring the end user to input their own API key. If you have a shared API key you want to distribute with your software (not recommended for security reasons), you can uncomment this and provide the key. - `DALLE_API_KEY`: This environment variable is intended for storing the OpenAI API key that grants access to both DALL-E 2 and DALL-E 3 services. Typically, this key should be kept private. If you are distributing a plugin or software that integrates with DALL-E, you may choose to leave this commented out, requiring the end user to input their own API key. If you have a shared API key you want to distribute with your software (not recommended for security reasons), you can uncomment this and provide the key.