From 63afb317c6fa22849e1675feb2561784e4ddd57a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 6 Feb 2025 18:13:18 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20fix:=20Resolve=20Google=20Client?= =?UTF-8?q?=20Issues,=20CDN=20Screenshots,=20Update=20Models=20(#5703)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🤖 refactor: streamline model selection logic for title model in GoogleClient * refactor: add options for empty object schemas in convertJsonSchemaToZod * refactor: add utility function to check for empty object schemas in convertJsonSchemaToZod * fix: Google MCP Tool errors, and remove Object Unescaping as Google fixed this * fix: google safetySettings * feat: add safety settings exclusion via GOOGLE_EXCLUDE_SAFETY_SETTINGS environment variable * fix: rename environment variable for console JSON string length * fix: disable portal for dropdown in ExportModal component * fix: screenshot functionality to use image placeholder for remote images * feat: add visionMode property to BaseClient and initialize in GoogleClient to fix resendFiles issue * fix: enhance formatMessages to include image URLs in message content for Vertex AI * fix: safety settings for titleChatCompletion * fix: remove deprecated model assignment in GoogleClient and streamline title model retrieval * fix: remove unused image preloading logic in ScreenshotContext * chore: update default google models to latest models shared by vertex ai and gen ai * refactor: enhance Google error messaging * fix: update token values and model limits for Gemini models * ci: fix model matching * chore: bump version of librechat-data-provider to 0.7.699 --- api/app/clients/BaseClient.js | 4 +- api/app/clients/GoogleClient.js | 103 +- api/config/parsers.js | 6 +- api/models/tx.js | 9 +- api/models/tx.spec.js | 78 ++ api/package.json | 2 +- api/server/services/Endpoints/google/llm.js | 46 +- api/server/services/Endpoints/google/title.js | 23 +- api/server/services/MCP.js | 19 +- api/utils/tokens.js | 13 +- api/utils/tokens.spec.js | 18 + .../src/components/Messages/Content/Error.tsx | 8 +- .../Nav/ExportConversation/ExportModal.tsx | 2 +- client/src/hooks/ScreenshotContext.tsx | 14 +- package-lock.json | 1146 +++++++++-------- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 29 +- packages/data-provider/src/zod.spec.ts | 113 +- packages/data-provider/src/zod.ts | 24 +- 19 files changed, 939 insertions(+), 720 deletions(-) diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 6ddcaa9721..ebf3ca12d9 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -57,6 +57,8 @@ class BaseClient { this.continued; /** @type {TMessage[]} */ this.currentMessages = []; + /** @type {import('librechat-data-provider').VisionModes | undefined} */ + this.visionMode; } setOptions() { @@ -1095,7 +1097,7 @@ class BaseClient { file_id: { $in: fileIds }, }); - await this.addImageURLs(message, files); + await this.addImageURLs(message, files, this.visionMode); this.message_file_map[message.messageId] = files; return message; diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index 6da8439664..03461a6796 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -10,11 +10,13 @@ const { getResponseSender, endpointSettings, EModelEndpoint, + ContentTypes, VisionModes, ErrorTypes, Constants, AuthKeys, } = require('librechat-data-provider'); +const { getSafetySettings } = require('~/server/services/Endpoints/google/llm'); const { encodeAndFormat } = require('~/server/services/Files/images'); const Tokenizer = require('~/server/services/Tokenizer'); const { spendTokens } = require('~/models/spendTokens'); @@ -70,7 +72,7 @@ class GoogleClient extends BaseClient { /** The key for the usage object's output tokens * @type {string} */ this.outputTokensKey = 'output_tokens'; - + this.visionMode = VisionModes.generative; if (options.skipSetOptions) { return; } @@ -215,10 +217,29 @@ class GoogleClient extends BaseClient { } formatMessages() { - return ((message) => ({ - author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel), - content: message?.content ?? message.text, - })).bind(this); + 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); } /** @@ -566,6 +587,7 @@ class GoogleClient extends BaseClient { 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; @@ -607,13 +629,14 @@ class GoogleClient extends BaseClient { } async getCompletion(_payload, options = {}) { - const safetySettings = this.getSafetySettings(); 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 {GenAI} */ @@ -714,8 +737,16 @@ class GoogleClient extends BaseClient { 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; } @@ -781,12 +812,11 @@ class GoogleClient extends BaseClient { * 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 = {}) { - const { abortController } = options; - const safetySettings = this.getSafetySettings(); - let reply = ''; + const { abortController } = options; const model = 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} */ @@ -844,17 +874,6 @@ class GoogleClient extends BaseClient { }, ]); - const model = process.env.GOOGLE_TITLE_MODEL ?? this.modelOptions.model; - const availableModels = this.options.modelsConfig?.[EModelEndpoint.google]; - this.isVisionModel = validateVisionModel({ model, availableModels }); - - if (this.isVisionModel) { - logger.warn( - `Current vision model does not support titling without an attachment; falling back to default model ${settings.model.default}`, - ); - this.modelOptions.model = settings.model.default; - } - try { this.initializeClient(); title = await this.titleChatCompletion(payload, { @@ -892,48 +911,6 @@ class GoogleClient extends BaseClient { return reply.trim(); } - getSafetySettings() { - const model = this.modelOptions.model; - const isGemini2 = model.includes('gemini-2.0') && !model.includes('thinking'); - const mapThreshold = (value) => { - if (isGemini2 && value === 'BLOCK_NONE') { - return 'OFF'; - } - return value; - }; - - return [ - { - category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', - threshold: mapThreshold( - process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED', - ), - }, - { - category: 'HARM_CATEGORY_HATE_SPEECH', - threshold: mapThreshold( - process.env.GOOGLE_SAFETY_HATE_SPEECH || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED', - ), - }, - { - category: 'HARM_CATEGORY_HARASSMENT', - threshold: mapThreshold( - process.env.GOOGLE_SAFETY_HARASSMENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED', - ), - }, - { - category: 'HARM_CATEGORY_DANGEROUS_CONTENT', - threshold: mapThreshold( - process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED', - ), - }, - { - category: 'HARM_CATEGORY_CIVIC_INTEGRITY', - threshold: mapThreshold(process.env.GOOGLE_SAFETY_CIVIC_INTEGRITY || 'BLOCK_NONE'), - }, - ]; - } - getEncoding() { return 'cl100k_base'; } diff --git a/api/config/parsers.js b/api/config/parsers.js index 51e9c052d0..7bf5be336e 100644 --- a/api/config/parsers.js +++ b/api/config/parsers.js @@ -4,7 +4,7 @@ const traverse = require('traverse'); const SPLAT_SYMBOL = Symbol.for('splat'); const MESSAGE_SYMBOL = Symbol.for('message'); -const CONSOLE_JSON_LONG_STRING_LENGTH=parseInt(process.env.CONSOLE_JSON_LONG_STRING_LENGTH) || 255; +const CONSOLE_JSON_STRING_LENGTH = parseInt(process.env.CONSOLE_JSON_STRING_LENGTH) || 255; const sensitiveKeys = [ /^(sk-)[^\s]+/, // OpenAI API key pattern @@ -206,13 +206,13 @@ const jsonTruncateFormat = winston.format((info) => { seen.add(obj); if (Array.isArray(obj)) { - return obj.map(item => truncateObject(item)); + return obj.map((item) => truncateObject(item)); } const newObj = {}; Object.entries(obj).forEach(([key, value]) => { if (typeof value === 'string') { - newObj[key] = truncateLongStrings(value, CONSOLE_JSON_LONG_STRING_LENGTH); + newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH); } else { newObj[key] = truncateObject(value); } diff --git a/api/models/tx.js b/api/models/tx.js index 776fac123c..05412430c7 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -102,9 +102,14 @@ const tokenValues = Object.assign( /* cohere doesn't have rates for the older command models, so this was from https://artificialanalysis.ai/models/command-light/providers */ command: { prompt: 0.38, completion: 0.38 }, + 'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 }, + 'gemini-2.0-flash': { prompt: 0.1, completion: 0.7 }, 'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing - 'gemini-1.5': { prompt: 7, completion: 21 }, // May 2nd, 2024 pricing - gemini: { prompt: 0.5, completion: 1.5 }, // May 2nd, 2024 pricing + 'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 }, + 'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 }, + 'gemini-1.5': { prompt: 2.5, completion: 10 }, + 'gemini-pro-vision': { prompt: 0.5, completion: 1.5 }, + gemini: { prompt: 0.5, completion: 1.5 }, }, bedrockValues, ); diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index 0492a0eedb..d77973a7f5 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -380,3 +380,81 @@ describe('getCacheMultiplier', () => { ).toBe(0.03); }); }); + +describe('Google Model Tests', () => { + const googleModels = [ + 'gemini-2.0-flash-lite-preview-02-05', + 'gemini-2.0-flash-001', + 'gemini-2.0-flash-exp', + 'gemini-2.0-pro-exp-02-05', + 'gemini-1.5-flash-8b', + 'gemini-1.5-flash-thinking', + 'gemini-1.5-pro-latest', + 'gemini-1.5-pro-preview-0409', + 'gemini-pro-vision', + 'gemini-1.0', + 'gemini-pro', + ]; + + it('should return the correct prompt and completion rates for all models', () => { + const results = googleModels.map((model) => { + const valueKey = getValueKey(model, EModelEndpoint.google); + const promptRate = getMultiplier({ + model, + tokenType: 'prompt', + endpoint: EModelEndpoint.google, + }); + const completionRate = getMultiplier({ + model, + tokenType: 'completion', + endpoint: EModelEndpoint.google, + }); + return { model, valueKey, promptRate, completionRate }; + }); + + results.forEach(({ valueKey, promptRate, completionRate }) => { + expect(promptRate).toBe(tokenValues[valueKey].prompt); + expect(completionRate).toBe(tokenValues[valueKey].completion); + }); + }); + + it('should map to the correct model keys', () => { + const expected = { + 'gemini-2.0-flash-lite-preview-02-05': 'gemini-2.0-flash-lite', + 'gemini-2.0-flash-001': 'gemini-2.0-flash', + 'gemini-2.0-flash-exp': 'gemini-2.0-flash', + 'gemini-2.0-pro-exp-02-05': 'gemini-2.0', + 'gemini-1.5-flash-8b': 'gemini-1.5-flash-8b', + 'gemini-1.5-flash-thinking': 'gemini-1.5-flash', + 'gemini-1.5-pro-latest': 'gemini-1.5', + 'gemini-1.5-pro-preview-0409': 'gemini-1.5', + 'gemini-pro-vision': 'gemini-pro-vision', + 'gemini-1.0': 'gemini', + 'gemini-pro': 'gemini', + }; + + Object.entries(expected).forEach(([model, expectedKey]) => { + const valueKey = getValueKey(model, EModelEndpoint.google); + expect(valueKey).toBe(expectedKey); + }); + }); + + it('should handle model names with different formats', () => { + const testCases = [ + { input: 'google/gemini-pro', expected: 'gemini' }, + { input: 'gemini-pro/google', expected: 'gemini' }, + { input: 'google/gemini-2.0-flash-lite', expected: 'gemini-2.0-flash-lite' }, + ]; + + testCases.forEach(({ input, expected }) => { + const valueKey = getValueKey(input, EModelEndpoint.google); + expect(valueKey).toBe(expected); + expect( + getMultiplier({ model: input, tokenType: 'prompt', endpoint: EModelEndpoint.google }), + ).toBe(tokenValues[expected].prompt); + expect( + getMultiplier({ model: input, tokenType: 'completion', endpoint: EModelEndpoint.google }), + ).toBe(tokenValues[expected].completion); + }); + }); +}); diff --git a/api/package.json b/api/package.json index db55b94508..10264309c9 100644 --- a/api/package.json +++ b/api/package.json @@ -45,7 +45,7 @@ "@langchain/google-genai": "^0.1.7", "@langchain/google-vertexai": "^0.1.8", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.0.1", + "@librechat/agents": "^2.0.2", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.7.7", "bcryptjs": "^2.4.3", diff --git a/api/server/services/Endpoints/google/llm.js b/api/server/services/Endpoints/google/llm.js index ae5e268ac3..a64b33480b 100644 --- a/api/server/services/Endpoints/google/llm.js +++ b/api/server/services/Endpoints/google/llm.js @@ -1,18 +1,42 @@ const { Providers } = require('@librechat/agents'); const { AuthKeys } = require('librechat-data-provider'); +const { isEnabled } = require('~/server/utils'); + +function getThresholdMapping(model) { + const gemini1Pattern = /gemini-(1\.0|1\.5|pro$|1\.0-pro|1\.5-pro|1\.5-flash-001)/; + const restrictedPattern = /(gemini-(1\.5-flash-8b|2\.0|exp)|learnlm)/; + + if (gemini1Pattern.test(model)) { + return (value) => { + if (value === 'OFF') { + return 'BLOCK_NONE'; + } + return value; + }; + } + + if (restrictedPattern.test(model)) { + return (value) => { + if (value === 'OFF' || value === 'HARM_BLOCK_THRESHOLD_UNSPECIFIED') { + return 'BLOCK_NONE'; + } + return value; + }; + } + + return (value) => value; +} /** * - * @param {boolean} isGemini2 - * @returns {Array<{category: string, threshold: string}>} + * @param {string} model + * @returns {Array<{category: string, threshold: string}> | undefined} */ -function getSafetySettings(isGemini2) { - const mapThreshold = (value) => { - if (isGemini2 && value === 'BLOCK_NONE') { - return 'OFF'; - } - return value; - }; +function getSafetySettings(model) { + if (isEnabled(process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS)) { + return undefined; + } + const mapThreshold = getThresholdMapping(model); return [ { @@ -85,8 +109,7 @@ function getLLMConfig(credentials, options = {}) { }; /** Used only for Safety Settings */ - const isGemini2 = llmConfig.model.includes('gemini-2.0') && !llmConfig.model.includes('thinking'); - llmConfig.safetySettings = getSafetySettings(isGemini2); + llmConfig.safetySettings = getSafetySettings(llmConfig.model); let provider; @@ -153,4 +176,5 @@ function getLLMConfig(credentials, options = {}) { module.exports = { getLLMConfig, + getSafetySettings, }; diff --git a/api/server/services/Endpoints/google/title.js b/api/server/services/Endpoints/google/title.js index b93f13797f..dd8aa7a220 100644 --- a/api/server/services/Endpoints/google/title.js +++ b/api/server/services/Endpoints/google/title.js @@ -1,9 +1,8 @@ -const { CacheKeys, Constants } = require('librechat-data-provider'); +const { EModelEndpoint, CacheKeys, Constants, googleSettings } = require('librechat-data-provider'); const getLogStores = require('~/cache/getLogStores'); +const initializeClient = require('./initialize'); const { isEnabled } = require('~/server/utils'); const { saveConvo } = require('~/models'); -const { logger } = require('~/config'); -const initializeClient = require('./initialize'); const addTitle = async (req, { text, response, client }) => { const { TITLE_CONVO = 'true' } = process.env ?? {}; @@ -14,22 +13,16 @@ const addTitle = async (req, { text, response, client }) => { if (client.options.titleConvo === false) { return; } - - const DEFAULT_TITLE_MODEL = 'gemini-pro'; const { GOOGLE_TITLE_MODEL } = process.env ?? {}; - - let model = GOOGLE_TITLE_MODEL ?? DEFAULT_TITLE_MODEL; + const providerConfig = req.app.locals[EModelEndpoint.google]; + let model = + providerConfig?.titleModel ?? + GOOGLE_TITLE_MODEL ?? + client.options?.modelOptions.model ?? + googleSettings.model.default; if (GOOGLE_TITLE_MODEL === Constants.CURRENT_MODEL) { model = client.options?.modelOptions.model; - - if (client.isVisionModel) { - logger.warn( - `current_model was specified for Google title request, but the model ${model} cannot process a text-only conversation. Falling back to ${DEFAULT_TITLE_MODEL}`, - ); - - model = DEFAULT_TITLE_MODEL; - } } const titleEndpointOptions = { diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 4b23939e62..f934f9d519 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -1,9 +1,11 @@ +const { z } = require('zod'); const { tool } = require('@langchain/core/tools'); -const { Constants: AgentConstants } = require('@librechat/agents'); +const { Constants: AgentConstants, Providers } = require('@librechat/agents'); const { Constants, - convertJsonSchemaToZod, + ContentTypes, isAssistantsEndpoint, + convertJsonSchemaToZod, } = require('librechat-data-provider'); const { logger, getMCPManager } = require('~/config'); @@ -25,7 +27,15 @@ async function createMCPTool({ req, toolKey, provider }) { } /** @type {LCTool} */ const { description, parameters } = toolDefinition; - const schema = convertJsonSchemaToZod(parameters); + const isGoogle = provider === Providers.VERTEXAI || provider === Providers.GOOGLE; + let schema = convertJsonSchemaToZod(parameters, { + allowEmptyObject: !isGoogle, + }); + + if (!schema) { + schema = z.object({ input: z.string().optional() }); + } + const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter); /** @type {(toolInput: Object | string) => Promise} */ const _call = async (toolInput) => { @@ -35,6 +45,9 @@ async function createMCPTool({ req, toolKey, provider }) { if (isAssistantsEndpoint(provider) && Array.isArray(result)) { return result[0]; } + if (isGoogle && Array.isArray(result[0]) && result[0][0]?.type === ContentTypes.TEXT) { + return [result[0][0].text, result[1]]; + } return result; } catch (error) { logger.error(`${toolName} MCP server tool call failed`, error); diff --git a/api/utils/tokens.js b/api/utils/tokens.js index b1c3a1e5a8..0541f4f301 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -49,11 +49,14 @@ const cohereModels = { const googleModels = { /* Max I/O is combined so we subtract the amount from max response tokens for actual total */ gemini: 30720, // -2048 from max - 'gemini-pro-vision': 12288, // -4096 from max - 'gemini-exp': 8000, - 'gemini-2.0-flash-thinking-exp': 30720, // -2048 from max - 'gemini-2.0': 1048576, - 'gemini-1.5': 1048576, + 'gemini-pro-vision': 12288, + 'gemini-exp': 2000000, + 'gemini-2.0': 2000000, + 'gemini-2.0-flash': 1000000, + 'gemini-2.0-flash-lite': 1000000, + 'gemini-1.5': 1000000, + 'gemini-1.5-flash': 1000000, + 'gemini-1.5-flash-8b': 1000000, 'text-bison-32k': 32758, // -10 from max 'chat-bison-32k': 32758, // -10 from max 'code-bison-32k': 32758, // -10 from max diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index f478c4769b..eb1fd85495 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -154,6 +154,24 @@ describe('getModelMaxTokens', () => { }); test('should return correct tokens for partial match - Google models', () => { + expect(getModelMaxTokens('gemini-2.0-flash-lite-preview-02-05', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-2.0-flash-lite'], + ); + expect(getModelMaxTokens('gemini-2.0-flash-001', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-2.0-flash'], + ); + expect(getModelMaxTokens('gemini-2.0-flash-exp', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-2.0-flash'], + ); + expect(getModelMaxTokens('gemini-2.0-pro-exp-02-05', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-2.0'], + ); + expect(getModelMaxTokens('gemini-1.5-flash-8b', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-1.5-flash-8b'], + ); + expect(getModelMaxTokens('gemini-1.5-flash-thinking', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-1.5-flash'], + ); expect(getModelMaxTokens('gemini-1.5-pro-latest', EModelEndpoint.google)).toBe( maxTokensMap[EModelEndpoint.google]['gemini-1.5'], ); diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index b33169813f..90b78dc669 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -33,7 +33,7 @@ type TExpiredKey = { endpoint: string; }; -type TInputLength = { +type TGenericError = { info: string; }; @@ -49,10 +49,14 @@ const errorMessages = { const { expiredAt, endpoint } = json; return localize('com_error_expired_user_key', endpoint, expiredAt); }, - [ErrorTypes.INPUT_LENGTH]: (json: TInputLength, localize: LocalizeFunction) => { + [ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => { const { info } = json; return localize('com_error_input_length', info); }, + [ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => { + const { info } = json; + return info; + }, [ViolationTypes.BAN]: 'Your account has been temporarily banned due to violations of our service.', invalid_api_key: diff --git a/client/src/components/Nav/ExportConversation/ExportModal.tsx b/client/src/components/Nav/ExportConversation/ExportModal.tsx index 026bf1869d..bae8327ebe 100644 --- a/client/src/components/Nav/ExportConversation/ExportModal.tsx +++ b/client/src/components/Nav/ExportConversation/ExportModal.tsx @@ -94,7 +94,7 @@ export default function ExportModal({ - +
diff --git a/client/src/hooks/ScreenshotContext.tsx b/client/src/hooks/ScreenshotContext.tsx index bd841b68aa..2f9069c5f1 100644 --- a/client/src/hooks/ScreenshotContext.tsx +++ b/client/src/hooks/ScreenshotContext.tsx @@ -12,7 +12,7 @@ export const useScreenshot = () => { const { ref } = useContext(ScreenshotContext); const { theme } = useContext(ThemeContext); - const takeScreenShot = async (node: HTMLElement) => { + const takeScreenShot = async (node?: HTMLElement) => { if (!node) { throw new Error('You should provide correct html node.'); } @@ -22,7 +22,13 @@ export const useScreenshot = () => { isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; } const backgroundColor = isDark ? '#171717' : 'white'; - const canvas = await toCanvas(node); + + const canvas = await toCanvas(node, { + backgroundColor, + imagePlaceholder: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=', + }); + const croppedCanvas = document.createElement('canvas'); const croppedCanvasContext = croppedCanvas.getContext('2d') as CanvasRenderingContext2D; // init data @@ -35,9 +41,9 @@ export const useScreenshot = () => { croppedCanvas.height = cropHeight; croppedCanvasContext.fillStyle = backgroundColor; - croppedCanvasContext?.fillRect(0, 0, cropWidth, cropHeight); + croppedCanvasContext.fillRect(0, 0, cropWidth, cropHeight); - croppedCanvasContext?.drawImage(canvas, cropPositionLeft, cropPositionTop); + croppedCanvasContext.drawImage(canvas, cropPositionLeft, cropPositionTop); const base64Image = croppedCanvas.toDataURL('image/png', 1); diff --git a/package-lock.json b/package-lock.json index 1941683965..f05d0d85b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "@langchain/google-genai": "^0.1.7", "@langchain/google-vertexai": "^0.1.8", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.0.1", + "@librechat/agents": "^2.0.2", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.7.7", "bcryptjs": "^2.4.3", @@ -646,565 +646,6 @@ "@langchain/core": ">=0.2.21 <0.4.0" } }, - "api/node_modules/@librechat/agents": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.0.1.tgz", - "integrity": "sha512-PVVqvJ90X8o2dV7To9MnrsidmtBS+Q/jcCL2TPNmIrsLDgK8rDQWL3p4mkDzenSJ/f8J6w5bt9bWoo18ibhkJw==", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-sdk/credential-provider-node": "^3.613.0", - "@aws-sdk/types": "^3.609.0", - "@langchain/anthropic": "^0.3.12", - "@langchain/aws": "^0.1.3", - "@langchain/community": "^0.3.27", - "@langchain/core": "^0.3.37", - "@langchain/google-genai": "^0.1.7", - "@langchain/google-vertexai": "^0.1.8", - "@langchain/langgraph": "^0.2.41", - "@langchain/mistralai": "^0.0.26", - "@langchain/ollama": "^0.1.5", - "@langchain/openai": "^0.4.2", - "@smithy/eventstream-codec": "^2.2.0", - "@smithy/protocol-http": "^3.0.6", - "@smithy/signature-v4": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "dotenv": "^16.4.5", - "nanoid": "^3.3.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "api/node_modules/@librechat/agents/node_modules/@langchain/community": { - "version": "0.3.28", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.28.tgz", - "integrity": "sha512-lr3rBe5qE1HGlfUieMayNNT8VGiH4yl4WdQnrf+i0IikDooJOvjTIFqsYqGvdLfpuNKN0JN05jwgvUwOGKTv7A==", - "dependencies": { - "@langchain/openai": ">=0.2.0 <0.5.0", - "binary-extensions": "^2.2.0", - "expr-eval": "^2.0.2", - "flat": "^5.0.2", - "js-yaml": "^4.1.0", - "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", - "langsmith": ">=0.2.8 <0.4.0", - "uuid": "^10.0.0", - "zod": "^3.22.3", - "zod-to-json-schema": "^3.22.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@arcjet/redact": "^v1.0.0-alpha.23", - "@aws-crypto/sha256-js": "^5.0.0", - "@aws-sdk/client-bedrock-agent-runtime": "^3.583.0", - "@aws-sdk/client-bedrock-runtime": "^3.422.0", - "@aws-sdk/client-dynamodb": "^3.310.0", - "@aws-sdk/client-kendra": "^3.352.0", - "@aws-sdk/client-lambda": "^3.310.0", - "@aws-sdk/client-s3": "^3.310.0", - "@aws-sdk/client-sagemaker-runtime": "^3.310.0", - "@aws-sdk/client-sfn": "^3.310.0", - "@aws-sdk/credential-provider-node": "^3.388.0", - "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.15.0", - "@browserbasehq/sdk": "*", - "@browserbasehq/stagehand": "^1.0.0", - "@clickhouse/client": "^0.2.5", - "@cloudflare/ai": "*", - "@datastax/astra-db-ts": "^1.0.0", - "@elastic/elasticsearch": "^8.4.0", - "@getmetal/metal-sdk": "*", - "@getzep/zep-cloud": "^1.0.6", - "@getzep/zep-js": "^0.9.0", - "@gomomento/sdk": "^1.51.1", - "@gomomento/sdk-core": "^1.51.1", - "@google-ai/generativelanguage": "*", - "@google-cloud/storage": "^6.10.1 || ^7.7.0", - "@gradientai/nodejs-sdk": "^1.2.0", - "@huggingface/inference": "^2.6.4", - "@huggingface/transformers": "^3.2.3", - "@ibm-cloud/watsonx-ai": "*", - "@lancedb/lancedb": "^0.12.0", - "@langchain/core": ">=0.2.21 <0.4.0", - "@layerup/layerup-security": "^1.5.12", - "@libsql/client": "^0.14.0", - "@mendable/firecrawl-js": "^1.4.3", - "@mlc-ai/web-llm": "*", - "@mozilla/readability": "*", - "@neondatabase/serverless": "*", - "@notionhq/client": "^2.2.10", - "@opensearch-project/opensearch": "*", - "@pinecone-database/pinecone": "*", - "@planetscale/database": "^1.8.0", - "@premai/prem-sdk": "^0.3.25", - "@qdrant/js-client-rest": "^1.8.2", - "@raycast/api": "^1.55.2", - "@rockset/client": "^0.9.1", - "@smithy/eventstream-codec": "^2.0.5", - "@smithy/protocol-http": "^3.0.6", - "@smithy/signature-v4": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "@spider-cloud/spider-client": "^0.0.21", - "@supabase/supabase-js": "^2.45.0", - "@tensorflow-models/universal-sentence-encoder": "*", - "@tensorflow/tfjs-converter": "*", - "@tensorflow/tfjs-core": "*", - "@upstash/ratelimit": "^1.1.3 || ^2.0.3", - "@upstash/redis": "^1.20.6", - "@upstash/vector": "^1.1.1", - "@vercel/kv": "*", - "@vercel/postgres": "*", - "@writerai/writer-sdk": "^0.40.2", - "@xata.io/client": "^0.28.0", - "@zilliz/milvus2-sdk-node": ">=2.3.5", - "apify-client": "^2.7.1", - "assemblyai": "^4.6.0", - "better-sqlite3": ">=9.4.0 <12.0.0", - "cassandra-driver": "^4.7.2", - "cborg": "^4.1.1", - "cheerio": "^1.0.0-rc.12", - "chromadb": "*", - "closevector-common": "0.1.3", - "closevector-node": "0.1.6", - "closevector-web": "0.1.6", - "cohere-ai": "*", - "convex": "^1.3.1", - "crypto-js": "^4.2.0", - "d3-dsv": "^2.0.0", - "discord.js": "^14.14.1", - "dria": "^0.0.3", - "duck-duck-scrape": "^2.2.5", - "epub2": "^3.0.1", - "faiss-node": "^0.5.1", - "fast-xml-parser": "*", - "firebase-admin": "^11.9.0 || ^12.0.0", - "google-auth-library": "*", - "googleapis": "*", - "hnswlib-node": "^3.0.0", - "html-to-text": "^9.0.5", - "ibm-cloud-sdk-core": "*", - "ignore": "^5.2.0", - "interface-datastore": "^8.2.11", - "ioredis": "^5.3.2", - "it-all": "^3.0.4", - "jsdom": "*", - "jsonwebtoken": "^9.0.2", - "llmonitor": "^0.5.9", - "lodash": "^4.17.21", - "lunary": "^0.7.10", - "mammoth": "^1.6.0", - "mongodb": ">=5.2.0", - "mysql2": "^3.9.8", - "neo4j-driver": "*", - "notion-to-md": "^3.1.0", - "officeparser": "^4.0.4", - "openai": "*", - "pdf-parse": "1.1.1", - "pg": "^8.11.0", - "pg-copy-streams": "^6.0.5", - "pickleparser": "^0.2.1", - "playwright": "^1.32.1", - "portkey-ai": "^0.1.11", - "puppeteer": "*", - "pyodide": ">=0.24.1 <0.27.0", - "redis": "*", - "replicate": "^0.29.4", - "sonix-speech-recognition": "^2.1.1", - "srt-parser-2": "^1.2.3", - "typeorm": "^0.3.20", - "typesense": "^1.5.3", - "usearch": "^1.1.1", - "voy-search": "0.6.2", - "weaviate-ts-client": "*", - "web-auth-library": "^1.0.3", - "word-extractor": "*", - "ws": "^8.14.2", - "youtubei.js": "*" - }, - "peerDependenciesMeta": { - "@arcjet/redact": { - "optional": true - }, - "@aws-crypto/sha256-js": { - "optional": true - }, - "@aws-sdk/client-bedrock-agent-runtime": { - "optional": true - }, - "@aws-sdk/client-bedrock-runtime": { - "optional": true - }, - "@aws-sdk/client-dynamodb": { - "optional": true - }, - "@aws-sdk/client-kendra": { - "optional": true - }, - "@aws-sdk/client-lambda": { - "optional": true - }, - "@aws-sdk/client-s3": { - "optional": true - }, - "@aws-sdk/client-sagemaker-runtime": { - "optional": true - }, - "@aws-sdk/client-sfn": { - "optional": true - }, - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@azure/search-documents": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@browserbasehq/sdk": { - "optional": true - }, - "@clickhouse/client": { - "optional": true - }, - "@cloudflare/ai": { - "optional": true - }, - "@datastax/astra-db-ts": { - "optional": true - }, - "@elastic/elasticsearch": { - "optional": true - }, - "@getmetal/metal-sdk": { - "optional": true - }, - "@getzep/zep-cloud": { - "optional": true - }, - "@getzep/zep-js": { - "optional": true - }, - "@gomomento/sdk": { - "optional": true - }, - "@gomomento/sdk-core": { - "optional": true - }, - "@google-ai/generativelanguage": { - "optional": true - }, - "@google-cloud/storage": { - "optional": true - }, - "@gradientai/nodejs-sdk": { - "optional": true - }, - "@huggingface/inference": { - "optional": true - }, - "@huggingface/transformers": { - "optional": true - }, - "@lancedb/lancedb": { - "optional": true - }, - "@layerup/layerup-security": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@mendable/firecrawl-js": { - "optional": true - }, - "@mlc-ai/web-llm": { - "optional": true - }, - "@mozilla/readability": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@notionhq/client": { - "optional": true - }, - "@opensearch-project/opensearch": { - "optional": true - }, - "@pinecone-database/pinecone": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@premai/prem-sdk": { - "optional": true - }, - "@qdrant/js-client-rest": { - "optional": true - }, - "@raycast/api": { - "optional": true - }, - "@rockset/client": { - "optional": true - }, - "@smithy/eventstream-codec": { - "optional": true - }, - "@smithy/protocol-http": { - "optional": true - }, - "@smithy/signature-v4": { - "optional": true - }, - "@smithy/util-utf8": { - "optional": true - }, - "@spider-cloud/spider-client": { - "optional": true - }, - "@supabase/supabase-js": { - "optional": true - }, - "@tensorflow-models/universal-sentence-encoder": { - "optional": true - }, - "@tensorflow/tfjs-converter": { - "optional": true - }, - "@tensorflow/tfjs-core": { - "optional": true - }, - "@upstash/ratelimit": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@upstash/vector": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@writerai/writer-sdk": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "@zilliz/milvus2-sdk-node": { - "optional": true - }, - "apify-client": { - "optional": true - }, - "assemblyai": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "cassandra-driver": { - "optional": true - }, - "cborg": { - "optional": true - }, - "cheerio": { - "optional": true - }, - "chromadb": { - "optional": true - }, - "closevector-common": { - "optional": true - }, - "closevector-node": { - "optional": true - }, - "closevector-web": { - "optional": true - }, - "cohere-ai": { - "optional": true - }, - "convex": { - "optional": true - }, - "crypto-js": { - "optional": true - }, - "d3-dsv": { - "optional": true - }, - "discord.js": { - "optional": true - }, - "dria": { - "optional": true - }, - "duck-duck-scrape": { - "optional": true - }, - "epub2": { - "optional": true - }, - "faiss-node": { - "optional": true - }, - "fast-xml-parser": { - "optional": true - }, - "firebase-admin": { - "optional": true - }, - "google-auth-library": { - "optional": true - }, - "googleapis": { - "optional": true - }, - "hnswlib-node": { - "optional": true - }, - "html-to-text": { - "optional": true - }, - "ignore": { - "optional": true - }, - "interface-datastore": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "it-all": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "jsonwebtoken": { - "optional": true - }, - "llmonitor": { - "optional": true - }, - "lodash": { - "optional": true - }, - "lunary": { - "optional": true - }, - "mammoth": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "neo4j-driver": { - "optional": true - }, - "notion-to-md": { - "optional": true - }, - "officeparser": { - "optional": true - }, - "pdf-parse": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-copy-streams": { - "optional": true - }, - "pickleparser": { - "optional": true - }, - "playwright": { - "optional": true - }, - "portkey-ai": { - "optional": true - }, - "puppeteer": { - "optional": true - }, - "pyodide": { - "optional": true - }, - "redis": { - "optional": true - }, - "replicate": { - "optional": true - }, - "sonix-speech-recognition": { - "optional": true - }, - "srt-parser-2": { - "optional": true - }, - "typeorm": { - "optional": true - }, - "typesense": { - "optional": true - }, - "usearch": { - "optional": true - }, - "voy-search": { - "optional": true - }, - "weaviate-ts-client": { - "optional": true - }, - "web-auth-library": { - "optional": true - }, - "word-extractor": { - "optional": true - }, - "ws": { - "optional": true - }, - "youtubei.js": { - "optional": true - } - } - }, - "api/node_modules/@librechat/agents/node_modules/@langchain/openai": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.4.2.tgz", - "integrity": "sha512-Cuj7qbVcycALTP0aqZuPpEc7As8cwiGaU21MhXRyZFs+dnWxKYxZ1Q1z4kcx6cYkq/I+CNwwmk+sP+YruU73Aw==", - "dependencies": { - "js-tiktoken": "^1.0.12", - "openai": "^4.77.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.29 <0.4.0" - } - }, "api/node_modules/@types/node": { "version": "18.19.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", @@ -6587,9 +6028,9 @@ } }, "node_modules/@browserbasehq/stagehand": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@browserbasehq/stagehand/-/stagehand-1.11.0.tgz", - "integrity": "sha512-I1gBsSouWXCOxl+FJMhB40XIjKnHmYFAfXDg4e/m2VR8YJqIOdLJeT6CQmgzKpre7JqplaAFqEcgifm+CK7mGQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@browserbasehq/stagehand/-/stagehand-1.12.0.tgz", + "integrity": "sha512-RWhdGxs2tUKyNpUh710ct/1Wwhv4jsEc1Qs8lz8Qngm3y7onRu0WYH0Cf3mnFZqYLTf3ni9x17VNju8YJACa5Q==", "peer": true, "dependencies": { "@anthropic-ai/sdk": "^0.27.3", @@ -6621,9 +6062,9 @@ } }, "node_modules/@browserbasehq/stagehand/node_modules/@types/node": { - "version": "18.19.74", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz", - "integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==", + "version": "18.19.75", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", + "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", "peer": true, "dependencies": { "undici-types": "~5.26.4" @@ -10694,6 +10135,577 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@librechat/agents": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.0.2.tgz", + "integrity": "sha512-ucH1zb2nHpAafXq6YNNFBHl5rwBEoTl5CUZ6M9r5Mp1oyk9vSAz+knOCaUgYMU5GJqY+6ReFWRH9tnvZfrzhTQ==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-sdk/credential-provider-node": "^3.613.0", + "@aws-sdk/types": "^3.609.0", + "@langchain/anthropic": "^0.3.12", + "@langchain/aws": "^0.1.3", + "@langchain/community": "^0.3.27", + "@langchain/core": "^0.3.37", + "@langchain/google-genai": "^0.1.7", + "@langchain/google-vertexai": "^0.1.8", + "@langchain/langgraph": "^0.2.41", + "@langchain/mistralai": "^0.0.26", + "@langchain/ollama": "^0.1.5", + "@langchain/openai": "^0.4.2", + "@smithy/eventstream-codec": "^2.2.0", + "@smithy/protocol-http": "^3.0.6", + "@smithy/signature-v4": "^2.0.10", + "@smithy/util-utf8": "^2.0.0", + "dotenv": "^16.4.5", + "nanoid": "^3.3.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@librechat/agents/node_modules/@langchain/community": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.29.tgz", + "integrity": "sha512-6XIPGctpH3KziFpdVDEvYBEQMMsNomffqy545shoxOLoMkZqgn1wfMs6R7ltzzS0p3LJUXW1RbwAUEuWp0rbuA==", + "dependencies": { + "@langchain/openai": ">=0.2.0 <0.5.0", + "binary-extensions": "^2.2.0", + "expr-eval": "^2.0.2", + "flat": "^5.0.2", + "js-yaml": "^4.1.0", + "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", + "langsmith": ">=0.2.8 <0.4.0", + "uuid": "^10.0.0", + "zod": "^3.22.3", + "zod-to-json-schema": "^3.22.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@arcjet/redact": "^v1.0.0-alpha.23", + "@aws-crypto/sha256-js": "^5.0.0", + "@aws-sdk/client-bedrock-agent-runtime": "^3.583.0", + "@aws-sdk/client-bedrock-runtime": "^3.422.0", + "@aws-sdk/client-dynamodb": "^3.310.0", + "@aws-sdk/client-kendra": "^3.352.0", + "@aws-sdk/client-lambda": "^3.310.0", + "@aws-sdk/client-s3": "^3.310.0", + "@aws-sdk/client-sagemaker-runtime": "^3.310.0", + "@aws-sdk/client-sfn": "^3.310.0", + "@aws-sdk/credential-provider-node": "^3.388.0", + "@azure/search-documents": "^12.0.0", + "@azure/storage-blob": "^12.15.0", + "@browserbasehq/sdk": "*", + "@browserbasehq/stagehand": "^1.0.0", + "@clickhouse/client": "^0.2.5", + "@cloudflare/ai": "*", + "@datastax/astra-db-ts": "^1.0.0", + "@elastic/elasticsearch": "^8.4.0", + "@getmetal/metal-sdk": "*", + "@getzep/zep-cloud": "^1.0.6", + "@getzep/zep-js": "^0.9.0", + "@gomomento/sdk": "^1.51.1", + "@gomomento/sdk-core": "^1.51.1", + "@google-ai/generativelanguage": "*", + "@google-cloud/storage": "^6.10.1 || ^7.7.0", + "@gradientai/nodejs-sdk": "^1.2.0", + "@huggingface/inference": "^2.6.4", + "@huggingface/transformers": "^3.2.3", + "@ibm-cloud/watsonx-ai": "*", + "@lancedb/lancedb": "^0.12.0", + "@langchain/core": ">=0.2.21 <0.4.0", + "@layerup/layerup-security": "^1.5.12", + "@libsql/client": "^0.14.0", + "@mendable/firecrawl-js": "^1.4.3", + "@mlc-ai/web-llm": "*", + "@mozilla/readability": "*", + "@neondatabase/serverless": "*", + "@notionhq/client": "^2.2.10", + "@opensearch-project/opensearch": "*", + "@pinecone-database/pinecone": "*", + "@planetscale/database": "^1.8.0", + "@premai/prem-sdk": "^0.3.25", + "@qdrant/js-client-rest": "^1.8.2", + "@raycast/api": "^1.55.2", + "@rockset/client": "^0.9.1", + "@smithy/eventstream-codec": "^2.0.5", + "@smithy/protocol-http": "^3.0.6", + "@smithy/signature-v4": "^2.0.10", + "@smithy/util-utf8": "^2.0.0", + "@spider-cloud/spider-client": "^0.0.21", + "@supabase/supabase-js": "^2.45.0", + "@tensorflow-models/universal-sentence-encoder": "*", + "@tensorflow/tfjs-converter": "*", + "@tensorflow/tfjs-core": "*", + "@upstash/ratelimit": "^1.1.3 || ^2.0.3", + "@upstash/redis": "^1.20.6", + "@upstash/vector": "^1.1.1", + "@vercel/kv": "*", + "@vercel/postgres": "*", + "@writerai/writer-sdk": "^0.40.2", + "@xata.io/client": "^0.28.0", + "@zilliz/milvus2-sdk-node": ">=2.3.5", + "apify-client": "^2.7.1", + "assemblyai": "^4.6.0", + "better-sqlite3": ">=9.4.0 <12.0.0", + "cassandra-driver": "^4.7.2", + "cborg": "^4.1.1", + "cheerio": "^1.0.0-rc.12", + "chromadb": "*", + "closevector-common": "0.1.3", + "closevector-node": "0.1.6", + "closevector-web": "0.1.6", + "cohere-ai": "*", + "convex": "^1.3.1", + "crypto-js": "^4.2.0", + "d3-dsv": "^2.0.0", + "discord.js": "^14.14.1", + "dria": "^0.0.3", + "duck-duck-scrape": "^2.2.5", + "epub2": "^3.0.1", + "faiss-node": "^0.5.1", + "fast-xml-parser": "*", + "firebase-admin": "^11.9.0 || ^12.0.0", + "google-auth-library": "*", + "googleapis": "*", + "hnswlib-node": "^3.0.0", + "html-to-text": "^9.0.5", + "ibm-cloud-sdk-core": "*", + "ignore": "^5.2.0", + "interface-datastore": "^8.2.11", + "ioredis": "^5.3.2", + "it-all": "^3.0.4", + "jsdom": "*", + "jsonwebtoken": "^9.0.2", + "llmonitor": "^0.5.9", + "lodash": "^4.17.21", + "lunary": "^0.7.10", + "mammoth": "^1.6.0", + "mongodb": ">=5.2.0", + "mysql2": "^3.9.8", + "neo4j-driver": "*", + "notion-to-md": "^3.1.0", + "officeparser": "^4.0.4", + "openai": "*", + "pdf-parse": "1.1.1", + "pg": "^8.11.0", + "pg-copy-streams": "^6.0.5", + "pickleparser": "^0.2.1", + "playwright": "^1.32.1", + "portkey-ai": "^0.1.11", + "puppeteer": "*", + "pyodide": ">=0.24.1 <0.27.0", + "redis": "*", + "replicate": "*", + "sonix-speech-recognition": "^2.1.1", + "srt-parser-2": "^1.2.3", + "typeorm": "^0.3.20", + "typesense": "^1.5.3", + "usearch": "^1.1.1", + "voy-search": "0.6.2", + "weaviate-ts-client": "*", + "web-auth-library": "^1.0.3", + "word-extractor": "*", + "ws": "^8.14.2", + "youtubei.js": "*" + }, + "peerDependenciesMeta": { + "@arcjet/redact": { + "optional": true + }, + "@aws-crypto/sha256-js": { + "optional": true + }, + "@aws-sdk/client-bedrock-agent-runtime": { + "optional": true + }, + "@aws-sdk/client-bedrock-runtime": { + "optional": true + }, + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "@aws-sdk/client-kendra": { + "optional": true + }, + "@aws-sdk/client-lambda": { + "optional": true + }, + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/client-sagemaker-runtime": { + "optional": true + }, + "@aws-sdk/client-sfn": { + "optional": true + }, + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@azure/search-documents": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@browserbasehq/sdk": { + "optional": true + }, + "@clickhouse/client": { + "optional": true + }, + "@cloudflare/ai": { + "optional": true + }, + "@datastax/astra-db-ts": { + "optional": true + }, + "@elastic/elasticsearch": { + "optional": true + }, + "@getmetal/metal-sdk": { + "optional": true + }, + "@getzep/zep-cloud": { + "optional": true + }, + "@getzep/zep-js": { + "optional": true + }, + "@gomomento/sdk": { + "optional": true + }, + "@gomomento/sdk-core": { + "optional": true + }, + "@google-ai/generativelanguage": { + "optional": true + }, + "@google-cloud/storage": { + "optional": true + }, + "@gradientai/nodejs-sdk": { + "optional": true + }, + "@huggingface/inference": { + "optional": true + }, + "@huggingface/transformers": { + "optional": true + }, + "@lancedb/lancedb": { + "optional": true + }, + "@layerup/layerup-security": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@mendable/firecrawl-js": { + "optional": true + }, + "@mlc-ai/web-llm": { + "optional": true + }, + "@mozilla/readability": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@notionhq/client": { + "optional": true + }, + "@opensearch-project/opensearch": { + "optional": true + }, + "@pinecone-database/pinecone": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@premai/prem-sdk": { + "optional": true + }, + "@qdrant/js-client-rest": { + "optional": true + }, + "@raycast/api": { + "optional": true + }, + "@rockset/client": { + "optional": true + }, + "@smithy/eventstream-codec": { + "optional": true + }, + "@smithy/protocol-http": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "@smithy/util-utf8": { + "optional": true + }, + "@spider-cloud/spider-client": { + "optional": true + }, + "@supabase/supabase-js": { + "optional": true + }, + "@tensorflow-models/universal-sentence-encoder": { + "optional": true + }, + "@tensorflow/tfjs-converter": { + "optional": true + }, + "@tensorflow/tfjs-core": { + "optional": true + }, + "@upstash/ratelimit": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@upstash/vector": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@writerai/writer-sdk": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "@zilliz/milvus2-sdk-node": { + "optional": true + }, + "apify-client": { + "optional": true + }, + "assemblyai": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "cassandra-driver": { + "optional": true + }, + "cborg": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "chromadb": { + "optional": true + }, + "closevector-common": { + "optional": true + }, + "closevector-node": { + "optional": true + }, + "closevector-web": { + "optional": true + }, + "cohere-ai": { + "optional": true + }, + "convex": { + "optional": true + }, + "crypto-js": { + "optional": true + }, + "d3-dsv": { + "optional": true + }, + "discord.js": { + "optional": true + }, + "dria": { + "optional": true + }, + "duck-duck-scrape": { + "optional": true + }, + "epub2": { + "optional": true + }, + "faiss-node": { + "optional": true + }, + "fast-xml-parser": { + "optional": true + }, + "firebase-admin": { + "optional": true + }, + "google-auth-library": { + "optional": true + }, + "googleapis": { + "optional": true + }, + "hnswlib-node": { + "optional": true + }, + "html-to-text": { + "optional": true + }, + "ignore": { + "optional": true + }, + "interface-datastore": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "it-all": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "jsonwebtoken": { + "optional": true + }, + "llmonitor": { + "optional": true + }, + "lodash": { + "optional": true + }, + "lunary": { + "optional": true + }, + "mammoth": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "neo4j-driver": { + "optional": true + }, + "notion-to-md": { + "optional": true + }, + "officeparser": { + "optional": true + }, + "pdf-parse": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-copy-streams": { + "optional": true + }, + "pickleparser": { + "optional": true + }, + "playwright": { + "optional": true + }, + "portkey-ai": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "pyodide": { + "optional": true + }, + "redis": { + "optional": true + }, + "replicate": { + "optional": true + }, + "sonix-speech-recognition": { + "optional": true + }, + "srt-parser-2": { + "optional": true + }, + "typeorm": { + "optional": true + }, + "typesense": { + "optional": true + }, + "usearch": { + "optional": true + }, + "voy-search": { + "optional": true + }, + "weaviate-ts-client": { + "optional": true + }, + "web-auth-library": { + "optional": true + }, + "word-extractor": { + "optional": true + }, + "ws": { + "optional": true + }, + "youtubei.js": { + "optional": true + } + } + }, + "node_modules/@librechat/agents/node_modules/@langchain/openai": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.4.2.tgz", + "integrity": "sha512-Cuj7qbVcycALTP0aqZuPpEc7As8cwiGaU21MhXRyZFs+dnWxKYxZ1Q1z4kcx6cYkq/I+CNwwmk+sP+YruU73Aw==", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^4.77.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.29 <0.4.0" + } + }, + "node_modules/@librechat/agents/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -35539,7 +35551,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.698", + "version": "0.7.699", "license": "ISC", "dependencies": { "axios": "^1.7.7", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index e2d63ce1f7..976cf85d12 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.698", + "version": "0.7.699", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 3ba6c1154e..6a2db199b2 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -699,18 +699,19 @@ export const defaultModels = { [EModelEndpoint.assistants]: ['chatgpt-4o-latest', ...sharedOpenAIModels], [EModelEndpoint.agents]: sharedOpenAIModels, // TODO: Add agent models (agentsModels) [EModelEndpoint.google]: [ - 'gemini-pro', - 'gemini-pro-vision', - 'chat-bison', - 'chat-bison-32k', - 'codechat-bison', - 'codechat-bison-32k', - 'text-bison', - 'text-bison-32k', - 'text-unicorn', - 'code-gecko', - 'code-bison', - 'code-bison-32k', + // Shared Google Models between Vertex AI & Gen AI + // Gemini 2.0 Models + 'gemini-2.0-flash-001', + 'gemini-2.0-flash-exp', + 'gemini-2.0-flash-lite-preview-02-05', + 'gemini-2.0-pro-exp-02-05', + // Gemini 1.5 Models + 'gemini-1.5-flash-001', + 'gemini-1.5-flash-002', + 'gemini-1.5-pro-001', + 'gemini-1.5-pro-002', + // Gemini 1.0 Models + 'gemini-1.0-pro-001', ], [EModelEndpoint.anthropic]: sharedAnthropicModels, [EModelEndpoint.openAI]: [ @@ -1019,6 +1020,10 @@ export enum ErrorTypes { * Invalid request error, API rejected request */ NO_SYSTEM_MESSAGES = 'no_system_messages', + /** + * Google provider returned an error + */ + GOOGLE_ERROR = 'google_error', } /** diff --git a/packages/data-provider/src/zod.spec.ts b/packages/data-provider/src/zod.spec.ts index 37915649bc..caa7d54e49 100644 --- a/packages/data-provider/src/zod.spec.ts +++ b/packages/data-provider/src/zod.spec.ts @@ -13,8 +13,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse('test')).toBe('test'); - expect(() => zodSchema.parse(123)).toThrow(); + expect(zodSchema?.parse('test')).toBe('test'); + expect(() => zodSchema?.parse(123)).toThrow(); }); it('should convert string enum schema', () => { @@ -24,8 +24,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse('foo')).toBe('foo'); - expect(() => zodSchema.parse('invalid')).toThrow(); + expect(zodSchema?.parse('foo')).toBe('foo'); + expect(() => zodSchema?.parse('invalid')).toThrow(); }); it('should convert number schema', () => { @@ -34,8 +34,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse(123)).toBe(123); - expect(() => zodSchema.parse('123')).toThrow(); + expect(zodSchema?.parse(123)).toBe(123); + expect(() => zodSchema?.parse('123')).toThrow(); }); it('should convert boolean schema', () => { @@ -44,8 +44,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse(true)).toBe(true); - expect(() => zodSchema.parse('true')).toThrow(); + expect(zodSchema?.parse(true)).toBe(true); + expect(() => zodSchema?.parse('true')).toThrow(); }); }); @@ -57,8 +57,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); - expect(() => zodSchema.parse(['a', 123, 'c'])).toThrow(); + expect(zodSchema?.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); + expect(() => zodSchema?.parse(['a', 123, 'c'])).toThrow(); }); it('should convert array of numbers schema', () => { @@ -68,8 +68,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse([1, 2, 3])).toEqual([1, 2, 3]); - expect(() => zodSchema.parse([1, '2', 3])).toThrow(); + expect(zodSchema?.parse([1, 2, 3])).toEqual([1, 2, 3]); + expect(() => zodSchema?.parse([1, '2', 3])).toThrow(); }); }); @@ -84,8 +84,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 }); - expect(() => zodSchema.parse({ name: 123, age: 30 })).toThrow(); + expect(zodSchema?.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 }); + expect(() => zodSchema?.parse({ name: 123, age: 30 })).toThrow(); }); it('should handle required fields', () => { @@ -99,8 +99,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse({ name: 'John' })).toEqual({ name: 'John' }); - expect(() => zodSchema.parse({})).toThrow(); + expect(zodSchema?.parse({ name: 'John' })).toEqual({ name: 'John' }); + expect(() => zodSchema?.parse({})).toThrow(); }); it('should handle nested objects', () => { @@ -120,10 +120,10 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse({ user: { name: 'John', age: 30 } })).toEqual({ + expect(zodSchema?.parse({ user: { name: 'John', age: 30 } })).toEqual({ user: { name: 'John', age: 30 }, }); - expect(() => zodSchema.parse({ user: { age: 30 } })).toThrow(); + expect(() => zodSchema?.parse({ user: { age: 30 } })).toThrow(); }); it('should handle objects with arrays', () => { @@ -138,8 +138,8 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse({ names: ['John', 'Jane'] })).toEqual({ names: ['John', 'Jane'] }); - expect(() => zodSchema.parse({ names: ['John', 123] })).toThrow(); + expect(zodSchema?.parse({ names: ['John', 'Jane'] })).toEqual({ names: ['John', 'Jane'] }); + expect(() => zodSchema?.parse({ names: ['John', 123] })).toThrow(); }); }); @@ -151,7 +151,7 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse({})).toEqual({}); + expect(zodSchema?.parse({})).toEqual({}); }); it('should handle unknown types as unknown', () => { @@ -160,8 +160,8 @@ describe('convertJsonSchemaToZod', () => { } as unknown as JsonSchemaType; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse('anything')).toBe('anything'); - expect(zodSchema.parse(123)).toBe(123); + expect(zodSchema?.parse('anything')).toBe('anything'); + expect(zodSchema?.parse(123)).toBe(123); }); it('should handle empty enum arrays as regular strings', () => { @@ -171,7 +171,7 @@ describe('convertJsonSchemaToZod', () => { }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.parse('test')).toBe('test'); + expect(zodSchema?.parse('test')).toBe('test'); }); }); @@ -223,6 +223,9 @@ describe('convertJsonSchemaToZod', () => { ], }, }; + if (zodSchema == null) { + throw new Error('Zod schema is null'); + } expect(zodSchema.parse(validData)).toEqual(validData); expect(() => @@ -253,7 +256,7 @@ describe('convertJsonSchemaToZod', () => { }, }; const zodSchema = convertJsonSchemaToZod(schema); - expect(zodSchema.description).toBe('A test schema description'); + expect(zodSchema?.description).toBe('A test schema description'); }); it('should preserve field descriptions', () => { @@ -309,7 +312,7 @@ describe('convertJsonSchemaToZod', () => { // Type assertions for better type safety const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {}; - expect(zodSchema.description).toBe('User record'); + expect(zodSchema?.description).toBe('User record'); if ('user' in shape) { expect(shape.user.description).toBe('User details'); @@ -436,7 +439,7 @@ describe('convertJsonSchemaToZod', () => { const zodSchema = convertJsonSchemaToZod(schema); // Test top-level description - expect(zodSchema.description).toBe('User profile configuration'); + expect(zodSchema?.description).toBe('User profile configuration'); const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {}; @@ -464,4 +467,60 @@ describe('convertJsonSchemaToZod', () => { } }); }); + + describe('empty object handling', () => { + it('should return undefined for empty object schemas when allowEmptyObject is false', () => { + const emptyObjectSchemas = [ + { type: 'object' as const }, + { type: 'object' as const, properties: {} }, + ]; + + emptyObjectSchemas.forEach((schema) => { + expect(convertJsonSchemaToZod(schema, { allowEmptyObject: false })).toBeUndefined(); + }); + }); + + it('should return zod schema for empty object schemas when allowEmptyObject is true', () => { + const emptyObjectSchemas = [ + { type: 'object' as const }, + { type: 'object' as const, properties: {} }, + ]; + + emptyObjectSchemas.forEach((schema) => { + const result = convertJsonSchemaToZod(schema, { allowEmptyObject: true }); + expect(result).toBeDefined(); + expect(result instanceof z.ZodObject).toBeTruthy(); + }); + }); + + it('should return zod schema for empty object schemas by default', () => { + const emptyObjectSchemas = [ + { type: 'object' as const }, + { type: 'object' as const, properties: {} }, + ]; + + emptyObjectSchemas.forEach((schema) => { + const result = convertJsonSchemaToZod(schema); + expect(result).toBeDefined(); + expect(result instanceof z.ZodObject).toBeTruthy(); + }); + }); + + it('should still convert non-empty object schemas regardless of allowEmptyObject setting', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + const resultWithFlag = convertJsonSchemaToZod(schema, { allowEmptyObject: false }); + const resultWithoutFlag = convertJsonSchemaToZod(schema); + + expect(resultWithFlag).toBeDefined(); + expect(resultWithoutFlag).toBeDefined(); + expect(resultWithFlag instanceof z.ZodObject).toBeTruthy(); + expect(resultWithoutFlag instanceof z.ZodObject).toBeTruthy(); + }); + }); }); diff --git a/packages/data-provider/src/zod.ts b/packages/data-provider/src/zod.ts index 455bc579d5..aa694fe148 100644 --- a/packages/data-provider/src/zod.ts +++ b/packages/data-provider/src/zod.ts @@ -9,7 +9,24 @@ export type JsonSchemaType = { description?: string; }; -export function convertJsonSchemaToZod(schema: JsonSchemaType): z.ZodType { +function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean { + return ( + jsonSchema != null && + typeof jsonSchema === 'object' && + jsonSchema.type === 'object' && + (jsonSchema.properties == null || Object.keys(jsonSchema.properties).length === 0) + ); +} + +export function convertJsonSchemaToZod( + schema: JsonSchemaType, + options: { allowEmptyObject?: boolean } = {}, +): z.ZodType | undefined { + const { allowEmptyObject = true } = options; + if (!allowEmptyObject && isEmptyObjectSchema(schema)) { + return undefined; + } + let zodSchema: z.ZodType; // Handle primitive types @@ -26,13 +43,16 @@ export function convertJsonSchemaToZod(schema: JsonSchemaType): z.ZodType { zodSchema = z.boolean(); } else if (schema.type === 'array' && schema.items !== undefined) { const itemSchema = convertJsonSchemaToZod(schema.items); - zodSchema = z.array(itemSchema); + zodSchema = z.array(itemSchema as z.ZodType); } else if (schema.type === 'object') { const shape: Record = {}; const properties = schema.properties ?? {}; for (const [key, value] of Object.entries(properties)) { let fieldSchema = convertJsonSchemaToZod(value); + if (!fieldSchema) { + continue; + } if (value.description != null && value.description !== '') { fieldSchema = fieldSchema.describe(value.description); }