mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🧩 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:
parent
927ce5395b
commit
39caeb2027
10 changed files with 328 additions and 113 deletions
|
|
@ -8,15 +8,6 @@ const { processFileURL } = require('~/server/services/Files/process');
|
|||
const extractBaseURL = require('~/utils/extractBaseURL');
|
||||
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 {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
|
|
@ -26,19 +17,22 @@ class OpenAICreateImage extends Tool {
|
|||
let apiKey = fields.DALLE2_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
|
||||
|
||||
const config = { apiKey };
|
||||
if (DALLE_REVERSE_PROXY) {
|
||||
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY);
|
||||
if (process.env.DALLE_REVERSE_PROXY) {
|
||||
config.baseURL = extractBaseURL(process.env.DALLE_REVERSE_PROXY);
|
||||
}
|
||||
|
||||
if (DALLE2_AZURE_API_VERSION && DALLE2_BASEURL) {
|
||||
config.baseURL = DALLE2_BASEURL;
|
||||
config.defaultQuery = { 'api-version': DALLE2_AZURE_API_VERSION };
|
||||
config.defaultHeaders = { 'api-key': DALLE2_API_KEY, 'Content-Type': 'application/json' };
|
||||
config.apiKey = DALLE2_API_KEY;
|
||||
if (process.env.DALLE2_AZURE_API_VERSION && process.env.DALLE2_BASEURL) {
|
||||
config.baseURL = process.env.DALLE2_BASEURL;
|
||||
config.defaultQuery = { 'api-version': process.env.DALLE2_AZURE_API_VERSION };
|
||||
config.defaultHeaders = {
|
||||
'api-key': process.env.DALLE2_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
config.apiKey = process.env.DALLE2_API_KEY;
|
||||
}
|
||||
|
||||
if (PROXY) {
|
||||
config.httpAgent = new HttpsProxyAgent(PROXY);
|
||||
if (process.env.PROXY) {
|
||||
config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
}
|
||||
|
||||
this.openai = new OpenAI(config);
|
||||
|
|
@ -51,7 +45,7 @@ Guidelines:
|
|||
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
|
||||
- Generate images only once per human query unless explicitly requested by the user`;
|
||||
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:
|
||||
// 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.
|
||||
|
|
@ -67,7 +61,7 @@ Guidelines:
|
|||
}
|
||||
|
||||
getApiKey() {
|
||||
const apiKey = DALLE2_API_KEY ?? DALLE_API_KEY ?? '';
|
||||
const apiKey = process.env.DALLE2_API_KEY ?? process.env.DALLE_API_KEY ?? '';
|
||||
if (!apiKey) {
|
||||
throw new Error('Missing DALLE_API_KEY environment variable.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
"icon": "https://i.imgur.com/u2TzXzH.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "DALLE2_API_KEY",
|
||||
"authField": "DALLE2_API_KEY||DALLE_API_KEY",
|
||||
"label": "OpenAI API Key",
|
||||
"description": "You can use DALL-E with your API Key from OpenAI."
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
"icon": "https://i.imgur.com/u2TzXzH.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "DALLE3_API_KEY",
|
||||
"authField": "DALLE3_API_KEY||DALLE_API_KEY",
|
||||
"label": "OpenAI API Key",
|
||||
"description": "You can use DALL-E with your API Key from OpenAI."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ const { processFileURL } = require('~/server/services/Files/process');
|
|||
const extractBaseURL = require('~/utils/extractBaseURL');
|
||||
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 {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
|
|
@ -25,19 +17,22 @@ class DALLE3 extends Tool {
|
|||
this.fileStrategy = fields.fileStrategy;
|
||||
let apiKey = fields.DALLE3_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
|
||||
const config = { apiKey };
|
||||
if (DALLE_REVERSE_PROXY) {
|
||||
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY);
|
||||
if (process.env.DALLE_REVERSE_PROXY) {
|
||||
config.baseURL = extractBaseURL(process.env.DALLE_REVERSE_PROXY);
|
||||
}
|
||||
|
||||
if (DALLE3_AZURE_API_VERSION && DALLE3_BASEURL) {
|
||||
config.baseURL = DALLE3_BASEURL;
|
||||
config.defaultQuery = { 'api-version': DALLE3_AZURE_API_VERSION };
|
||||
config.defaultHeaders = { 'api-key': DALLE3_API_KEY, 'Content-Type': 'application/json' };
|
||||
config.apiKey = DALLE3_API_KEY;
|
||||
if (process.env.DALLE3_AZURE_API_VERSION && process.env.DALLE3_BASEURL) {
|
||||
config.baseURL = process.env.DALLE3_BASEURL;
|
||||
config.defaultQuery = { 'api-version': process.env.DALLE3_AZURE_API_VERSION };
|
||||
config.defaultHeaders = {
|
||||
'api-key': process.env.DALLE3_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
config.apiKey = process.env.DALLE3_API_KEY;
|
||||
}
|
||||
|
||||
if (PROXY) {
|
||||
config.httpAgent = new HttpsProxyAgent(PROXY);
|
||||
if (process.env.PROXY) {
|
||||
config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
}
|
||||
|
||||
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.
|
||||
- Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`;
|
||||
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:
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ const getOpenAIKey = async (options, user) => {
|
|||
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 = []) => {
|
||||
try {
|
||||
const validToolsSet = new Set(tools);
|
||||
|
|
@ -37,16 +45,34 @@ const validateTools = async (user, tools = []) => {
|
|||
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 adminAuth = process.env[authField];
|
||||
const fields = authField.split('||');
|
||||
for (const field of fields) {
|
||||
const adminAuth = process.env[field];
|
||||
if (adminAuth && adminAuth.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userAuth = await getUserPluginAuthValue(user, authField);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
validToolsSet.delete(toolName);
|
||||
};
|
||||
|
||||
|
|
@ -63,20 +89,55 @@ const validateTools = async (user, tools = []) => {
|
|||
return Array.from(validToolsSet.values());
|
||||
} catch (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 () {
|
||||
let authValues = {};
|
||||
|
||||
for (const authField of authFields) {
|
||||
let authValue = process.env[authField];
|
||||
if (!authValue) {
|
||||
authValue = await getUserPluginAuthValue(userId, authField);
|
||||
/**
|
||||
* Finds the first non-empty value for the given authentication field, supporting alternate fields.
|
||||
* @param {string[]} fields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
|
||||
* @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 });
|
||||
|
|
@ -194,7 +255,7 @@ const loadTools = async ({
|
|||
|
||||
if (toolConstructors[tool]) {
|
||||
const options = toolOptions[tool] || {};
|
||||
const toolInstance = await loadToolWithAuth(
|
||||
const toolInstance = loadToolWithAuth(
|
||||
user,
|
||||
toolAuthFields[tool],
|
||||
toolConstructors[tool],
|
||||
|
|
@ -250,6 +311,7 @@ const loadTools = async ({
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
loadToolWithAuth,
|
||||
validateTools,
|
||||
loadTools,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,26 +4,33 @@ const mockUser = {
|
|||
findByIdAndDelete: jest.fn(),
|
||||
};
|
||||
|
||||
var mockPluginService = {
|
||||
const mockPluginService = {
|
||||
updateUserPluginAuth: jest.fn(),
|
||||
deleteUserPluginAuth: jest.fn(),
|
||||
getUserPluginAuthValue: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../../../models/User', () => {
|
||||
jest.mock('~/models/User', () => {
|
||||
return function () {
|
||||
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 { 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', () => {
|
||||
let fakeUser;
|
||||
|
|
@ -44,7 +51,10 @@ describe('Tool Handlers', () => {
|
|||
});
|
||||
mockPluginService.updateUserPluginAuth.mockImplementation(
|
||||
(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]];
|
||||
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 () => {
|
||||
expect(loadTool1).toBeDefined();
|
||||
expect(loadTool2).toBeDefined();
|
||||
|
|
@ -150,6 +172,86 @@ describe('Tool Handlers', () => {
|
|||
expect(authTool).toBeInstanceOf(ToolClass);
|
||||
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 () => {
|
||||
try {
|
||||
await loadTool2();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,48 @@
|
|||
const { getUserPluginAuthValue } = require('../../../../server/services/PluginService');
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
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 suite = [];
|
||||
const authValues = {};
|
||||
|
||||
for (const auth of authConfig) {
|
||||
let authValue = process.env[auth.authField];
|
||||
if (!authValue) {
|
||||
authValue = await getUserPluginAuthValue(user, auth.authField);
|
||||
const findAuthValue = async (authField) => {
|
||||
const fields = authField.split('||');
|
||||
for (const field of fields) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tool of tools) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ const { CacheKeys } = require('librechat-data-provider');
|
|||
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
||||
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 seen = new Set();
|
||||
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) => {
|
||||
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return plugin.authConfig.every((authFieldObj) => {
|
||||
const envValue = process.env[authFieldObj.authField];
|
||||
if (envValue === 'user_provided') {
|
||||
return false;
|
||||
const authFieldOptions = authFieldObj.authField.split('||');
|
||||
let isFieldAuthenticated = 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;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TPlugin
|
||||
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TCustomConfig
|
||||
* @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ function PluginAuthForm({ plugin, onSubmit }: TPluginAuthFormProps) {
|
|||
onSubmit({ pluginKey: plugin?.pluginKey ?? '', action: 'install', auth }),
|
||||
)}
|
||||
>
|
||||
{plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => (
|
||||
<div key={`${config.authField}-${i}`} className="flex w-full flex-col gap-1">
|
||||
{plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => {
|
||||
const authField = config.authField.split('||')[0];
|
||||
return (
|
||||
<div key={`${authField}-${i}`} className="flex w-full flex-col gap-1">
|
||||
<label
|
||||
htmlFor={config.authField}
|
||||
htmlFor={authField}
|
||||
className="mb-1 text-left text-sm font-medium text-slate-700/70 dark:text-slate-50/70"
|
||||
>
|
||||
{config.label}
|
||||
|
|
@ -39,12 +41,12 @@ function PluginAuthForm({ plugin, onSubmit }: TPluginAuthFormProps) {
|
|||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
id={config.authField}
|
||||
aria-invalid={!!errors[config.authField]}
|
||||
aria-describedby={`${config.authField}-error`}
|
||||
id={authField}
|
||||
aria-invalid={!!errors[authField]}
|
||||
aria-describedby={`${authField}-error`}
|
||||
aria-label={config.label}
|
||||
aria-required="true"
|
||||
{...register(config.authField, {
|
||||
{...register(authField, {
|
||||
required: `${config.label} is required.`,
|
||||
minLength: {
|
||||
value: 10,
|
||||
|
|
@ -56,14 +58,15 @@ function PluginAuthForm({ plugin, onSubmit }: TPluginAuthFormProps) {
|
|||
</HoverCardTrigger>
|
||||
<PluginTooltip content={config.description} position="right" />
|
||||
</HoverCard>
|
||||
{errors[config.authField] && (
|
||||
{errors[authField] && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-400">
|
||||
{/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */}
|
||||
{errors[config.authField].message}
|
||||
{errors[authField].message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
<button
|
||||
disabled={!isDirty || !isValid || isSubmitting}
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -407,6 +407,8 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
|
|||
|
||||
#### DALL-E:
|
||||
|
||||
**Note:** Make sure the `gptPlugins` endpoint is set in the [`ENDPOINTS`](#endpoints) environment variable if it was configured before.
|
||||
|
||||
**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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue