Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: issue-1028, fetch models when user enters api key #3251

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions api/server/controllers/ModelController.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,10 @@ const getModelsConfig = async (req) => {
* @returns {Promise<TModelsConfig>} The models config.
*/
async function loadModels(req) {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedModelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
if (cachedModelsConfig) {
return cachedModelsConfig;
}
const defaultModelsConfig = await loadDefaultModels(req);
const customModelsConfig = await loadConfigModels(req);

const modelConfig = { ...defaultModelsConfig, ...customModelsConfig };
const modelConfig = { ...(await loadDefaultModels(req)), ...(await loadConfigModels(req)) };
normunds-wipo marked this conversation as resolved.
Show resolved Hide resolved

// caching for other services
const cache = getLogStores(CacheKeys.CONFIG_STORE);
await cache.set(CacheKeys.MODELS_CONFIG, modelConfig);
return modelConfig;
}
Expand Down
19 changes: 16 additions & 3 deletions api/server/services/Config/loadConfigModels.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { EModelEndpoint, extractEnvVariable } = require('librechat-data-provider');
const { fetchModels } = require('~/server/services/ModelService');
const { getUserKeyWithExpiry } = require('../UserService');
const { isUserProvided } = require('~/server/utils');
const getCustomConfig = require('./getCustomConfig');

Expand Down Expand Up @@ -64,12 +65,24 @@ async function loadConfigModels(req) {
const { models, name, baseURL, apiKey } = endpoint;
endpointsMap[name] = endpoint;

const API_KEY = extractEnvVariable(apiKey);
let API_KEY = extractEnvVariable(apiKey);
const BASE_URL = extractEnvVariable(baseURL);

const uniqueKey = `${BASE_URL}__${API_KEY}`;

modelsConfig[name] = [];
/** if key user provided and not expired use it instead of user_defined */
if (models.fetch && isUserProvided(API_KEY)) {
try {
const userKey = await getUserKeyWithExpiry({ userId: req.user.id, name });
if (userKey.expiresAt && new Date(userKey.expiresAt).getTime() > Date.now()) {
// in case key is valid replace the default key with the user provided key
API_KEY = userKey.apiKey || API_KEY;
}
} catch (e) {
// ignore if key is missing or invalid
}
}

const uniqueKey = `${BASE_URL}__${API_KEY}`;

if (models.fetch && !isUserProvided(API_KEY) && !isUserProvided(BASE_URL)) {
fetchPromisesMap[uniqueKey] =
Expand Down
32 changes: 31 additions & 1 deletion api/server/services/UserService.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,36 @@ const getUserKey = async ({ userId, name }) => {
}
return decrypt(keyValue.value);
};

/**
* Retrieves and decrypts the key object including expiry date 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<Record<string,string>>} The decrypted key object.
* @throws {Error} Throws an error if the key is not found, there is a problem during key retrieval, parsing or decryption
* @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 as object including expiry date.
* If no key is found, it throws an error indicating that there is no user key available.
*/
const getUserKeyWithExpiry = async ({ userId, name }) => {
const keyValue = await Key.findOne({ userId, name }).lean();
if (!keyValue) {
throw new Error(
JSON.stringify({
type: ErrorTypes.NO_USER_KEY,
}),
);
}
try {
return { ...JSON.parse(decrypt(keyValue.value)), expiresAt: keyValue.expiresAt };
} catch (e) {
throw new Error(
JSON.stringify({
type: ErrorTypes.INVALID_USER_KEY,
}),
);
}
};
/**
* Retrieves, decrypts, and parses the key values for a given user identified by userId and name.
* @param {Object} params - The parameters object.
Expand Down Expand Up @@ -169,6 +198,7 @@ module.exports = {
deleteUserKey,
getUserKeyValues,
getUserKeyExpiry,
getUserKeyWithExpiry,
checkUserKeyExpiry,
updateUserPluginsService,
};
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const useUpdateUserKeysMutation = (): UseMutationResult<
return useMutation((payload: t.TUpdateUserKeyRequest) => dataService.updateUserKey(payload), {
onSuccess: (data, variables) => {
queryClient.invalidateQueries([QueryKeys.name, variables.name]);
queryClient.invalidateQueries([QueryKeys.models]); // force models reload after user key update
},
});
};
Expand Down
Loading