mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🪦 refactor: Remove Legacy Code (#10533)
* 🗑️ chore: Remove unused Legacy Provider clients and related helpers * Deleted OpenAIClient and GoogleClient files along with their associated tests. * Removed references to these clients in the clients index file. * Cleaned up typedefs by removing the OpenAISpecClient export. * Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes. * chore/remove-openapi-specs * 🗑️ chore: Remove unused mergeSort and misc utility functions * Deleted mergeSort.js and misc.js files as they are no longer needed. * Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic. * Updated mongoMeili.ts to eliminate local implementations of removed functions. * chore: remove legacy endpoints * chore: remove all plugins endpoint related code * chore: remove unused prompt handling code and clean up imports * Deleted handleInputs.js and instructions.js files as they are no longer needed. * Removed references to these files in the prompts index.js. * Updated docker-compose.yml to simplify reverse proxy configuration. * chore: remove unused LightningIcon import from Icons.tsx * chore: clean up translation.json by removing deprecated and unused keys * chore: update Jest configuration and remove unused mock file * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock. * Deleted the fetchEventSource.js mock file as it is no longer needed. * fix: simplify endpoint type check in Landing and ConversationStarters components * Updated the endpoint type check to use strict equality for better clarity and performance. * Ensured consistency in the handling of the azureOpenAI endpoint across both components. * chore: remove unused dependencies from package.json and package-lock.json * chore: remove legacy EditController, associated routes and imports * chore: update banResponse logic to refine request handling for banned users * chore: remove unused validateEndpoint middleware and its references * chore: remove unused 'res' parameter from initializeClient in multiple endpoint files * chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions * chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import * chore: remove unused TResPlugin type and related references; clean up imports in types and schemas
This commit is contained in:
parent
c901e25ce1
commit
59c589114b
161 changed files with 256 additions and 10511 deletions
|
|
@ -20,8 +20,7 @@ services:
|
|||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- MONGO_URI=mongodb://mongodb:27017/LibreChat
|
||||
# - CHATGPT_REVERSE_PROXY=http://host.docker.internal:8080/api/conversation # if you are hosting your own chatgpt reverse proxy with docker
|
||||
# - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1/chat/completions # if you are hosting your own chatgpt reverse proxy with docker
|
||||
# - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
|
||||
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ ANTHROPIC_API_KEY=user_provided
|
|||
# AZURE_OPENAI_API_VERSION= # Deprecated
|
||||
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated
|
||||
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated
|
||||
# PLUGINS_USE_AZURE="true" # Deprecated
|
||||
|
||||
#=================#
|
||||
# AWS Bedrock #
|
||||
|
|
@ -230,14 +229,6 @@ ASSISTANTS_API_KEY=user_provided
|
|||
# More info, including how to enable use of Assistants with Azure here:
|
||||
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure
|
||||
|
||||
#============#
|
||||
# Plugins #
|
||||
#============#
|
||||
|
||||
# PLUGIN_MODELS=gpt-4o,gpt-4o-mini,gpt-4,gpt-4-turbo-preview,gpt-4-0125-preview,gpt-4-1106-preview,gpt-4-0613,gpt-3.5-turbo,gpt-3.5-turbo-0125,gpt-3.5-turbo-1106,gpt-3.5-turbo-0613
|
||||
|
||||
DEBUG_PLUGINS=true
|
||||
|
||||
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
|
||||
CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
|
||||
|
||||
|
|
|
|||
|
|
@ -1,991 +0,0 @@
|
|||
const Anthropic = require('@anthropic-ai/sdk');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const {
|
||||
Constants,
|
||||
ErrorTypes,
|
||||
EModelEndpoint,
|
||||
parseTextParts,
|
||||
anthropicSettings,
|
||||
getResponseSender,
|
||||
validateVisionModel,
|
||||
} = require('librechat-data-provider');
|
||||
const { sleep, SplitStreamHandler: _Handler, addCacheControl } = require('@librechat/agents');
|
||||
const {
|
||||
Tokenizer,
|
||||
createFetch,
|
||||
matchModelName,
|
||||
getClaudeHeaders,
|
||||
getModelMaxTokens,
|
||||
configureReasoning,
|
||||
checkPromptCacheSupport,
|
||||
getModelMaxOutputTokens,
|
||||
createStreamEventHandlers,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
truncateText,
|
||||
formatMessage,
|
||||
titleFunctionPrompt,
|
||||
parseParamFromPrompt,
|
||||
createContextHandlers,
|
||||
} = require('./prompts');
|
||||
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const BaseClient = require('./BaseClient');
|
||||
|
||||
const HUMAN_PROMPT = '\n\nHuman:';
|
||||
const AI_PROMPT = '\n\nAssistant:';
|
||||
|
||||
class SplitStreamHandler extends _Handler {
|
||||
getDeltaContent(chunk) {
|
||||
return (chunk?.delta?.text ?? chunk?.completion) || '';
|
||||
}
|
||||
getReasoningDelta(chunk) {
|
||||
return chunk?.delta?.thinking || '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to introduce a delay before retrying */
|
||||
function delayBeforeRetry(attempts, baseDelay = 1000) {
|
||||
return new Promise((resolve) => setTimeout(resolve, baseDelay * attempts));
|
||||
}
|
||||
|
||||
const tokenEventTypes = new Set(['message_start', 'message_delta']);
|
||||
const { legacy } = anthropicSettings;
|
||||
|
||||
class AnthropicClient extends BaseClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
super(apiKey, options);
|
||||
this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY;
|
||||
this.userLabel = HUMAN_PROMPT;
|
||||
this.assistantLabel = AI_PROMPT;
|
||||
this.contextStrategy = options.contextStrategy
|
||||
? options.contextStrategy.toLowerCase()
|
||||
: 'discard';
|
||||
this.setOptions(options);
|
||||
/** @type {string | undefined} */
|
||||
this.systemMessage;
|
||||
/** @type {AnthropicMessageStartEvent| undefined} */
|
||||
this.message_start;
|
||||
/** @type {AnthropicMessageDeltaEvent| undefined} */
|
||||
this.message_delta;
|
||||
/** Whether the model is part of the Claude 3 Family
|
||||
* @type {boolean} */
|
||||
this.isClaudeLatest;
|
||||
/** Whether to use Messages API or Completions API
|
||||
* @type {boolean} */
|
||||
this.useMessages;
|
||||
/** Whether or not the model supports Prompt Caching
|
||||
* @type {boolean} */
|
||||
this.supportsCacheControl;
|
||||
/** The key for the usage object's input tokens
|
||||
* @type {string} */
|
||||
this.inputTokensKey = 'input_tokens';
|
||||
/** The key for the usage object's output tokens
|
||||
* @type {string} */
|
||||
this.outputTokensKey = 'output_tokens';
|
||||
/** @type {SplitStreamHandler | undefined} */
|
||||
this.streamHandler;
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
this.modelOptions = Object.assign(
|
||||
{
|
||||
model: anthropicSettings.model.default,
|
||||
},
|
||||
this.modelOptions,
|
||||
this.options.modelOptions,
|
||||
);
|
||||
|
||||
const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic);
|
||||
this.isClaudeLatest =
|
||||
/claude-[3-9]/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch);
|
||||
const isLegacyOutput = !(
|
||||
/claude-3[-.]5-sonnet/.test(modelMatch) ||
|
||||
/claude-3[-.]7/.test(modelMatch) ||
|
||||
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch) ||
|
||||
/claude-[4-9]/.test(modelMatch)
|
||||
);
|
||||
this.supportsCacheControl = this.options.promptCache && checkPromptCacheSupport(modelMatch);
|
||||
|
||||
if (
|
||||
isLegacyOutput &&
|
||||
this.modelOptions.maxOutputTokens &&
|
||||
this.modelOptions.maxOutputTokens > legacy.maxOutputTokens.default
|
||||
) {
|
||||
this.modelOptions.maxOutputTokens = legacy.maxOutputTokens.default;
|
||||
}
|
||||
|
||||
this.useMessages = this.isClaudeLatest || !!this.options.attachments;
|
||||
|
||||
this.defaultVisionModel = this.options.visionModel ?? 'claude-3-sonnet-20240229';
|
||||
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
|
||||
|
||||
this.maxContextTokens =
|
||||
this.options.maxContextTokens ??
|
||||
getModelMaxTokens(this.modelOptions.model, EModelEndpoint.anthropic) ??
|
||||
100000;
|
||||
this.maxResponseTokens =
|
||||
this.modelOptions.maxOutputTokens ??
|
||||
getModelMaxOutputTokens(
|
||||
this.modelOptions.model,
|
||||
this.options.endpointType ?? this.options.endpoint,
|
||||
this.options.endpointTokenConfig,
|
||||
) ??
|
||||
anthropicSettings.maxOutputTokens.reset(this.modelOptions.model);
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
const reservedTokens = this.maxPromptTokens + this.maxResponseTokens;
|
||||
if (reservedTokens > this.maxContextTokens) {
|
||||
const info = `Total Possible Tokens + Max Output Tokens must be less than or equal to Max Context Tokens: ${this.maxPromptTokens} (total possible output) + ${this.maxResponseTokens} (max output) = ${reservedTokens}/${this.maxContextTokens} (max context)`;
|
||||
const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`;
|
||||
logger.warn(info);
|
||||
throw new Error(errorMessage);
|
||||
} else if (this.maxResponseTokens === this.maxContextTokens) {
|
||||
const info = `Max Output Tokens must be less than Max Context Tokens: ${this.maxResponseTokens} (max output) = ${this.maxContextTokens} (max context)`;
|
||||
const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`;
|
||||
logger.warn(info);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
this.sender =
|
||||
this.options.sender ??
|
||||
getResponseSender({
|
||||
model: this.modelOptions.model,
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
modelLabel: this.options.modelLabel,
|
||||
});
|
||||
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initialized Anthropic client.
|
||||
* @param {Partial<Anthropic.ClientOptions>} requestOptions - The options for the client.
|
||||
* @returns {Anthropic} The Anthropic client instance.
|
||||
*/
|
||||
getClient(requestOptions) {
|
||||
/** @type {Anthropic.ClientOptions} */
|
||||
const options = {
|
||||
fetch: createFetch({
|
||||
directEndpoint: this.options.directEndpoint,
|
||||
reverseProxyUrl: this.options.reverseProxyUrl,
|
||||
}),
|
||||
apiKey: this.apiKey,
|
||||
fetchOptions: {},
|
||||
};
|
||||
|
||||
if (this.options.proxy) {
|
||||
options.fetchOptions.agent = new HttpsProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
options.baseURL = this.options.reverseProxyUrl;
|
||||
}
|
||||
|
||||
const headers = getClaudeHeaders(requestOptions?.model, this.supportsCacheControl);
|
||||
if (headers) {
|
||||
options.defaultHeaders = headers;
|
||||
}
|
||||
|
||||
return new Anthropic(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream usage as returned by this client's API response.
|
||||
* @returns {AnthropicStreamUsage} The stream usage object.
|
||||
*/
|
||||
getStreamUsage() {
|
||||
const inputUsage = this.message_start?.message?.usage ?? {};
|
||||
const outputUsage = this.message_delta?.usage ?? {};
|
||||
return Object.assign({}, inputUsage, outputUsage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the correct token count for the current user message based on the token count map and API usage.
|
||||
* Edge case: If the calculation results in a negative value, it returns the original estimate.
|
||||
* If revisiting a conversation with a chat history entirely composed of token estimates,
|
||||
* the cumulative token count going forward should become more accurate as the conversation progresses.
|
||||
* @param {Object} params - The parameters for the calculation.
|
||||
* @param {Record<string, number>} params.tokenCountMap - A map of message IDs to their token counts.
|
||||
* @param {string} params.currentMessageId - The ID of the current message to calculate.
|
||||
* @param {AnthropicStreamUsage} params.usage - The usage object returned by the API.
|
||||
* @returns {number} The correct token count for the current user message.
|
||||
*/
|
||||
calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage }) {
|
||||
const originalEstimate = tokenCountMap[currentMessageId] || 0;
|
||||
|
||||
if (!usage || typeof usage.input_tokens !== 'number') {
|
||||
return originalEstimate;
|
||||
}
|
||||
|
||||
tokenCountMap[currentMessageId] = 0;
|
||||
const totalTokensFromMap = Object.values(tokenCountMap).reduce((sum, count) => {
|
||||
const numCount = Number(count);
|
||||
return sum + (isNaN(numCount) ? 0 : numCount);
|
||||
}, 0);
|
||||
const totalInputTokens =
|
||||
(usage.input_tokens ?? 0) +
|
||||
(usage.cache_creation_input_tokens ?? 0) +
|
||||
(usage.cache_read_input_tokens ?? 0);
|
||||
|
||||
const currentMessageTokens = totalInputTokens - totalTokensFromMap;
|
||||
return currentMessageTokens > 0 ? currentMessageTokens : originalEstimate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Token Count for LibreChat Message
|
||||
* @param {TMessage} responseMessage
|
||||
* @returns {number}
|
||||
*/
|
||||
getTokenCountForResponse(responseMessage) {
|
||||
return this.getTokenCountForMessage({
|
||||
role: 'assistant',
|
||||
content: responseMessage.text,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks if the model is a vision model based on request attachments and sets the appropriate options:
|
||||
* - Sets `this.modelOptions.model` to `gpt-4-vision-preview` if the request is a vision request.
|
||||
* - Sets `this.isVisionModel` to `true` if vision request.
|
||||
* - Deletes `this.modelOptions.stop` if vision request.
|
||||
* @param {MongoFile[]} attachments
|
||||
*/
|
||||
checkVisionRequest(attachments) {
|
||||
const availableModels = this.options.modelsConfig?.[EModelEndpoint.anthropic];
|
||||
this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
||||
|
||||
const visionModelAvailable = availableModels?.includes(this.defaultVisionModel);
|
||||
if (
|
||||
attachments &&
|
||||
attachments.some((file) => file?.type && file?.type?.includes('image')) &&
|
||||
visionModelAvailable &&
|
||||
!this.isVisionModel
|
||||
) {
|
||||
this.modelOptions.model = this.defaultVisionModel;
|
||||
this.isVisionModel = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the token cost in tokens for an image based on its dimensions and detail level.
|
||||
*
|
||||
* For reference, see: https://docs.anthropic.com/claude/docs/vision#image-costs
|
||||
*
|
||||
* @param {Object} image - The image object.
|
||||
* @param {number} image.width - The width of the image.
|
||||
* @param {number} image.height - The height of the image.
|
||||
* @returns {number} The calculated token cost measured by tokens.
|
||||
*
|
||||
*/
|
||||
calculateImageTokenCost({ width, height }) {
|
||||
return Math.ceil((width * height) / 750);
|
||||
}
|
||||
|
||||
async addImageURLs(message, attachments) {
|
||||
const { files, image_urls } = await encodeAndFormat(this.options.req, attachments, {
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
});
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {number} params.promptTokens
|
||||
* @param {number} params.completionTokens
|
||||
* @param {AnthropicStreamUsage} [params.usage]
|
||||
* @param {string} [params.model]
|
||||
* @param {string} [params.context='message']
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async recordTokenUsage({ promptTokens, completionTokens, usage, model, context = 'message' }) {
|
||||
if (usage != null && usage?.input_tokens != null) {
|
||||
const input = usage.input_tokens ?? 0;
|
||||
const write = usage.cache_creation_input_tokens ?? 0;
|
||||
const read = usage.cache_read_input_tokens ?? 0;
|
||||
|
||||
await spendStructuredTokens(
|
||||
{
|
||||
context,
|
||||
user: this.user,
|
||||
conversationId: this.conversationId,
|
||||
model: model ?? this.modelOptions.model,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
{
|
||||
promptTokens: { input, write, read },
|
||||
completionTokens,
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await spendTokens(
|
||||
{
|
||||
context,
|
||||
user: this.user,
|
||||
conversationId: this.conversationId,
|
||||
model: model ?? this.modelOptions.model,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
{ promptTokens, completionTokens },
|
||||
);
|
||||
}
|
||||
|
||||
async buildMessages(messages, parentMessageId) {
|
||||
const orderedMessages = this.constructor.getMessagesForConversation({
|
||||
messages,
|
||||
parentMessageId,
|
||||
});
|
||||
|
||||
logger.debug('[AnthropicClient] orderedMessages', { orderedMessages, parentMessageId });
|
||||
|
||||
if (this.options.attachments) {
|
||||
const attachments = await this.options.attachments;
|
||||
const images = attachments.filter((file) => file.type.includes('image'));
|
||||
|
||||
if (images.length && !this.isVisionModel) {
|
||||
throw new Error('Images are only supported with the Claude 3 family of models');
|
||||
}
|
||||
|
||||
const latestMessage = orderedMessages[orderedMessages.length - 1];
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.message_file_map[latestMessage.messageId] = attachments;
|
||||
} else {
|
||||
this.message_file_map = {
|
||||
[latestMessage.messageId]: attachments,
|
||||
};
|
||||
}
|
||||
|
||||
const files = await this.addImageURLs(latestMessage, attachments);
|
||||
|
||||
this.options.attachments = files;
|
||||
}
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.contextHandlers = createContextHandlers(
|
||||
this.options.req,
|
||||
orderedMessages[orderedMessages.length - 1].text,
|
||||
);
|
||||
}
|
||||
|
||||
const formattedMessages = orderedMessages.map((message, i) => {
|
||||
const formattedMessage = this.useMessages
|
||||
? formatMessage({
|
||||
message,
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
})
|
||||
: {
|
||||
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
||||
content: message?.content ?? message.text,
|
||||
};
|
||||
|
||||
const needsTokenCount = this.contextStrategy && !orderedMessages[i].tokenCount;
|
||||
/* If tokens were never counted, or, is a Vision request and the message has files, count again */
|
||||
if (needsTokenCount || (this.isVisionModel && (message.image_urls || message.files))) {
|
||||
orderedMessages[i].tokenCount = this.getTokenCountForMessage(formattedMessage);
|
||||
}
|
||||
|
||||
/* If message has files, calculate image token cost */
|
||||
if (this.message_file_map && this.message_file_map[message.messageId]) {
|
||||
const attachments = this.message_file_map[message.messageId];
|
||||
for (const file of attachments) {
|
||||
if (file.embedded) {
|
||||
this.contextHandlers?.processFile(file);
|
||||
continue;
|
||||
}
|
||||
if (file.metadata?.fileIdentifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
orderedMessages[i].tokenCount += this.calculateImageTokenCost({
|
||||
width: file.width,
|
||||
height: file.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formattedMessage.tokenCount = orderedMessages[i].tokenCount;
|
||||
return formattedMessage;
|
||||
});
|
||||
|
||||
if (this.contextHandlers) {
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
this.options.promptPrefix = this.augmentedPrompt + (this.options.promptPrefix ?? '');
|
||||
}
|
||||
|
||||
let { context: messagesInWindow, remainingContextTokens } =
|
||||
await this.getMessagesWithinTokenLimit({ messages: formattedMessages });
|
||||
|
||||
const tokenCountMap = orderedMessages
|
||||
.slice(orderedMessages.length - messagesInWindow.length)
|
||||
.reduce((map, message, index) => {
|
||||
const { messageId } = message;
|
||||
if (!messageId) {
|
||||
return map;
|
||||
}
|
||||
|
||||
map[messageId] = orderedMessages[index].tokenCount;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
logger.debug('[AnthropicClient]', {
|
||||
messagesInWindow: messagesInWindow.length,
|
||||
remainingContextTokens,
|
||||
});
|
||||
|
||||
let lastAuthor = '';
|
||||
let groupedMessages = [];
|
||||
|
||||
for (let i = 0; i < messagesInWindow.length; i++) {
|
||||
const message = messagesInWindow[i];
|
||||
const author = message.role ?? message.author;
|
||||
// If last author is not same as current author, add to new group
|
||||
if (lastAuthor !== author) {
|
||||
const newMessage = {
|
||||
content: [message.content],
|
||||
};
|
||||
|
||||
if (message.role) {
|
||||
newMessage.role = message.role;
|
||||
} else {
|
||||
newMessage.author = message.author;
|
||||
}
|
||||
|
||||
groupedMessages.push(newMessage);
|
||||
lastAuthor = author;
|
||||
// If same author, append content to the last group
|
||||
} else {
|
||||
groupedMessages[groupedMessages.length - 1].content.push(message.content);
|
||||
}
|
||||
}
|
||||
|
||||
groupedMessages = groupedMessages.map((msg, i) => {
|
||||
const isLast = i === groupedMessages.length - 1;
|
||||
if (msg.content.length === 1) {
|
||||
const content = msg.content[0];
|
||||
return {
|
||||
...msg,
|
||||
// reason: final assistant content cannot end with trailing whitespace
|
||||
content:
|
||||
isLast && this.useMessages && msg.role === 'assistant' && typeof content === 'string'
|
||||
? content?.trim()
|
||||
: content,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.useMessages && msg.tokenCount) {
|
||||
delete msg.tokenCount;
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
let identityPrefix = '';
|
||||
if (this.options.userLabel) {
|
||||
identityPrefix = `\nHuman's name: ${this.options.userLabel}`;
|
||||
}
|
||||
|
||||
if (this.options.modelLabel) {
|
||||
identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`;
|
||||
}
|
||||
|
||||
let promptPrefix = (this.options.promptPrefix ?? '').trim();
|
||||
if (typeof this.options.artifactsPrompt === 'string' && this.options.artifactsPrompt) {
|
||||
promptPrefix = `${promptPrefix ?? ''}\n${this.options.artifactsPrompt}`.trim();
|
||||
}
|
||||
if (promptPrefix) {
|
||||
// If the prompt prefix doesn't end with the end token, add it.
|
||||
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
||||
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
||||
}
|
||||
promptPrefix = `\nContext:\n${promptPrefix}`;
|
||||
}
|
||||
|
||||
if (identityPrefix) {
|
||||
promptPrefix = `${identityPrefix}${promptPrefix}`;
|
||||
}
|
||||
|
||||
// Prompt AI to respond, empty if last message was from AI
|
||||
let isEdited = lastAuthor === this.assistantLabel;
|
||||
const promptSuffix = isEdited ? '' : `${promptPrefix}${this.assistantLabel}\n`;
|
||||
let currentTokenCount =
|
||||
isEdited || this.useMessages
|
||||
? this.getTokenCount(promptPrefix)
|
||||
: this.getTokenCount(promptSuffix);
|
||||
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
|
||||
const context = [];
|
||||
|
||||
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
||||
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
||||
// Also, remove the next message when the message that puts us over the token limit is created by the user.
|
||||
// Otherwise, remove only the exceeding message. This is due to Anthropic's strict payload rule to start with "Human:".
|
||||
const nextMessage = {
|
||||
remove: false,
|
||||
tokenCount: 0,
|
||||
messageString: '',
|
||||
};
|
||||
|
||||
const buildPromptBody = async () => {
|
||||
if (currentTokenCount < maxTokenCount && groupedMessages.length > 0) {
|
||||
const message = groupedMessages.pop();
|
||||
const isCreatedByUser = message.author === this.userLabel;
|
||||
// Use promptPrefix if message is edited assistant'
|
||||
const messagePrefix =
|
||||
isCreatedByUser || !isEdited ? message.author : `${promptPrefix}${message.author}`;
|
||||
const messageString = `${messagePrefix}\n${message.content}${this.endToken}\n`;
|
||||
let newPromptBody = `${messageString}${promptBody}`;
|
||||
|
||||
context.unshift(message);
|
||||
|
||||
const tokenCountForMessage = this.getTokenCount(messageString);
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
|
||||
if (!isCreatedByUser) {
|
||||
nextMessage.messageString = messageString;
|
||||
nextMessage.tokenCount = tokenCountForMessage;
|
||||
}
|
||||
|
||||
if (newTokenCount > maxTokenCount) {
|
||||
if (!promptBody) {
|
||||
// This is the first message, so we can't add it. Just throw an error.
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, ths message would put us over the token limit, so don't add it.
|
||||
// if created by user, remove next message, otherwise remove only this message
|
||||
if (isCreatedByUser) {
|
||||
nextMessage.remove = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
promptBody = newPromptBody;
|
||||
currentTokenCount = newTokenCount;
|
||||
|
||||
// Switch off isEdited after using it for the first time
|
||||
if (isEdited) {
|
||||
isEdited = false;
|
||||
}
|
||||
|
||||
// wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
return buildPromptBody();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const messagesPayload = [];
|
||||
const buildMessagesPayload = async () => {
|
||||
let canContinue = true;
|
||||
|
||||
if (promptPrefix) {
|
||||
this.systemMessage = promptPrefix;
|
||||
}
|
||||
|
||||
while (currentTokenCount < maxTokenCount && groupedMessages.length > 0 && canContinue) {
|
||||
const message = groupedMessages.pop();
|
||||
|
||||
let tokenCountForMessage = message.tokenCount ?? this.getTokenCountForMessage(message);
|
||||
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
const exceededMaxCount = newTokenCount > maxTokenCount;
|
||||
|
||||
if (exceededMaxCount && messagesPayload.length === 0) {
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
} else if (exceededMaxCount) {
|
||||
canContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
delete message.tokenCount;
|
||||
messagesPayload.unshift(message);
|
||||
currentTokenCount = newTokenCount;
|
||||
|
||||
// Switch off isEdited after using it once
|
||||
if (isEdited && message.role === 'assistant') {
|
||||
isEdited = false;
|
||||
}
|
||||
|
||||
// Wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
};
|
||||
|
||||
const processTokens = () => {
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.maxOutputTokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
};
|
||||
|
||||
if (
|
||||
/claude-[3-9]/.test(this.modelOptions.model) ||
|
||||
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(this.modelOptions.model)
|
||||
) {
|
||||
await buildMessagesPayload();
|
||||
processTokens();
|
||||
return {
|
||||
prompt: messagesPayload,
|
||||
context: messagesInWindow,
|
||||
promptTokens: currentTokenCount,
|
||||
tokenCountMap,
|
||||
};
|
||||
} else {
|
||||
await buildPromptBody();
|
||||
processTokens();
|
||||
}
|
||||
|
||||
if (nextMessage.remove) {
|
||||
promptBody = promptBody.replace(nextMessage.messageString, '');
|
||||
currentTokenCount -= nextMessage.tokenCount;
|
||||
context.shift();
|
||||
}
|
||||
|
||||
let prompt = `${promptBody}${promptSuffix}`;
|
||||
|
||||
return { prompt, context, promptTokens: currentTokenCount, tokenCountMap };
|
||||
}
|
||||
|
||||
getCompletion() {
|
||||
logger.debug("AnthropicClient doesn't use getCompletion (all handled in sendCompletion)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message or completion response using the Anthropic client.
|
||||
* @param {Anthropic} client - The Anthropic client instance.
|
||||
* @param {Anthropic.default.MessageCreateParams | Anthropic.default.CompletionCreateParams} options - The options for the message or completion.
|
||||
* @param {boolean} useMessages - Whether to use messages or completions. Defaults to `this.useMessages`.
|
||||
* @returns {Promise<Anthropic.default.Message | Anthropic.default.Completion>} The response from the Anthropic client.
|
||||
*/
|
||||
async createResponse(client, options, useMessages) {
|
||||
return (useMessages ?? this.useMessages)
|
||||
? await client.messages.create(options)
|
||||
: await client.completions.create(options);
|
||||
}
|
||||
|
||||
getMessageMapMethod() {
|
||||
/**
|
||||
* @param {TMessage} msg
|
||||
*/
|
||||
return (msg) => {
|
||||
if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) {
|
||||
msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim();
|
||||
} else if (msg.content != null) {
|
||||
msg.text = parseTextParts(msg.content, true);
|
||||
delete msg.content;
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} [intermediateReply]
|
||||
* @returns {string}
|
||||
*/
|
||||
getStreamText(intermediateReply) {
|
||||
if (!this.streamHandler) {
|
||||
return intermediateReply?.join('') ?? '';
|
||||
}
|
||||
|
||||
const reasoningText = this.streamHandler.reasoningTokens.join('');
|
||||
|
||||
const reasoningBlock = reasoningText.length > 0 ? `:::thinking\n${reasoningText}\n:::\n` : '';
|
||||
|
||||
return `${reasoningBlock}${this.streamHandler.tokens.join('')}`;
|
||||
}
|
||||
|
||||
async sendCompletion(payload, { onProgress, abortController }) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
|
||||
const { signal } = abortController;
|
||||
|
||||
const modelOptions = { ...this.modelOptions };
|
||||
if (typeof onProgress === 'function') {
|
||||
modelOptions.stream = true;
|
||||
}
|
||||
|
||||
logger.debug('modelOptions', { modelOptions });
|
||||
const metadata = {
|
||||
user_id: this.user,
|
||||
};
|
||||
|
||||
const {
|
||||
stream,
|
||||
model,
|
||||
temperature,
|
||||
maxOutputTokens,
|
||||
stop: stop_sequences,
|
||||
topP: top_p,
|
||||
topK: top_k,
|
||||
} = this.modelOptions;
|
||||
|
||||
let requestOptions = {
|
||||
model,
|
||||
stream: stream || true,
|
||||
stop_sequences,
|
||||
temperature,
|
||||
metadata,
|
||||
};
|
||||
|
||||
if (this.useMessages) {
|
||||
requestOptions.messages = payload;
|
||||
requestOptions.max_tokens =
|
||||
maxOutputTokens || anthropicSettings.maxOutputTokens.reset(requestOptions.model);
|
||||
} else {
|
||||
requestOptions.prompt = payload;
|
||||
requestOptions.max_tokens_to_sample = maxOutputTokens || legacy.maxOutputTokens.default;
|
||||
}
|
||||
|
||||
requestOptions = configureReasoning(requestOptions, {
|
||||
thinking: this.options.thinking,
|
||||
thinkingBudget: this.options.thinkingBudget,
|
||||
});
|
||||
|
||||
if (!/claude-3[-.]7/.test(model)) {
|
||||
requestOptions.top_p = top_p;
|
||||
requestOptions.top_k = top_k;
|
||||
} else if (requestOptions.thinking == null) {
|
||||
requestOptions.topP = top_p;
|
||||
requestOptions.topK = top_k;
|
||||
}
|
||||
|
||||
if (this.systemMessage && this.supportsCacheControl === true) {
|
||||
requestOptions.system = [
|
||||
{
|
||||
type: 'text',
|
||||
text: this.systemMessage,
|
||||
cache_control: { type: 'ephemeral' },
|
||||
},
|
||||
];
|
||||
} else if (this.systemMessage) {
|
||||
requestOptions.system = this.systemMessage;
|
||||
}
|
||||
|
||||
if (this.supportsCacheControl === true && this.useMessages) {
|
||||
requestOptions.messages = addCacheControl(requestOptions.messages);
|
||||
}
|
||||
|
||||
logger.debug('[AnthropicClient]', { ...requestOptions });
|
||||
const handlers = createStreamEventHandlers(this.options.res);
|
||||
this.streamHandler = new SplitStreamHandler({
|
||||
accumulate: true,
|
||||
runId: this.responseMessageId,
|
||||
handlers,
|
||||
});
|
||||
|
||||
let intermediateReply = this.streamHandler.tokens;
|
||||
|
||||
const maxRetries = 3;
|
||||
const streamRate = this.options.streamRate ?? Constants.DEFAULT_STREAM_RATE;
|
||||
async function processResponse() {
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxRetries) {
|
||||
let response;
|
||||
try {
|
||||
const client = this.getClient(requestOptions);
|
||||
response = await this.createResponse(client, requestOptions);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
logger.debug('[AnthropicClient] message aborted!');
|
||||
if (response.controller?.abort) {
|
||||
response.controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
for await (const completion of response) {
|
||||
const type = completion?.type ?? '';
|
||||
if (tokenEventTypes.has(type)) {
|
||||
logger.debug(`[AnthropicClient] ${type}`, completion);
|
||||
this[type] = completion;
|
||||
}
|
||||
this.streamHandler.handle(completion);
|
||||
await sleep(streamRate);
|
||||
}
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
attempts += 1;
|
||||
logger.warn(
|
||||
`User: ${this.user} | Anthropic Request ${attempts} failed: ${error.message}`,
|
||||
);
|
||||
|
||||
if (attempts < maxRetries) {
|
||||
await delayBeforeRetry(attempts, 350);
|
||||
} else if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
|
||||
return this.getStreamText();
|
||||
} else if (intermediateReply.length > 0) {
|
||||
return this.getStreamText(intermediateReply);
|
||||
} else {
|
||||
throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener('abort', () => {
|
||||
logger.debug('[AnthropicClient] message aborted!');
|
||||
if (response.controller?.abort) {
|
||||
response.controller.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await processResponse.bind(this)();
|
||||
return this.getStreamText(intermediateReply);
|
||||
}
|
||||
|
||||
getSaveOptions() {
|
||||
return {
|
||||
maxContextTokens: this.options.maxContextTokens,
|
||||
artifacts: this.options.artifacts,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
modelLabel: this.options.modelLabel,
|
||||
promptCache: this.options.promptCache,
|
||||
thinking: this.options.thinking,
|
||||
thinkingBudget: this.options.thinkingBudget,
|
||||
resendFiles: this.options.resendFiles,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
getBuildMessagesOptions() {
|
||||
logger.debug("AnthropicClient doesn't use getBuildMessagesOptions");
|
||||
}
|
||||
|
||||
getEncoding() {
|
||||
return 'cl100k_base';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
|
||||
* @param {string} text - The text to get the token count for.
|
||||
* @returns {number} The token count of the given text.
|
||||
*/
|
||||
getTokenCount(text) {
|
||||
const encoding = this.getEncoding();
|
||||
return Tokenizer.getTokenCount(text, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a concise title for a conversation based on the user's input text and response.
|
||||
* Involves sending a chat completion request with specific instructions for title generation.
|
||||
*
|
||||
* This function capitlizes on [Anthropic's function calling training](https://docs.anthropic.com/claude/docs/functions-external-tools).
|
||||
*
|
||||
* @param {Object} params - The parameters for the conversation title generation.
|
||||
* @param {string} params.text - The user's input.
|
||||
* @param {string} [params.responseText=''] - The AI's immediate response to the user.
|
||||
*
|
||||
* @returns {Promise<string | 'New Chat'>} A promise that resolves to the generated conversation title.
|
||||
* In case of failure, it will return the default title, "New Chat".
|
||||
*/
|
||||
async titleConvo({ text, responseText = '' }) {
|
||||
let title = 'New Chat';
|
||||
this.message_delta = undefined;
|
||||
this.message_start = undefined;
|
||||
const convo = `<initial_message>
|
||||
${truncateText(text)}
|
||||
</initial_message>
|
||||
<response>
|
||||
${JSON.stringify(truncateText(responseText))}
|
||||
</response>`;
|
||||
|
||||
const { ANTHROPIC_TITLE_MODEL } = process.env ?? {};
|
||||
const model = this.options.titleModel ?? ANTHROPIC_TITLE_MODEL ?? 'claude-3-haiku-20240307';
|
||||
const system = titleFunctionPrompt;
|
||||
|
||||
const titleChatCompletion = async () => {
|
||||
const content = `<conversation_context>
|
||||
${convo}
|
||||
</conversation_context>
|
||||
|
||||
Please generate a title for this conversation.`;
|
||||
|
||||
const titleMessage = { role: 'user', content };
|
||||
const requestOptions = {
|
||||
model,
|
||||
temperature: 0.3,
|
||||
max_tokens: 1024,
|
||||
system,
|
||||
stop_sequences: ['\n\nHuman:', '\n\nAssistant', '</function_calls>'],
|
||||
messages: [titleMessage],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.createResponse(
|
||||
this.getClient(requestOptions),
|
||||
requestOptions,
|
||||
true,
|
||||
);
|
||||
let promptTokens = response?.usage?.input_tokens;
|
||||
let completionTokens = response?.usage?.output_tokens;
|
||||
if (!promptTokens) {
|
||||
promptTokens = this.getTokenCountForMessage(titleMessage);
|
||||
promptTokens += this.getTokenCountForMessage({ role: 'system', content: system });
|
||||
}
|
||||
if (!completionTokens) {
|
||||
completionTokens = this.getTokenCountForMessage(response.content[0]);
|
||||
}
|
||||
await this.recordTokenUsage({
|
||||
model,
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
context: 'title',
|
||||
});
|
||||
const text = response.content[0].text;
|
||||
title = parseParamFromPrompt(text, 'title');
|
||||
} catch (e) {
|
||||
logger.error('[AnthropicClient] There was an issue generating the title', e);
|
||||
}
|
||||
};
|
||||
|
||||
await titleChatCompletion();
|
||||
logger.debug('[AnthropicClient] Convo Title: ' + title);
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnthropicClient;
|
||||
|
|
@ -1,994 +0,0 @@
|
|||
const { google } = require('googleapis');
|
||||
const { sleep } = require('@librechat/agents');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { getModelMaxTokens } = require('@librechat/api');
|
||||
const { concat } = require('@langchain/core/utils/stream');
|
||||
const { ChatVertexAI } = require('@langchain/google-vertexai');
|
||||
const { Tokenizer, getSafetySettings } = require('@librechat/api');
|
||||
const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
|
||||
const { GoogleGenerativeAI: GenAI } = require('@google/generative-ai');
|
||||
const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
|
||||
const {
|
||||
googleGenConfigSchema,
|
||||
validateVisionModel,
|
||||
getResponseSender,
|
||||
endpointSettings,
|
||||
parseTextParts,
|
||||
EModelEndpoint,
|
||||
googleSettings,
|
||||
ContentTypes,
|
||||
VisionModes,
|
||||
ErrorTypes,
|
||||
Constants,
|
||||
AuthKeys,
|
||||
} = require('librechat-data-provider');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images');
|
||||
const { spendTokens } = require('~/models/spendTokens');
|
||||
const {
|
||||
formatMessage,
|
||||
createContextHandlers,
|
||||
titleInstruction,
|
||||
truncateText,
|
||||
} = require('./prompts');
|
||||
const BaseClient = require('./BaseClient');
|
||||
|
||||
const loc = process.env.GOOGLE_LOC || 'us-central1';
|
||||
const publisher = 'google';
|
||||
const endpointPrefix =
|
||||
loc === 'global' ? 'aiplatform.googleapis.com' : `${loc}-aiplatform.googleapis.com`;
|
||||
|
||||
const settings = endpointSettings[EModelEndpoint.google];
|
||||
const EXCLUDED_GENAI_MODELS = /gemini-(?:1\.0|1-0|pro)/;
|
||||
|
||||
class GoogleClient extends BaseClient {
|
||||
constructor(credentials, options = {}) {
|
||||
super('apiKey', options);
|
||||
let creds = {};
|
||||
|
||||
if (typeof credentials === 'string') {
|
||||
creds = JSON.parse(credentials);
|
||||
} else if (credentials) {
|
||||
creds = credentials;
|
||||
}
|
||||
|
||||
const serviceKey = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {};
|
||||
this.serviceKey =
|
||||
serviceKey && typeof serviceKey === 'string' ? JSON.parse(serviceKey) : (serviceKey ?? {});
|
||||
/** @type {string | null | undefined} */
|
||||
this.project_id = this.serviceKey.project_id;
|
||||
this.client_email = this.serviceKey.client_email;
|
||||
this.private_key = this.serviceKey.private_key;
|
||||
this.access_token = null;
|
||||
|
||||
this.apiKey = creds[AuthKeys.GOOGLE_API_KEY];
|
||||
|
||||
this.reverseProxyUrl = options.reverseProxyUrl;
|
||||
|
||||
this.authHeader = options.authHeader;
|
||||
|
||||
/** @type {UsageMetadata | undefined} */
|
||||
this.usage;
|
||||
/** The key for the usage object's input tokens
|
||||
* @type {string} */
|
||||
this.inputTokensKey = 'input_tokens';
|
||||
/** The key for the usage object's output tokens
|
||||
* @type {string} */
|
||||
this.outputTokensKey = 'output_tokens';
|
||||
this.visionMode = VisionModes.generative;
|
||||
/** @type {string} */
|
||||
this.systemMessage;
|
||||
if (options.skipSetOptions) {
|
||||
return;
|
||||
}
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
/* Google specific methods */
|
||||
constructUrl() {
|
||||
return `https://${endpointPrefix}/v1/projects/${this.project_id}/locations/${loc}/publishers/${publisher}/models/${this.modelOptions.model}:serverStreamingPredict`;
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
|
||||
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
|
||||
|
||||
jwtClient.authorize((err) => {
|
||||
if (err) {
|
||||
logger.error('jwtClient failed to authorize', err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return jwtClient;
|
||||
}
|
||||
|
||||
async getAccessToken() {
|
||||
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
|
||||
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
jwtClient.authorize((err, tokens) => {
|
||||
if (err) {
|
||||
logger.error('jwtClient failed to authorize', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(tokens.access_token);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* Required Client methods */
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
this.modelOptions = this.options.modelOptions || {};
|
||||
|
||||
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
|
||||
|
||||
/** @type {boolean} Whether using a "GenerativeAI" Model */
|
||||
this.isGenerativeModel = /gemini|learnlm|gemma/.test(this.modelOptions.model);
|
||||
|
||||
this.maxContextTokens =
|
||||
this.options.maxContextTokens ??
|
||||
getModelMaxTokens(this.modelOptions.model, EModelEndpoint.google);
|
||||
|
||||
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
|
||||
// Earlier messages will be dropped until the prompt is within the limit.
|
||||
this.maxResponseTokens = this.modelOptions.maxOutputTokens || settings.maxOutputTokens.default;
|
||||
|
||||
if (this.maxContextTokens > 32000) {
|
||||
this.maxContextTokens = this.maxContextTokens - this.maxResponseTokens;
|
||||
}
|
||||
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
||||
throw new Error(
|
||||
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Add thinking configuration
|
||||
this.modelOptions.thinkingConfig = {
|
||||
thinkingBudget:
|
||||
(this.modelOptions.thinking ?? googleSettings.thinking.default)
|
||||
? this.modelOptions.thinkingBudget
|
||||
: 0,
|
||||
};
|
||||
delete this.modelOptions.thinking;
|
||||
delete this.modelOptions.thinkingBudget;
|
||||
|
||||
this.sender =
|
||||
this.options.sender ??
|
||||
getResponseSender({
|
||||
model: this.modelOptions.model,
|
||||
endpoint: EModelEndpoint.google,
|
||||
modelLabel: this.options.modelLabel,
|
||||
});
|
||||
|
||||
this.userLabel = this.options.userLabel || 'User';
|
||||
this.modelLabel = this.options.modelLabel || 'Assistant';
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
this.completionsUrl = this.options.reverseProxyUrl;
|
||||
} else {
|
||||
this.completionsUrl = this.constructUrl();
|
||||
}
|
||||
|
||||
let promptPrefix = (this.options.promptPrefix ?? '').trim();
|
||||
if (typeof this.options.artifactsPrompt === 'string' && this.options.artifactsPrompt) {
|
||||
promptPrefix = `${promptPrefix ?? ''}\n${this.options.artifactsPrompt}`.trim();
|
||||
}
|
||||
this.systemMessage = promptPrefix;
|
||||
this.initializeClient();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks if the model is a vision model based on request attachments and sets the appropriate options:
|
||||
* @param {MongoFile[]} attachments
|
||||
*/
|
||||
checkVisionRequest(attachments) {
|
||||
/* Validation vision request */
|
||||
this.defaultVisionModel =
|
||||
this.options.visionModel ??
|
||||
(!EXCLUDED_GENAI_MODELS.test(this.modelOptions.model)
|
||||
? this.modelOptions.model
|
||||
: 'gemini-pro-vision');
|
||||
const availableModels = this.options.modelsConfig?.[EModelEndpoint.google];
|
||||
this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
||||
|
||||
if (
|
||||
attachments &&
|
||||
attachments.some((file) => file?.type && file?.type?.includes('image')) &&
|
||||
availableModels?.includes(this.defaultVisionModel) &&
|
||||
!this.isVisionModel
|
||||
) {
|
||||
this.modelOptions.model = this.defaultVisionModel;
|
||||
this.isVisionModel = true;
|
||||
}
|
||||
|
||||
if (this.isVisionModel && !attachments && this.modelOptions.model.includes('gemini-pro')) {
|
||||
this.modelOptions.model = 'gemini-pro';
|
||||
this.isVisionModel = false;
|
||||
}
|
||||
}
|
||||
|
||||
formatMessages() {
|
||||
return ((message) => {
|
||||
const msg = {
|
||||
author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel),
|
||||
content: message?.content ?? message.text,
|
||||
};
|
||||
|
||||
if (!message.image_urls?.length) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.content = (
|
||||
!Array.isArray(msg.content)
|
||||
? [
|
||||
{
|
||||
type: ContentTypes.TEXT,
|
||||
[ContentTypes.TEXT]: msg.content,
|
||||
},
|
||||
]
|
||||
: msg.content
|
||||
).concat(message.image_urls);
|
||||
|
||||
return msg;
|
||||
}).bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats messages for generative AI
|
||||
* @param {TMessage[]} messages
|
||||
* @returns
|
||||
*/
|
||||
async formatGenerativeMessages(messages) {
|
||||
const formattedMessages = [];
|
||||
const attachments = await this.options.attachments;
|
||||
const latestMessage = { ...messages[messages.length - 1] };
|
||||
const files = await this.addImageURLs(latestMessage, attachments, VisionModes.generative);
|
||||
this.options.attachments = files;
|
||||
messages[messages.length - 1] = latestMessage;
|
||||
|
||||
for (const _message of messages) {
|
||||
const role = _message.isCreatedByUser ? this.userLabel : this.modelLabel;
|
||||
const parts = [];
|
||||
parts.push({ text: _message.text });
|
||||
if (!_message.image_urls?.length) {
|
||||
formattedMessages.push({ role, parts });
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const images of _message.image_urls) {
|
||||
if (images.inlineData) {
|
||||
parts.push({ inlineData: images.inlineData });
|
||||
}
|
||||
}
|
||||
|
||||
formattedMessages.push({ role, parts });
|
||||
}
|
||||
|
||||
return formattedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds image URLs to the message object and returns the files
|
||||
*
|
||||
* @param {TMessage[]} messages
|
||||
* @param {MongoFile[]} files
|
||||
* @returns {Promise<MongoFile[]>}
|
||||
*/
|
||||
async addImageURLs(message, attachments, mode = '') {
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
endpoint: EModelEndpoint.google,
|
||||
},
|
||||
mode,
|
||||
);
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the augmented prompt for attachments
|
||||
* TODO: Add File API Support
|
||||
* @param {TMessage[]} messages
|
||||
*/
|
||||
async buildAugmentedPrompt(messages = []) {
|
||||
const attachments = await this.options.attachments;
|
||||
const latestMessage = { ...messages[messages.length - 1] };
|
||||
this.contextHandlers = createContextHandlers(this.options.req, latestMessage.text);
|
||||
|
||||
if (this.contextHandlers) {
|
||||
for (const file of attachments) {
|
||||
if (file.embedded) {
|
||||
this.contextHandlers?.processFile(file);
|
||||
continue;
|
||||
}
|
||||
if (file.metadata?.fileIdentifier) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
this.systemMessage = this.augmentedPrompt + this.systemMessage;
|
||||
}
|
||||
}
|
||||
|
||||
async buildVisionMessages(messages = [], parentMessageId) {
|
||||
const attachments = await this.options.attachments;
|
||||
const latestMessage = { ...messages[messages.length - 1] };
|
||||
await this.buildAugmentedPrompt(messages);
|
||||
|
||||
const { prompt } = await this.buildMessagesPrompt(messages, parentMessageId);
|
||||
|
||||
const files = await this.addImageURLs(latestMessage, attachments);
|
||||
|
||||
this.options.attachments = files;
|
||||
|
||||
latestMessage.text = prompt;
|
||||
|
||||
const payload = {
|
||||
instances: [
|
||||
{
|
||||
messages: [new HumanMessage(formatMessage({ message: latestMessage }))],
|
||||
},
|
||||
],
|
||||
};
|
||||
return { prompt: payload };
|
||||
}
|
||||
|
||||
/** @param {TMessage[]} [messages=[]] */
|
||||
async buildGenerativeMessages(messages = []) {
|
||||
this.userLabel = 'user';
|
||||
this.modelLabel = 'model';
|
||||
const promises = [];
|
||||
promises.push(await this.formatGenerativeMessages(messages));
|
||||
promises.push(this.buildAugmentedPrompt(messages));
|
||||
const [formattedMessages] = await Promise.all(promises);
|
||||
return { prompt: formattedMessages };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TMessage[]} [messages=[]]
|
||||
* @param {string} [parentMessageId]
|
||||
*/
|
||||
async buildMessages(_messages = [], parentMessageId) {
|
||||
if (!this.isGenerativeModel && !this.project_id) {
|
||||
throw new Error('[GoogleClient] PaLM 2 and Codey models are no longer supported.');
|
||||
}
|
||||
|
||||
if (this.systemMessage) {
|
||||
const instructionsTokenCount = this.getTokenCount(this.systemMessage);
|
||||
|
||||
this.maxContextTokens = this.maxContextTokens - instructionsTokenCount;
|
||||
if (this.maxContextTokens < 0) {
|
||||
const info = `${instructionsTokenCount} / ${this.maxContextTokens}`;
|
||||
const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`;
|
||||
logger.warn(`Instructions token count exceeds max context (${info}).`);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < _messages.length; i++) {
|
||||
const message = _messages[i];
|
||||
if (!message.tokenCount) {
|
||||
_messages[i].tokenCount = this.getTokenCountForMessage({
|
||||
role: message.isCreatedByUser ? 'user' : 'assistant',
|
||||
content: message.content ?? message.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
payload: messages,
|
||||
tokenCountMap,
|
||||
promptTokens,
|
||||
} = await this.handleContextStrategy({
|
||||
orderedMessages: _messages,
|
||||
formattedMessages: _messages,
|
||||
});
|
||||
|
||||
if (!this.project_id && !EXCLUDED_GENAI_MODELS.test(this.modelOptions.model)) {
|
||||
const result = await this.buildGenerativeMessages(messages);
|
||||
result.tokenCountMap = tokenCountMap;
|
||||
result.promptTokens = promptTokens;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this.options.attachments && this.isGenerativeModel) {
|
||||
const result = this.buildVisionMessages(messages, parentMessageId);
|
||||
result.tokenCountMap = tokenCountMap;
|
||||
result.promptTokens = promptTokens;
|
||||
return result;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
instances: [
|
||||
{
|
||||
messages: messages
|
||||
.map(this.formatMessages())
|
||||
.map((msg) => ({ ...msg, role: msg.author === 'User' ? 'user' : 'assistant' }))
|
||||
.map((message) => formatMessage({ message, langChain: true })),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (this.systemMessage) {
|
||||
payload.instances[0].context = this.systemMessage;
|
||||
}
|
||||
|
||||
logger.debug('[GoogleClient] buildMessages', payload);
|
||||
return { prompt: payload, tokenCountMap, promptTokens };
|
||||
}
|
||||
|
||||
async buildMessagesPrompt(messages, parentMessageId) {
|
||||
const orderedMessages = this.constructor.getMessagesForConversation({
|
||||
messages,
|
||||
parentMessageId,
|
||||
});
|
||||
|
||||
logger.debug('[GoogleClient]', {
|
||||
orderedMessages,
|
||||
parentMessageId,
|
||||
});
|
||||
|
||||
const formattedMessages = orderedMessages.map(this.formatMessages());
|
||||
|
||||
let lastAuthor = '';
|
||||
let groupedMessages = [];
|
||||
|
||||
for (let message of formattedMessages) {
|
||||
// If last author is not same as current author, add to new group
|
||||
if (lastAuthor !== message.author) {
|
||||
groupedMessages.push({
|
||||
author: message.author,
|
||||
content: [message.content],
|
||||
});
|
||||
lastAuthor = message.author;
|
||||
// If same author, append content to the last group
|
||||
} else {
|
||||
groupedMessages[groupedMessages.length - 1].content.push(message.content);
|
||||
}
|
||||
}
|
||||
|
||||
let identityPrefix = '';
|
||||
if (this.options.userLabel) {
|
||||
identityPrefix = `\nHuman's name: ${this.options.userLabel}`;
|
||||
}
|
||||
|
||||
if (this.options.modelLabel) {
|
||||
identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`;
|
||||
}
|
||||
|
||||
let promptPrefix = (this.systemMessage ?? '').trim();
|
||||
|
||||
if (identityPrefix) {
|
||||
promptPrefix = `${identityPrefix}${promptPrefix}`;
|
||||
}
|
||||
|
||||
// Prompt AI to respond, empty if last message was from AI
|
||||
let isEdited = lastAuthor === this.modelLabel;
|
||||
const promptSuffix = isEdited ? '' : `${promptPrefix}\n\n${this.modelLabel}:\n`;
|
||||
let currentTokenCount = isEdited
|
||||
? this.getTokenCount(promptPrefix)
|
||||
: this.getTokenCount(promptSuffix);
|
||||
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
|
||||
const context = [];
|
||||
|
||||
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
||||
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
||||
// Also, remove the next message when the message that puts us over the token limit is created by the user.
|
||||
// Otherwise, remove only the exceeding message. This is due to Anthropic's strict payload rule to start with "Human:".
|
||||
const nextMessage = {
|
||||
remove: false,
|
||||
tokenCount: 0,
|
||||
messageString: '',
|
||||
};
|
||||
|
||||
const buildPromptBody = async () => {
|
||||
if (currentTokenCount < maxTokenCount && groupedMessages.length > 0) {
|
||||
const message = groupedMessages.pop();
|
||||
const isCreatedByUser = message.author === this.userLabel;
|
||||
// Use promptPrefix if message is edited assistant'
|
||||
const messagePrefix =
|
||||
isCreatedByUser || !isEdited
|
||||
? `\n\n${message.author}:`
|
||||
: `${promptPrefix}\n\n${message.author}:`;
|
||||
const messageString = `${messagePrefix}\n${message.content}\n`;
|
||||
let newPromptBody = `${messageString}${promptBody}`;
|
||||
|
||||
context.unshift(message);
|
||||
|
||||
const tokenCountForMessage = this.getTokenCount(messageString);
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
|
||||
if (!isCreatedByUser) {
|
||||
nextMessage.messageString = messageString;
|
||||
nextMessage.tokenCount = tokenCountForMessage;
|
||||
}
|
||||
|
||||
if (newTokenCount > maxTokenCount) {
|
||||
if (!promptBody) {
|
||||
// This is the first message, so we can't add it. Just throw an error.
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, ths message would put us over the token limit, so don't add it.
|
||||
// if created by user, remove next message, otherwise remove only this message
|
||||
if (isCreatedByUser) {
|
||||
nextMessage.remove = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
promptBody = newPromptBody;
|
||||
currentTokenCount = newTokenCount;
|
||||
|
||||
// Switch off isEdited after using it for the first time
|
||||
if (isEdited) {
|
||||
isEdited = false;
|
||||
}
|
||||
|
||||
// wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
return buildPromptBody();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
await buildPromptBody();
|
||||
|
||||
if (nextMessage.remove) {
|
||||
promptBody = promptBody.replace(nextMessage.messageString, '');
|
||||
currentTokenCount -= nextMessage.tokenCount;
|
||||
context.shift();
|
||||
}
|
||||
|
||||
let prompt = `${promptBody}${promptSuffix}`.trim();
|
||||
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.maxOutputTokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
|
||||
return { prompt, context };
|
||||
}
|
||||
|
||||
createLLM(clientOptions) {
|
||||
const model = clientOptions.modelName ?? clientOptions.model;
|
||||
clientOptions.location = loc;
|
||||
clientOptions.endpoint = endpointPrefix;
|
||||
|
||||
let requestOptions = null;
|
||||
if (this.reverseProxyUrl) {
|
||||
requestOptions = {
|
||||
baseUrl: this.reverseProxyUrl,
|
||||
};
|
||||
|
||||
if (this.authHeader) {
|
||||
requestOptions.customHeaders = {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.project_id != null) {
|
||||
logger.debug('Creating VertexAI client');
|
||||
this.visionMode = undefined;
|
||||
clientOptions.streaming = true;
|
||||
const client = new ChatVertexAI(clientOptions);
|
||||
client.temperature = clientOptions.temperature;
|
||||
client.topP = clientOptions.topP;
|
||||
client.topK = clientOptions.topK;
|
||||
client.topLogprobs = clientOptions.topLogprobs;
|
||||
client.frequencyPenalty = clientOptions.frequencyPenalty;
|
||||
client.presencePenalty = clientOptions.presencePenalty;
|
||||
client.maxOutputTokens = clientOptions.maxOutputTokens;
|
||||
return client;
|
||||
} else if (!EXCLUDED_GENAI_MODELS.test(model)) {
|
||||
logger.debug('Creating GenAI client');
|
||||
return new GenAI(this.apiKey).getGenerativeModel({ model }, requestOptions);
|
||||
}
|
||||
|
||||
logger.debug('Creating Chat Google Generative AI client');
|
||||
return new ChatGoogleGenerativeAI({ ...clientOptions, apiKey: this.apiKey });
|
||||
}
|
||||
|
||||
initializeClient() {
|
||||
let clientOptions = { ...this.modelOptions };
|
||||
|
||||
if (this.project_id) {
|
||||
clientOptions['authOptions'] = {
|
||||
credentials: {
|
||||
...this.serviceKey,
|
||||
},
|
||||
projectId: this.project_id,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isGenerativeModel && !this.project_id) {
|
||||
clientOptions.modelName = clientOptions.model;
|
||||
delete clientOptions.model;
|
||||
}
|
||||
|
||||
this.client = this.createLLM(clientOptions);
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async getCompletion(_payload, options = {}) {
|
||||
const { onProgress, abortController } = options;
|
||||
const safetySettings = getSafetySettings(this.modelOptions.model);
|
||||
const streamRate = this.options.streamRate ?? Constants.DEFAULT_STREAM_RATE;
|
||||
const modelName = this.modelOptions.modelName ?? this.modelOptions.model ?? '';
|
||||
|
||||
let reply = '';
|
||||
/** @type {Error} */
|
||||
let error;
|
||||
try {
|
||||
if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) {
|
||||
/** @type {GenerativeModel} */
|
||||
const client = this.client;
|
||||
/** @type {GenerateContentRequest} */
|
||||
const requestOptions = {
|
||||
safetySettings,
|
||||
contents: _payload,
|
||||
generationConfig: googleGenConfigSchema.parse(this.modelOptions),
|
||||
};
|
||||
|
||||
const promptPrefix = (this.systemMessage ?? '').trim();
|
||||
if (promptPrefix.length) {
|
||||
requestOptions.systemInstruction = {
|
||||
parts: [
|
||||
{
|
||||
text: promptPrefix,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const delay = modelName.includes('flash') ? 8 : 15;
|
||||
/** @type {GenAIUsageMetadata} */
|
||||
let usageMetadata;
|
||||
|
||||
abortController.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
logger.warn('[GoogleClient] Request was aborted', abortController.signal.reason);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
const result = await client.generateContentStream(requestOptions, {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
for await (const chunk of result.stream) {
|
||||
usageMetadata = !usageMetadata
|
||||
? chunk?.usageMetadata
|
||||
: Object.assign(usageMetadata, chunk?.usageMetadata);
|
||||
const chunkText = chunk.text();
|
||||
await this.generateTextStream(chunkText, onProgress, {
|
||||
delay,
|
||||
});
|
||||
reply += chunkText;
|
||||
await sleep(streamRate);
|
||||
}
|
||||
|
||||
if (usageMetadata) {
|
||||
this.usage = {
|
||||
input_tokens: usageMetadata.promptTokenCount,
|
||||
output_tokens: usageMetadata.candidatesTokenCount,
|
||||
};
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
const { instances } = _payload;
|
||||
const { messages: messages, context } = instances?.[0] ?? {};
|
||||
|
||||
if (!this.isVisionModel && context && messages?.length > 0) {
|
||||
messages.unshift(new SystemMessage(context));
|
||||
}
|
||||
|
||||
/** @type {import('@langchain/core/messages').AIMessageChunk['usage_metadata']} */
|
||||
let usageMetadata;
|
||||
/** @type {ChatVertexAI} */
|
||||
const client = this.client;
|
||||
const stream = await client.stream(messages, {
|
||||
signal: abortController.signal,
|
||||
streamUsage: true,
|
||||
safetySettings,
|
||||
});
|
||||
|
||||
let delay = this.options.streamRate || 8;
|
||||
|
||||
if (!this.options.streamRate) {
|
||||
if (this.isGenerativeModel) {
|
||||
delay = 15;
|
||||
}
|
||||
if (modelName.includes('flash')) {
|
||||
delay = 5;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk?.usage_metadata) {
|
||||
const metadata = chunk.usage_metadata;
|
||||
for (const key in metadata) {
|
||||
if (Number.isNaN(metadata[key])) {
|
||||
delete metadata[key];
|
||||
}
|
||||
}
|
||||
|
||||
usageMetadata = !usageMetadata ? metadata : concat(usageMetadata, metadata);
|
||||
}
|
||||
|
||||
const chunkText = chunk?.content ?? '';
|
||||
await this.generateTextStream(chunkText, onProgress, {
|
||||
delay,
|
||||
});
|
||||
reply += chunkText;
|
||||
}
|
||||
|
||||
if (usageMetadata) {
|
||||
this.usage = usageMetadata;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e;
|
||||
logger.error('[GoogleClient] There was an issue generating the completion', e);
|
||||
}
|
||||
|
||||
if (error != null && reply === '') {
|
||||
const errorMessage = `{ "type": "${ErrorTypes.GoogleError}", "info": "${
|
||||
error.message ?? 'The Google provider failed to generate content, please contact the Admin.'
|
||||
}" }`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream usage as returned by this client's API response.
|
||||
* @returns {UsageMetadata} The stream usage object.
|
||||
*/
|
||||
getStreamUsage() {
|
||||
return this.usage;
|
||||
}
|
||||
|
||||
getMessageMapMethod() {
|
||||
/**
|
||||
* @param {TMessage} msg
|
||||
*/
|
||||
return (msg) => {
|
||||
if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) {
|
||||
msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim();
|
||||
} else if (msg.content != null) {
|
||||
msg.text = parseTextParts(msg.content, true);
|
||||
delete msg.content;
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the correct token count for the current user message based on the token count map and API usage.
|
||||
* Edge case: If the calculation results in a negative value, it returns the original estimate.
|
||||
* If revisiting a conversation with a chat history entirely composed of token estimates,
|
||||
* the cumulative token count going forward should become more accurate as the conversation progresses.
|
||||
* @param {Object} params - The parameters for the calculation.
|
||||
* @param {Record<string, number>} params.tokenCountMap - A map of message IDs to their token counts.
|
||||
* @param {string} params.currentMessageId - The ID of the current message to calculate.
|
||||
* @param {UsageMetadata} params.usage - The usage object returned by the API.
|
||||
* @returns {number} The correct token count for the current user message.
|
||||
*/
|
||||
calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage }) {
|
||||
const originalEstimate = tokenCountMap[currentMessageId] || 0;
|
||||
|
||||
if (!usage || typeof usage.input_tokens !== 'number') {
|
||||
return originalEstimate;
|
||||
}
|
||||
|
||||
tokenCountMap[currentMessageId] = 0;
|
||||
const totalTokensFromMap = Object.values(tokenCountMap).reduce((sum, count) => {
|
||||
const numCount = Number(count);
|
||||
return sum + (isNaN(numCount) ? 0 : numCount);
|
||||
}, 0);
|
||||
const totalInputTokens = usage.input_tokens ?? 0;
|
||||
const currentMessageTokens = totalInputTokens - totalTokensFromMap;
|
||||
return currentMessageTokens > 0 ? currentMessageTokens : originalEstimate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {number} params.promptTokens
|
||||
* @param {number} params.completionTokens
|
||||
* @param {UsageMetadata} [params.usage]
|
||||
* @param {string} [params.model]
|
||||
* @param {string} [params.context='message']
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async recordTokenUsage({ promptTokens, completionTokens, model, context = 'message' }) {
|
||||
await spendTokens(
|
||||
{
|
||||
context,
|
||||
user: this.user ?? this.options.req?.user?.id,
|
||||
conversationId: this.conversationId,
|
||||
model: model ?? this.modelOptions.model,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
{ promptTokens, completionTokens },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stripped-down logic for generating a title. This uses the non-streaming APIs, since the user does not see titles streaming
|
||||
*/
|
||||
async titleChatCompletion(_payload, options = {}) {
|
||||
let reply = '';
|
||||
const { abortController } = options;
|
||||
|
||||
const model =
|
||||
this.options.titleModel ?? this.modelOptions.modelName ?? this.modelOptions.model ?? '';
|
||||
const safetySettings = getSafetySettings(model);
|
||||
if (!EXCLUDED_GENAI_MODELS.test(model) && !this.project_id) {
|
||||
logger.debug('Identified titling model as GenAI version');
|
||||
/** @type {GenerativeModel} */
|
||||
const client = this.client;
|
||||
const requestOptions = {
|
||||
contents: _payload,
|
||||
safetySettings,
|
||||
generationConfig: {
|
||||
temperature: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await client.generateContent(requestOptions);
|
||||
reply = result.response?.text();
|
||||
return reply;
|
||||
} else {
|
||||
const { instances } = _payload;
|
||||
const { messages } = instances?.[0] ?? {};
|
||||
const titleResponse = await this.client.invoke(messages, {
|
||||
signal: abortController.signal,
|
||||
timeout: 7000,
|
||||
safetySettings,
|
||||
});
|
||||
|
||||
if (titleResponse.usage_metadata) {
|
||||
await this.recordTokenUsage({
|
||||
model,
|
||||
promptTokens: titleResponse.usage_metadata.input_tokens,
|
||||
completionTokens: titleResponse.usage_metadata.output_tokens,
|
||||
context: 'title',
|
||||
});
|
||||
}
|
||||
|
||||
reply = titleResponse.content;
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
|
||||
async titleConvo({ text, responseText = '' }) {
|
||||
let title = 'New Chat';
|
||||
const convo = `||>User:
|
||||
"${truncateText(text)}"
|
||||
||>Response:
|
||||
"${JSON.stringify(truncateText(responseText))}"`;
|
||||
|
||||
let { prompt: payload } = await this.buildMessages([
|
||||
{
|
||||
text: `Please generate ${titleInstruction}
|
||||
|
||||
${convo}
|
||||
|
||||
||>Title:`,
|
||||
isCreatedByUser: true,
|
||||
author: this.userLabel,
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
this.initializeClient();
|
||||
title = await this.titleChatCompletion(payload, {
|
||||
abortController: new AbortController(),
|
||||
onProgress: () => {},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('[GoogleClient] There was an issue generating the title', e);
|
||||
}
|
||||
logger.debug(`Title response: ${title}`);
|
||||
return title;
|
||||
}
|
||||
|
||||
getSaveOptions() {
|
||||
return {
|
||||
endpointType: null,
|
||||
artifacts: this.options.artifacts,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
maxContextTokens: this.options.maxContextTokens,
|
||||
modelLabel: this.options.modelLabel,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
getBuildMessagesOptions() {
|
||||
// logger.debug('GoogleClient doesn\'t use getBuildMessagesOptions');
|
||||
}
|
||||
|
||||
async sendCompletion(payload, opts = {}) {
|
||||
let reply = '';
|
||||
reply = await this.getCompletion(payload, opts);
|
||||
return reply.trim();
|
||||
}
|
||||
|
||||
getEncoding() {
|
||||
return 'cl100k_base';
|
||||
}
|
||||
|
||||
async getVertexTokenCount(text) {
|
||||
/** @type {ChatVertexAI} */
|
||||
const client = this.client ?? this.initializeClient();
|
||||
const connection = client.connection;
|
||||
const gAuthClient = connection.client;
|
||||
const tokenEndpoint = `https://${connection._endpoint}/${connection.apiVersion}/projects/${this.project_id}/locations/${connection._location}/publishers/google/models/${connection.model}/:countTokens`;
|
||||
const result = await gAuthClient.request({
|
||||
url: tokenEndpoint,
|
||||
method: 'POST',
|
||||
data: {
|
||||
contents: [{ role: 'user', parts: [{ text }] }],
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
|
||||
* @param {string} text - The text to get the token count for.
|
||||
* @returns {number} The token count of the given text.
|
||||
*/
|
||||
getTokenCount(text) {
|
||||
const encoding = this.getEncoding();
|
||||
return Tokenizer.getTokenCount(text, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleClient;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +0,0 @@
|
|||
const tokenSplit = require('./tokenSplit');
|
||||
|
||||
module.exports = {
|
||||
tokenSplit,
|
||||
};
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
const { TokenTextSplitter } = require('@langchain/textsplitters');
|
||||
|
||||
/**
|
||||
* Splits a given text by token chunks, based on the provided parameters for the TokenTextSplitter.
|
||||
* Note: limit or memoize use of this function as its calculation is expensive.
|
||||
*
|
||||
* @param {Object} obj - Configuration object for the text splitting operation.
|
||||
* @param {string} obj.text - The text to be split.
|
||||
* @param {string} [obj.encodingName='cl100k_base'] - Encoding name. Defaults to 'cl100k_base'.
|
||||
* @param {number} [obj.chunkSize=1] - The token size of each chunk. Defaults to 1.
|
||||
* @param {number} [obj.chunkOverlap=0] - The number of chunk elements to be overlapped between adjacent chunks. Defaults to 0.
|
||||
* @param {number} [obj.returnSize] - If specified and not 0, slices the return array from the end by this amount.
|
||||
*
|
||||
* @returns {Promise<Array>} Returns a promise that resolves to an array of text chunks.
|
||||
* If no text is provided, an empty array is returned.
|
||||
* If returnSize is specified and not 0, slices the return array from the end by returnSize.
|
||||
*
|
||||
* @async
|
||||
* @function tokenSplit
|
||||
*/
|
||||
async function tokenSplit({
|
||||
text,
|
||||
encodingName = 'cl100k_base',
|
||||
chunkSize = 1,
|
||||
chunkOverlap = 0,
|
||||
returnSize,
|
||||
}) {
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const splitter = new TokenTextSplitter({
|
||||
encodingName,
|
||||
chunkSize,
|
||||
chunkOverlap,
|
||||
});
|
||||
|
||||
if (!returnSize) {
|
||||
return await splitter.splitText(text);
|
||||
}
|
||||
|
||||
const splitText = await splitter.splitText(text);
|
||||
|
||||
if (returnSize && returnSize > 0 && splitText.length > 0) {
|
||||
return splitText.slice(-Math.abs(returnSize));
|
||||
}
|
||||
|
||||
return splitText;
|
||||
}
|
||||
|
||||
module.exports = tokenSplit;
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
const tokenSplit = require('./tokenSplit');
|
||||
|
||||
describe('tokenSplit', () => {
|
||||
const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id.';
|
||||
|
||||
it('returns correct text chunks with provided parameters', async () => {
|
||||
const result = await tokenSplit({
|
||||
text: text,
|
||||
encodingName: 'gpt2',
|
||||
chunkSize: 2,
|
||||
chunkOverlap: 1,
|
||||
returnSize: 5,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['it.', '. Null', ' Nullam', 'am id', ' id.']);
|
||||
});
|
||||
|
||||
it('returns correct text chunks with default parameters', async () => {
|
||||
const result = await tokenSplit({ text });
|
||||
expect(result).toEqual([
|
||||
'Lorem',
|
||||
' ipsum',
|
||||
' dolor',
|
||||
' sit',
|
||||
' amet',
|
||||
',',
|
||||
' consectetur',
|
||||
' adipiscing',
|
||||
' elit',
|
||||
'.',
|
||||
' Null',
|
||||
'am',
|
||||
' id',
|
||||
'.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns correct text chunks with specific return size', async () => {
|
||||
const result = await tokenSplit({ text, returnSize: 2 });
|
||||
expect(result.length).toEqual(2);
|
||||
expect(result).toEqual([' id', '.']);
|
||||
});
|
||||
|
||||
it('returns correct text chunks with specified chunk size', async () => {
|
||||
const result = await tokenSplit({ text, chunkSize: 10 });
|
||||
expect(result).toEqual([
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
' Nullam id.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty array with no text', async () => {
|
||||
const result = await tokenSplit({ text: '' });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
const OpenAIClient = require('./OpenAIClient');
|
||||
const GoogleClient = require('./GoogleClient');
|
||||
const TextStream = require('./TextStream');
|
||||
const AnthropicClient = require('./AnthropicClient');
|
||||
const toolUtils = require('./tools/util');
|
||||
|
||||
module.exports = {
|
||||
OpenAIClient,
|
||||
GoogleClient,
|
||||
TextStream,
|
||||
AnthropicClient,
|
||||
...toolUtils,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
const { CohereConstants } = require('librechat-data-provider');
|
||||
const { titleInstruction } = require('../prompts/titlePrompts');
|
||||
|
||||
// Mapping OpenAI roles to Cohere roles
|
||||
const roleMap = {
|
||||
user: CohereConstants.ROLE_USER,
|
||||
assistant: CohereConstants.ROLE_CHATBOT,
|
||||
system: CohereConstants.ROLE_SYSTEM, // Recognize and map the system role explicitly
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts an OpenAI ChatCompletionPayload to conform with Cohere's expected chat payload format.
|
||||
* Now includes handling for "system" roles explicitly mentioned.
|
||||
*
|
||||
* @param {Object} options - Object containing the model options.
|
||||
* @param {ChatCompletionPayload} options.modelOptions - The OpenAI model payload options.
|
||||
* @returns {CohereChatStreamRequest} Cohere-compatible chat API payload.
|
||||
*/
|
||||
function createCoherePayload({ modelOptions }) {
|
||||
/** @type {string | undefined} */
|
||||
let preamble;
|
||||
let latestUserMessageContent = '';
|
||||
const {
|
||||
stream,
|
||||
stop,
|
||||
top_p,
|
||||
temperature,
|
||||
frequency_penalty,
|
||||
presence_penalty,
|
||||
max_tokens,
|
||||
messages,
|
||||
model,
|
||||
...rest
|
||||
} = modelOptions;
|
||||
|
||||
// Filter out the latest user message and transform remaining messages to Cohere's chat_history format
|
||||
let chatHistory = messages.reduce((acc, message, index, arr) => {
|
||||
const isLastUserMessage = index === arr.length - 1 && message.role === 'user';
|
||||
|
||||
const messageContent =
|
||||
typeof message.content === 'string'
|
||||
? message.content
|
||||
: message.content.map((part) => (part.type === 'text' ? part.text : '')).join(' ');
|
||||
|
||||
if (isLastUserMessage) {
|
||||
latestUserMessageContent = messageContent;
|
||||
} else {
|
||||
acc.push({
|
||||
role: roleMap[message.role] || CohereConstants.ROLE_USER,
|
||||
message: messageContent,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (
|
||||
chatHistory.length === 1 &&
|
||||
chatHistory[0].role === CohereConstants.ROLE_SYSTEM &&
|
||||
!latestUserMessageContent.length
|
||||
) {
|
||||
const message = chatHistory[0].message;
|
||||
latestUserMessageContent = message.includes(titleInstruction)
|
||||
? CohereConstants.TITLE_MESSAGE
|
||||
: '.';
|
||||
preamble = message;
|
||||
}
|
||||
|
||||
return {
|
||||
message: latestUserMessageContent,
|
||||
model: model,
|
||||
chatHistory,
|
||||
stream: stream ?? false,
|
||||
temperature: temperature,
|
||||
frequencyPenalty: frequency_penalty,
|
||||
presencePenalty: presence_penalty,
|
||||
maxTokens: max_tokens,
|
||||
stopSequences: stop,
|
||||
preamble,
|
||||
p: top_p,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createCoherePayload;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
const createCoherePayload = require('./createCoherePayload');
|
||||
|
||||
module.exports = {
|
||||
createCoherePayload,
|
||||
};
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
const { getBasePath } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
|
||||
/**
|
||||
* The `addImages` function corrects any erroneous image URLs in the `responseMessage.text`
|
||||
* and appends image observations from `intermediateSteps` if they are not already present.
|
||||
*
|
||||
* @function
|
||||
* @module addImages
|
||||
*
|
||||
* @param {Array.<Object>} intermediateSteps - An array of objects, each containing an observation.
|
||||
* @param {Object} responseMessage - An object containing the text property which might have image URLs.
|
||||
*
|
||||
* @property {string} intermediateSteps[].observation - The observation string which might contain an image markdown.
|
||||
* @property {string} responseMessage.text - The text which might contain image URLs.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const intermediateSteps = [
|
||||
* { observation: '' }
|
||||
* ];
|
||||
* const responseMessage = { text: 'Some text with ' };
|
||||
*
|
||||
* addImages(intermediateSteps, responseMessage);
|
||||
*
|
||||
* logger.debug(responseMessage.text);
|
||||
* // Outputs: 'Some text with \n'
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function addImages(intermediateSteps, responseMessage) {
|
||||
if (!intermediateSteps || !responseMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const basePath = getBasePath();
|
||||
|
||||
// Correct any erroneous URLs in the responseMessage.text first
|
||||
intermediateSteps.forEach((step) => {
|
||||
const { observation } = step;
|
||||
if (!observation || !observation.includes('![')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = observation.match(/\/images\/.*\.\w*/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const essentialImagePath = match[0];
|
||||
const fullImagePath = `${basePath}${essentialImagePath}`;
|
||||
|
||||
const regex = /!\[.*?\]\((.*?)\)/g;
|
||||
let matchErroneous;
|
||||
while ((matchErroneous = regex.exec(responseMessage.text)) !== null) {
|
||||
if (matchErroneous[1] && !matchErroneous[1].startsWith(`${basePath}/images/`)) {
|
||||
// Replace with the full path including base path
|
||||
responseMessage.text = responseMessage.text.replace(matchErroneous[1], fullImagePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now, check if the responseMessage already includes the correct image file path and append if not
|
||||
intermediateSteps.forEach((step) => {
|
||||
const { observation } = step;
|
||||
if (!observation || !observation.includes('![')) {
|
||||
return;
|
||||
}
|
||||
const observedImagePath = observation.match(/!\[[^(]*\]\([^)]*\)/g);
|
||||
if (observedImagePath) {
|
||||
// Fix the image path to include base path if it doesn't already
|
||||
let imageMarkdown = observedImagePath[0];
|
||||
const urlMatch = imageMarkdown.match(/\(([^)]+)\)/);
|
||||
if (
|
||||
urlMatch &&
|
||||
urlMatch[1] &&
|
||||
!urlMatch[1].startsWith(`${basePath}/images/`) &&
|
||||
urlMatch[1].startsWith('/images/')
|
||||
) {
|
||||
imageMarkdown = imageMarkdown.replace(urlMatch[1], `${basePath}${urlMatch[1]}`);
|
||||
}
|
||||
|
||||
if (!responseMessage.text.includes(imageMarkdown)) {
|
||||
responseMessage.text += '\n' + imageMarkdown;
|
||||
logger.debug('[addImages] added image from intermediateSteps:', imageMarkdown);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = addImages;
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
let addImages = require('./addImages');
|
||||
|
||||
describe('addImages', () => {
|
||||
let intermediateSteps;
|
||||
let responseMessage;
|
||||
let options;
|
||||
|
||||
beforeEach(() => {
|
||||
intermediateSteps = [];
|
||||
responseMessage = { text: '' };
|
||||
options = { debug: false };
|
||||
this.options = options;
|
||||
addImages = addImages.bind(this);
|
||||
});
|
||||
|
||||
it('should handle null or undefined parameters', () => {
|
||||
addImages(null, responseMessage);
|
||||
expect(responseMessage.text).toBe('');
|
||||
|
||||
addImages(intermediateSteps, null);
|
||||
expect(responseMessage.text).toBe('');
|
||||
|
||||
addImages(null, null);
|
||||
expect(responseMessage.text).toBe('');
|
||||
});
|
||||
|
||||
it('should append correct image markdown if not present in responseMessage', () => {
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should not append image markdown if already present in responseMessage', () => {
|
||||
responseMessage.text = '';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('');
|
||||
});
|
||||
|
||||
it('should correct and append image markdown with erroneous URL', () => {
|
||||
responseMessage.text = '';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('');
|
||||
});
|
||||
|
||||
it('should correct multiple erroneous URLs in responseMessage', () => {
|
||||
responseMessage.text =
|
||||
' ';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe(' ');
|
||||
});
|
||||
|
||||
it('should not append non-image markdown observations', () => {
|
||||
intermediateSteps.push({ observation: '[desc](/images/test.png)' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('');
|
||||
});
|
||||
|
||||
it('should handle multiple observations', () => {
|
||||
intermediateSteps.push({ observation: '' });
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n\n');
|
||||
});
|
||||
|
||||
it('should not append if observation does not contain image markdown', () => {
|
||||
intermediateSteps.push({ observation: 'This is a test observation without image markdown.' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('');
|
||||
});
|
||||
|
||||
it('should append correctly from a real scenario', () => {
|
||||
responseMessage.text =
|
||||
"Here is the generated image based on your request. It depicts a surreal landscape filled with floating musical notes. The style is impressionistic, with vibrant sunset hues dominating the scene. At the center, there's a silhouette of a grand piano, adding a dreamy emotion to the overall image. This could serve as a unique and creative music album cover. Would you like to make any changes or generate another image?";
|
||||
const originalText = responseMessage.text;
|
||||
const imageMarkdown = '';
|
||||
intermediateSteps.push({ observation: imageMarkdown });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe(`${originalText}\n${imageMarkdown}`);
|
||||
});
|
||||
|
||||
it('should extract only image markdowns when there is text between them', () => {
|
||||
const markdownWithTextBetweenImages = `
|
||||

|
||||
Some text between images that should not be included.
|
||||

|
||||
More text that should be ignored.
|
||||

|
||||
`;
|
||||
intermediateSteps.push({ observation: markdownWithTextBetweenImages });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should only return the first image when multiple images are present', () => {
|
||||
const markdownWithMultipleImages = `
|
||||

|
||||

|
||||

|
||||
`;
|
||||
intermediateSteps.push({ observation: markdownWithMultipleImages });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should not include any text or metadata surrounding the image markdown', () => {
|
||||
const markdownWithMetadata = `
|
||||
Title: Test Document
|
||||
Author: John Doe
|
||||

|
||||
Some content after the image.
|
||||
Vector values: [0.1, 0.2, 0.3]
|
||||
`;
|
||||
intermediateSteps.push({ observation: markdownWithMetadata });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle complex markdown with multiple images and only return the first one', () => {
|
||||
const complexMarkdown = `
|
||||
# Document Title
|
||||
|
||||
## Section 1
|
||||
Here's some text with an embedded image:
|
||||

|
||||
|
||||
## Section 2
|
||||
More text here...
|
||||

|
||||
|
||||
### Subsection
|
||||
Even more content
|
||||

|
||||
`;
|
||||
intermediateSteps.push({ observation: complexMarkdown });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
describe('basePath functionality', () => {
|
||||
let originalDomainClient;
|
||||
|
||||
beforeEach(() => {
|
||||
originalDomainClient = process.env.DOMAIN_CLIENT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.DOMAIN_CLIENT = originalDomainClient;
|
||||
});
|
||||
|
||||
it('should prepend base path to image URLs when DOMAIN_CLIENT is set', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should not prepend base path when image URL already has base path', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should correct erroneous URLs with base path', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
responseMessage.text = '';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty base path (root deployment)', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle missing DOMAIN_CLIENT', () => {
|
||||
delete process.env.DOMAIN_CLIENT;
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle observation without image path match', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle nested subdirectories in base path', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/apps/librechat';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle multiple observations with mixed base path scenarios', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe(
|
||||
'\n\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle complex markdown with base path', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
const complexMarkdown = `
|
||||
# Document Title
|
||||

|
||||
Some text between images
|
||||

|
||||
`;
|
||||
intermediateSteps.push({ observation: complexMarkdown });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle URLs that are already absolute', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
intermediateSteps.push({ observation: '' });
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle data URLs', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
intermediateSteps.push({
|
||||
observation:
|
||||
'',
|
||||
});
|
||||
addImages(intermediateSteps, responseMessage);
|
||||
expect(responseMessage.text).toBe(
|
||||
'\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
const { instructions, imageInstructions, errorInstructions } = require('../prompts');
|
||||
|
||||
function getActions(actions = [], functionsAgent = false) {
|
||||
let output = 'Internal thoughts & actions taken:\n"';
|
||||
|
||||
if (actions[0]?.action && functionsAgent) {
|
||||
actions = actions.map((step) => ({
|
||||
log: `Action: ${step.action?.tool || ''}\nInput: ${
|
||||
JSON.stringify(step.action?.toolInput) || ''
|
||||
}\nObservation: ${step.observation}`,
|
||||
}));
|
||||
} else if (actions[0]?.action) {
|
||||
actions = actions.map((step) => ({
|
||||
log: `${step.action.log}\nObservation: ${step.observation}`,
|
||||
}));
|
||||
}
|
||||
|
||||
actions.forEach((actionObj, index) => {
|
||||
output += `${actionObj.log}`;
|
||||
if (index < actions.length - 1) {
|
||||
output += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
return output + '"';
|
||||
}
|
||||
|
||||
function buildErrorInput({ message, errorMessage, actions, functionsAgent }) {
|
||||
const log = errorMessage.includes('Could not parse LLM output:')
|
||||
? `A formatting error occurred with your response to the human's last message. You didn't follow the formatting instructions. Remember to ${instructions}`
|
||||
: `You encountered an error while replying to the human's last message. Attempt to answer again or admit an answer cannot be given.\nError: ${errorMessage}`;
|
||||
|
||||
return `
|
||||
${log}
|
||||
|
||||
${getActions(actions, functionsAgent)}
|
||||
|
||||
Human's last message: ${message}
|
||||
`;
|
||||
}
|
||||
|
||||
function buildPromptPrefix({ result, message, functionsAgent }) {
|
||||
if ((result.output && result.output.includes('N/A')) || result.output === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
result?.intermediateSteps?.length === 1 &&
|
||||
result?.intermediateSteps[0]?.action?.toolInput === 'N/A'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const internalActions =
|
||||
result?.intermediateSteps?.length > 0
|
||||
? getActions(result.intermediateSteps, functionsAgent)
|
||||
: 'Internal Actions Taken: None';
|
||||
|
||||
const toolBasedInstructions = internalActions.toLowerCase().includes('image')
|
||||
? imageInstructions
|
||||
: '';
|
||||
|
||||
const errorMessage = result.errorMessage ? `${errorInstructions} ${result.errorMessage}\n` : '';
|
||||
|
||||
const preliminaryAnswer =
|
||||
result.output?.length > 0 ? `Preliminary Answer: "${result.output.trim()}"` : '';
|
||||
const prefix = preliminaryAnswer
|
||||
? 'review and improve the answer you generated using plugins in response to the User Message below. The user hasn\'t seen your answer or thoughts yet.'
|
||||
: 'respond to the User Message below based on your preliminary thoughts & actions.';
|
||||
|
||||
return `As a helpful AI Assistant, ${prefix}${errorMessage}\n${internalActions}
|
||||
${preliminaryAnswer}
|
||||
Reply conversationally to the User based on your ${
|
||||
preliminaryAnswer ? 'preliminary answer, ' : ''
|
||||
}internal actions, thoughts, and observations, making improvements wherever possible, but do not modify URLs.
|
||||
${
|
||||
preliminaryAnswer
|
||||
? ''
|
||||
: '\nIf there is an incomplete thought or action, you are expected to complete it in your response now.\n'
|
||||
}You must cite sources if you are using any web links. ${toolBasedInstructions}
|
||||
Only respond with your conversational reply to the following User Message:
|
||||
"${message}"`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildErrorInput,
|
||||
buildPromptPrefix,
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const addImages = require('./addImages');
|
||||
const handleOutputs = require('./handleOutputs');
|
||||
|
||||
module.exports = {
|
||||
addImages,
|
||||
...handleOutputs,
|
||||
};
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// Escaping curly braces is necessary for LangChain to correctly process the prompt
|
||||
function escapeBraces(str) {
|
||||
return str
|
||||
.replace(/({{2,})|(}{2,})/g, (match) => `${match[0]}`)
|
||||
.replace(/{|}/g, (match) => `${match}${match}`);
|
||||
}
|
||||
|
||||
function getSnippet(text) {
|
||||
let limit = 50;
|
||||
let splitText = escapeBraces(text).split(' ');
|
||||
|
||||
if (splitText.length === 1 && splitText[0].length > limit) {
|
||||
return splitText[0].substring(0, limit);
|
||||
}
|
||||
|
||||
let result = '';
|
||||
let spaceCount = 0;
|
||||
|
||||
for (let i = 0; i < splitText.length; i++) {
|
||||
if (result.length + splitText[i].length <= limit) {
|
||||
result += splitText[i] + ' ';
|
||||
spaceCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if (spaceCount == 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
escapeBraces,
|
||||
getSnippet,
|
||||
};
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
const formatMessages = require('./formatMessages');
|
||||
const summaryPrompts = require('./summaryPrompts');
|
||||
const handleInputs = require('./handleInputs');
|
||||
const instructions = require('./instructions');
|
||||
const truncate = require('./truncate');
|
||||
const createVisionPrompt = require('./createVisionPrompt');
|
||||
const createContextHandlers = require('./createContextHandlers');
|
||||
|
|
@ -9,8 +7,6 @@ const createContextHandlers = require('./createContextHandlers');
|
|||
module.exports = {
|
||||
...formatMessages,
|
||||
...summaryPrompts,
|
||||
...handleInputs,
|
||||
...instructions,
|
||||
...truncate,
|
||||
createVisionPrompt,
|
||||
createContextHandlers,
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
instructions:
|
||||
'Remember, all your responses MUST be in the format described. Do not respond unless it\'s in the format described, using the structure of Action, Action Input, etc.',
|
||||
errorInstructions:
|
||||
'\nYou encountered an error in attempting a response. The user is not aware of the error so you shouldn\'t mention it.\nReview the actions taken carefully in case there is a partial or complete answer within them.\nError Message:',
|
||||
imageInstructions:
|
||||
'You must include the exact image paths from above, formatted in Markdown syntax: ',
|
||||
completionInstructions:
|
||||
'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI. Respond conversationally.\nCurrent date:',
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,630 +0,0 @@
|
|||
jest.mock('~/cache/getLogStores');
|
||||
require('dotenv').config();
|
||||
const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const OpenAIClient = require('../OpenAIClient');
|
||||
jest.mock('meilisearch');
|
||||
|
||||
jest.mock('~/db/connect');
|
||||
jest.mock('~/models', () => ({
|
||||
User: jest.fn(),
|
||||
Key: jest.fn(),
|
||||
Session: jest.fn(),
|
||||
Balance: jest.fn(),
|
||||
Transaction: jest.fn(),
|
||||
getMessages: jest.fn().mockResolvedValue([]),
|
||||
saveMessage: jest.fn(),
|
||||
updateMessage: jest.fn(),
|
||||
deleteMessagesSince: jest.fn(),
|
||||
deleteMessages: jest.fn(),
|
||||
getConvoTitle: jest.fn(),
|
||||
getConvo: jest.fn(),
|
||||
saveConvo: jest.fn(),
|
||||
deleteConvos: jest.fn(),
|
||||
getPreset: jest.fn(),
|
||||
getPresets: jest.fn(),
|
||||
savePreset: jest.fn(),
|
||||
deletePresets: jest.fn(),
|
||||
findFileById: jest.fn(),
|
||||
createFile: jest.fn(),
|
||||
updateFile: jest.fn(),
|
||||
deleteFile: jest.fn(),
|
||||
deleteFiles: jest.fn(),
|
||||
getFiles: jest.fn(),
|
||||
updateFileUsage: jest.fn(),
|
||||
}));
|
||||
|
||||
// Import the actual module but mock specific parts
|
||||
const agents = jest.requireActual('@librechat/agents');
|
||||
const { CustomOpenAIClient } = agents;
|
||||
|
||||
// Also mock ChatOpenAI to prevent real API calls
|
||||
agents.ChatOpenAI = jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
});
|
||||
agents.AzureChatOpenAI = jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
});
|
||||
|
||||
// Mock only the CustomOpenAIClient constructor
|
||||
jest.spyOn(CustomOpenAIClient, 'constructor').mockImplementation(function (...options) {
|
||||
return new CustomOpenAIClient(...options);
|
||||
});
|
||||
|
||||
const finalChatCompletion = jest.fn().mockResolvedValue({
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'Mock message content' },
|
||||
finish_reason: 'Mock finish reason',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const stream = jest.fn().mockImplementation(() => {
|
||||
let isDone = false;
|
||||
let isError = false;
|
||||
let errorCallback = null;
|
||||
|
||||
const onEventHandlers = {
|
||||
abort: () => {
|
||||
// Mock abort behavior
|
||||
},
|
||||
error: (callback) => {
|
||||
errorCallback = callback; // Save the error callback for later use
|
||||
},
|
||||
finalMessage: (callback) => {
|
||||
callback({ role: 'assistant', content: 'Mock Response' });
|
||||
isDone = true; // Set stream to done
|
||||
},
|
||||
};
|
||||
|
||||
const mockStream = {
|
||||
on: jest.fn((event, callback) => {
|
||||
if (onEventHandlers[event]) {
|
||||
onEventHandlers[event](callback);
|
||||
}
|
||||
return mockStream;
|
||||
}),
|
||||
finalChatCompletion,
|
||||
controller: { abort: jest.fn() },
|
||||
triggerError: () => {
|
||||
isError = true;
|
||||
if (errorCallback) {
|
||||
errorCallback(new Error('Mock error'));
|
||||
}
|
||||
},
|
||||
[Symbol.asyncIterator]: () => {
|
||||
return {
|
||||
next: () => {
|
||||
if (isError) {
|
||||
return Promise.reject(new Error('Mock error'));
|
||||
}
|
||||
if (isDone) {
|
||||
return Promise.resolve({ done: true });
|
||||
}
|
||||
const chunk = { choices: [{ delta: { content: 'Mock chunk' } }] };
|
||||
return Promise.resolve({ value: chunk, done: false });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return mockStream;
|
||||
});
|
||||
|
||||
const create = jest.fn().mockResolvedValue({
|
||||
choices: [
|
||||
{
|
||||
message: { content: 'Mock message content' },
|
||||
finish_reason: 'Mock finish reason',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Mock the implementation of CustomOpenAIClient instances
|
||||
jest.spyOn(CustomOpenAIClient.prototype, 'constructor').mockImplementation(function () {
|
||||
return this;
|
||||
});
|
||||
|
||||
// Create a mock for the CustomOpenAIClient class
|
||||
const mockCustomOpenAIClient = jest.fn().mockImplementation(() => ({
|
||||
beta: {
|
||||
chat: {
|
||||
completions: {
|
||||
stream,
|
||||
},
|
||||
},
|
||||
},
|
||||
chat: {
|
||||
completions: {
|
||||
create,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
CustomOpenAIClient.mockImplementation = mockCustomOpenAIClient;
|
||||
|
||||
describe('OpenAIClient', () => {
|
||||
beforeEach(() => {
|
||||
const mockCache = {
|
||||
get: jest.fn().mockResolvedValue({}),
|
||||
set: jest.fn(),
|
||||
};
|
||||
getLogStores.mockReturnValue(mockCache);
|
||||
});
|
||||
let client;
|
||||
const model = 'gpt-4';
|
||||
const parentMessageId = '1';
|
||||
const messages = [
|
||||
{ role: 'user', sender: 'User', text: 'Hello', messageId: parentMessageId },
|
||||
{ role: 'assistant', sender: 'Assistant', text: 'Hi', messageId: '2' },
|
||||
];
|
||||
|
||||
const defaultOptions = {
|
||||
// debug: true,
|
||||
req: {},
|
||||
openaiApiKey: 'new-api-key',
|
||||
modelOptions: {
|
||||
model,
|
||||
temperature: 0.7,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultAzureOptions = {
|
||||
azureOpenAIApiInstanceName: 'your-instance-name',
|
||||
azureOpenAIApiDeploymentName: 'your-deployment-name',
|
||||
azureOpenAIApiVersion: '2020-07-01-preview',
|
||||
};
|
||||
|
||||
let originalWarn;
|
||||
|
||||
beforeAll(() => {
|
||||
originalWarn = console.warn;
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
console.warn.mockClear();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const options = { ...defaultOptions };
|
||||
client = new OpenAIClient('test-api-key', options);
|
||||
client.summarizeMessages = jest.fn().mockResolvedValue({
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 30,
|
||||
});
|
||||
client.buildPrompt = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ prompt: messages.map((m) => m.text).join('\n') });
|
||||
client.getMessages = jest.fn().mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe('setOptions', () => {
|
||||
it('should set the options correctly', () => {
|
||||
expect(client.apiKey).toBe('new-api-key');
|
||||
expect(client.modelOptions.model).toBe(model);
|
||||
expect(client.modelOptions.temperature).toBe(0.7);
|
||||
});
|
||||
|
||||
it('should set FORCE_PROMPT based on OPENAI_FORCE_PROMPT or reverseProxyUrl', () => {
|
||||
process.env.OPENAI_FORCE_PROMPT = 'true';
|
||||
client.setOptions({});
|
||||
expect(client.FORCE_PROMPT).toBe(true);
|
||||
delete process.env.OPENAI_FORCE_PROMPT; // Cleanup
|
||||
client.FORCE_PROMPT = undefined;
|
||||
|
||||
client.setOptions({ reverseProxyUrl: 'https://example.com/completions' });
|
||||
expect(client.FORCE_PROMPT).toBe(true);
|
||||
client.FORCE_PROMPT = undefined;
|
||||
|
||||
client.setOptions({ reverseProxyUrl: 'https://example.com/chat' });
|
||||
expect(client.FORCE_PROMPT).toBe(false);
|
||||
});
|
||||
|
||||
it('should set isChatCompletion based on useOpenRouter, reverseProxyUrl, or model', () => {
|
||||
client.setOptions({ reverseProxyUrl: null });
|
||||
// true by default since default model will be gpt-4o-mini
|
||||
expect(client.isChatCompletion).toBe(true);
|
||||
client.isChatCompletion = undefined;
|
||||
|
||||
// false because completions url will force prompt payload
|
||||
client.setOptions({ reverseProxyUrl: 'https://example.com/completions' });
|
||||
expect(client.isChatCompletion).toBe(false);
|
||||
client.isChatCompletion = undefined;
|
||||
|
||||
client.setOptions({ modelOptions: { model: 'gpt-4o-mini' }, reverseProxyUrl: null });
|
||||
expect(client.isChatCompletion).toBe(true);
|
||||
});
|
||||
|
||||
it('should set completionsUrl and langchainProxy based on reverseProxyUrl', () => {
|
||||
client.setOptions({ reverseProxyUrl: 'https://localhost:8080/v1/chat/completions' });
|
||||
expect(client.completionsUrl).toBe('https://localhost:8080/v1/chat/completions');
|
||||
expect(client.langchainProxy).toBe('https://localhost:8080/v1');
|
||||
|
||||
client.setOptions({ reverseProxyUrl: 'https://example.com/completions' });
|
||||
expect(client.completionsUrl).toBe('https://example.com/completions');
|
||||
expect(client.langchainProxy).toBe('https://example.com/completions');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOptions with Simplified Azure Integration', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.AZURE_OPENAI_DEFAULT_MODEL;
|
||||
delete process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME;
|
||||
});
|
||||
|
||||
const azureOpenAIApiInstanceName = 'test-instance';
|
||||
const azureOpenAIApiDeploymentName = 'test-deployment';
|
||||
const azureOpenAIApiVersion = '2020-07-01-preview';
|
||||
|
||||
const createOptions = (model) => ({
|
||||
modelOptions: { model },
|
||||
azure: {
|
||||
azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiVersion,
|
||||
},
|
||||
});
|
||||
|
||||
it('should set model from AZURE_OPENAI_DEFAULT_MODEL when Azure is enabled', () => {
|
||||
process.env.AZURE_OPENAI_DEFAULT_MODEL = 'gpt-4-azure';
|
||||
const options = createOptions('test');
|
||||
client.azure = options.azure;
|
||||
client.setOptions(options);
|
||||
expect(client.modelOptions.model).toBe('gpt-4-azure');
|
||||
});
|
||||
|
||||
it('should not change model if Azure is not enabled', () => {
|
||||
process.env.AZURE_OPENAI_DEFAULT_MODEL = 'gpt-4-azure';
|
||||
const originalModel = 'test';
|
||||
client.azure = false;
|
||||
client.setOptions(createOptions('test'));
|
||||
expect(client.modelOptions.model).toBe(originalModel);
|
||||
});
|
||||
|
||||
it('should not change model if AZURE_OPENAI_DEFAULT_MODEL is not set and model is passed', () => {
|
||||
const originalModel = 'GROK-LLM';
|
||||
const options = createOptions(originalModel);
|
||||
client.azure = options.azure;
|
||||
client.setOptions(options);
|
||||
expect(client.modelOptions.model).toBe(originalModel);
|
||||
});
|
||||
|
||||
it('should change model if AZURE_OPENAI_DEFAULT_MODEL is set and model is passed', () => {
|
||||
process.env.AZURE_OPENAI_DEFAULT_MODEL = 'gpt-4-azure';
|
||||
const originalModel = 'GROK-LLM';
|
||||
const options = createOptions(originalModel);
|
||||
client.azure = options.azure;
|
||||
client.setOptions(options);
|
||||
expect(client.modelOptions.model).toBe(process.env.AZURE_OPENAI_DEFAULT_MODEL);
|
||||
});
|
||||
|
||||
it('should include model in deployment name if AZURE_USE_MODEL_AS_DEPLOYMENT_NAME is set', () => {
|
||||
process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME = 'true';
|
||||
const model = 'gpt-4-azure';
|
||||
|
||||
const AzureClient = new OpenAIClient('test-api-key', createOptions(model));
|
||||
|
||||
const expectedValue = `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${model}/chat/completions?api-version=${azureOpenAIApiVersion}`;
|
||||
|
||||
expect(AzureClient.modelOptions.model).toBe(model);
|
||||
expect(AzureClient.azureEndpoint).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('should include model in deployment name if AZURE_USE_MODEL_AS_DEPLOYMENT_NAME and default model is set', () => {
|
||||
const defaultModel = 'gpt-4-azure';
|
||||
process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME = 'true';
|
||||
process.env.AZURE_OPENAI_DEFAULT_MODEL = defaultModel;
|
||||
const model = 'gpt-4-this-is-a-test-model-name';
|
||||
|
||||
const AzureClient = new OpenAIClient('test-api-key', createOptions(model));
|
||||
|
||||
const expectedValue = `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${model}/chat/completions?api-version=${azureOpenAIApiVersion}`;
|
||||
|
||||
expect(AzureClient.modelOptions.model).toBe(defaultModel);
|
||||
expect(AzureClient.azureEndpoint).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('should not include model in deployment name if AZURE_USE_MODEL_AS_DEPLOYMENT_NAME is not set', () => {
|
||||
const model = 'gpt-4-azure';
|
||||
|
||||
const AzureClient = new OpenAIClient('test-api-key', createOptions(model));
|
||||
|
||||
const expectedValue = `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}/chat/completions?api-version=${azureOpenAIApiVersion}`;
|
||||
|
||||
expect(AzureClient.modelOptions.model).toBe(model);
|
||||
expect(AzureClient.azureEndpoint).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenCount', () => {
|
||||
it('should return the correct token count', () => {
|
||||
const count = client.getTokenCount('Hello, world!');
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSaveOptions', () => {
|
||||
it('should return the correct save options', () => {
|
||||
const options = client.getSaveOptions();
|
||||
expect(options).toHaveProperty('chatGptLabel');
|
||||
expect(options).toHaveProperty('modelLabel');
|
||||
expect(options).toHaveProperty('promptPrefix');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBuildMessagesOptions', () => {
|
||||
it('should return the correct build messages options', () => {
|
||||
const options = client.getBuildMessagesOptions({ promptPrefix: 'Hello' });
|
||||
expect(options).toHaveProperty('isChatCompletion');
|
||||
expect(options).toHaveProperty('promptPrefix');
|
||||
expect(options.promptPrefix).toBe('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildMessages', () => {
|
||||
it('should build messages correctly for chat completion', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
});
|
||||
|
||||
it('should build messages correctly for non-chat completion', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: false,
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
});
|
||||
|
||||
it('should build messages correctly with a promptPrefix', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
promptPrefix: 'Test Prefix',
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
const instructions = result.prompt.find((item) => item.content.includes('Test Prefix'));
|
||||
expect(instructions).toBeDefined();
|
||||
expect(instructions.content).toContain('Test Prefix');
|
||||
});
|
||||
|
||||
it('should handle context strategy correctly', async () => {
|
||||
client.contextStrategy = 'summarize';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
expect(result).toHaveProperty('tokenCountMap');
|
||||
});
|
||||
|
||||
it('should assign name property for user messages when options.name is set', async () => {
|
||||
client.options.name = 'Test User';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const hasUserWithName = result.prompt.some(
|
||||
(item) => item.role === 'user' && item.name === 'Test_User',
|
||||
);
|
||||
expect(hasUserWithName).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle promptPrefix from options when promptPrefix argument is not provided', async () => {
|
||||
client.options.promptPrefix = 'Test Prefix from options';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const instructions = result.prompt.find((item) =>
|
||||
item.content.includes('Test Prefix from options'),
|
||||
);
|
||||
expect(instructions.content).toContain('Test Prefix from options');
|
||||
});
|
||||
|
||||
it('should handle case when neither promptPrefix argument nor options.promptPrefix is set', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const instructions = result.prompt.find((item) => item.content.includes('Test Prefix'));
|
||||
expect(instructions).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle case when getMessagesForConversation returns null or an empty array', async () => {
|
||||
const messages = [];
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
expect(result.prompt).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenCountForMessage', () => {
|
||||
const example_messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are a helpful, pattern-following assistant that translates corporate jargon into plain English.',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
name: 'example_user',
|
||||
content: 'New synergies will help drive top-line growth.',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
name: 'example_assistant',
|
||||
content: 'Things working well together will increase revenue.',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
name: 'example_user',
|
||||
content:
|
||||
"Let's circle back when we have more bandwidth to touch base on opportunities for increased leverage.",
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
name: 'example_assistant',
|
||||
content: "Let's talk later when we're less busy about how to do better.",
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content:
|
||||
"This late pivot means we don't have time to boil the ocean for the client deliverable.",
|
||||
},
|
||||
];
|
||||
|
||||
const testCases = [
|
||||
{ model: 'gpt-3.5-turbo-0301', expected: 127 },
|
||||
{ model: 'gpt-3.5-turbo-0613', expected: 129 },
|
||||
{ model: 'gpt-3.5-turbo', expected: 129 },
|
||||
{ model: 'gpt-4-0314', expected: 129 },
|
||||
{ model: 'gpt-4-0613', expected: 129 },
|
||||
{ model: 'gpt-4', expected: 129 },
|
||||
{ model: 'unknown', expected: 129 },
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
it(`should return ${testCase.expected} tokens for model ${testCase.model}`, () => {
|
||||
client.modelOptions.model = testCase.model;
|
||||
// 3 tokens for assistant label
|
||||
let totalTokens = 3;
|
||||
for (let message of example_messages) {
|
||||
totalTokens += client.getTokenCountForMessage(message);
|
||||
}
|
||||
expect(totalTokens).toBe(testCase.expected);
|
||||
});
|
||||
});
|
||||
|
||||
const vision_request = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'describe what is in this image?',
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'https://venturebeat.com/wp-content/uploads/2019/03/openai-1.png',
|
||||
detail: 'high',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expectedTokens = 14;
|
||||
const visionModel = 'gpt-4-vision-preview';
|
||||
|
||||
it(`should return ${expectedTokens} tokens for model ${visionModel} (Vision Request)`, () => {
|
||||
client.modelOptions.model = visionModel;
|
||||
// 3 tokens for assistant label
|
||||
let totalTokens = 3;
|
||||
for (let message of vision_request) {
|
||||
totalTokens += client.getTokenCountForMessage(message);
|
||||
}
|
||||
expect(totalTokens).toBe(expectedTokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkVisionRequest functionality', () => {
|
||||
let client;
|
||||
const attachments = [{ type: 'image/png' }];
|
||||
|
||||
beforeEach(() => {
|
||||
client = new OpenAIClient('test-api-key', {
|
||||
endpoint: 'ollama',
|
||||
modelOptions: {
|
||||
model: 'initial-model',
|
||||
},
|
||||
modelsConfig: {
|
||||
ollama: ['initial-model', 'llava', 'other-model'],
|
||||
},
|
||||
});
|
||||
|
||||
client.defaultVisionModel = 'non-valid-default-model';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set "llava" as the model if it is the first valid model when default validation fails', () => {
|
||||
client.checkVisionRequest(attachments);
|
||||
|
||||
expect(client.modelOptions.model).toBe('llava');
|
||||
expect(client.isVisionModel).toBeTruthy();
|
||||
expect(client.modelOptions.stop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStreamUsage', () => {
|
||||
it('should return this.usage when completion_tokens_details is null', () => {
|
||||
const client = new OpenAIClient('test-api-key', defaultOptions);
|
||||
client.usage = {
|
||||
completion_tokens_details: null,
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 20,
|
||||
};
|
||||
client.inputTokensKey = 'prompt_tokens';
|
||||
client.outputTokensKey = 'completion_tokens';
|
||||
|
||||
const result = client.getStreamUsage();
|
||||
|
||||
expect(result).toEqual(client.usage);
|
||||
});
|
||||
|
||||
it('should return this.usage when completion_tokens_details is missing reasoning_tokens', () => {
|
||||
const client = new OpenAIClient('test-api-key', defaultOptions);
|
||||
client.usage = {
|
||||
completion_tokens_details: {
|
||||
other_tokens: 5,
|
||||
},
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 20,
|
||||
};
|
||||
client.inputTokensKey = 'prompt_tokens';
|
||||
client.outputTokensKey = 'completion_tokens';
|
||||
|
||||
const result = client.getStreamUsage();
|
||||
|
||||
expect(result).toEqual(client.usage);
|
||||
});
|
||||
|
||||
it('should calculate output tokens correctly when completion_tokens_details is present with reasoning_tokens', () => {
|
||||
const client = new OpenAIClient('test-api-key', defaultOptions);
|
||||
client.usage = {
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 30,
|
||||
other_tokens: 5,
|
||||
},
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 20,
|
||||
};
|
||||
client.inputTokensKey = 'prompt_tokens';
|
||||
client.outputTokensKey = 'completion_tokens';
|
||||
|
||||
const result = client.getStreamUsage();
|
||||
|
||||
expect(result).toEqual({
|
||||
reasoning_tokens: 30,
|
||||
other_tokens: 5,
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 10, // |30 - 20| = 10
|
||||
});
|
||||
});
|
||||
|
||||
it('should return this.usage when it is undefined', () => {
|
||||
const client = new OpenAIClient('test-api-key', defaultOptions);
|
||||
client.usage = undefined;
|
||||
|
||||
const result = client.getStreamUsage();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
This is a test script to see how much memory is used by the client when encoding.
|
||||
On my work machine, it was able to process 10,000 encoding requests / 48.686 seconds = approximately 205.4 RPS
|
||||
I've significantly reduced the amount of encoding needed by saving token counts in the database, so these
|
||||
numbers should only be hit with a large amount of concurrent users
|
||||
It would take 103 concurrent users sending 1 message every 1 second to hit these numbers, which is rather unrealistic,
|
||||
and at that point, out-sourcing the encoding to a separate server would be a better solution
|
||||
Also, for scaling, could increase the rate at which the encoder resets; the trade-off is more resource usage on the server.
|
||||
Initial memory usage: 25.93 megabytes
|
||||
Peak memory usage: 55 megabytes
|
||||
Final memory usage: 28.03 megabytes
|
||||
Post-test (timeout of 15s): 21.91 megabytes
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { OpenAIClient } = require('../');
|
||||
|
||||
function timeout(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const text = `
|
||||
The standard Lorem Ipsum passage, used since the 1500s
|
||||
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."
|
||||
`;
|
||||
const model = 'gpt-3.5-turbo';
|
||||
let maxContextTokens = 4095;
|
||||
if (model === 'gpt-4') {
|
||||
maxContextTokens = 8191;
|
||||
} else if (model === 'gpt-4-32k') {
|
||||
maxContextTokens = 32767;
|
||||
}
|
||||
const clientOptions = {
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
maxContextTokens,
|
||||
modelOptions: {
|
||||
model,
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: true,
|
||||
};
|
||||
|
||||
let apiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
const maxMemory = 0.05 * 1024 * 1024 * 1024;
|
||||
|
||||
// Calculate initial percentage of memory used
|
||||
const initialMemoryUsage = process.memoryUsage().heapUsed;
|
||||
|
||||
function printProgressBar(percentageUsed) {
|
||||
const filledBlocks = Math.round(percentageUsed / 2); // Each block represents 2%
|
||||
const emptyBlocks = 50 - filledBlocks; // Total blocks is 50 (each represents 2%), so the rest are empty
|
||||
const progressBar =
|
||||
'[' +
|
||||
'█'.repeat(filledBlocks) +
|
||||
' '.repeat(emptyBlocks) +
|
||||
'] ' +
|
||||
percentageUsed.toFixed(2) +
|
||||
'%';
|
||||
console.log(progressBar);
|
||||
}
|
||||
|
||||
const iterations = 10000;
|
||||
console.time('loopTime');
|
||||
// Trying to catch the error doesn't help; all future calls will immediately crash
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
try {
|
||||
console.log(`Iteration ${i}`);
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
|
||||
client.getTokenCount(text);
|
||||
// const encoder = client.constructor.getTokenizer('cl100k_base');
|
||||
// console.log(`Iteration ${i}: call encode()...`);
|
||||
// encoder.encode(text, 'all');
|
||||
// encoder.free();
|
||||
|
||||
const memoryUsageDuringLoop = process.memoryUsage().heapUsed;
|
||||
const percentageUsed = (memoryUsageDuringLoop / maxMemory) * 100;
|
||||
printProgressBar(percentageUsed);
|
||||
|
||||
if (i === iterations - 1) {
|
||||
console.log(' done');
|
||||
// encoder.free();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`caught error! in Iteration ${i}`);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd('loopTime');
|
||||
// Calculate final percentage of memory used
|
||||
const finalMemoryUsage = process.memoryUsage().heapUsed;
|
||||
// const finalPercentageUsed = finalMemoryUsage / maxMemory * 100;
|
||||
console.log(`Initial memory usage: ${initialMemoryUsage / 1024 / 1024} megabytes`);
|
||||
console.log(`Final memory usage: ${finalMemoryUsage / 1024 / 1024} megabytes`);
|
||||
await timeout(15000);
|
||||
const memoryUsageAfterTimeout = process.memoryUsage().heapUsed;
|
||||
console.log(`Post timeout: ${memoryUsageAfterTimeout / 1024 / 1024} megabytes`);
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
if (!err.message.includes('fetch failed')) {
|
||||
console.error('There was an uncaught error:');
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (err.message.includes('fetch failed')) {
|
||||
console.log('fetch failed error caught');
|
||||
// process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Ai PDF",
|
||||
"name_for_model": "Ai_PDF",
|
||||
"description_for_human": "Super-fast, interactive chats with PDFs of any size, complete with page references for fact checking.",
|
||||
"description_for_model": "Provide a URL to a PDF and search the document. Break the user question in multiple semantic search queries and calls as needed. Think step by step.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/logo.png",
|
||||
"contact_email": "support@promptapps.ai",
|
||||
"legal_info_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/legal.html"
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "BrowserOp",
|
||||
"name_for_model": "BrowserOp",
|
||||
"description_for_human": "Browse dozens of webpages in one query. Fetch information more efficiently.",
|
||||
"description_for_model": "This tool offers the feature for users to input a URL or multiple URLs and interact with them as needed. It's designed to comprehend the user's intent and proffer tailored suggestions in line with the content and functionality of the webpage at hand. Services like text rewrites, translations and more can be requested. When users need specific information to finish a task or if they intend to perform a search, this tool becomes a bridge to the search engine and generates responses based on the results. Whether the user is seeking information about restaurants, rentals, weather, or shopping, this tool connects to the internet and delivers the most recent results.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://testplugin.feednews.com/.well-known/openapi.yaml"
|
||||
},
|
||||
"logo_url": "https://openapi-af.op-mobile.opera.com/openapi/testplugin/.well-known/logo.png",
|
||||
"contact_email": "aiplugins-contact-list@opera.com",
|
||||
"legal_info_url": "https://legal.apexnews.com/terms/"
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Dr. Thoth's Tarot",
|
||||
"name_for_model": "Dr_Thoths_Tarot",
|
||||
"description_for_human": "Tarot card novelty entertainment & analysis, by Mnemosyne Labs.",
|
||||
"description_for_model": "Intelligent analysis program for tarot card entertaiment, data, & prompts, by Mnemosyne Labs, a division of AzothCorp.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://dr-thoth-tarot.herokuapp.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://dr-thoth-tarot.herokuapp.com/logo.png",
|
||||
"contact_email": "legal@AzothCorp.com",
|
||||
"legal_info_url": "http://AzothCorp.com/legal",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "Draw Card",
|
||||
"path": "/drawcard",
|
||||
"method": "GET",
|
||||
"description": "Generate a single tarot card from the deck of 78 cards."
|
||||
},
|
||||
{
|
||||
"name": "Occult Card",
|
||||
"path": "/occult_card",
|
||||
"method": "GET",
|
||||
"description": "Generate a tarot card using the specified planet's Kamea matrix.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "planet",
|
||||
"type": "string",
|
||||
"enum": ["Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon"],
|
||||
"required": true,
|
||||
"description": "The planet name to use the corresponding Kamea matrix."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Three Card Spread",
|
||||
"path": "/threecardspread",
|
||||
"method": "GET",
|
||||
"description": "Perform a three-card tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Celtic Cross Spread",
|
||||
"path": "/celticcross",
|
||||
"method": "GET",
|
||||
"description": "Perform a Celtic Cross tarot spread with 10 cards."
|
||||
},
|
||||
{
|
||||
"name": "Past, Present, Future Spread",
|
||||
"path": "/pastpresentfuture",
|
||||
"method": "GET",
|
||||
"description": "Perform a Past, Present, Future tarot spread with 3 cards."
|
||||
},
|
||||
{
|
||||
"name": "Horseshoe Spread",
|
||||
"path": "/horseshoe",
|
||||
"method": "GET",
|
||||
"description": "Perform a Horseshoe tarot spread with 7 cards."
|
||||
},
|
||||
{
|
||||
"name": "Relationship Spread",
|
||||
"path": "/relationship",
|
||||
"method": "GET",
|
||||
"description": "Perform a Relationship tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Career Spread",
|
||||
"path": "/career",
|
||||
"method": "GET",
|
||||
"description": "Perform a Career tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Yes/No Spread",
|
||||
"path": "/yesno",
|
||||
"method": "GET",
|
||||
"description": "Perform a Yes/No tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Chakra Spread",
|
||||
"path": "/chakra",
|
||||
"method": "GET",
|
||||
"description": "Perform a Chakra tarot spread with 7 cards."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_model": "DreamInterpreter",
|
||||
"name_for_human": "Dream Interpreter",
|
||||
"description_for_model": "Interprets your dreams using advanced techniques.",
|
||||
"description_for_human": "Interprets your dreams using advanced techniques.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://dreamplugin.bgnetmobile.com/.well-known/openapi.json",
|
||||
"has_user_authentication": false
|
||||
},
|
||||
"logo_url": "https://dreamplugin.bgnetmobile.com/.well-known/logo.png",
|
||||
"contact_email": "ismail.orkler@bgnetmobile.com",
|
||||
"legal_info_url": "https://dreamplugin.bgnetmobile.com/terms.html"
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "VoxScript",
|
||||
"name_for_model": "VoxScript",
|
||||
"description_for_human": "Enables searching of YouTube transcripts, financial data sources Google Search results, and more!",
|
||||
"description_for_model": "Plugin for searching through varius data sources.",
|
||||
"auth": {
|
||||
"type": "service_http",
|
||||
"authorization_type": "bearer",
|
||||
"verification_tokens": {
|
||||
"openai": "ffc5226d1af346c08a98dee7deec9f76"
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://voxscript.awt.icu/swagger/v1/swagger.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://voxscript.awt.icu/images/VoxScript_logo_32x32.png",
|
||||
"contact_email": "voxscript@allwiretech.com",
|
||||
"legal_info_url": "https://voxscript.awt.icu/legal/"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_model": "askyourpdf",
|
||||
"name_for_human": "AskYourPDF",
|
||||
"description_for_model": "This plugin is designed to expedite the extraction of information from PDF documents. It works by accepting a URL link to a PDF or a document ID (doc_id) from the user. If a URL is provided, the plugin first validates that it is a correct URL. \\nAfter validating the URL, the plugin proceeds to download the PDF and store its content in a vector database. If the user provides a doc_id, the plugin directly retrieves the document from the database. The plugin then scans through the stored PDFs to find answers to user queries or retrieve specific details.\\n\\nHowever, if an error occurs while querying the API, the user is prompted to download their document first, then manually upload it to [](https://askyourpdf.com/upload). Once the upload is complete, the user should copy the resulting doc_id and paste it back into the chat for further interaction.\nThe plugin is particularly useful when the user's question pertains to content within a PDF document. When providing answers, the plugin also specifies the page number (highlighted in bold) where the relevant information was found. Remember, the URL must be valid for a successful query. Failure to validate the URL may lead to errors or unsuccessful queries.",
|
||||
"description_for_human": "Unlock the power of your PDFs!, dive into your documents, find answers, and bring information to your fingertips.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "askyourpdf.yaml",
|
||||
"has_user_authentication": false
|
||||
},
|
||||
"logo_url": "https://plugin.askyourpdf.com/.well-known/logo.png",
|
||||
"contact_email": "plugin@askyourpdf.com",
|
||||
"legal_info_url": "https://askyourpdf.com/terms"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Drink Maestro",
|
||||
"name_for_model": "drink_maestro",
|
||||
"description_for_human": "Learn to mix any drink you can imagine (real or made-up), and discover new ones. Includes drink images.",
|
||||
"description_for_model": "You are a silly bartender/comic who knows how to make any drink imaginable. You provide recipes for specific drinks, suggest new drinks, and show pictures of drinks. Be creative in your descriptions and make jokes and puns. Use a lot of emojis. If the user makes a request in another language, send API call in English, and then translate the response.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.drinkmaestro.space/.well-known/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://i.imgur.com/6q8HWdz.png",
|
||||
"contact_email": "nikkmitchell@gmail.com",
|
||||
"legal_info_url": "https://github.com/nikkmitchell/DrinkMaestro/blob/main/Legal.txt"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Earth",
|
||||
"name_for_model": "earthImagesAndVisualizations",
|
||||
"description_for_human": "Generates a map image based on provided location, tilt and style.",
|
||||
"description_for_model": "Generates a map image based on provided coordinates or location, tilt and style, and even geoJson to provide markers, paths, and polygons. Responds with an image-link. For the styles choose one of these: [light, dark, streets, outdoors, satellite, satellite-streets]",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.earth-plugin.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://api.earth-plugin.com/logo.png",
|
||||
"contact_email": "contact@earth-plugin.com",
|
||||
"legal_info_url": "https://api.earth-plugin.com/legal.html"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Scholarly Graph Link",
|
||||
"name_for_model": "scholarly_graph_link",
|
||||
"description_for_human": "You can search papers, authors, datasets and software. It has access to Figshare, Arxiv, and many others.",
|
||||
"description_for_model": "Run GraphQL queries against an API hosted by DataCite API. The API supports most GraphQL query but does not support mutations statements. Use `{ __schema { types { name kind } } }` to get all the types in the GraphQL schema. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{person(id:ORCID) {works(first:50) {nodes {id titles(first: 1){title} publicationYear}}}}` to get the first 50 works of a person based on their ORCID. All Ids are urls, e.g., https://orcid.org/0012-0000-1012-1110. Mutations statements are not allowed.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.datacite.org/graphql-openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://raw.githubusercontent.com/kjgarza/scholarly_graph_link/master/logo.png",
|
||||
"contact_email": "kj.garza@gmail.com",
|
||||
"legal_info_url": "https://github.com/kjgarza/scholarly_graph_link/blob/master/LICENSE"
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "WebPilot",
|
||||
"name_for_model": "web_pilot",
|
||||
"description_for_human": "Browse & QA Webpage/PDF/Data. Generate articles, from one or more URLs.",
|
||||
"description_for_model": "This tool allows users to provide a URL(or URLs) and optionally requests for interacting with, extracting specific information or how to do with the content from the URL. Requests may include rewrite, translate, and others. If there any requests, when accessing the /api/visit-web endpoint, the parameter 'user_has_request' should be set to 'true. And if there's no any requests, 'user_has_request' should be set to 'false'.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://webreader.webpilotai.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://webreader.webpilotai.com/logo.png",
|
||||
"contact_email": "dev@webpilot.ai",
|
||||
"legal_info_url": "https://webreader.webpilotai.com/legal_info.html",
|
||||
"headers": {
|
||||
"id": "WebPilot-Friend-UID"
|
||||
},
|
||||
"params": {
|
||||
"user_has_request": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Image Prompt Enhancer",
|
||||
"name_for_model": "image_prompt_enhancer",
|
||||
"description_for_human": "Transform your ideas into complex, personalized image generation prompts.",
|
||||
"description_for_model": "Provides instructions for crafting an enhanced image prompt. Use this whenever the user wants to enhance a prompt.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://image-prompt-enhancer.gafo.tech/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://image-prompt-enhancer.gafo.tech/logo.png",
|
||||
"contact_email": "gafotech1@gmail.com",
|
||||
"legal_info_url": "https://image-prompt-enhancer.gafo.tech/legal"
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
openapi: 3.0.2
|
||||
info:
|
||||
title: FastAPI
|
||||
version: 0.1.0
|
||||
servers:
|
||||
- url: https://plugin.askyourpdf.com
|
||||
paths:
|
||||
/api/download_pdf:
|
||||
post:
|
||||
summary: Download Pdf
|
||||
description: Download a PDF file from a URL and save it to the vector database.
|
||||
operationId: download_pdf_api_download_pdf_post
|
||||
parameters:
|
||||
- required: true
|
||||
schema:
|
||||
title: Url
|
||||
type: string
|
||||
name: url
|
||||
in: query
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FileResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/query:
|
||||
post:
|
||||
summary: Perform Query
|
||||
description: Perform a query on a document.
|
||||
operationId: perform_query_query_post
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InputData'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResponseModel'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
components:
|
||||
schemas:
|
||||
DocumentMetadata:
|
||||
title: DocumentMetadata
|
||||
required:
|
||||
- source
|
||||
- page_number
|
||||
- author
|
||||
type: object
|
||||
properties:
|
||||
source:
|
||||
title: Source
|
||||
type: string
|
||||
page_number:
|
||||
title: Page Number
|
||||
type: integer
|
||||
author:
|
||||
title: Author
|
||||
type: string
|
||||
FileResponse:
|
||||
title: FileResponse
|
||||
required:
|
||||
- docId
|
||||
type: object
|
||||
properties:
|
||||
docId:
|
||||
title: Docid
|
||||
type: string
|
||||
error:
|
||||
title: Error
|
||||
type: string
|
||||
HTTPValidationError:
|
||||
title: HTTPValidationError
|
||||
type: object
|
||||
properties:
|
||||
detail:
|
||||
title: Detail
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
InputData:
|
||||
title: InputData
|
||||
required:
|
||||
- doc_id
|
||||
- query
|
||||
type: object
|
||||
properties:
|
||||
doc_id:
|
||||
title: Doc Id
|
||||
type: string
|
||||
query:
|
||||
title: Query
|
||||
type: string
|
||||
ResponseModel:
|
||||
title: ResponseModel
|
||||
required:
|
||||
- results
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
title: Results
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SearchResult'
|
||||
SearchResult:
|
||||
title: SearchResult
|
||||
required:
|
||||
- doc_id
|
||||
- text
|
||||
- metadata
|
||||
type: object
|
||||
properties:
|
||||
doc_id:
|
||||
title: Doc Id
|
||||
type: string
|
||||
text:
|
||||
title: Text
|
||||
type: string
|
||||
metadata:
|
||||
$ref: '#/components/schemas/DocumentMetadata'
|
||||
ValidationError:
|
||||
title: ValidationError
|
||||
required:
|
||||
- loc
|
||||
- msg
|
||||
- type
|
||||
type: object
|
||||
properties:
|
||||
loc:
|
||||
title: Location
|
||||
type: array
|
||||
items:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
msg:
|
||||
title: Message
|
||||
type: string
|
||||
type:
|
||||
title: Error Type
|
||||
type: string
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
openapi: 3.0.1
|
||||
info:
|
||||
title: ScholarAI
|
||||
description: Allows the user to search facts and findings from scientific articles
|
||||
version: 'v1'
|
||||
servers:
|
||||
- url: https://scholar-ai.net
|
||||
paths:
|
||||
/api/abstracts:
|
||||
get:
|
||||
operationId: searchAbstracts
|
||||
summary: Get relevant paper abstracts by keywords search
|
||||
parameters:
|
||||
- name: keywords
|
||||
in: query
|
||||
description: Keywords of inquiry which should appear in article. Must be in English.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sort
|
||||
in: query
|
||||
description: The sort order for results. Valid values are cited_by_count or publication_date. Excluding this value does a relevance based search.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- cited_by_count
|
||||
- publication_date
|
||||
- name: query
|
||||
in: query
|
||||
description: The user query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: peer_reviewed_only
|
||||
in: query
|
||||
description: Whether to only return peer reviewed articles. Defaults to true, ChatGPT should cautiously suggest this value can be set to false
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: start_year
|
||||
in: query
|
||||
description: The first year, inclusive, to include in the search range. Excluding this value will include all years.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: end_year
|
||||
in: query
|
||||
description: The last year, inclusive, to include in the search range. Excluding this value will include all years.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: offset
|
||||
in: query
|
||||
description: The offset of the first result to return. Defaults to 0.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/searchAbstractsResponse'
|
||||
/api/fulltext:
|
||||
get:
|
||||
operationId: getFullText
|
||||
summary: Get full text of a paper by URL for PDF
|
||||
parameters:
|
||||
- name: pdf_url
|
||||
in: query
|
||||
description: URL for PDF
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: chunk
|
||||
in: query
|
||||
description: chunk number to retrieve, defaults to 1
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/getFullTextResponse'
|
||||
/api/save-citation:
|
||||
get:
|
||||
operationId: saveCitation
|
||||
summary: Save citation to reference manager
|
||||
parameters:
|
||||
- name: doi
|
||||
in: query
|
||||
description: Digital Object Identifier (DOI) of article
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: zotero_user_id
|
||||
in: query
|
||||
description: Zotero User ID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: zotero_api_key
|
||||
in: query
|
||||
description: Zotero API Key
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/saveCitationResponse'
|
||||
components:
|
||||
schemas:
|
||||
searchAbstractsResponse:
|
||||
type: object
|
||||
properties:
|
||||
next_offset:
|
||||
type: number
|
||||
description: The offset of the next page of results.
|
||||
total_num_results:
|
||||
type: number
|
||||
description: The total number of results.
|
||||
abstracts:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
abstract:
|
||||
type: string
|
||||
description: Summary of the context, methods, results, and conclusions of the paper.
|
||||
doi:
|
||||
type: string
|
||||
description: The DOI of the paper.
|
||||
landing_page_url:
|
||||
type: string
|
||||
description: Link to the paper on its open-access host.
|
||||
pdf_url:
|
||||
type: string
|
||||
description: Link to the paper PDF.
|
||||
publicationDate:
|
||||
type: string
|
||||
description: The date the paper was published in YYYY-MM-DD format.
|
||||
relevance:
|
||||
type: number
|
||||
description: The relevance of the paper to the search query. 1 is the most relevant.
|
||||
creators:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: The name of the creator.
|
||||
cited_by_count:
|
||||
type: number
|
||||
description: The number of citations of the article.
|
||||
description: The list of relevant abstracts.
|
||||
getFullTextResponse:
|
||||
type: object
|
||||
properties:
|
||||
full_text:
|
||||
type: string
|
||||
description: The full text of the paper.
|
||||
pdf_url:
|
||||
type: string
|
||||
description: The PDF URL of the paper.
|
||||
chunk:
|
||||
type: number
|
||||
description: The chunk of the paper.
|
||||
total_chunk_num:
|
||||
type: number
|
||||
description: The total chunks of the paper.
|
||||
saveCitationResponse:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: Confirmation of successful save or error message.
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "QR Codes",
|
||||
"name_for_model": "qrCodes",
|
||||
"description_for_human": "Create QR codes.",
|
||||
"description_for_model": "Plugin for generating QR codes.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://chatgpt-qrcode-46d7d4ebefc8.herokuapp.com/openapi.yaml"
|
||||
},
|
||||
"logo_url": "https://chatgpt-qrcode-46d7d4ebefc8.herokuapp.com/logo.png",
|
||||
"contact_email": "chrismountzou@gmail.com",
|
||||
"legal_info_url": "https://raw.githubusercontent.com/mountzou/qrCodeGPTv1/master/legal"
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "ScholarAI",
|
||||
"name_for_model": "scholarai",
|
||||
"description_for_human": "Unleash scientific research: search 40M+ peer-reviewed papers, explore scientific PDFs, and save to reference managers.",
|
||||
"description_for_model": "Access open access scientific literature from peer-reviewed journals. The abstract endpoint finds relevant papers based on 2 to 6 keywords. After getting abstracts, ALWAYS prompt the user offering to go into more detail. Use the fulltext endpoint to retrieve the entire paper's text and access specific details using the provided pdf_url, if available. ALWAYS hyperlink the pdf_url from the responses if available. Offer to dive into the fulltext or search for additional papers. Always ask if the user wants save any paper to the user’s Zotero reference manager by using the save-citation endpoint and providing the doi and requesting the user’s zotero_user_id and zotero_api_key.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "scholarai.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"params": {
|
||||
"sort": "cited_by_count"
|
||||
},
|
||||
"logo_url": "https://scholar-ai.net/logo.png",
|
||||
"contact_email": "lakshb429@gmail.com",
|
||||
"legal_info_url": "https://scholar-ai.net/legal.txt",
|
||||
"HttpAuthorizationType": "basic"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Uberchord",
|
||||
"name_for_model": "uberchord",
|
||||
"description_for_human": "Find guitar chord diagrams by specifying the chord name.",
|
||||
"description_for_model": "Fetch guitar chord diagrams, their positions on the guitar fretboard.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://guitarchords.pluginboost.com/.well-known/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://guitarchords.pluginboost.com/logo.png",
|
||||
"contact_email": "info.bluelightweb@gmail.com",
|
||||
"legal_info_url": "https://guitarchords.pluginboost.com/legal"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Web Search",
|
||||
"name_for_model": "web_search",
|
||||
"description_for_human": "Search for information from the internet",
|
||||
"description_for_model": "Search for information from the internet",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://websearch.plugsugar.com/api/openapi_yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://websearch.plugsugar.com/200x200.png",
|
||||
"contact_email": "support@plugsugar.com",
|
||||
"legal_info_url": "https://websearch.plugsugar.com/contact"
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
const OpenAI = require('openai');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
|
||||
/**
|
||||
* Handles errors that may occur when making requests to OpenAI's API.
|
||||
* It checks the instance of the error and prints a specific warning message
|
||||
* to the console depending on the type of error encountered.
|
||||
* It then calls an optional error callback function with the error object.
|
||||
*
|
||||
* @param {Error} err - The error object thrown by OpenAI API.
|
||||
* @param {Function} errorCallback - A callback function that is called with the error object.
|
||||
* @param {string} [context='stream'] - A string providing context where the error occurred, defaults to 'stream'.
|
||||
*/
|
||||
async function handleOpenAIErrors(err, errorCallback, context = 'stream') {
|
||||
if (err instanceof OpenAI.APIError && err?.message?.includes('abort')) {
|
||||
logger.warn(`[OpenAIClient.chatCompletion][${context}] Aborted Message`);
|
||||
}
|
||||
if (err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason')) {
|
||||
logger.warn(`[OpenAIClient.chatCompletion][${context}] Missing finish_reason`);
|
||||
} else if (err instanceof OpenAI.APIError) {
|
||||
logger.warn(`[OpenAIClient.chatCompletion][${context}] API error`);
|
||||
} else {
|
||||
logger.warn(`[OpenAIClient.chatCompletion][${context}] Unhandled error type`);
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
|
||||
if (errorCallback) {
|
||||
errorCallback(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = handleOpenAIErrors;
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
const { validateTools, loadTools } = require('./handleTools');
|
||||
const handleOpenAIErrors = require('./handleOpenAIErrors');
|
||||
|
||||
module.exports = {
|
||||
handleOpenAIErrors,
|
||||
validateTools,
|
||||
loadTools,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ module.exports = {
|
|||
roots: ['<rootDir>'],
|
||||
coverageDirectory: 'coverage',
|
||||
testTimeout: 30000, // 30 seconds timeout for all tests
|
||||
setupFiles: [
|
||||
'./test/jestSetup.js',
|
||||
'./test/__mocks__/logger.js',
|
||||
'./test/__mocks__/fetchEventSource.js',
|
||||
],
|
||||
setupFiles: ['./test/jestSetup.js', './test/__mocks__/logger.js'],
|
||||
moduleNameMapper: {
|
||||
'~/(.*)': '<rootDir>/$1',
|
||||
'~/data/auth.json': '<rootDir>/__mocks__/auth.mock.json',
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
function mergeSort(arr, compareFn) {
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const mid = Math.floor(arr.length / 2);
|
||||
const leftArr = arr.slice(0, mid);
|
||||
const rightArr = arr.slice(mid);
|
||||
|
||||
return merge(mergeSort(leftArr, compareFn), mergeSort(rightArr, compareFn), compareFn);
|
||||
}
|
||||
|
||||
function merge(leftArr, rightArr, compareFn) {
|
||||
const result = [];
|
||||
let leftIndex = 0;
|
||||
let rightIndex = 0;
|
||||
|
||||
while (leftIndex < leftArr.length && rightIndex < rightArr.length) {
|
||||
if (compareFn(leftArr[leftIndex], rightArr[rightIndex]) < 0) {
|
||||
result.push(leftArr[leftIndex++]);
|
||||
} else {
|
||||
result.push(rightArr[rightIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
return result.concat(leftArr.slice(leftIndex)).concat(rightArr.slice(rightIndex));
|
||||
}
|
||||
|
||||
module.exports = mergeSort;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
const cleanUpPrimaryKeyValue = (value) => {
|
||||
// For Bing convoId handling
|
||||
return value.replace(/--/g, '|');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
cleanUpPrimaryKeyValue,
|
||||
};
|
||||
|
|
@ -34,26 +34,20 @@
|
|||
},
|
||||
"homepage": "https://librechat.ai",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.758.0",
|
||||
"@azure/identity": "^4.7.0",
|
||||
"@azure/search-documents": "^12.0.0",
|
||||
"@azure/storage-blob": "^12.27.0",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@googleapis/youtube": "^20.0.0",
|
||||
"@keyv/redis": "^4.3.3",
|
||||
"@langchain/core": "^0.3.79",
|
||||
"@langchain/google-genai": "^0.2.13",
|
||||
"@langchain/google-vertexai": "^0.2.13",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^3.0.36",
|
||||
"@librechat/api": "*",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@modelcontextprotocol/sdk": "^1.21.0",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||
"axios": "^1.12.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.8.1",
|
||||
|
|
@ -72,7 +66,6 @@
|
|||
"file-type": "^18.7.0",
|
||||
"firebase": "^11.0.2",
|
||||
"form-data": "^4.0.4",
|
||||
"googleapis": "^126.0.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ioredis": "^5.3.2",
|
||||
|
|
|
|||
|
|
@ -1,247 +0,0 @@
|
|||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { getResponseSender } = require('librechat-data-provider');
|
||||
const {
|
||||
handleAbortError,
|
||||
createAbortController,
|
||||
cleanupAbortController,
|
||||
} = require('~/server/middleware');
|
||||
const {
|
||||
disposeClient,
|
||||
processReqData,
|
||||
clientRegistry,
|
||||
requestDataMap,
|
||||
} = require('~/server/cleanup');
|
||||
const { createOnProgress } = require('~/server/utils');
|
||||
const { saveMessage } = require('~/models');
|
||||
|
||||
const EditController = async (req, res, next, initializeClient) => {
|
||||
let {
|
||||
text,
|
||||
generation,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
modelDisplayLabel,
|
||||
responseMessageId,
|
||||
isContinued = false,
|
||||
parentMessageId = null,
|
||||
overrideParentMessageId = null,
|
||||
} = req.body;
|
||||
|
||||
let client = null;
|
||||
let abortKey = null;
|
||||
let cleanupHandlers = [];
|
||||
let clientRef = null; // Declare clientRef here
|
||||
|
||||
logger.debug('[EditController]', {
|
||||
text,
|
||||
generation,
|
||||
isContinued,
|
||||
conversationId,
|
||||
...endpointOption,
|
||||
modelsConfig: endpointOption.modelsConfig ? 'exists' : '',
|
||||
});
|
||||
|
||||
let userMessage = null;
|
||||
let userMessagePromise = null;
|
||||
let promptTokens = null;
|
||||
let getAbortData = null;
|
||||
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.modelOptions.model,
|
||||
modelDisplayLabel,
|
||||
});
|
||||
const userMessageId = parentMessageId;
|
||||
const userId = req.user.id;
|
||||
|
||||
let reqDataContext = { userMessage, userMessagePromise, responseMessageId, promptTokens };
|
||||
|
||||
const updateReqData = (data = {}) => {
|
||||
reqDataContext = processReqData(data, reqDataContext);
|
||||
abortKey = reqDataContext.abortKey;
|
||||
userMessage = reqDataContext.userMessage;
|
||||
userMessagePromise = reqDataContext.userMessagePromise;
|
||||
responseMessageId = reqDataContext.responseMessageId;
|
||||
promptTokens = reqDataContext.promptTokens;
|
||||
};
|
||||
|
||||
let { onProgress: progressCallback, getPartialText } = createOnProgress({
|
||||
generation,
|
||||
});
|
||||
|
||||
const performCleanup = () => {
|
||||
logger.debug('[EditController] Performing cleanup');
|
||||
if (Array.isArray(cleanupHandlers)) {
|
||||
for (const handler of cleanupHandlers) {
|
||||
try {
|
||||
if (typeof handler === 'function') {
|
||||
handler();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (abortKey) {
|
||||
logger.debug('[EditController] Cleaning up abort controller');
|
||||
cleanupAbortController(abortKey);
|
||||
abortKey = null;
|
||||
}
|
||||
|
||||
if (client) {
|
||||
disposeClient(client);
|
||||
client = null;
|
||||
}
|
||||
|
||||
reqDataContext = null;
|
||||
userMessage = null;
|
||||
userMessagePromise = null;
|
||||
promptTokens = null;
|
||||
getAbortData = null;
|
||||
progressCallback = null;
|
||||
endpointOption = null;
|
||||
cleanupHandlers = null;
|
||||
|
||||
if (requestDataMap.has(req)) {
|
||||
requestDataMap.delete(req);
|
||||
}
|
||||
logger.debug('[EditController] Cleanup completed');
|
||||
};
|
||||
|
||||
try {
|
||||
({ client } = await initializeClient({ req, res, endpointOption }));
|
||||
|
||||
if (clientRegistry && client) {
|
||||
clientRegistry.register(client, { userId }, client);
|
||||
}
|
||||
|
||||
if (client) {
|
||||
requestDataMap.set(req, { client });
|
||||
}
|
||||
|
||||
clientRef = new WeakRef(client);
|
||||
|
||||
getAbortData = () => {
|
||||
const currentClient = clientRef?.deref();
|
||||
const currentText =
|
||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||
|
||||
return {
|
||||
sender,
|
||||
conversationId,
|
||||
messageId: reqDataContext.responseMessageId,
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId,
|
||||
text: currentText,
|
||||
userMessage: userMessage,
|
||||
userMessagePromise: userMessagePromise,
|
||||
promptTokens: reqDataContext.promptTokens,
|
||||
};
|
||||
};
|
||||
|
||||
const { onStart, abortController } = createAbortController(
|
||||
req,
|
||||
res,
|
||||
getAbortData,
|
||||
updateReqData,
|
||||
);
|
||||
|
||||
const closeHandler = () => {
|
||||
logger.debug('[EditController] Request closed');
|
||||
if (!abortController || abortController.signal.aborted || abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
abortController.abort();
|
||||
logger.debug('[EditController] Request aborted on close');
|
||||
};
|
||||
|
||||
res.on('close', closeHandler);
|
||||
cleanupHandlers.push(() => {
|
||||
try {
|
||||
res.removeListener('close', closeHandler);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
});
|
||||
|
||||
let response = await client.sendMessage(text, {
|
||||
user: userId,
|
||||
generation,
|
||||
isContinued,
|
||||
isEdited: true,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
responseMessageId: reqDataContext.responseMessageId,
|
||||
overrideParentMessageId,
|
||||
getReqData: updateReqData,
|
||||
onStart,
|
||||
abortController,
|
||||
progressCallback,
|
||||
progressOptions: {
|
||||
res,
|
||||
},
|
||||
});
|
||||
|
||||
const databasePromise = response.databasePromise;
|
||||
delete response.databasePromise;
|
||||
|
||||
const { conversation: convoData = {} } = await databasePromise;
|
||||
const conversation = { ...convoData };
|
||||
conversation.title =
|
||||
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
||||
|
||||
if (client?.options?.attachments && endpointOption?.modelOptions?.model) {
|
||||
conversation.model = endpointOption.modelOptions.model;
|
||||
}
|
||||
|
||||
if (!abortController.signal.aborted) {
|
||||
const finalUserMessage = reqDataContext.userMessage;
|
||||
const finalResponseMessage = { ...response };
|
||||
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: finalUserMessage,
|
||||
responseMessage: finalResponseMessage,
|
||||
});
|
||||
res.end();
|
||||
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...finalResponseMessage, user: userId },
|
||||
{ context: 'api/server/controllers/EditController.js - response end' },
|
||||
);
|
||||
}
|
||||
|
||||
performCleanup();
|
||||
} catch (error) {
|
||||
logger.error('[EditController] Error handling request', error);
|
||||
let partialText = '';
|
||||
try {
|
||||
const currentClient = clientRef?.deref();
|
||||
partialText =
|
||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||
} catch (getTextError) {
|
||||
logger.error('[EditController] Error calling getText() during error handling', getTextError);
|
||||
}
|
||||
|
||||
handleAbortError(res, req, error, {
|
||||
sender,
|
||||
partialText,
|
||||
conversationId,
|
||||
messageId: reqDataContext.responseMessageId,
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
||||
userMessageId,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[EditController] Error in `handleAbortError` during catch block', err);
|
||||
})
|
||||
.finally(() => {
|
||||
performCleanup();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EditController;
|
||||
|
|
@ -7,6 +7,7 @@ const {
|
|||
Constants,
|
||||
RunStatus,
|
||||
CacheKeys,
|
||||
VisionModes,
|
||||
ContentTypes,
|
||||
EModelEndpoint,
|
||||
ViolationTypes,
|
||||
|
|
@ -25,6 +26,7 @@ const {
|
|||
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
|
||||
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
||||
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { createRunBody } = require('~/server/services/createRunBody');
|
||||
|
|
@ -64,7 +66,7 @@ const chatV1 = async (req, res) => {
|
|||
clientTimestamp,
|
||||
} = req.body;
|
||||
|
||||
/** @type {OpenAIClient} */
|
||||
/** @type {OpenAI} */
|
||||
let openai;
|
||||
/** @type {string|undefined} - the current thread id */
|
||||
let thread_id = _thread_id;
|
||||
|
|
@ -285,11 +287,10 @@ const chatV1 = async (req, res) => {
|
|||
});
|
||||
};
|
||||
|
||||
const { openai: _openai, client } = await getOpenAIClient({
|
||||
const { openai: _openai } = await getOpenAIClient({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
initAppClient: true,
|
||||
});
|
||||
|
||||
openai = _openai;
|
||||
|
|
@ -364,7 +365,15 @@ const chatV1 = async (req, res) => {
|
|||
role: 'user',
|
||||
content: '',
|
||||
};
|
||||
const files = await client.addImageURLs(visionMessage, attachments);
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
req,
|
||||
attachments,
|
||||
{
|
||||
endpoint: EModelEndpoint.assistants,
|
||||
},
|
||||
VisionModes.generative,
|
||||
);
|
||||
visionMessage.image_urls = image_urls.length ? image_urls : undefined;
|
||||
if (!visionMessage.image_urls?.length) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -609,7 +618,6 @@ const chatV1 = async (req, res) => {
|
|||
text,
|
||||
responseText: response.text,
|
||||
conversationId,
|
||||
client,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const chatV2 = async (req, res) => {
|
|||
clientTimestamp,
|
||||
} = req.body;
|
||||
|
||||
/** @type {OpenAIClient} */
|
||||
/** @type {OpenAI} */
|
||||
let openai;
|
||||
/** @type {string|undefined} - the current thread id */
|
||||
let thread_id = _thread_id;
|
||||
|
|
@ -160,11 +160,10 @@ const chatV2 = async (req, res) => {
|
|||
});
|
||||
};
|
||||
|
||||
const { openai: _openai, client } = await getOpenAIClient({
|
||||
const { openai: _openai } = await getOpenAIClient({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
initAppClient: true,
|
||||
});
|
||||
|
||||
openai = _openai;
|
||||
|
|
@ -453,7 +452,6 @@ const chatV2 = async (req, res) => {
|
|||
text,
|
||||
responseText: response.text,
|
||||
conversationId,
|
||||
client,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const _listAssistants = async ({ req, res, version, query }) => {
|
|||
* @returns {Promise<Array<Assistant>>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
|
||||
*/
|
||||
const listAllAssistants = async ({ req, res, version, query }) => {
|
||||
/** @type {{ openai: OpenAIClient }} */
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await getOpenAIClient({ req, res, version });
|
||||
const allAssistants = [];
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que
|
|||
* @param {TEndpointOption} params.endpointOption - The endpoint options.
|
||||
* @param {boolean} params.initAppClient - Whether to initialize the app client.
|
||||
* @param {string} params.overrideEndpoint - The endpoint to override.
|
||||
* @returns {Promise<{ openai: OpenAIClient, openAIApiKey: string; client: import('~/app/clients/OpenAIClient') }>} - The initialized OpenAI client.
|
||||
* @returns {Promise<{ openai: OpenAI, openAIApiKey: string }>} - The initialized OpenAI SDK client.
|
||||
*/
|
||||
async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) {
|
||||
let endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint;
|
||||
|
|
|
|||
|
|
@ -286,7 +286,6 @@ if (cluster.isMaster) {
|
|||
app.use('/api/keys', routes.keys);
|
||||
app.use('/api/user', routes.user);
|
||||
app.use('/api/search', routes.search);
|
||||
app.use('/api/edit', routes.edit);
|
||||
app.use('/api/messages', routes.messages);
|
||||
app.use('/api/convos', routes.convos);
|
||||
app.use('/api/presets', routes.presets);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,6 @@ const startServer = async () => {
|
|||
app.use('/api/keys', routes.keys);
|
||||
app.use('/api/user', routes.user);
|
||||
app.use('/api/search', routes.search);
|
||||
app.use('/api/edit', routes.edit);
|
||||
app.use('/api/messages', routes.messages);
|
||||
app.use('/api/convos', routes.convos);
|
||||
app.use('/api/presets', routes.presets);
|
||||
|
|
@ -131,7 +130,6 @@ const startServer = async () => {
|
|||
app.use('/api/endpoints', routes.endpoints);
|
||||
app.use('/api/balance', routes.balance);
|
||||
app.use('/api/models', routes.models);
|
||||
app.use('/api/plugins', routes.plugins);
|
||||
app.use('/api/config', routes.config);
|
||||
app.use('/api/assistants', routes.assistants);
|
||||
app.use('/api/files', await routes.files.initialize());
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ const message = 'Your account has been temporarily banned due to violations of o
|
|||
* @param {Object} req - Express Request object.
|
||||
* @param {Object} res - Express Response object.
|
||||
*
|
||||
* @returns {Promise<Object>} - Returns a Promise which when resolved sends a response status of 403 with a specific message if request is not of api/ask or api/edit types. If it is, calls `denyRequest()` function.
|
||||
* @returns {Promise<Object>} - Returns a Promise which when resolved sends a response status of 403 with a specific message if request is not of api/agents/chat. If it is, calls `denyRequest()` function.
|
||||
*/
|
||||
const banResponse = async (req, res) => {
|
||||
const ua = uap(req.headers['user-agent']);
|
||||
const { baseUrl } = req;
|
||||
const { baseUrl, originalUrl } = req;
|
||||
if (!ua.browser.name) {
|
||||
return res.status(403).json({ message });
|
||||
} else if (baseUrl === '/api/ask' || baseUrl === '/api/edit') {
|
||||
} else if (baseUrl === '/api/agents' && originalUrl.startsWith('/api/agents/chat')) {
|
||||
return await denyRequest(req, res, { type: ViolationTypes.BAN });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ const buildEndpointOption = require('./buildEndpointOption');
|
|||
const validateMessageReq = require('./validateMessageReq');
|
||||
const checkDomainAllowed = require('./checkDomainAllowed');
|
||||
const concurrentLimiter = require('./concurrentLimiter');
|
||||
const validateEndpoint = require('./validateEndpoint');
|
||||
const requireLocalAuth = require('./requireLocalAuth');
|
||||
const canDeleteAccount = require('./canDeleteAccount');
|
||||
const accessResources = require('./accessResources');
|
||||
|
|
@ -42,7 +41,6 @@ module.exports = {
|
|||
requireLdapAuth,
|
||||
requireLocalAuth,
|
||||
canDeleteAccount,
|
||||
validateEndpoint,
|
||||
configMiddleware,
|
||||
concurrentLimiter,
|
||||
checkDomainAllowed,
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
const { handleError } = require('@librechat/api');
|
||||
|
||||
function validateEndpoint(req, res, next) {
|
||||
const { endpoint: _endpoint, endpointType } = req.body;
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
|
||||
if (!req.body.text || req.body.text.length === 0) {
|
||||
return handleError(res, { text: 'Prompt empty or too short' });
|
||||
}
|
||||
|
||||
const pathEndpoint = req.baseUrl.split('/')[3];
|
||||
|
||||
if (endpoint !== pathEndpoint) {
|
||||
return handleError(res, { text: 'Illegal request: Endpoint mismatch' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = validateEndpoint;
|
||||
|
|
@ -5,7 +5,6 @@ const {
|
|||
setHeaders,
|
||||
handleAbort,
|
||||
validateModel,
|
||||
// validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
const validateConvoAccess = require('~/server/middleware/validate/convoAccess');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const {
|
|||
setHeaders,
|
||||
handleAbort,
|
||||
validateModel,
|
||||
// validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
const validateConvoAccess = require('~/server/middleware/validate/convoAccess');
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
const express = require('express');
|
||||
const EditController = require('~/server/controllers/EditController');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/anthropic');
|
||||
const {
|
||||
setHeaders,
|
||||
validateModel,
|
||||
validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
validateEndpoint,
|
||||
validateModel,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await EditController(req, res, next, initializeClient);
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
const express = require('express');
|
||||
const EditController = require('~/server/controllers/EditController');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/custom');
|
||||
const { addTitle } = require('~/server/services/Endpoints/openAI');
|
||||
const {
|
||||
handleAbort,
|
||||
setHeaders,
|
||||
validateModel,
|
||||
validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
validateEndpoint,
|
||||
validateModel,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await EditController(req, res, next, initializeClient, addTitle);
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
const express = require('express');
|
||||
const EditController = require('~/server/controllers/EditController');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/google');
|
||||
const {
|
||||
setHeaders,
|
||||
validateModel,
|
||||
validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
validateEndpoint,
|
||||
validateModel,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await EditController(req, res, next, initializeClient);
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
const { isEnabled } = require('@librechat/api');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const {
|
||||
validateConvoAccess,
|
||||
messageUserLimiter,
|
||||
concurrentLimiter,
|
||||
messageIpLimiter,
|
||||
requireJwtAuth,
|
||||
checkBan,
|
||||
uaParser,
|
||||
} = require('~/server/middleware');
|
||||
const anthropic = require('./anthropic');
|
||||
const express = require('express');
|
||||
const openAI = require('./openAI');
|
||||
const custom = require('./custom');
|
||||
const google = require('./google');
|
||||
|
||||
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
router.use(checkBan);
|
||||
router.use(uaParser);
|
||||
|
||||
if (isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
|
||||
router.use(concurrentLimiter);
|
||||
}
|
||||
|
||||
if (isEnabled(LIMIT_MESSAGE_IP)) {
|
||||
router.use(messageIpLimiter);
|
||||
}
|
||||
|
||||
if (isEnabled(LIMIT_MESSAGE_USER)) {
|
||||
router.use(messageUserLimiter);
|
||||
}
|
||||
|
||||
router.use(validateConvoAccess);
|
||||
|
||||
router.use([`/${EModelEndpoint.azureOpenAI}`, `/${EModelEndpoint.openAI}`], openAI);
|
||||
router.use(`/${EModelEndpoint.anthropic}`, anthropic);
|
||||
router.use(`/${EModelEndpoint.google}`, google);
|
||||
router.use(`/${EModelEndpoint.custom}`, custom);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
const express = require('express');
|
||||
const EditController = require('~/server/controllers/EditController');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/openAI');
|
||||
const {
|
||||
setHeaders,
|
||||
validateModel,
|
||||
validateEndpoint,
|
||||
buildEndpointOption,
|
||||
moderateText,
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(moderateText);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
validateEndpoint,
|
||||
validateModel,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await EditController(req, res, next, initializeClient);
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -8,7 +8,6 @@ const memories = require('./memories');
|
|||
const presets = require('./presets');
|
||||
const prompts = require('./prompts');
|
||||
const balance = require('./balance');
|
||||
const plugins = require('./plugins');
|
||||
const actions = require('./actions');
|
||||
const banner = require('./banner');
|
||||
const search = require('./search');
|
||||
|
|
@ -22,14 +21,12 @@ const files = require('./files');
|
|||
const share = require('./share');
|
||||
const tags = require('./tags');
|
||||
const auth = require('./auth');
|
||||
const edit = require('./edit');
|
||||
const keys = require('./keys');
|
||||
const user = require('./user');
|
||||
const mcp = require('./mcp');
|
||||
|
||||
module.exports = {
|
||||
mcp,
|
||||
edit,
|
||||
auth,
|
||||
keys,
|
||||
user,
|
||||
|
|
@ -45,7 +42,6 @@ module.exports = {
|
|||
config,
|
||||
models,
|
||||
prompts,
|
||||
plugins,
|
||||
actions,
|
||||
presets,
|
||||
balance,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ const {
|
|||
} = require('~/models');
|
||||
const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update');
|
||||
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
|
||||
const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc');
|
||||
const { getConvosQueried } = require('~/models/Conversation');
|
||||
const { Message } = require('~/db/models');
|
||||
|
||||
|
|
@ -68,9 +67,6 @@ router.get('/', async (req, res) => {
|
|||
const cleanedMessages = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
let message = messages[i];
|
||||
if (message.conversationId.includes('--')) {
|
||||
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
|
||||
}
|
||||
if (result.convoMap[message.conversationId]) {
|
||||
messageIds.push(message.messageId);
|
||||
cleanedMessages.push(message);
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
const express = require('express');
|
||||
const { getAvailablePluginsController } = require('~/server/controllers/PluginController');
|
||||
const { requireJwtAuth } = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', requireJwtAuth, getAvailablePluginsController);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -23,7 +23,7 @@ const { TextStream } = require('~/app/clients');
|
|||
* Sorts, processes, and flattens messages to a single string.
|
||||
*
|
||||
* @param {Object} params - Params for creating the onTextProgress function.
|
||||
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
||||
* @param {OpenAI} params.openai - The OpenAI SDK client instance.
|
||||
* @param {string} params.conversationId - The current conversation ID.
|
||||
* @param {string} params.userMessageId - The user message ID; response's `parentMessageId`.
|
||||
* @param {string} params.messageId - The response message ID.
|
||||
|
|
@ -74,7 +74,7 @@ async function createOnTextProgress({
|
|||
* Retrieves the response from an OpenAI run.
|
||||
*
|
||||
* @param {Object} params - The parameters for getting the response.
|
||||
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
||||
* @param {OpenAI} params.openai - The OpenAI SDK client instance.
|
||||
* @param {string} params.run_id - The ID of the run to get the response for.
|
||||
* @param {string} params.thread_id - The ID of the thread associated with the run.
|
||||
* @return {Promise<OpenAIAssistantFinish | OpenAIAssistantAction[] | ThreadMessage[] | RequiredActionFunctionToolCall[]>}
|
||||
|
|
@ -162,7 +162,7 @@ function hasToolCallChanged(previousCall, currentCall) {
|
|||
* Creates a handler function for steps in progress, specifically for
|
||||
* processing messages and managing seen completed messages.
|
||||
*
|
||||
* @param {OpenAIClient} openai - The OpenAI client instance.
|
||||
* @param {OpenAI} openai - The OpenAI SDK client instance.
|
||||
* @param {string} thread_id - The ID of the thread the run is in.
|
||||
* @param {ThreadMessage[]} messages - The accumulated messages for the run.
|
||||
* @return {InProgressFunction} a function to handle steps in progress.
|
||||
|
|
@ -334,7 +334,7 @@ function createInProgressHandler(openai, thread_id, messages) {
|
|||
* Initializes a RunManager with handlers, then invokes waitForRun to monitor and manage an OpenAI run.
|
||||
*
|
||||
* @param {Object} params - The parameters for managing and monitoring the run.
|
||||
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
||||
* @param {OpenAI} params.openai - The OpenAI SDK client instance.
|
||||
* @param {string} params.run_id - The ID of the run to manage and monitor.
|
||||
* @param {string} params.thread_id - The ID of the thread associated with the run.
|
||||
* @param {RunStep[]} params.accumulatedSteps - The accumulated steps for the run.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ const {
|
|||
ASSISTANTS_API_KEY: assistantsApiKey,
|
||||
AZURE_API_KEY: azureOpenAIApiKey,
|
||||
ANTHROPIC_API_KEY: anthropicApiKey,
|
||||
CHATGPT_TOKEN: chatGPTToken,
|
||||
PLUGINS_USE_AZURE,
|
||||
GOOGLE_KEY: googleKey,
|
||||
OPENAI_REVERSE_PROXY,
|
||||
AZURE_OPENAI_BASEURL,
|
||||
|
|
@ -17,21 +15,15 @@ const {
|
|||
AZURE_ASSISTANTS_BASE_URL,
|
||||
} = process.env ?? {};
|
||||
|
||||
const useAzurePlugins = !!PLUGINS_USE_AZURE;
|
||||
|
||||
const userProvidedOpenAI = useAzurePlugins
|
||||
? isUserProvided(azureOpenAIApiKey)
|
||||
: isUserProvided(openAIApiKey);
|
||||
const userProvidedOpenAI = isUserProvided(openAIApiKey);
|
||||
|
||||
module.exports = {
|
||||
config: {
|
||||
googleKey,
|
||||
openAIApiKey,
|
||||
azureOpenAIApiKey,
|
||||
useAzurePlugins,
|
||||
userProvidedOpenAI,
|
||||
googleKey,
|
||||
[EModelEndpoint.anthropic]: generateConfig(anthropicApiKey),
|
||||
[EModelEndpoint.chatGPTBrowser]: generateConfig(chatGPTToken),
|
||||
[EModelEndpoint.openAI]: generateConfig(openAIApiKey, OPENAI_REVERSE_PROXY),
|
||||
[EModelEndpoint.azureOpenAI]: generateConfig(azureOpenAIApiKey, AZURE_OPENAI_BASEURL),
|
||||
[EModelEndpoint.assistants]: generateConfig(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
const path = require('path');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { loadServiceKey, isUserProvided } = require('@librechat/api');
|
||||
const { config } = require('./EndpointService');
|
||||
|
||||
const { openAIApiKey, azureOpenAIApiKey, useAzurePlugins, userProvidedOpenAI, googleKey } = config;
|
||||
|
||||
/**
|
||||
* Load async endpoints and return a configuration object
|
||||
* @param {AppConfig} [appConfig] - The app configuration object
|
||||
*/
|
||||
async function loadAsyncEndpoints(appConfig) {
|
||||
async function loadAsyncEndpoints() {
|
||||
let serviceKey, googleUserProvides;
|
||||
const { googleKey } = config;
|
||||
|
||||
/** Check if GOOGLE_KEY is provided at all(including 'user_provided') */
|
||||
const isGoogleKeyProvided = googleKey && googleKey.trim() !== '';
|
||||
|
|
@ -34,21 +28,7 @@ async function loadAsyncEndpoints(appConfig) {
|
|||
|
||||
const google = serviceKey || isGoogleKeyProvided ? { userProvide: googleUserProvides } : false;
|
||||
|
||||
const useAzure = !!appConfig?.endpoints?.[EModelEndpoint.azureOpenAI]?.plugins;
|
||||
const gptPlugins =
|
||||
useAzure || openAIApiKey || azureOpenAIApiKey
|
||||
? {
|
||||
availableAgents: ['classic', 'functions'],
|
||||
userProvide: useAzure ? false : userProvidedOpenAI,
|
||||
userProvideURL: useAzure
|
||||
? false
|
||||
: config[EModelEndpoint.openAI]?.userProvideURL ||
|
||||
config[EModelEndpoint.azureOpenAI]?.userProvideURL,
|
||||
azure: useAzurePlugins || useAzure,
|
||||
}
|
||||
: false;
|
||||
|
||||
return { google, gptPlugins };
|
||||
return { google };
|
||||
}
|
||||
|
||||
module.exports = loadAsyncEndpoints;
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ async function loadConfigModels(req) {
|
|||
modelsConfig[EModelEndpoint.azureOpenAI] = modelNames;
|
||||
}
|
||||
|
||||
if (modelNames && azureConfig && azureConfig.plugins) {
|
||||
modelsConfig[EModelEndpoint.gptPlugins] = modelNames;
|
||||
}
|
||||
|
||||
if (azureConfig?.assistants && azureConfig.assistantModels) {
|
||||
modelsConfig[EModelEndpoint.azureAssistants] = azureConfig.assistantModels;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ const { config } = require('./EndpointService');
|
|||
* @returns {Promise<Object.<string, EndpointWithOrder>>} An object whose keys are endpoint names and values are objects that contain the endpoint configuration and an order.
|
||||
*/
|
||||
async function loadDefaultEndpointsConfig(appConfig) {
|
||||
const { google, gptPlugins } = await loadAsyncEndpoints(appConfig);
|
||||
const { assistants, azureAssistants, azureOpenAI, chatGPTBrowser } = config;
|
||||
const { google } = await loadAsyncEndpoints(appConfig);
|
||||
const { assistants, azureAssistants, azureOpenAI } = config;
|
||||
|
||||
const enabledEndpoints = getEnabledEndpoints();
|
||||
|
||||
|
|
@ -20,8 +20,6 @@ async function loadDefaultEndpointsConfig(appConfig) {
|
|||
[EModelEndpoint.azureAssistants]: azureAssistants,
|
||||
[EModelEndpoint.azureOpenAI]: azureOpenAI,
|
||||
[EModelEndpoint.google]: google,
|
||||
[EModelEndpoint.chatGPTBrowser]: chatGPTBrowser,
|
||||
[EModelEndpoint.gptPlugins]: gptPlugins,
|
||||
[EModelEndpoint.anthropic]: config[EModelEndpoint.anthropic],
|
||||
[EModelEndpoint.bedrock]: config[EModelEndpoint.bedrock],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
const { getLLMConfig } = require('@librechat/api');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const AnthropicClient = require('~/app/clients/AnthropicClient');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
||||
const initializeClient = async ({ req, endpointOption, overrideModel }) => {
|
||||
const appConfig = req.config;
|
||||
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
|
||||
const expiresAt = req.body.key;
|
||||
|
|
@ -36,35 +35,19 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
|||
clientOptions._lc_stream_delay = allConfig.streamRate;
|
||||
}
|
||||
|
||||
if (optionsOnly) {
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
proxy: PROXY ?? null,
|
||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||
modelOptions: endpointOption?.model_parameters ?? {},
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
if (overrideModel) {
|
||||
clientOptions.modelOptions.model = overrideModel;
|
||||
}
|
||||
clientOptions.modelOptions.user = req.user.id;
|
||||
return getLLMConfig(anthropicApiKey, clientOptions);
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
proxy: PROXY ?? null,
|
||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||
modelOptions: endpointOption?.model_parameters ?? {},
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
if (overrideModel) {
|
||||
clientOptions.modelOptions.model = overrideModel;
|
||||
}
|
||||
|
||||
const client = new AnthropicClient(anthropicApiKey, {
|
||||
req,
|
||||
res,
|
||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||
proxy: PROXY ?? null,
|
||||
...clientOptions,
|
||||
...endpointOption,
|
||||
});
|
||||
|
||||
return {
|
||||
client,
|
||||
anthropicApiKey,
|
||||
};
|
||||
clientOptions.modelOptions.user = req.user.id;
|
||||
return getLLMConfig(anthropicApiKey, clientOptions);
|
||||
};
|
||||
|
||||
module.exports = initializeClient;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ const {
|
|||
getUserKeyExpiry,
|
||||
checkUserKeyExpiry,
|
||||
} = require('~/server/services/UserService');
|
||||
const OAIClient = require('~/app/clients/OpenAIClient');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption, version, initAppClient = false }) => {
|
||||
const initializeClient = async ({ req, res, version }) => {
|
||||
const { PROXY, OPENAI_ORGANIZATION, ASSISTANTS_API_KEY, ASSISTANTS_BASE_URL } = process.env;
|
||||
|
||||
const userProvidesKey = isUserProvided(ASSISTANTS_API_KEY);
|
||||
|
|
@ -34,14 +33,6 @@ const initializeClient = async ({ req, res, endpointOption, version, initAppClie
|
|||
},
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
reverseProxyUrl: baseURL ?? null,
|
||||
proxy: PROXY ?? null,
|
||||
req,
|
||||
res,
|
||||
...endpointOption,
|
||||
};
|
||||
|
||||
if (userProvidesKey & !apiKey) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
|
|
@ -78,15 +69,6 @@ const initializeClient = async ({ req, res, endpointOption, version, initAppClie
|
|||
openai.req = req;
|
||||
openai.res = res;
|
||||
|
||||
if (endpointOption && initAppClient) {
|
||||
const client = new OAIClient(apiKey, clientOptions);
|
||||
return {
|
||||
client,
|
||||
openai,
|
||||
openAIApiKey: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
openai,
|
||||
openAIApiKey: apiKey,
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
// const OpenAI = require('openai');
|
||||
const { ProxyAgent } = require('undici');
|
||||
const { ErrorTypes } = require('librechat-data-provider');
|
||||
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initalize');
|
||||
// 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,
|
||||
}));
|
||||
|
||||
const today = new Date();
|
||||
const tenDaysFromToday = new Date(today.setDate(today.getDate() + 10));
|
||||
const isoString = tenDaysFromToday.toISOString();
|
||||
|
||||
describe('initializeClient', () => {
|
||||
// Set up environment variables
|
||||
const originalEnvironment = process.env;
|
||||
const app = {
|
||||
locals: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules(); // Clears the cache
|
||||
process.env = { ...originalEnvironment }; // Make a copy
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnvironment; // Restore original env vars
|
||||
});
|
||||
|
||||
test('initializes OpenAI client with default API key and URL', async () => {
|
||||
process.env.ASSISTANTS_API_KEY = 'default-api-key';
|
||||
process.env.ASSISTANTS_BASE_URL = 'https://default.api.url';
|
||||
|
||||
// Assuming 'isUserProvided' to return false for this test case
|
||||
jest.mock('~/server/utils', () => ({
|
||||
isUserProvided: jest.fn().mockReturnValueOnce(false),
|
||||
}));
|
||||
|
||||
const req = { user: { id: 'user123' }, app };
|
||||
const res = {};
|
||||
|
||||
const { openai, openAIApiKey } = await initializeClient({ req, res });
|
||||
expect(openai.apiKey).toBe('default-api-key');
|
||||
expect(openAIApiKey).toBe('default-api-key');
|
||||
expect(openai.baseURL).toBe('https://default.api.url');
|
||||
});
|
||||
|
||||
test('initializes OpenAI client with user-provided API key and URL', async () => {
|
||||
process.env.ASSISTANTS_API_KEY = 'user_provided';
|
||||
process.env.ASSISTANTS_BASE_URL = 'user_provided';
|
||||
|
||||
getUserKeyValues.mockResolvedValue({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' });
|
||||
getUserKeyExpiry.mockResolvedValue(isoString);
|
||||
|
||||
const req = { user: { id: 'user123' }, app };
|
||||
const res = {};
|
||||
|
||||
const { openai, openAIApiKey } = await initializeClient({ req, res });
|
||||
expect(openAIApiKey).toBe('user-api-key');
|
||||
expect(openai.apiKey).toBe('user-api-key');
|
||||
expect(openai.baseURL).toBe('https://user.api.url');
|
||||
});
|
||||
|
||||
test('throws error for invalid JSON in user-provided values', async () => {
|
||||
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_user_key/);
|
||||
});
|
||||
|
||||
test('throws error if API key is not provided', async () => {
|
||||
delete process.env.ASSISTANTS_API_KEY; // Simulate missing API key
|
||||
|
||||
const req = { user: { id: 'user123' }, app };
|
||||
const res = {};
|
||||
|
||||
await expect(initializeClient({ req, res })).rejects.toThrow(/Assistants API key not/);
|
||||
});
|
||||
|
||||
test('initializes OpenAI client with proxy configuration', async () => {
|
||||
process.env.ASSISTANTS_API_KEY = 'test-key';
|
||||
process.env.PROXY = 'http://proxy.server';
|
||||
|
||||
const req = { user: { id: 'user123' }, app };
|
||||
const res = {};
|
||||
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
expect(openai.fetchOptions).toBeDefined();
|
||||
expect(openai.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +1,84 @@
|
|||
const { isEnabled } = require('@librechat/api');
|
||||
const { isEnabled, sanitizeTitle } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const { saveConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const initializeClient = require('./initalize');
|
||||
|
||||
const addTitle = async (req, { text, responseText, conversationId, client }) => {
|
||||
/**
|
||||
* Generates a conversation title using OpenAI SDK
|
||||
* @param {Object} params
|
||||
* @param {OpenAI} params.openai - The OpenAI SDK client instance
|
||||
* @param {string} params.text - User's message text
|
||||
* @param {string} params.responseText - Assistant's response text
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const generateTitle = async ({ openai, text, responseText }) => {
|
||||
const titlePrompt = `Please generate a concise title (max 40 characters) for a conversation that starts with:
|
||||
User: ${text}
|
||||
Assistant: ${responseText}
|
||||
|
||||
Title:`;
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: titlePrompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 20,
|
||||
});
|
||||
|
||||
const title = completion.choices[0]?.message?.content?.trim() || 'New conversation';
|
||||
return sanitizeTitle(title);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a title to a conversation asynchronously
|
||||
* @param {ServerRequest} req
|
||||
* @param {Object} params
|
||||
* @param {string} params.text - User's message text
|
||||
* @param {string} params.responseText - Assistant's response text
|
||||
* @param {string} params.conversationId - Conversation ID
|
||||
*/
|
||||
const addTitle = async (req, { text, responseText, conversationId }) => {
|
||||
const { TITLE_CONVO = 'true' } = process.env ?? {};
|
||||
if (!isEnabled(TITLE_CONVO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.options.titleConvo === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const titleCache = getLogStores(CacheKeys.GEN_TITLE);
|
||||
const key = `${req.user.id}-${conversationId}`;
|
||||
|
||||
const title = await client.titleConvo({ text, conversationId, responseText });
|
||||
await titleCache.set(key, title, 120000);
|
||||
try {
|
||||
const { openai } = await initializeClient({ req });
|
||||
const title = await generateTitle({ openai, text, responseText });
|
||||
await titleCache.set(key, title, 120000);
|
||||
|
||||
await saveConvo(
|
||||
req,
|
||||
{
|
||||
conversationId,
|
||||
title,
|
||||
},
|
||||
{ context: 'api/server/services/Endpoints/assistants/addTitle.js' },
|
||||
);
|
||||
await saveConvo(
|
||||
req,
|
||||
{
|
||||
conversationId,
|
||||
title,
|
||||
},
|
||||
{ context: 'api/server/services/Endpoints/assistants/addTitle.js' },
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[addTitle] Error generating title:', error);
|
||||
const fallbackTitle = text.length > 40 ? text.substring(0, 37) + '...' : text;
|
||||
await titleCache.set(key, fallbackTitle, 120000);
|
||||
await saveConvo(
|
||||
req,
|
||||
{
|
||||
conversationId,
|
||||
title: fallbackTitle,
|
||||
},
|
||||
{ context: 'api/server/services/Endpoints/assistants/addTitle.js' },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = addTitle;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ const {
|
|||
getUserKeyValues,
|
||||
getUserKeyExpiry,
|
||||
} = require('~/server/services/UserService');
|
||||
const OAIClient = require('~/app/clients/OpenAIClient');
|
||||
|
||||
class Files {
|
||||
constructor(client) {
|
||||
|
|
@ -184,15 +183,6 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
|||
openai.locals = { ...(openai.locals ?? {}), azureOptions };
|
||||
}
|
||||
|
||||
if (endpointOption && initAppClient) {
|
||||
const client = new OAIClient(apiKey, clientOptions);
|
||||
return {
|
||||
client,
|
||||
openai,
|
||||
openAIApiKey: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
openai,
|
||||
openAIApiKey: apiKey,
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
// const OpenAI = require('openai');
|
||||
const { ProxyAgent } = require('undici');
|
||||
const { ErrorTypes, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initialize');
|
||||
// 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,
|
||||
}));
|
||||
|
||||
// Config is now passed via req.config, not getAppConfig
|
||||
|
||||
const today = new Date();
|
||||
const tenDaysFromToday = new Date(today.setDate(today.getDate() + 10));
|
||||
const isoString = tenDaysFromToday.toISOString();
|
||||
|
||||
describe('initializeClient', () => {
|
||||
// Set up environment variables
|
||||
const originalEnvironment = process.env;
|
||||
const app = {
|
||||
locals: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules(); // Clears the cache
|
||||
process.env = { ...originalEnvironment }; // Make a copy
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnvironment; // Restore original env vars
|
||||
});
|
||||
|
||||
test('initializes OpenAI client with default API key and URL', async () => {
|
||||
process.env.AZURE_ASSISTANTS_API_KEY = 'default-api-key';
|
||||
process.env.AZURE_ASSISTANTS_BASE_URL = 'https://default.api.url';
|
||||
|
||||
// Assuming 'isUserProvided' to return false for this test case
|
||||
jest.mock('~/server/utils', () => ({
|
||||
isUserProvided: jest.fn().mockReturnValueOnce(false),
|
||||
}));
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app,
|
||||
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
|
||||
};
|
||||
const res = {};
|
||||
|
||||
const { openai, openAIApiKey } = await initializeClient({ req, res });
|
||||
expect(openai.apiKey).toBe('default-api-key');
|
||||
expect(openAIApiKey).toBe('default-api-key');
|
||||
expect(openai.baseURL).toBe('https://default.api.url');
|
||||
});
|
||||
|
||||
test('initializes OpenAI client with user-provided API key and URL', async () => {
|
||||
process.env.AZURE_ASSISTANTS_API_KEY = 'user_provided';
|
||||
process.env.AZURE_ASSISTANTS_BASE_URL = 'user_provided';
|
||||
|
||||
getUserKeyValues.mockResolvedValue({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' });
|
||||
getUserKeyExpiry.mockResolvedValue(isoString);
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app,
|
||||
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
|
||||
};
|
||||
const res = {};
|
||||
|
||||
const { openai, openAIApiKey } = await initializeClient({ req, res });
|
||||
expect(openAIApiKey).toBe('user-api-key');
|
||||
expect(openai.apiKey).toBe('user-api-key');
|
||||
expect(openai.baseURL).toBe('https://user.api.url');
|
||||
});
|
||||
|
||||
test('throws error for invalid JSON in user-provided values', async () => {
|
||||
process.env.AZURE_ASSISTANTS_API_KEY = 'user_provided';
|
||||
getUserKey.mockResolvedValue('invalid-json');
|
||||
getUserKeyExpiry.mockResolvedValue(isoString);
|
||||
getUserKeyValues.mockImplementation(() => {
|
||||
let userValues = getUserKey();
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
});
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
|
||||
};
|
||||
const res = {};
|
||||
|
||||
await expect(initializeClient({ req, res })).rejects.toThrow(/invalid_user_key/);
|
||||
});
|
||||
|
||||
test('throws error if API key is not provided', async () => {
|
||||
delete process.env.AZURE_ASSISTANTS_API_KEY; // Simulate missing API key
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app,
|
||||
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
|
||||
};
|
||||
const res = {};
|
||||
|
||||
await expect(initializeClient({ req, res })).rejects.toThrow(/Assistants API key not/);
|
||||
});
|
||||
|
||||
test('initializes OpenAI client with proxy configuration', async () => {
|
||||
process.env.AZURE_ASSISTANTS_API_KEY = 'test-key';
|
||||
process.env.PROXY = 'http://proxy.server';
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app,
|
||||
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
|
||||
};
|
||||
const res = {};
|
||||
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
expect(openai.fetchOptions).toBeDefined();
|
||||
expect(openai.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
});
|
||||
});
|
||||
|
|
@ -8,12 +8,11 @@ const {
|
|||
} = require('librechat-data-provider');
|
||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { fetchModels } = require('~/server/services/ModelService');
|
||||
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
||||
const { PROXY } = process.env;
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => {
|
||||
const initializeClient = async ({ req, endpointOption, overrideEndpoint }) => {
|
||||
const appConfig = req.config;
|
||||
const { key: expiresAt } = req.body;
|
||||
const endpoint = overrideEndpoint ?? req.body.endpoint;
|
||||
|
|
@ -120,38 +119,27 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
|
|||
let clientOptions = {
|
||||
reverseProxyUrl: baseURL ?? null,
|
||||
proxy: PROXY ?? null,
|
||||
req,
|
||||
res,
|
||||
...customOptions,
|
||||
...endpointOption,
|
||||
};
|
||||
|
||||
if (optionsOnly) {
|
||||
const modelOptions = endpointOption?.model_parameters ?? {};
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
modelOptions,
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
clientOptions.modelOptions.user = req.user.id;
|
||||
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
|
||||
if (options != null) {
|
||||
options.useLegacyContent = true;
|
||||
options.endpointTokenConfig = endpointTokenConfig;
|
||||
}
|
||||
if (!clientOptions.streamRate) {
|
||||
return options;
|
||||
}
|
||||
options.llmConfig._lc_stream_delay = clientOptions.streamRate;
|
||||
return options;
|
||||
const modelOptions = endpointOption?.model_parameters ?? {};
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
modelOptions,
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
clientOptions.modelOptions.user = req.user.id;
|
||||
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
|
||||
if (options != null) {
|
||||
options.useLegacyContent = true;
|
||||
options.endpointTokenConfig = endpointTokenConfig;
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
return {
|
||||
client,
|
||||
openAIApiKey: apiKey,
|
||||
};
|
||||
if (clientOptions.streamRate) {
|
||||
options.llmConfig._lc_stream_delay = clientOptions.streamRate;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
module.exports = initializeClient;
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
const initializeClient = require('./initialize');
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
...jest.requireActual('@librechat/api'),
|
||||
resolveHeaders: jest.fn(),
|
||||
getOpenAIConfig: jest.fn(),
|
||||
getCustomEndpointConfig: jest.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
baseURL: 'https://test.com',
|
||||
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||
models: { default: ['test-model'] },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/UserService', () => ({
|
||||
getUserKeyValues: jest.fn(),
|
||||
checkUserKeyExpiry: jest.fn(),
|
||||
}));
|
||||
|
||||
// Config is now passed via req.config, not getAppConfig
|
||||
|
||||
jest.mock('~/server/services/ModelService', () => ({
|
||||
fetchModels: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/app/clients/OpenAIClient', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
options: {},
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('~/cache/getLogStores', () =>
|
||||
jest.fn().mockReturnValue({
|
||||
get: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('custom/initializeClient', () => {
|
||||
const mockRequest = {
|
||||
body: { endpoint: 'test-endpoint' },
|
||||
user: { id: 'user-123', email: 'test@example.com', role: 'user' },
|
||||
app: { locals: {} },
|
||||
config: {
|
||||
endpoints: {
|
||||
all: {
|
||||
streamRate: 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockResponse = {};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const { getCustomEndpointConfig, resolveHeaders, getOpenAIConfig } = require('@librechat/api');
|
||||
getCustomEndpointConfig.mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
baseURL: 'https://test.com',
|
||||
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||
models: { default: ['test-model'] },
|
||||
});
|
||||
resolveHeaders.mockReturnValue({ 'x-user': 'user-123', 'x-email': 'test@example.com' });
|
||||
getOpenAIConfig.mockReturnValue({
|
||||
useLegacyContent: true,
|
||||
endpointTokenConfig: null,
|
||||
llmConfig: {
|
||||
callbacks: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('stores original template headers for deferred resolution', async () => {
|
||||
/**
|
||||
* Note: Request-based Header Resolution is deferred until right before LLM request is made
|
||||
* in the OpenAIClient or AgentClient, not during initialization.
|
||||
* This test verifies that the initialize function completes successfully with optionsOnly flag,
|
||||
* and that headers are passed through to be resolved later during the actual LLM request.
|
||||
*/
|
||||
const result = await initializeClient({
|
||||
req: mockRequest,
|
||||
res: mockResponse,
|
||||
optionsOnly: true,
|
||||
});
|
||||
// Verify that options are returned for later use
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveProperty('useLegacyContent', true);
|
||||
});
|
||||
|
||||
it('throws if endpoint config is missing', async () => {
|
||||
const { getCustomEndpointConfig } = require('@librechat/api');
|
||||
getCustomEndpointConfig.mockReturnValueOnce(null);
|
||||
await expect(
|
||||
initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }),
|
||||
).rejects.toThrow('Config not found for the test-endpoint custom endpoint.');
|
||||
});
|
||||
|
||||
it('throws if user is missing', async () => {
|
||||
await expect(
|
||||
initializeClient({
|
||||
req: { ...mockRequest, user: undefined },
|
||||
res: mockResponse,
|
||||
optionsOnly: true,
|
||||
}),
|
||||
).rejects.toThrow("Cannot read properties of undefined (reading 'id')");
|
||||
});
|
||||
});
|
||||
|
|
@ -2,9 +2,8 @@ const path = require('path');
|
|||
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
|
||||
const { getGoogleConfig, isEnabled, loadServiceKey } = require('@librechat/api');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { GoogleClient } = require('~/app');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
||||
const initializeClient = async ({ req, endpointOption, overrideModel }) => {
|
||||
const { GOOGLE_KEY, GOOGLE_REVERSE_PROXY, GOOGLE_AUTH_HEADER, PROXY } = process.env;
|
||||
const isUserProvided = GOOGLE_KEY === 'user_provided';
|
||||
const { key: expiresAt } = req.body;
|
||||
|
|
@ -62,8 +61,6 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
|||
}
|
||||
|
||||
clientOptions = {
|
||||
req,
|
||||
res,
|
||||
reverseProxyUrl: GOOGLE_REVERSE_PROXY ?? null,
|
||||
authHeader: isEnabled(GOOGLE_AUTH_HEADER) ?? null,
|
||||
proxy: PROXY ?? null,
|
||||
|
|
@ -71,25 +68,16 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
|||
...endpointOption,
|
||||
};
|
||||
|
||||
if (optionsOnly) {
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
modelOptions: endpointOption?.model_parameters ?? {},
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
if (overrideModel) {
|
||||
clientOptions.modelOptions.model = overrideModel;
|
||||
}
|
||||
return getGoogleConfig(credentials, clientOptions);
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
modelOptions: endpointOption?.model_parameters ?? {},
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
if (overrideModel) {
|
||||
clientOptions.modelOptions.model = overrideModel;
|
||||
}
|
||||
|
||||
const client = new GoogleClient(credentials, clientOptions);
|
||||
|
||||
return {
|
||||
client,
|
||||
credentials,
|
||||
};
|
||||
return getGoogleConfig(credentials, clientOptions);
|
||||
};
|
||||
|
||||
module.exports = initializeClient;
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets
|
||||
const { getUserKey } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initialize');
|
||||
const { GoogleClient } = require('~/app');
|
||||
|
||||
jest.mock('~/server/services/UserService', () => ({
|
||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||
getUserKey: jest.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
|
||||
// Config is now passed via req.config, not getAppConfig
|
||||
|
||||
const app = { locals: {} };
|
||||
|
||||
describe('google/initializeClient', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should initialize GoogleClient with user-provided credentials', async () => {
|
||||
process.env.GOOGLE_KEY = 'user_provided';
|
||||
process.env.GOOGLE_REVERSE_PROXY = 'http://reverse.proxy';
|
||||
process.env.PROXY = 'http://proxy';
|
||||
|
||||
const expiresAt = new Date(Date.now() + 60000).toISOString();
|
||||
|
||||
const req = {
|
||||
body: { key: expiresAt },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: {
|
||||
endpoints: {
|
||||
all: {},
|
||||
google: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'default-model' } };
|
||||
|
||||
const { client, credentials } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(getUserKey).toHaveBeenCalledWith({ userId: '123', name: 'google' });
|
||||
expect(client).toBeInstanceOf(GoogleClient);
|
||||
expect(client.options.reverseProxyUrl).toBe('http://reverse.proxy');
|
||||
expect(client.options.proxy).toBe('http://proxy');
|
||||
expect(credentials).toEqual({});
|
||||
});
|
||||
|
||||
test('should initialize GoogleClient with service key credentials', async () => {
|
||||
process.env.GOOGLE_KEY = 'service_key';
|
||||
process.env.GOOGLE_REVERSE_PROXY = 'http://reverse.proxy';
|
||||
process.env.PROXY = 'http://proxy';
|
||||
|
||||
const req = {
|
||||
body: { key: null },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: {
|
||||
endpoints: {
|
||||
all: {},
|
||||
google: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'default-model' } };
|
||||
|
||||
const { client, credentials } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(client).toBeInstanceOf(GoogleClient);
|
||||
expect(client.options.reverseProxyUrl).toBe('http://reverse.proxy');
|
||||
expect(client.options.proxy).toBe('http://proxy');
|
||||
expect(credentials).toEqual({
|
||||
GOOGLE_SERVICE_KEY: {},
|
||||
GOOGLE_API_KEY: 'service_key',
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle expired user-provided key', async () => {
|
||||
process.env.GOOGLE_KEY = 'user_provided';
|
||||
|
||||
const expiresAt = new Date(Date.now() - 10000).toISOString(); // Expired
|
||||
const req = {
|
||||
body: { key: expiresAt },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: {
|
||||
endpoints: {
|
||||
all: {},
|
||||
google: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = { modelOptions: { model: 'default-model' } };
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/expired_user_key/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,16 +7,8 @@ const {
|
|||
getAzureCredentials,
|
||||
} = require('@librechat/api');
|
||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
||||
|
||||
const initializeClient = async ({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
optionsOnly,
|
||||
overrideEndpoint,
|
||||
overrideModel,
|
||||
}) => {
|
||||
const initializeClient = async ({ req, endpointOption, overrideEndpoint, overrideModel }) => {
|
||||
const appConfig = req.config;
|
||||
const {
|
||||
PROXY,
|
||||
|
|
@ -137,28 +129,19 @@ const initializeClient = async ({
|
|||
throw new Error(`${endpoint} API Key not provided.`);
|
||||
}
|
||||
|
||||
if (optionsOnly) {
|
||||
const modelOptions = endpointOption?.model_parameters ?? {};
|
||||
modelOptions.model = modelName;
|
||||
clientOptions = Object.assign({ modelOptions }, clientOptions);
|
||||
clientOptions.modelOptions.user = req.user.id;
|
||||
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
|
||||
if (options != null && serverless === true) {
|
||||
options.useLegacyContent = true;
|
||||
}
|
||||
const streamRate = clientOptions.streamRate;
|
||||
if (!streamRate) {
|
||||
return options;
|
||||
}
|
||||
options.llmConfig._lc_stream_delay = streamRate;
|
||||
return options;
|
||||
const modelOptions = endpointOption?.model_parameters ?? {};
|
||||
modelOptions.model = modelName;
|
||||
clientOptions = Object.assign({ modelOptions }, clientOptions);
|
||||
clientOptions.modelOptions.user = req.user.id;
|
||||
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
|
||||
if (options != null && serverless === true) {
|
||||
options.useLegacyContent = true;
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, Object.assign({ req, res }, clientOptions));
|
||||
return {
|
||||
client,
|
||||
openAIApiKey: apiKey,
|
||||
};
|
||||
const streamRate = clientOptions.streamRate;
|
||||
if (streamRate) {
|
||||
options.llmConfig._lc_stream_delay = streamRate;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
module.exports = initializeClient;
|
||||
|
|
|
|||
|
|
@ -1,431 +0,0 @@
|
|||
jest.mock('~/cache/getLogStores', () => ({
|
||||
getLogStores: jest.fn().mockReturnValue({
|
||||
get: jest.fn().mockResolvedValue({
|
||||
openAI: { apiKey: 'test-key' },
|
||||
}),
|
||||
set: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
|
||||
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
|
||||
const initializeClient = require('./initialize');
|
||||
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,
|
||||
}));
|
||||
|
||||
const mockAppConfig = {
|
||||
endpoints: {
|
||||
openAI: {
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
azureOpenAI: {
|
||||
apiKey: 'test-azure-key',
|
||||
modelNames: ['gpt-4-vision-preview', 'gpt-3.5-turbo', 'gpt-4'],
|
||||
modelGroupMap: {
|
||||
'gpt-4-vision-preview': {
|
||||
group: 'librechat-westus',
|
||||
deploymentName: 'gpt-4-vision-preview',
|
||||
version: '2024-02-15-preview',
|
||||
},
|
||||
},
|
||||
groupMap: {
|
||||
'librechat-westus': {
|
||||
apiKey: 'WESTUS_API_KEY',
|
||||
instanceName: 'librechat-westus',
|
||||
version: '2023-12-01-preview',
|
||||
models: {
|
||||
'gpt-4-vision-preview': {
|
||||
deploymentName: 'gpt-4-vision-preview',
|
||||
version: '2024-02-15-preview',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('initializeClient', () => {
|
||||
// Set up environment variables
|
||||
const originalEnvironment = process.env;
|
||||
const app = {
|
||||
locals: {},
|
||||
};
|
||||
|
||||
const validAzureConfigs = [
|
||||
{
|
||||
group: 'librechat-westus',
|
||||
apiKey: 'WESTUS_API_KEY',
|
||||
instanceName: 'librechat-westus',
|
||||
version: '2023-12-01-preview',
|
||||
models: {
|
||||
'gpt-4-vision-preview': {
|
||||
deploymentName: 'gpt-4-vision-preview',
|
||||
version: '2024-02-15-preview',
|
||||
},
|
||||
'gpt-3.5-turbo': {
|
||||
deploymentName: 'gpt-35-turbo',
|
||||
},
|
||||
'gpt-3.5-turbo-1106': {
|
||||
deploymentName: 'gpt-35-turbo-1106',
|
||||
},
|
||||
'gpt-4': {
|
||||
deploymentName: 'gpt-4',
|
||||
},
|
||||
'gpt-4-1106-preview': {
|
||||
deploymentName: 'gpt-4-1106-preview',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
group: 'librechat-eastus',
|
||||
apiKey: 'EASTUS_API_KEY',
|
||||
instanceName: 'librechat-eastus',
|
||||
deploymentName: 'gpt-4-turbo',
|
||||
version: '2024-02-15-preview',
|
||||
models: {
|
||||
'gpt-4-turbo': true,
|
||||
},
|
||||
baseURL: 'https://eastus.example.com',
|
||||
additionalHeaders: {
|
||||
'x-api-key': 'x-api-key-value',
|
||||
},
|
||||
},
|
||||
{
|
||||
group: 'mistral-inference',
|
||||
apiKey: 'AZURE_MISTRAL_API_KEY',
|
||||
baseURL:
|
||||
'https://Mistral-large-vnpet-serverless.region.inference.ai.azure.com/v1/chat/completions',
|
||||
serverless: true,
|
||||
models: {
|
||||
'mistral-large': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
group: 'llama-70b-chat',
|
||||
apiKey: 'AZURE_LLAMA2_70B_API_KEY',
|
||||
baseURL:
|
||||
'https://Llama-2-70b-chat-qmvyb-serverless.region.inference.ai.azure.com/v1/chat/completions',
|
||||
serverless: true,
|
||||
models: {
|
||||
'llama-70b-chat': true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { modelNames } = validateAzureGroups(validAzureConfigs);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules(); // Clears the cache
|
||||
process.env = { ...originalEnvironment }; // Make a copy
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnvironment; // Restore original env vars
|
||||
});
|
||||
|
||||
test('should initialize client with OpenAI API key and default options', async () => {
|
||||
process.env.OPENAI_API_KEY = 'test-openai-api-key';
|
||||
process.env.DEBUG_OPENAI = 'false';
|
||||
process.env.OPENAI_SUMMARIZE = 'false';
|
||||
|
||||
const req = {
|
||||
body: { key: null, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(result.openAIApiKey).toBe('test-openai-api-key');
|
||||
expect(result.client).toBeInstanceOf(OpenAIClient);
|
||||
});
|
||||
|
||||
test('should initialize client with Azure credentials when endpoint is azureOpenAI', async () => {
|
||||
process.env.AZURE_API_KEY = 'test-azure-api-key';
|
||||
(process.env.AZURE_OPENAI_API_INSTANCE_NAME = 'some-value'),
|
||||
(process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = 'some-value'),
|
||||
(process.env.AZURE_OPENAI_API_VERSION = 'some-value'),
|
||||
(process.env.AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME = 'some-value'),
|
||||
(process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME = 'some-value'),
|
||||
(process.env.OPENAI_API_KEY = 'test-openai-api-key');
|
||||
process.env.DEBUG_OPENAI = 'false';
|
||||
process.env.OPENAI_SUMMARIZE = 'false';
|
||||
|
||||
const req = {
|
||||
body: {
|
||||
key: null,
|
||||
endpoint: 'azureOpenAI',
|
||||
model: 'gpt-4-vision-preview',
|
||||
},
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const client = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(client.openAIApiKey).toBe('WESTUS_API_KEY');
|
||||
expect(client.client).toBeInstanceOf(OpenAIClient);
|
||||
});
|
||||
|
||||
test('should use the debug option when DEBUG_OPENAI is enabled', async () => {
|
||||
process.env.OPENAI_API_KEY = 'test-openai-api-key';
|
||||
process.env.DEBUG_OPENAI = 'true';
|
||||
|
||||
const req = {
|
||||
body: { key: null, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const client = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(client.client.options.debug).toBe(true);
|
||||
});
|
||||
|
||||
test('should set contextStrategy to summarize when OPENAI_SUMMARIZE is enabled', async () => {
|
||||
process.env.OPENAI_API_KEY = 'test-openai-api-key';
|
||||
process.env.OPENAI_SUMMARIZE = 'true';
|
||||
|
||||
const req = {
|
||||
body: { key: null, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const client = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(client.client.options.contextStrategy).toBe('summarize');
|
||||
});
|
||||
|
||||
test('should set reverseProxyUrl and proxy when they are provided in the environment', async () => {
|
||||
process.env.OPENAI_API_KEY = 'test-openai-api-key';
|
||||
process.env.OPENAI_REVERSE_PROXY = 'http://reverse.proxy';
|
||||
process.env.PROXY = 'http://proxy';
|
||||
|
||||
const req = {
|
||||
body: { key: null, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const client = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(client.client.options.reverseProxyUrl).toBe('http://reverse.proxy');
|
||||
expect(client.client.options.proxy).toBe('http://proxy');
|
||||
});
|
||||
|
||||
test('should throw an error if the user-provided key has expired', async () => {
|
||||
process.env.OPENAI_API_KEY = 'user_provided';
|
||||
process.env.AZURE_API_KEY = 'user_provided';
|
||||
process.env.DEBUG_OPENAI = 'false';
|
||||
process.env.OPENAI_SUMMARIZE = 'false';
|
||||
|
||||
const expiresAt = new Date(Date.now() - 10000).toISOString(); // Expired
|
||||
const req = {
|
||||
body: { key: expiresAt, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
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 () => {
|
||||
// Clear the environment variables for API keys
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.AZURE_API_KEY;
|
||||
|
||||
const req = {
|
||||
body: { key: null, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
`${EModelEndpoint.openAI} API Key not provided.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle user-provided keys and check expiry', async () => {
|
||||
// Set up the req.body to simulate user-provided key scenario
|
||||
const req = {
|
||||
body: {
|
||||
key: new Date(Date.now() + 10000).toISOString(),
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
},
|
||||
user: {
|
||||
id: '123',
|
||||
},
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
// Ensure the environment variable is set to 'user_provided' to match the isUserProvided condition
|
||||
process.env.OPENAI_API_KEY = 'user_provided';
|
||||
|
||||
// Mock getUserKey to return the expected key
|
||||
getUserKeyValues.mockResolvedValue({ apiKey: 'test-user-provided-openai-api-key' });
|
||||
|
||||
// Call the initializeClient function
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
// Assertions
|
||||
expect(result.openAIApiKey).toBe('test-user-provided-openai-api-key');
|
||||
});
|
||||
|
||||
test('should throw an error if the user-provided key is invalid', async () => {
|
||||
const invalidKey = new Date(Date.now() - 100000).toISOString();
|
||||
const req = {
|
||||
body: { key: invalidKey, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
// Ensure the environment variable is set to 'user_provided' to match the isUserProvided condition
|
||||
process.env.OPENAI_API_KEY = 'user_provided';
|
||||
|
||||
// Mock getUserKey to return an invalid key
|
||||
getUserKey.mockResolvedValue(invalidKey);
|
||||
|
||||
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 () => {
|
||||
process.env.OPENAI_API_KEY = 'user_provided';
|
||||
const req = {
|
||||
body: { key: new Date(Date.now() + 10000).toISOString(), endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
// Mock getUserKey to return a non-JSON string
|
||||
getUserKey.mockResolvedValue('not-a-json');
|
||||
getUserKeyValues.mockImplementation(() => {
|
||||
let userValues = getUserKey();
|
||||
try {
|
||||
userValues = JSON.parse(userValues);
|
||||
} catch {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: ErrorTypes.INVALID_USER_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return userValues;
|
||||
});
|
||||
|
||||
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
|
||||
/invalid_user_key/,
|
||||
);
|
||||
});
|
||||
|
||||
test('should initialize client correctly for Azure OpenAI with valid configuration', async () => {
|
||||
// Set up Azure environment variables
|
||||
process.env.WESTUS_API_KEY = 'test-westus-key';
|
||||
|
||||
const req = {
|
||||
body: {
|
||||
key: null,
|
||||
endpoint: EModelEndpoint.azureOpenAI,
|
||||
model: modelNames[0],
|
||||
},
|
||||
user: { id: '123' },
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const client = await initializeClient({ req, res, endpointOption });
|
||||
expect(client.client.options.azure).toBeDefined();
|
||||
});
|
||||
|
||||
test('should initialize client with default options when certain env vars are not set', async () => {
|
||||
delete process.env.DEBUG_OPENAI;
|
||||
delete process.env.OPENAI_SUMMARIZE;
|
||||
process.env.OPENAI_API_KEY = 'some-api-key';
|
||||
|
||||
const req = {
|
||||
body: { key: null, endpoint: EModelEndpoint.openAI },
|
||||
user: { id: '123' },
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
const client = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(client.client.options.debug).toBe(false);
|
||||
expect(client.client.options.contextStrategy).toBe(null);
|
||||
});
|
||||
|
||||
test('should correctly use user-provided apiKey and baseURL when provided', async () => {
|
||||
process.env.OPENAI_API_KEY = 'user_provided';
|
||||
process.env.OPENAI_REVERSE_PROXY = 'user_provided';
|
||||
const req = {
|
||||
body: {
|
||||
key: new Date(Date.now() + 10000).toISOString(),
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
},
|
||||
user: {
|
||||
id: '123',
|
||||
},
|
||||
app,
|
||||
config: mockAppConfig,
|
||||
};
|
||||
const res = {};
|
||||
const endpointOption = {};
|
||||
|
||||
getUserKeyValues.mockResolvedValue({
|
||||
apiKey: 'test',
|
||||
baseURL: 'https://user-provided-url.com',
|
||||
});
|
||||
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
expect(result.openAIApiKey).toBe('test');
|
||||
expect(result.client.options.reverseProxyUrl).toBe('https://user-provided-url.com');
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ const {
|
|||
EModelEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const { OllamaClient } = require('~/app/clients/OllamaClient');
|
||||
const { config } = require('./Config/EndpointService');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { extractBaseURL } = require('~/utils');
|
||||
|
||||
|
|
@ -27,8 +28,6 @@ const splitAndTrim = (input) => {
|
|||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService').config;
|
||||
|
||||
/**
|
||||
* Fetches OpenAI models from the specified base API path or Azure, based on the provided configuration.
|
||||
*
|
||||
|
|
@ -138,11 +137,11 @@ const fetchModels = async ({
|
|||
* @param {string} opts.user - The user ID to send to the API.
|
||||
* @param {boolean} [opts.azure=false] - Whether to fetch models from Azure.
|
||||
* @param {boolean} [opts.assistants=false] - Whether to fetch models from Azure.
|
||||
* @param {boolean} [opts.plugins=false] - Whether to fetch models from the plugins.
|
||||
* @param {string[]} [_models=[]] - The models to use as a fallback.
|
||||
*/
|
||||
const fetchOpenAIModels = async (opts, _models = []) => {
|
||||
let models = _models.slice() ?? [];
|
||||
const { openAIApiKey } = config;
|
||||
let apiKey = openAIApiKey;
|
||||
const openaiBaseURL = 'https://api.openai.com/v1';
|
||||
let baseURL = openaiBaseURL;
|
||||
|
|
@ -204,7 +203,6 @@ const fetchOpenAIModels = async (opts, _models = []) => {
|
|||
* @param {object} opts - The options for fetching the models.
|
||||
* @param {string} opts.user - The user ID to send to the API.
|
||||
* @param {boolean} [opts.azure=false] - Whether to fetch models from Azure.
|
||||
* @param {boolean} [opts.plugins=false] - Whether to fetch models for the plugins endpoint.
|
||||
* @param {boolean} [opts.assistants=false] - Whether to fetch models for the Assistants endpoint.
|
||||
*/
|
||||
const getOpenAIModels = async (opts) => {
|
||||
|
|
@ -216,24 +214,11 @@ const getOpenAIModels = async (opts) => {
|
|||
models = defaultModels[EModelEndpoint.azureAssistants];
|
||||
}
|
||||
|
||||
if (opts.plugins) {
|
||||
models = models.filter(
|
||||
(model) =>
|
||||
!model.includes('text-davinci') &&
|
||||
!model.includes('instruct') &&
|
||||
!model.includes('0613') &&
|
||||
!model.includes('0314') &&
|
||||
!model.includes('0301'),
|
||||
);
|
||||
}
|
||||
|
||||
let key;
|
||||
if (opts.assistants) {
|
||||
key = 'ASSISTANTS_MODELS';
|
||||
} else if (opts.azure) {
|
||||
key = 'AZURE_OPENAI_MODELS';
|
||||
} else if (opts.plugins) {
|
||||
key = 'PLUGIN_MODELS';
|
||||
} else {
|
||||
key = 'OPENAI_MODELS';
|
||||
}
|
||||
|
|
@ -243,22 +228,13 @@ const getOpenAIModels = async (opts) => {
|
|||
return models;
|
||||
}
|
||||
|
||||
if (userProvidedOpenAI) {
|
||||
if (config.userProvidedOpenAI) {
|
||||
return models;
|
||||
}
|
||||
|
||||
return await fetchOpenAIModels(opts, models);
|
||||
};
|
||||
|
||||
const getChatGPTBrowserModels = () => {
|
||||
let models = ['text-davinci-002-render-sha', 'gpt-4'];
|
||||
if (process.env.CHATGPT_MODELS) {
|
||||
models = splitAndTrim(process.env.CHATGPT_MODELS);
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches models from the Anthropic API.
|
||||
* @async
|
||||
|
|
@ -348,8 +324,7 @@ module.exports = {
|
|||
fetchModels,
|
||||
splitAndTrim,
|
||||
getOpenAIModels,
|
||||
getBedrockModels,
|
||||
getChatGPTBrowserModels,
|
||||
getAnthropicModels,
|
||||
getGoogleModels,
|
||||
getBedrockModels,
|
||||
getAnthropicModels,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -216,12 +216,6 @@ describe('getOpenAIModels', () => {
|
|||
expect(models).toEqual(expect.arrayContaining(['azure-model', 'azure-model-2']));
|
||||
});
|
||||
|
||||
it('returns `PLUGIN_MODELS` with `plugins` flag (and fetch fails)', async () => {
|
||||
process.env.PLUGIN_MODELS = 'plugins-model,plugins-model-2';
|
||||
const models = await getOpenAIModels({ plugins: true });
|
||||
expect(models).toEqual(expect.arrayContaining(['plugins-model', 'plugins-model-2']));
|
||||
});
|
||||
|
||||
it('returns `OPENAI_MODELS` with no flags (and fetch fails)', async () => {
|
||||
process.env.OPENAI_MODELS = 'openai-model,openai-model-2';
|
||||
const models = await getOpenAIModels({});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const partialRight = require('lodash/partialRight');
|
||||
const {
|
||||
Capabilities,
|
||||
EModelEndpoint,
|
||||
|
|
@ -7,8 +8,7 @@ const {
|
|||
defaultAssistantsVersion,
|
||||
defaultAgentCapabilities,
|
||||
} = require('librechat-data-provider');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const partialRight = require('lodash/partialRight');
|
||||
const { sendEvent, isUserProvided } = require('@librechat/api');
|
||||
|
||||
const addSpaceIfNeeded = (text) => (text.length > 0 && !text.endsWith(' ') ? text + ' ' : text);
|
||||
|
||||
|
|
@ -117,14 +117,6 @@ function formatAction(action) {
|
|||
return formattedAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided value is 'user_provided'.
|
||||
*
|
||||
* @param {string} value - The value to check.
|
||||
* @returns {boolean} - Returns true if the value is 'user_provided', otherwise false.
|
||||
*/
|
||||
const isUserProvided = (value) => value === 'user_provided';
|
||||
|
||||
/**
|
||||
* Generate the configuration for a given key and base URL.
|
||||
* @param {string} key
|
||||
|
|
@ -174,7 +166,6 @@ module.exports = {
|
|||
handleText,
|
||||
formatSteps,
|
||||
formatAction,
|
||||
isUserProvided,
|
||||
generateConfig,
|
||||
addSpaceIfNeeded,
|
||||
createOnProgress,
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
jest.mock('@waylaidwanderer/fetch-event-source', () => ({
|
||||
fetchEventSource: jest
|
||||
.fn()
|
||||
.mockImplementation((url, { onopen, onmessage, onclose, onerror, error }) => {
|
||||
// Simulating the onopen event
|
||||
onopen && onopen({ status: 200 });
|
||||
|
||||
// Simulating a few onmessage events
|
||||
onmessage &&
|
||||
onmessage({ data: JSON.stringify({ message: 'First message' }), event: 'message' });
|
||||
onmessage &&
|
||||
onmessage({ data: JSON.stringify({ message: 'Second message' }), event: 'message' });
|
||||
onmessage &&
|
||||
onmessage({ data: JSON.stringify({ message: 'Third message' }), event: 'message' });
|
||||
|
||||
// Simulate the onclose event
|
||||
onclose && onclose();
|
||||
|
||||
if (error) {
|
||||
// Simulate the onerror event
|
||||
onerror && onerror({ status: 500 });
|
||||
}
|
||||
|
||||
// Return a Promise that resolves to simulate async behavior
|
||||
return Promise.resolve();
|
||||
}),
|
||||
}));
|
||||
|
|
@ -1264,12 +1264,6 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports OpenAISpecClient
|
||||
* @typedef {import('./app/clients/OpenAIClient')} OpenAISpecClient
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TAgentClient
|
||||
* @typedef {import('./server/controllers/agents/client')} TAgentClient
|
||||
|
|
@ -1498,13 +1492,11 @@
|
|||
* @typedef {Object} EndpointServiceConfig
|
||||
* @property {string} openAIApiKey - The API key for OpenAI.
|
||||
* @property {string} azureOpenAIApiKey - The API key for Azure OpenAI.
|
||||
* @property {boolean} useAzurePlugins - Flag to indicate if Azure plugins are used.
|
||||
* @property {boolean} userProvidedOpenAI - Flag to indicate if OpenAI API key is user provided.
|
||||
* @property {string} googleKey - The Palm key.
|
||||
* @property {boolean|{userProvide: boolean}} [openAI] - Flag to indicate if OpenAI endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [assistant] - Flag to indicate if Assistant endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [azureOpenAI] - Flag to indicate if Azure OpenAI endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [chatGPTBrowser] - Flag to indicate if ChatGPT Browser endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [anthropic] - Flag to indicate if Anthropic endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [google] - Flag to indicate if Google endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean, userProvideURL: boolean, name: string}} [custom] - Custom Endpoint configuration.
|
||||
|
|
@ -1519,23 +1511,12 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports GptPlugins
|
||||
* @typedef {Object} GptPlugins
|
||||
* @property {Plugin[]} plugins - An array of plugins available.
|
||||
* @property {string[]} availableAgents - Available agents, 'classic' or 'functions'.
|
||||
* @property {boolean} userProvide - A flag indicating if the user has provided the data.
|
||||
* @property {boolean} azure - A flag indicating if azure plugins are used.
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports DefaultConfig
|
||||
* @typedef {Object} DefaultConfig
|
||||
* @property {boolean|{userProvide: boolean}} [openAI] - Flag to indicate if OpenAI endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [assistant] - Flag to indicate if Assistant endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [azureOpenAI] - Flag to indicate if Azure OpenAI endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [chatGPTBrowser] - Flag to indicate if ChatGPT Browser endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [anthropic] - Flag to indicate if Anthropic endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean}} [google] - Flag to indicate if Google endpoint is user provided, or its configuration.
|
||||
* @property {boolean|{userProvide: boolean, userProvideURL: boolean, name: string}} [custom] - Custom Endpoint configuration.
|
||||
|
|
|
|||
|
|
@ -323,10 +323,6 @@ export type TSetOptionsPayload = {
|
|||
setExample: TSetExample;
|
||||
addExample: () => void;
|
||||
removeExample: () => void;
|
||||
setAgentOption: TSetOption;
|
||||
// getConversation: () => t.TConversation | t.TPreset | null;
|
||||
checkPluginSelection: (value: string) => boolean;
|
||||
setTools: (newValue: string, remove?: boolean) => void;
|
||||
setOptions?: TSetOptions;
|
||||
};
|
||||
|
||||
|
|
@ -447,7 +443,7 @@ export type TDialogProps = {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export type TPluginStoreDialogProps = {
|
||||
export type ToolDialogProps = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
};
|
||||
|
|
@ -602,7 +598,6 @@ export type NewConversationParams = {
|
|||
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;
|
||||
|
||||
export type TBaseResData = {
|
||||
plugin?: t.TResPlugin;
|
||||
final?: boolean;
|
||||
initial?: boolean;
|
||||
previousMessages?: t.TMessage[];
|
||||
|
|
|
|||
|
|
@ -13,13 +13,7 @@ const ConversationStarters = () => {
|
|||
|
||||
const endpointType = useMemo(() => {
|
||||
let ep = conversation?.endpoint ?? '';
|
||||
if (
|
||||
[
|
||||
EModelEndpoint.chatGPTBrowser,
|
||||
EModelEndpoint.azureOpenAI,
|
||||
EModelEndpoint.gptPlugins,
|
||||
].includes(ep as EModelEndpoint)
|
||||
) {
|
||||
if (ep === EModelEndpoint.azureOpenAI) {
|
||||
ep = EModelEndpoint.openAI;
|
||||
}
|
||||
return getIconEndpoint({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { EModelEndpoint, SettingsViews } from 'librechat-data-provider';
|
||||
import { Button, MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '@librechat/client';
|
||||
import { Button, MessagesSquared, AssistantIcon, DataIcon } from '@librechat/client';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils/';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
type TPopoverButton = {
|
||||
|
|
@ -28,14 +28,8 @@ export default function PopoverButtons({
|
|||
endpointType?: EModelEndpoint | string | null;
|
||||
model?: string | null;
|
||||
}) {
|
||||
const {
|
||||
conversation,
|
||||
optionSettings,
|
||||
setOptionSettings,
|
||||
showAgentSettings,
|
||||
setShowAgentSettings,
|
||||
} = useChatContext();
|
||||
const localize = useLocalize();
|
||||
const { conversation, optionSettings, setOptionSettings } = useChatContext();
|
||||
const [settingsView, setSettingsView] = useRecoilState(store.currentSettingsView);
|
||||
|
||||
const { model: _model, endpoint: _endpoint, endpointType } = conversation ?? {};
|
||||
|
|
@ -64,19 +58,6 @@ export default function PopoverButtons({
|
|||
icon: <MessagesSquared className={cn('mr-1 w-[14px]', iconClass)} />,
|
||||
},
|
||||
],
|
||||
[EModelEndpoint.gptPlugins]: [
|
||||
{
|
||||
label: localize(
|
||||
showAgentSettings ? 'com_show_completion_settings' : 'com_show_agent_settings',
|
||||
),
|
||||
buttonClass: '',
|
||||
handler: () => {
|
||||
setSettingsView(SettingsViews.default);
|
||||
setShowAgentSettings((prev) => !prev);
|
||||
},
|
||||
icon: <GPTIcon className={cn('mr-1 w-[14px]', iconClass)} size={24} />,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (!endpoint) {
|
||||
|
|
|
|||
|
|
@ -43,13 +43,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
|
||||
const endpointType = useMemo(() => {
|
||||
let ep = conversation?.endpoint ?? '';
|
||||
if (
|
||||
[
|
||||
EModelEndpoint.chatGPTBrowser,
|
||||
EModelEndpoint.azureOpenAI,
|
||||
EModelEndpoint.gptPlugins,
|
||||
].includes(ep as EModelEndpoint)
|
||||
) {
|
||||
if (ep === EModelEndpoint.azureOpenAI) {
|
||||
ep = EModelEndpoint.openAI;
|
||||
}
|
||||
return getIconEndpoint({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { SettingsIcon } from 'lucide-react';
|
||||
import { TooltipAnchor, Spinner } from '@librechat/client';
|
||||
import { Spinner } from '@librechat/client';
|
||||
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TModelSpec } from 'librechat-data-provider';
|
||||
import type { Endpoint } from '~/common';
|
||||
|
|
@ -82,7 +82,10 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
|
|||
}, [modelSpecs, endpoint.value]);
|
||||
|
||||
const searchValue = endpointSearchValues[endpoint.value] || '';
|
||||
const isUserProvided = useMemo(() => endpointRequiresUserKey(endpoint.value), [endpoint.value]);
|
||||
const isUserProvided = useMemo(
|
||||
() => endpointRequiresUserKey(endpoint.value),
|
||||
[endpointRequiresUserKey, endpoint.value],
|
||||
);
|
||||
|
||||
const renderIconLabel = () => (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -99,18 +102,6 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
|
|||
>
|
||||
{endpoint.label}
|
||||
</span>
|
||||
{/* TODO: remove this after deprecation */}
|
||||
{endpoint.value === 'gptPlugins' && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_endpoint_deprecated_info')}
|
||||
aria-label={localize('com_endpoint_deprecated_info_a11y')}
|
||||
render={
|
||||
<span className="ml-2 rounded bg-amber-600/70 px-2 py-0.5 text-xs font-semibold text-white">
|
||||
{localize('com_endpoint_deprecated')}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const EditPresetDialog = ({
|
|||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const { preset, setPreset } = useChatContext();
|
||||
const { setOption, setOptions, setAgentOption } = useSetIndexOptions(preset);
|
||||
const { setOption, setOptions } = useSetIndexOptions(preset);
|
||||
const [onTitleChange, title] = useDebouncedInput({
|
||||
setOption,
|
||||
optionKey: 'title',
|
||||
|
|
@ -87,20 +87,7 @@ const EditPresetDialog = ({
|
|||
console.log('setting model', models[0]);
|
||||
setOption('model')(models[0]);
|
||||
}
|
||||
|
||||
if (preset.agentOptions?.model === models[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
preset.agentOptions?.model != null &&
|
||||
preset.agentOptions.model &&
|
||||
!models.includes(preset.agentOptions.model)
|
||||
) {
|
||||
console.log('setting agent model', models[0]);
|
||||
setAgentOption('model')(models[0]);
|
||||
}
|
||||
}, [preset, queryClient, setOption, setAgentOption]);
|
||||
}, [preset, queryClient, setOption]);
|
||||
|
||||
const switchEndpoint = useCallback(
|
||||
(newEndpoint: string) => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
|
|||
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
||||
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
import { fontSizeAtom } from '~/store/fontSize';
|
||||
import { MessageContext } from '~/Providers';
|
||||
|
|
@ -178,7 +177,6 @@ const MessageRender = memo(
|
|||
isLatestMessage,
|
||||
}}
|
||||
>
|
||||
{msg.plugin && <Plugin plugin={msg.plugin} />}
|
||||
<MessageContent
|
||||
ask={ask}
|
||||
edit={edit}
|
||||
|
|
|
|||
|
|
@ -57,16 +57,7 @@ function getGoogleModelName(model: string | null | undefined) {
|
|||
}
|
||||
|
||||
const MessageEndpointIcon: React.FC<IconProps> = (props) => {
|
||||
const {
|
||||
error,
|
||||
button,
|
||||
iconURL = '',
|
||||
endpoint,
|
||||
size = 30,
|
||||
model = '',
|
||||
assistantName,
|
||||
agentName,
|
||||
} = props;
|
||||
const { error, iconURL = '', endpoint, size = 30, model = '', assistantName, agentName } = props;
|
||||
|
||||
const assistantsIcon = {
|
||||
icon: iconURL ? (
|
||||
|
|
@ -142,11 +133,6 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
|
|||
bg: getOpenAIColor(model),
|
||||
name: 'ChatGPT',
|
||||
},
|
||||
[EModelEndpoint.gptPlugins]: {
|
||||
icon: <Plugin size={size * 0.7} />,
|
||||
bg: `rgba(69, 89, 164, ${button === true ? 0.75 : 1})`,
|
||||
name: 'Plugins',
|
||||
},
|
||||
[EModelEndpoint.google]: {
|
||||
icon: getGoogleIcon(model, size),
|
||||
name: getGoogleModelName(model),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { Feather } from 'lucide-react';
|
||||
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
|
||||
import {
|
||||
Sparkles,
|
||||
BedrockIcon,
|
||||
AnthropicIcon,
|
||||
AzureMinimalIcon,
|
||||
OpenAIMinimalIcon,
|
||||
LightningIcon,
|
||||
MinimalPlugin,
|
||||
GoogleMinimalIcon,
|
||||
CustomMinimalIcon,
|
||||
AnthropicIcon,
|
||||
BedrockIcon,
|
||||
Sparkles,
|
||||
} from '@librechat/client';
|
||||
import UnknownIcon from '~/hooks/Endpoint/UnknownIcon';
|
||||
import { IconProps } from '~/common';
|
||||
|
|
@ -33,7 +31,6 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
|||
icon: <OpenAIMinimalIcon className={iconClassName} />,
|
||||
name: props.chatGptLabel ?? 'ChatGPT',
|
||||
},
|
||||
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
|
||||
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel ?? 'Google' },
|
||||
[EModelEndpoint.anthropic]: {
|
||||
icon: <AnthropicIcon className="icon-md shrink-0 dark:text-white" />,
|
||||
|
|
@ -43,7 +40,6 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
|||
icon: <CustomMinimalIcon />,
|
||||
name: 'Custom',
|
||||
},
|
||||
[EModelEndpoint.chatGPTBrowser]: { icon: <LightningIcon />, name: 'ChatGPT' },
|
||||
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
|
||||
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
|
||||
[EModelEndpoint.agents]: {
|
||||
|
|
|
|||
|
|
@ -1,248 +0,0 @@
|
|||
import {
|
||||
Switch,
|
||||
Label,
|
||||
Slider,
|
||||
HoverCard,
|
||||
InputNumber,
|
||||
SelectDropDown,
|
||||
HoverCardTrigger,
|
||||
} from '@librechat/client';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import { cn, optionText, defaultTextProps, removeFocusRings } from '~/utils';
|
||||
import OptionHover from './OptionHover';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) {
|
||||
const localize = useLocalize();
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
const { agent, skipCompletion, model, temperature } = conversation.agentOptions ?? {};
|
||||
|
||||
const setModel = setOption('model');
|
||||
const setTemperature = setOption('temperature');
|
||||
const setAgent = setOption('agent');
|
||||
const setSkipCompletion = setOption('skipCompletion');
|
||||
const onCheckedChangeAgent = (checked: boolean) => {
|
||||
setAgent(checked ? 'functions' : 'classic');
|
||||
};
|
||||
|
||||
const onCheckedChangeSkip = (checked: boolean) => {
|
||||
setSkipCompletion(checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-6">
|
||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
title={localize('com_endpoint_agent_model')}
|
||||
value={model ?? ''}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
disabled={readonly}
|
||||
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusRings)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_temperature')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
onChange={(value) => setTemperature(Number(value))}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature ?? 0]}
|
||||
onValueChange={(value: number[]) => setTemperature(value[0])}
|
||||
onDoubleClick={() => setTemperature(1)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
aria-labelledby="temp-int"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
|
||||
</HoverCard>
|
||||
<div className="grid w-full grid-cols-2 items-center gap-10">
|
||||
<HoverCard openDelay={500}>
|
||||
<HoverCardTrigger className="flex w-[100px] flex-col items-center space-y-4 text-center">
|
||||
<label
|
||||
htmlFor="functions-agent"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
<small>{localize('com_endpoint_plug_use_functions')}</small>
|
||||
</label>
|
||||
<Switch
|
||||
id="functions-agent"
|
||||
checked={agent === 'functions'}
|
||||
onCheckedChange={onCheckedChangeAgent}
|
||||
disabled={readonly}
|
||||
className="ml-4 mt-2"
|
||||
aria-label={localize('com_endpoint_plug_use_functions')}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="func" side={ESide.Bottom} />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={500}>
|
||||
<HoverCardTrigger className="ml-[-60px] flex w-[100px] flex-col items-center space-y-4 text-center">
|
||||
<label
|
||||
htmlFor="skip-completion"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
<small>{localize('com_endpoint_plug_skip_completion')}</small>
|
||||
</label>
|
||||
<Switch
|
||||
id="skip-completion"
|
||||
checked={skipCompletion === true}
|
||||
onCheckedChange={onCheckedChangeSkip}
|
||||
disabled={readonly}
|
||||
className="ml-4 mt-2"
|
||||
aria-label={localize('com_endpoint_plug_skip_completion')}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="skip" side={ESide.Bottom} />
|
||||
</HoverCard>
|
||||
</div>
|
||||
{/* <HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
Top P <small className="opacity-40">(default: 1)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||
Frequency Penalty <small className="opacity-40">(default: 0)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="freq-penalty-int"
|
||||
disabled={readonly}
|
||||
value={freqP}
|
||||
onChange={(value) => setFreqP(value)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[freqP]}
|
||||
onValueChange={(value) => setFreqP(value[0])}
|
||||
doubleClickHandler={() => setFreqP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="freq" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||
Presence Penalty <small className="opacity-40">(default: 0)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="pres-penalty-int"
|
||||
disabled={readonly}
|
||||
value={presP}
|
||||
onChange={(value) => setPresP(value)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[presP]}
|
||||
onValueChange={(value) => setPresP(value[0])}
|
||||
doubleClickHandler={() => setPresP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="pres" side="left" />
|
||||
</HoverCard> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import Settings from '../Plugins';
|
||||
import AgentSettings from '../AgentSettings';
|
||||
import { useSetIndexOptions } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
export default function PluginsView({ conversation, models, isPreset = false }) {
|
||||
const { showAgentSettings } = useChatContext();
|
||||
const { setOption, setTools, setAgentOption, checkPluginSelection } = useSetIndexOptions(
|
||||
isPreset ? conversation : null,
|
||||
);
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return showAgentSettings ? (
|
||||
<AgentSettings conversation={conversation} setOption={setAgentOption} models={models} />
|
||||
) : (
|
||||
<Settings
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
setTools={setTools}
|
||||
checkPluginSelection={checkPluginSelection}
|
||||
models={models}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
export { default as GoogleSettings } from './GoogleSettings';
|
||||
export { default as PluginSettings } from './PluginSettings';
|
||||
|
|
|
|||
|
|
@ -36,11 +36,6 @@ const types = {
|
|||
},
|
||||
openAI,
|
||||
azureOpenAI: openAI,
|
||||
gptPlugins: {
|
||||
func: 'com_endpoint_func_hover',
|
||||
skip: 'com_endpoint_skip_hover',
|
||||
...openAI,
|
||||
},
|
||||
};
|
||||
|
||||
function OptionHover({ endpoint, type, side }: TOptionHoverProps) {
|
||||
|
|
|
|||
|
|
@ -1,392 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
Slider,
|
||||
HoverCard,
|
||||
InputNumber,
|
||||
SelectDropDown,
|
||||
HoverCardTrigger,
|
||||
} from '@librechat/client';
|
||||
import type { TModelSelectProps, OnInputNumberChange } from '~/common';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
import {
|
||||
removeFocusOutlines,
|
||||
defaultTextProps,
|
||||
removeFocusRings,
|
||||
processPlugins,
|
||||
selectPlugins,
|
||||
optionText,
|
||||
cn,
|
||||
} from '~/utils';
|
||||
import OptionHoverAlt from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import MultiSelectDropDown from '~/components/Input/ModelSelect/MultiSelectDropDown';
|
||||
import { useLocalize, useDebouncedInput } from '~/hooks';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
export default function Settings({
|
||||
conversation,
|
||||
setOption,
|
||||
setTools,
|
||||
checkPluginSelection,
|
||||
models,
|
||||
readonly,
|
||||
}: TModelSelectProps & {
|
||||
setTools: (newValue: string, remove?: boolean | undefined) => void;
|
||||
checkPluginSelection: (value: string) => boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
const { data: allPlugins } = useAvailablePluginsQuery({
|
||||
select: selectPlugins,
|
||||
});
|
||||
|
||||
const conversationTools: TPlugin[] = useMemo(() => {
|
||||
if (!conversation?.tools) {
|
||||
return [];
|
||||
}
|
||||
return processPlugins(conversation.tools, allPlugins?.map);
|
||||
}, [conversation, allPlugins]);
|
||||
|
||||
const availablePlugins = useMemo(() => {
|
||||
if (!availableTools) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.values(availableTools);
|
||||
}, [availableTools]);
|
||||
|
||||
const {
|
||||
model,
|
||||
modelLabel,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
top_p: topP,
|
||||
frequency_penalty: freqP,
|
||||
presence_penalty: presP,
|
||||
maxContextTokens,
|
||||
} = conversation ?? {};
|
||||
|
||||
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput<string | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'chatGptLabel',
|
||||
initialValue: modelLabel ?? chatGptLabel,
|
||||
});
|
||||
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput<string | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'promptPrefix',
|
||||
initialValue: promptPrefix,
|
||||
});
|
||||
const [setTemperature, temperatureValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'temperature',
|
||||
initialValue: temperature,
|
||||
});
|
||||
const [setTopP, topPValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'top_p',
|
||||
initialValue: topP,
|
||||
});
|
||||
const [setFreqP, freqPValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'frequency_penalty',
|
||||
initialValue: freqP,
|
||||
});
|
||||
const [setPresP, presPValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'presence_penalty',
|
||||
initialValue: presP,
|
||||
});
|
||||
const [setMaxContextTokens, maxContextTokensValue] = useDebouncedInput<number | null | undefined>(
|
||||
{
|
||||
setOption,
|
||||
optionKey: 'maxContextTokens',
|
||||
initialValue: maxContextTokens,
|
||||
},
|
||||
);
|
||||
|
||||
const setModel = setOption('model');
|
||||
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-6">
|
||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
title={localize('com_endpoint_completion_model')}
|
||||
value={model ?? ''}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
disabled={readonly}
|
||||
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusRings)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_custom_name')}{' '}
|
||||
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
disabled={readonly}
|
||||
value={chatGptLabelValue || ''}
|
||||
onChange={(e) => setChatGptLabel(e.target.value ?? null)}
|
||||
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_prompt_prefix')}{' '}
|
||||
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefixValue || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value ?? null)}
|
||||
placeholder={localize(
|
||||
'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder',
|
||||
)}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
|
||||
<MultiSelectDropDown
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
setSelected={setTools}
|
||||
value={conversationTools}
|
||||
optionValueKey="pluginKey"
|
||||
availableValues={availablePlugins}
|
||||
isSelected={checkPluginSelection}
|
||||
searchPlaceholder={localize('com_ui_select_search_plugin')}
|
||||
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusOutlines)}
|
||||
optionsClassName="w-full max-h-[275px] dark:bg-gray-700 z-10 border dark:border-gray-600"
|
||||
containerClassName="flex w-full resize-none border border-transparent"
|
||||
labelClassName="dark:text-white"
|
||||
/>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="mt-1 flex w-full justify-between">
|
||||
<Label htmlFor="max-context-tokens" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_context_tokens')}{' '}
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="max-context-tokens"
|
||||
stringMode={false}
|
||||
disabled={readonly}
|
||||
value={maxContextTokensValue as number}
|
||||
onChange={setMaxContextTokens as OnInputNumberChange}
|
||||
placeholder={localize('com_nav_theme_system')}
|
||||
min={10}
|
||||
max={2000000}
|
||||
step={1000}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
'w-1/3',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<OptionHoverAlt
|
||||
description="com_endpoint_context_info"
|
||||
langCode={true}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_temperature')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', { 0: '0.8' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperatureValue}
|
||||
onChange={(value) => setTemperature(Number(value))}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperatureValue ?? 0.8]}
|
||||
onValueChange={(value) => setTemperature(value[0])}
|
||||
onDoubleClick={() => setTemperature(0.8)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
aria-labelledby="temp-int"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_top_p')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topPValue}
|
||||
onChange={(value) => setTopP(Number(value))}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topPValue ?? 1]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
onDoubleClick={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
aria-labelledby="top-p-int"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="topp" side={ESide.Left} />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_frequency_penalty')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="freq-penalty-int"
|
||||
disabled={readonly}
|
||||
value={freqPValue}
|
||||
onChange={(value) => setFreqP(Number(value))}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[freqPValue ?? 0]}
|
||||
onValueChange={(value) => setFreqP(value[0])}
|
||||
onDoubleClick={() => setFreqP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
aria-labelledby="freq-penalty-int"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="freq" side={ESide.Left} />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_presence_penalty')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="pres-penalty-int"
|
||||
disabled={readonly}
|
||||
value={presPValue}
|
||||
onChange={(value) => setPresP(Number(value))}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[presPValue ?? 0]}
|
||||
onValueChange={(value) => setPresP(value[0])}
|
||||
onDoubleClick={() => setPresP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
aria-labelledby="pres-penalty-int"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover endpoint={conversation.endpoint ?? ''} type="pres" side={ESide.Left} />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ export { default as AssistantsSettings } from './Assistants';
|
|||
export { default as BedrockSettings } from './Bedrock';
|
||||
export { default as OpenAISettings } from './OpenAI';
|
||||
export { default as GoogleSettings } from './Google';
|
||||
export { default as PluginsSettings } from './Plugins';
|
||||
export { default as Examples } from './Examples';
|
||||
export { default as AgentSettings } from './AgentSettings';
|
||||
export { default as AnthropicSettings } from './Anthropic';
|
||||
export * from './settings';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue