mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +01:00
🧑💻 refactor: Display Client-facing Errors (#2476)
* fix(Google): allow presets to configure expected maxOutputTokens * refactor: standardize client-facing errors * refactor(checkUserKeyExpiry): pass endpoint instead of custom message * feat(UserService): JSDocs and getUserKeyValues * refactor: add NO_BASE_URL error type, make use of getUserKeyValues, throw user-specific errors * ci: update tests with recent changes
This commit is contained in:
parent
6db91978ca
commit
c937b8cd07
24 changed files with 343 additions and 149 deletions
|
|
@ -117,7 +117,9 @@ const handleAbortError = async (res, req, error, data) => {
|
|||
);
|
||||
}
|
||||
|
||||
const errorText = 'An error occurred while processing your request. Please contact the Admin.';
|
||||
const errorText = error?.message?.includes('"type"')
|
||||
? error.message
|
||||
: 'An error occurred while processing your request. Please contact the Admin.';
|
||||
|
||||
const respondWithError = async (partialText) => {
|
||||
let options = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const axios = require('axios');
|
||||
const { ErrorTypes } = require('librechat-data-provider');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ async function moderateText(req, res, next) {
|
|||
const flagged = results.some((result) => result.flagged);
|
||||
|
||||
if (flagged) {
|
||||
const type = 'moderation';
|
||||
const type = ErrorTypes.MODERATION;
|
||||
const errorMessage = { type };
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { AnthropicClient } = require('~/app');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { AnthropicClient } = require('~/app');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
|
||||
|
|
@ -7,14 +8,15 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
|
||||
|
||||
const anthropicApiKey = isUserProvided
|
||||
? await getAnthropicUserKey(req.user.id)
|
||||
? await getUserKey({ userId: req.user.id, name: EModelEndpoint.anthropic })
|
||||
: ANTHROPIC_API_KEY;
|
||||
|
||||
if (!anthropicApiKey) {
|
||||
throw new Error('Anthropic API key not provided. Please provide it again.');
|
||||
}
|
||||
|
||||
if (expiresAt && isUserProvided) {
|
||||
checkUserKeyExpiry(
|
||||
expiresAt,
|
||||
'Your ANTHROPIC_API_KEY has expired. Please provide your API key again.',
|
||||
);
|
||||
checkUserKeyExpiry(expiresAt, EModelEndpoint.anthropic);
|
||||
}
|
||||
|
||||
const client = new AnthropicClient(anthropicApiKey, {
|
||||
|
|
@ -31,8 +33,4 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getAnthropicUserKey = async (userId) => {
|
||||
return await getUserKey({ userId, name: 'anthropic' });
|
||||
};
|
||||
|
||||
module.exports = initializeClient;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
const OpenAI = require('openai');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const {
|
||||
ErrorTypes,
|
||||
EModelEndpoint,
|
||||
resolveHeaders,
|
||||
mapModelToAzureConfig,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
getUserKey,
|
||||
getUserKeyValues,
|
||||
getUserKeyExpiry,
|
||||
checkUserKeyExpiry,
|
||||
} = require('~/server/services/UserService');
|
||||
|
|
@ -26,18 +27,8 @@ const initializeClient = async ({ req, res, endpointOption, initAppClient = fals
|
|||
userId: req.user.id,
|
||||
name: EModelEndpoint.assistants,
|
||||
});
|
||||
checkUserKeyExpiry(
|
||||
expiresAt,
|
||||
'Your Assistants API key has expired. Please provide your API key again.',
|
||||
);
|
||||
userValues = await getUserKey({ userId: req.user.id, name: EModelEndpoint.assistants });
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
'Invalid JSON provided for Assistants API user values. Please provide them again.',
|
||||
);
|
||||
}
|
||||
checkUserKeyExpiry(expiresAt, EModelEndpoint.assistants);
|
||||
userValues = await getUserKeyValues({ userId: req.user.id, name: EModelEndpoint.assistants });
|
||||
}
|
||||
|
||||
let apiKey = userProvidesKey ? userValues.apiKey : ASSISTANTS_API_KEY;
|
||||
|
|
@ -101,6 +92,14 @@ const initializeClient = async ({ req, res, endpointOption, initAppClient = fals
|
|||
}
|
||||
}
|
||||
|
||||
if (userProvidesKey & !apiKey) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.NO_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('Assistants API key not provided. Please provide it again.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// const OpenAI = require('openai');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { getUserKey, getUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { ErrorTypes } = require('librechat-data-provider');
|
||||
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initializeClient');
|
||||
// const { OpenAIClient } = require('~/app');
|
||||
|
||||
jest.mock('~/server/services/UserService', () => ({
|
||||
getUserKey: jest.fn(),
|
||||
getUserKeyExpiry: jest.fn(),
|
||||
getUserKeyValues: jest.fn(),
|
||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||
}));
|
||||
|
||||
|
|
@ -52,9 +54,7 @@ describe('initializeClient', () => {
|
|||
process.env.ASSISTANTS_API_KEY = 'user_provided';
|
||||
process.env.ASSISTANTS_BASE_URL = 'user_provided';
|
||||
|
||||
getUserKey.mockResolvedValue(
|
||||
JSON.stringify({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' }),
|
||||
);
|
||||
getUserKeyValues.mockResolvedValue({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' });
|
||||
getUserKeyExpiry.mockResolvedValue(isoString);
|
||||
|
||||
const req = { user: { id: 'user123' }, app };
|
||||
|
|
@ -70,11 +70,24 @@ describe('initializeClient', () => {
|
|||
process.env.ASSISTANTS_API_KEY = 'user_provided';
|
||||
getUserKey.mockResolvedValue('invalid-json');
|
||||
getUserKeyExpiry.mockResolvedValue(isoString);
|
||||
getUserKeyValues.mockImplementation(() => {
|
||||
let userValues = getUserKey();
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
});
|
||||
|
||||
const req = { user: { id: 'user123' } };
|
||||
const res = {};
|
||||
|
||||
await expect(initializeClient({ req, res })).rejects.toThrow(/Invalid JSON/);
|
||||
await expect(initializeClient({ req, res })).rejects.toThrow(/invalid_user_key/);
|
||||
});
|
||||
|
||||
test('throws error if API key is not provided', async () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
const {
|
||||
CacheKeys,
|
||||
ErrorTypes,
|
||||
envVarRegex,
|
||||
EModelEndpoint,
|
||||
FetchTokenConfig,
|
||||
extractEnvVariable,
|
||||
} = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
|
||||
const { fetchModels } = require('~/server/services/ModelService');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
|
@ -48,21 +49,29 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
|
||||
let userValues = null;
|
||||
if (expiresAt && (userProvidesKey || userProvidesURL)) {
|
||||
checkUserKeyExpiry(
|
||||
expiresAt,
|
||||
`Your API values for ${endpoint} have expired. Please configure them again.`,
|
||||
);
|
||||
userValues = await getUserKey({ userId: req.user.id, name: endpoint });
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid JSON provided for ${endpoint} user values.`);
|
||||
}
|
||||
checkUserKeyExpiry(expiresAt, endpoint);
|
||||
userValues = await getUserKeyValues({ userId: req.user.id, name: endpoint });
|
||||
}
|
||||
|
||||
let apiKey = userProvidesKey ? userValues?.apiKey : CUSTOM_API_KEY;
|
||||
let baseURL = userProvidesURL ? userValues?.baseURL : CUSTOM_BASE_URL;
|
||||
|
||||
if (userProvidesKey & !apiKey) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.NO_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (userProvidesURL && !baseURL) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.NO_BASE_URL,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`${endpoint} API key not provided.`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { GoogleClient } = require('~/app');
|
||||
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { GoogleClient } = require('~/app');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
const { GOOGLE_KEY, GOOGLE_REVERSE_PROXY, PROXY } = process.env;
|
||||
|
|
@ -9,10 +9,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
|
||||
let userKey = null;
|
||||
if (expiresAt && isUserProvided) {
|
||||
checkUserKeyExpiry(
|
||||
expiresAt,
|
||||
'Your Google Credentials have expired. Please provide your Service Account JSON Key or Generative Language API Key again.',
|
||||
);
|
||||
checkUserKeyExpiry(expiresAt, EModelEndpoint.google);
|
||||
userKey = await getUserKey({ userId: req.user.id, name: EModelEndpoint.google });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets
|
||||
|
||||
const { getUserKey } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initializeClient');
|
||||
const { GoogleClient } = require('~/app');
|
||||
const { checkUserKeyExpiry, getUserKey } = require('../../UserService');
|
||||
|
||||
jest.mock('../../UserService', () => ({
|
||||
checkUserKeyExpiry: jest.fn().mockImplementation((expiresAt, errorMessage) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}),
|
||||
jest.mock('~/server/services/UserService', () => ({
|
||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||
getUserKey: jest.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
|
||||
|
|
@ -74,13 +69,8 @@ describe('google/initializeClient', () => {
|
|||
};
|
||||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'default-model' } };
|
||||
|
||||
checkUserKeyExpiry.mockImplementation((expiresAt, errorMessage) => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/Your Google Credentials have expired/,
|
||||
/expired_user_key/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const {
|
|||
mapModelToAzureConfig,
|
||||
resolveHeaders,
|
||||
} = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { isEnabled, isUserProvided } = require('~/server/utils');
|
||||
const { getAzureCredentials } = require('~/utils');
|
||||
const { PluginsClient } = require('~/app');
|
||||
|
|
@ -49,18 +49,8 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
|
||||
let userValues = null;
|
||||
if (expiresAt && (userProvidesKey || userProvidesURL)) {
|
||||
checkUserKeyExpiry(
|
||||
expiresAt,
|
||||
'Your OpenAI API values have expired. Please provide them again.',
|
||||
);
|
||||
userValues = await getUserKey({ userId: req.user.id, name: endpoint });
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid JSON provided for ${endpoint} user values. Please provide them again.`,
|
||||
);
|
||||
}
|
||||
checkUserKeyExpiry(expiresAt, endpoint);
|
||||
userValues = await getUserKeyValues({ userId: req.user.id, name: endpoint });
|
||||
}
|
||||
|
||||
let apiKey = userProvidesKey ? userValues?.apiKey : credentials[endpoint];
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// gptPlugins/initializeClient.spec.js
|
||||
const { EModelEndpoint, validateAzureGroups } = require('librechat-data-provider');
|
||||
const { getUserKey } = require('~/server/services/UserService');
|
||||
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
|
||||
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initializeClient');
|
||||
const { PluginsClient } = require('~/app');
|
||||
|
||||
// Mock getUserKey since it's the only function we want to mock
|
||||
jest.mock('~/server/services/UserService', () => ({
|
||||
getUserKey: jest.fn(),
|
||||
getUserKeyValues: jest.fn(),
|
||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||
}));
|
||||
|
||||
|
|
@ -205,7 +206,7 @@ describe('gptPlugins/initializeClient', () => {
|
|||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'default-model' } };
|
||||
|
||||
getUserKey.mockResolvedValue(JSON.stringify({ apiKey: 'test-user-provided-openai-api-key' }));
|
||||
getUserKeyValues.mockResolvedValue({ apiKey: 'test-user-provided-openai-api-key' });
|
||||
|
||||
const { openAIApiKey } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
|
|
@ -225,14 +226,12 @@ describe('gptPlugins/initializeClient', () => {
|
|||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'test-model' } };
|
||||
|
||||
getUserKey.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
apiKey: JSON.stringify({
|
||||
azureOpenAIApiKey: 'test-user-provided-azure-api-key',
|
||||
azureOpenAIApiDeploymentName: 'test-deployment',
|
||||
}),
|
||||
getUserKeyValues.mockResolvedValue({
|
||||
apiKey: JSON.stringify({
|
||||
azureOpenAIApiKey: 'test-user-provided-azure-api-key',
|
||||
azureOpenAIApiDeploymentName: 'test-deployment',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const { azure } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
|
|
@ -251,7 +250,9 @@ describe('gptPlugins/initializeClient', () => {
|
|||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'default-model' } };
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(/Your OpenAI API/);
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/expired_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw an error if the user-provided Azure key is invalid JSON', async () => {
|
||||
|
|
@ -268,9 +269,22 @@ describe('gptPlugins/initializeClient', () => {
|
|||
|
||||
// Simulate an invalid JSON string returned from getUserKey
|
||||
getUserKey.mockResolvedValue('invalid-json');
|
||||
getUserKeyValues.mockImplementation(() => {
|
||||
let userValues = getUserKey();
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
});
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/Invalid JSON provided/,
|
||||
/invalid_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -305,9 +319,22 @@ describe('gptPlugins/initializeClient', () => {
|
|||
|
||||
// Mock getUserKey to return a non-JSON string
|
||||
getUserKey.mockResolvedValue('not-a-json');
|
||||
getUserKeyValues.mockImplementation(() => {
|
||||
let userValues = getUserKey();
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
});
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/Invalid JSON provided for openAI user values/,
|
||||
/invalid_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -369,9 +396,10 @@ describe('gptPlugins/initializeClient', () => {
|
|||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
getUserKey.mockResolvedValue(
|
||||
JSON.stringify({ apiKey: 'test', baseURL: 'https://user-provided-url.com' }),
|
||||
);
|
||||
getUserKeyValues.mockResolvedValue({
|
||||
apiKey: 'test',
|
||||
baseURL: 'https://user-provided-url.com',
|
||||
});
|
||||
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
const {
|
||||
ErrorTypes,
|
||||
EModelEndpoint,
|
||||
resolveHeaders,
|
||||
mapModelToAzureConfig,
|
||||
} = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { isEnabled, isUserProvided } = require('~/server/utils');
|
||||
const { getAzureCredentials } = require('~/utils');
|
||||
const { OpenAIClient } = require('~/app');
|
||||
|
|
@ -36,18 +37,8 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
|
||||
let userValues = null;
|
||||
if (expiresAt && (userProvidesKey || userProvidesURL)) {
|
||||
checkUserKeyExpiry(
|
||||
expiresAt,
|
||||
'Your OpenAI API values have expired. Please provide them again.',
|
||||
);
|
||||
userValues = await getUserKey({ userId: req.user.id, name: endpoint });
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid JSON provided for ${endpoint} user values. Please provide them again.`,
|
||||
);
|
||||
}
|
||||
checkUserKeyExpiry(expiresAt, endpoint);
|
||||
userValues = await getUserKeyValues({ userId: req.user.id, name: endpoint });
|
||||
}
|
||||
|
||||
let apiKey = userProvidesKey ? userValues?.apiKey : credentials[endpoint];
|
||||
|
|
@ -99,8 +90,16 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
apiKey = clientOptions.azure.azureOpenAIApiKey;
|
||||
}
|
||||
|
||||
if (userProvidesKey & !apiKey) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.NO_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`${endpoint} API key not provided. Please provide it again.`);
|
||||
throw new Error(`${endpoint} API Key not provided.`);
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
const { EModelEndpoint, validateAzureGroups } = require('librechat-data-provider');
|
||||
const { getUserKey } = require('~/server/services/UserService');
|
||||
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
|
||||
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initializeClient');
|
||||
const { OpenAIClient } = require('~/app');
|
||||
|
||||
// Mock getUserKey since it's the only function we want to mock
|
||||
jest.mock('~/server/services/UserService', () => ({
|
||||
getUserKey: jest.fn(),
|
||||
getUserKeyValues: jest.fn(),
|
||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||
}));
|
||||
|
||||
|
|
@ -200,7 +201,9 @@ describe('initializeClient', () => {
|
|||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(/Your OpenAI API/);
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/expired_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw an error if no API keys are provided in the environment', async () => {
|
||||
|
|
@ -217,7 +220,7 @@ describe('initializeClient', () => {
|
|||
const endpointOption = {};
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
`${EModelEndpoint.openAI} API key not provided.`,
|
||||
`${EModelEndpoint.openAI} API Key not provided.`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -241,7 +244,7 @@ describe('initializeClient', () => {
|
|||
process.env.OPENAI_API_KEY = 'user_provided';
|
||||
|
||||
// Mock getUserKey to return the expected key
|
||||
getUserKey.mockResolvedValue(JSON.stringify({ apiKey: 'test-user-provided-openai-api-key' }));
|
||||
getUserKeyValues.mockResolvedValue({ apiKey: 'test-user-provided-openai-api-key' });
|
||||
|
||||
// Call the initializeClient function
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
|
|
@ -266,7 +269,9 @@ describe('initializeClient', () => {
|
|||
// Mock getUserKey to return an invalid key
|
||||
getUserKey.mockResolvedValue(invalidKey);
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(/Your OpenAI API/);
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/expired_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw an error when user-provided values are not valid JSON', async () => {
|
||||
|
|
@ -281,9 +286,22 @@ describe('initializeClient', () => {
|
|||
|
||||
// Mock getUserKey to return a non-JSON string
|
||||
getUserKey.mockResolvedValue('not-a-json');
|
||||
getUserKeyValues.mockImplementation(() => {
|
||||
let userValues = getUserKey();
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
});
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/Invalid JSON provided for openAI user values/,
|
||||
/invalid_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -347,9 +365,10 @@ describe('initializeClient', () => {
|
|||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
getUserKey.mockResolvedValue(
|
||||
JSON.stringify({ apiKey: 'test', baseURL: 'https://user-provided-url.com' }),
|
||||
);
|
||||
getUserKeyValues.mockResolvedValue({
|
||||
apiKey: 'test',
|
||||
baseURL: 'https://user-provided-url.com',
|
||||
});
|
||||
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
const { User, Key } = require('~/models');
|
||||
const { ErrorTypes } = require('librechat-data-provider');
|
||||
const { encrypt, decrypt } = require('~/server/utils');
|
||||
const { User, Key } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Updates the plugins for a user based on the action specified (install/uninstall).
|
||||
* @async
|
||||
* @param {Object} user - The user whose plugins are to be updated.
|
||||
* @param {string} pluginKey - The key of the plugin to install or uninstall.
|
||||
* @param {'install' | 'uninstall'} action - The action to perform, 'install' or 'uninstall'.
|
||||
* @returns {Promise<Object>} The result of the update operation.
|
||||
* @throws Logs the error internally if the update operation fails.
|
||||
* @description This function updates the plugin array of a user document based on the specified action.
|
||||
* It adds a plugin key to the plugins array for an 'install' action, and removes it for an 'uninstall' action.
|
||||
*/
|
||||
const updateUserPluginsService = async (user, pluginKey, action) => {
|
||||
try {
|
||||
if (action === 'install') {
|
||||
|
|
@ -21,14 +33,64 @@ const updateUserPluginsService = async (user, pluginKey, action) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves and decrypts the key value for a given user identified by userId and identifier name.
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The unique identifier for the user.
|
||||
* @param {string} params.name - The name associated with the key.
|
||||
* @returns {Promise<string>} The decrypted key value.
|
||||
* @throws {Error} Throws an error if the key is not found or if there is a problem during key retrieval.
|
||||
* @description This function searches for a user's key in the database using their userId and name.
|
||||
* If found, it decrypts the value of the key and returns it. If no key is found, it throws
|
||||
* an error indicating that there is no user key available.
|
||||
*/
|
||||
const getUserKey = async ({ userId, name }) => {
|
||||
const keyValue = await Key.findOne({ userId, name }).lean();
|
||||
if (!keyValue) {
|
||||
throw new Error('User-provided key not found');
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.NO_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return decrypt(keyValue.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves, decrypts, and parses the key values for a given user identified by userId and name.
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The unique identifier for the user.
|
||||
* @param {string} params.name - The name associated with the key.
|
||||
* @returns {Promise<Record<string,string>>} The decrypted and parsed key values.
|
||||
* @throws {Error} Throws an error if the key is invalid or if there is a problem during key value parsing.
|
||||
* @description This function retrieves a user's encrypted key using their userId and name, decrypts it,
|
||||
* and then attempts to parse the decrypted string into a JSON object. If the parsing fails,
|
||||
* it throws an error indicating that the user key is invalid.
|
||||
*/
|
||||
const getUserKeyValues = async ({ userId, name }) => {
|
||||
let userValues = await getUserKey({ userId, name });
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expiry information of a user's key identified by userId and name.
|
||||
* @async
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The unique identifier for the user.
|
||||
* @param {string} params.name - The name associated with the key.
|
||||
* @returns {Promise<{expiresAt: Date | null}>} The expiry date of the key or null if the key doesn't exist.
|
||||
* @description This function fetches a user's key from the database using their userId and name and
|
||||
* returns its expiry date. If the key is not found, it returns null for the expiry date.
|
||||
*/
|
||||
const getUserKeyExpiry = async ({ userId, name }) => {
|
||||
const keyValue = await Key.findOne({ userId, name }).lean();
|
||||
if (!keyValue) {
|
||||
|
|
@ -37,6 +99,18 @@ const getUserKeyExpiry = async ({ userId, name }) => {
|
|||
return { expiresAt: keyValue.expiresAt };
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates or inserts a new key for a given user identified by userId and name, with a specified value and expiry date.
|
||||
* @async
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The unique identifier for the user.
|
||||
* @param {string} params.name - The name associated with the key.
|
||||
* @param {string} params.value - The value to be encrypted and stored as the key's value.
|
||||
* @param {Date} params.expiresAt - The expiry date for the key.
|
||||
* @returns {Promise<Object>} The updated or newly inserted key document.
|
||||
* @description This function either updates an existing user key or inserts a new one into the database,
|
||||
* after encrypting the provided value. It sets the provided expiry date for the key.
|
||||
*/
|
||||
const updateUserKey = async ({ userId, name, value, expiresAt }) => {
|
||||
const encryptedValue = encrypt(value);
|
||||
return await Key.findOneAndUpdate(
|
||||
|
|
@ -51,6 +125,18 @@ const updateUserKey = async ({ userId, name, value, expiresAt }) => {
|
|||
).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a key or all keys for a given user identified by userId, optionally based on a specified name.
|
||||
* @async
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The unique identifier for the user.
|
||||
* @param {string} [params.name] - The name associated with the key to delete. If not provided and all is true, deletes all keys.
|
||||
* @param {boolean} [params.all=false] - Whether to delete all keys for the user.
|
||||
* @returns {Promise<Object>} The result of the deletion operation.
|
||||
* @description This function deletes a specific key or all keys for a user from the database.
|
||||
* If a name is provided and all is false, it deletes only the key with that name.
|
||||
* If all is true, it ignores the name and deletes all keys for the user.
|
||||
*/
|
||||
const deleteUserKey = async ({ userId, name, all = false }) => {
|
||||
if (all) {
|
||||
return await Key.deleteMany({ userId });
|
||||
|
|
@ -59,11 +145,23 @@ const deleteUserKey = async ({ userId, name, all = false }) => {
|
|||
await Key.findOneAndDelete({ userId, name }).lean();
|
||||
};
|
||||
|
||||
const checkUserKeyExpiry = (expiresAt, message) => {
|
||||
/**
|
||||
* Checks if a user key has expired based on the provided expiration date and endpoint.
|
||||
* If the key has expired, it throws an Error with details including the type of error, the expiration date, and the endpoint.
|
||||
*
|
||||
* @param {string} expiresAt - The expiration date of the user key in a format that can be parsed by the Date constructor.
|
||||
* @param {string} endpoint - The endpoint associated with the user key to be checked.
|
||||
* @throws {Error} Throws an error if the user key has expired. The error message is a stringified JSON object
|
||||
* containing the type of error (`ErrorTypes.EXPIRED_USER_KEY`), the expiration date in the local string format, and the endpoint.
|
||||
*/
|
||||
const checkUserKeyExpiry = (expiresAt, endpoint) => {
|
||||
const expiresAtDate = new Date(expiresAt);
|
||||
if (expiresAtDate < new Date()) {
|
||||
const expiryStr = `User-provided key expired at ${expiresAtDate.toLocaleString()}`;
|
||||
const errorMessage = message ? `${message}\n${expiryStr}` : expiryStr;
|
||||
const errorMessage = JSON.stringify({
|
||||
type: ErrorTypes.EXPIRED_USER_KEY,
|
||||
expiredAt: expiresAtDate.toLocaleString(),
|
||||
endpoint,
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
|
@ -71,6 +169,7 @@ const checkUserKeyExpiry = (expiresAt, message) => {
|
|||
module.exports = {
|
||||
updateUserPluginsService,
|
||||
getUserKey,
|
||||
getUserKeyValues,
|
||||
getUserKeyExpiry,
|
||||
updateUserKey,
|
||||
deleteUserKey,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue