🚀 fix: Resolve Google Client Issues, CDN Screenshots, Update Models (#5703)

* 🤖 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
This commit is contained in:
Danny Avila 2025-02-06 18:13:18 -05:00 committed by GitHub
parent 33e60c379b
commit 63afb317c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 939 additions and 720 deletions

View file

@ -57,6 +57,8 @@ class BaseClient {
this.continued; this.continued;
/** @type {TMessage[]} */ /** @type {TMessage[]} */
this.currentMessages = []; this.currentMessages = [];
/** @type {import('librechat-data-provider').VisionModes | undefined} */
this.visionMode;
} }
setOptions() { setOptions() {
@ -1095,7 +1097,7 @@ class BaseClient {
file_id: { $in: fileIds }, file_id: { $in: fileIds },
}); });
await this.addImageURLs(message, files); await this.addImageURLs(message, files, this.visionMode);
this.message_file_map[message.messageId] = files; this.message_file_map[message.messageId] = files;
return message; return message;

View file

@ -10,11 +10,13 @@ const {
getResponseSender, getResponseSender,
endpointSettings, endpointSettings,
EModelEndpoint, EModelEndpoint,
ContentTypes,
VisionModes, VisionModes,
ErrorTypes, ErrorTypes,
Constants, Constants,
AuthKeys, AuthKeys,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { getSafetySettings } = require('~/server/services/Endpoints/google/llm');
const { encodeAndFormat } = require('~/server/services/Files/images'); const { encodeAndFormat } = require('~/server/services/Files/images');
const Tokenizer = require('~/server/services/Tokenizer'); const Tokenizer = require('~/server/services/Tokenizer');
const { spendTokens } = require('~/models/spendTokens'); const { spendTokens } = require('~/models/spendTokens');
@ -70,7 +72,7 @@ class GoogleClient extends BaseClient {
/** The key for the usage object's output tokens /** The key for the usage object's output tokens
* @type {string} */ * @type {string} */
this.outputTokensKey = 'output_tokens'; this.outputTokensKey = 'output_tokens';
this.visionMode = VisionModes.generative;
if (options.skipSetOptions) { if (options.skipSetOptions) {
return; return;
} }
@ -215,10 +217,29 @@ class GoogleClient extends BaseClient {
} }
formatMessages() { formatMessages() {
return ((message) => ({ return ((message) => {
author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel), const msg = {
content: message?.content ?? message.text, author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel),
})).bind(this); 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) { if (this.project_id != null) {
logger.debug('Creating VertexAI client'); logger.debug('Creating VertexAI client');
this.visionMode = undefined;
clientOptions.streaming = true; clientOptions.streaming = true;
const client = new ChatVertexAI(clientOptions); const client = new ChatVertexAI(clientOptions);
client.temperature = clientOptions.temperature; client.temperature = clientOptions.temperature;
@ -607,13 +629,14 @@ class GoogleClient extends BaseClient {
} }
async getCompletion(_payload, options = {}) { async getCompletion(_payload, options = {}) {
const safetySettings = this.getSafetySettings();
const { onProgress, abortController } = options; const { onProgress, abortController } = options;
const safetySettings = getSafetySettings(this.modelOptions.model);
const streamRate = this.options.streamRate ?? Constants.DEFAULT_STREAM_RATE; const streamRate = this.options.streamRate ?? Constants.DEFAULT_STREAM_RATE;
const modelName = this.modelOptions.modelName ?? this.modelOptions.model ?? ''; const modelName = this.modelOptions.modelName ?? this.modelOptions.model ?? '';
let reply = ''; let reply = '';
/** @type {Error} */
let error;
try { try {
if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) { if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) {
/** @type {GenAI} */ /** @type {GenAI} */
@ -714,8 +737,16 @@ class GoogleClient extends BaseClient {
this.usage = usageMetadata; this.usage = usageMetadata;
} }
} catch (e) { } catch (e) {
error = e;
logger.error('[GoogleClient] There was an issue generating the completion', 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; 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 * 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 = {}) { async titleChatCompletion(_payload, options = {}) {
const { abortController } = options;
const safetySettings = this.getSafetySettings();
let reply = ''; let reply = '';
const { abortController } = options;
const model = this.modelOptions.modelName ?? this.modelOptions.model ?? ''; const model = this.modelOptions.modelName ?? this.modelOptions.model ?? '';
const safetySettings = getSafetySettings(model);
if (!EXCLUDED_GENAI_MODELS.test(model) && !this.project_id) { if (!EXCLUDED_GENAI_MODELS.test(model) && !this.project_id) {
logger.debug('Identified titling model as GenAI version'); logger.debug('Identified titling model as GenAI version');
/** @type {GenerativeModel} */ /** @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 { try {
this.initializeClient(); this.initializeClient();
title = await this.titleChatCompletion(payload, { title = await this.titleChatCompletion(payload, {
@ -892,48 +911,6 @@ class GoogleClient extends BaseClient {
return reply.trim(); 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() { getEncoding() {
return 'cl100k_base'; return 'cl100k_base';
} }

View file

@ -4,7 +4,7 @@ const traverse = require('traverse');
const SPLAT_SYMBOL = Symbol.for('splat'); const SPLAT_SYMBOL = Symbol.for('splat');
const MESSAGE_SYMBOL = Symbol.for('message'); 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 = [ const sensitiveKeys = [
/^(sk-)[^\s]+/, // OpenAI API key pattern /^(sk-)[^\s]+/, // OpenAI API key pattern
@ -206,13 +206,13 @@ const jsonTruncateFormat = winston.format((info) => {
seen.add(obj); seen.add(obj);
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map(item => truncateObject(item)); return obj.map((item) => truncateObject(item));
} }
const newObj = {}; const newObj = {};
Object.entries(obj).forEach(([key, value]) => { Object.entries(obj).forEach(([key, value]) => {
if (typeof value === 'string') { if (typeof value === 'string') {
newObj[key] = truncateLongStrings(value, CONSOLE_JSON_LONG_STRING_LENGTH); newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH);
} else { } else {
newObj[key] = truncateObject(value); newObj[key] = truncateObject(value);
} }

View file

@ -102,9 +102,14 @@ const tokenValues = Object.assign(
/* cohere doesn't have rates for the older command models, /* cohere doesn't have rates for the older command models,
so this was from https://artificialanalysis.ai/models/command-light/providers */ so this was from https://artificialanalysis.ai/models/command-light/providers */
command: { prompt: 0.38, completion: 0.38 }, 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-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemini-1.5': { prompt: 7, completion: 21 }, // May 2nd, 2024 pricing 'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 },
gemini: { prompt: 0.5, completion: 1.5 }, // May 2nd, 2024 pricing '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, bedrockValues,
); );

View file

@ -380,3 +380,81 @@ describe('getCacheMultiplier', () => {
).toBe(0.03); ).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);
});
});
});

View file

@ -45,7 +45,7 @@
"@langchain/google-genai": "^0.1.7", "@langchain/google-genai": "^0.1.7",
"@langchain/google-vertexai": "^0.1.8", "@langchain/google-vertexai": "^0.1.8",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.0.1", "@librechat/agents": "^2.0.2",
"@waylaidwanderer/fetch-event-source": "^3.0.1", "@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",

View file

@ -1,18 +1,42 @@
const { Providers } = require('@librechat/agents'); const { Providers } = require('@librechat/agents');
const { AuthKeys } = require('librechat-data-provider'); 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 * @param {string} model
* @returns {Array<{category: string, threshold: string}>} * @returns {Array<{category: string, threshold: string}> | undefined}
*/ */
function getSafetySettings(isGemini2) { function getSafetySettings(model) {
const mapThreshold = (value) => { if (isEnabled(process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS)) {
if (isGemini2 && value === 'BLOCK_NONE') { return undefined;
return 'OFF'; }
} const mapThreshold = getThresholdMapping(model);
return value;
};
return [ return [
{ {
@ -85,8 +109,7 @@ function getLLMConfig(credentials, options = {}) {
}; };
/** Used only for Safety Settings */ /** Used only for Safety Settings */
const isGemini2 = llmConfig.model.includes('gemini-2.0') && !llmConfig.model.includes('thinking'); llmConfig.safetySettings = getSafetySettings(llmConfig.model);
llmConfig.safetySettings = getSafetySettings(isGemini2);
let provider; let provider;
@ -153,4 +176,5 @@ function getLLMConfig(credentials, options = {}) {
module.exports = { module.exports = {
getLLMConfig, getLLMConfig,
getSafetySettings,
}; };

View file

@ -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 getLogStores = require('~/cache/getLogStores');
const initializeClient = require('./initialize');
const { isEnabled } = require('~/server/utils'); const { isEnabled } = require('~/server/utils');
const { saveConvo } = require('~/models'); const { saveConvo } = require('~/models');
const { logger } = require('~/config');
const initializeClient = require('./initialize');
const addTitle = async (req, { text, response, client }) => { const addTitle = async (req, { text, response, client }) => {
const { TITLE_CONVO = 'true' } = process.env ?? {}; const { TITLE_CONVO = 'true' } = process.env ?? {};
@ -14,22 +13,16 @@ const addTitle = async (req, { text, response, client }) => {
if (client.options.titleConvo === false) { if (client.options.titleConvo === false) {
return; return;
} }
const DEFAULT_TITLE_MODEL = 'gemini-pro';
const { GOOGLE_TITLE_MODEL } = process.env ?? {}; const { GOOGLE_TITLE_MODEL } = process.env ?? {};
const providerConfig = req.app.locals[EModelEndpoint.google];
let model = GOOGLE_TITLE_MODEL ?? DEFAULT_TITLE_MODEL; let model =
providerConfig?.titleModel ??
GOOGLE_TITLE_MODEL ??
client.options?.modelOptions.model ??
googleSettings.model.default;
if (GOOGLE_TITLE_MODEL === Constants.CURRENT_MODEL) { if (GOOGLE_TITLE_MODEL === Constants.CURRENT_MODEL) {
model = client.options?.modelOptions.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 = { const titleEndpointOptions = {

View file

@ -1,9 +1,11 @@
const { z } = require('zod');
const { tool } = require('@langchain/core/tools'); const { tool } = require('@langchain/core/tools');
const { Constants: AgentConstants } = require('@librechat/agents'); const { Constants: AgentConstants, Providers } = require('@librechat/agents');
const { const {
Constants, Constants,
convertJsonSchemaToZod, ContentTypes,
isAssistantsEndpoint, isAssistantsEndpoint,
convertJsonSchemaToZod,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { logger, getMCPManager } = require('~/config'); const { logger, getMCPManager } = require('~/config');
@ -25,7 +27,15 @@ async function createMCPTool({ req, toolKey, provider }) {
} }
/** @type {LCTool} */ /** @type {LCTool} */
const { description, parameters } = toolDefinition; 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); const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
/** @type {(toolInput: Object | string) => Promise<unknown>} */ /** @type {(toolInput: Object | string) => Promise<unknown>} */
const _call = async (toolInput) => { const _call = async (toolInput) => {
@ -35,6 +45,9 @@ async function createMCPTool({ req, toolKey, provider }) {
if (isAssistantsEndpoint(provider) && Array.isArray(result)) { if (isAssistantsEndpoint(provider) && Array.isArray(result)) {
return result[0]; return result[0];
} }
if (isGoogle && Array.isArray(result[0]) && result[0][0]?.type === ContentTypes.TEXT) {
return [result[0][0].text, result[1]];
}
return result; return result;
} catch (error) { } catch (error) {
logger.error(`${toolName} MCP server tool call failed`, error); logger.error(`${toolName} MCP server tool call failed`, error);

View file

@ -49,11 +49,14 @@ const cohereModels = {
const googleModels = { const googleModels = {
/* Max I/O is combined so we subtract the amount from max response tokens for actual total */ /* Max I/O is combined so we subtract the amount from max response tokens for actual total */
gemini: 30720, // -2048 from max gemini: 30720, // -2048 from max
'gemini-pro-vision': 12288, // -4096 from max 'gemini-pro-vision': 12288,
'gemini-exp': 8000, 'gemini-exp': 2000000,
'gemini-2.0-flash-thinking-exp': 30720, // -2048 from max 'gemini-2.0': 2000000,
'gemini-2.0': 1048576, 'gemini-2.0-flash': 1000000,
'gemini-1.5': 1048576, '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 'text-bison-32k': 32758, // -10 from max
'chat-bison-32k': 32758, // -10 from max 'chat-bison-32k': 32758, // -10 from max
'code-bison-32k': 32758, // -10 from max 'code-bison-32k': 32758, // -10 from max

View file

@ -154,6 +154,24 @@ describe('getModelMaxTokens', () => {
}); });
test('should return correct tokens for partial match - Google models', () => { 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( expect(getModelMaxTokens('gemini-1.5-pro-latest', EModelEndpoint.google)).toBe(
maxTokensMap[EModelEndpoint.google]['gemini-1.5'], maxTokensMap[EModelEndpoint.google]['gemini-1.5'],
); );

View file

@ -33,7 +33,7 @@ type TExpiredKey = {
endpoint: string; endpoint: string;
}; };
type TInputLength = { type TGenericError = {
info: string; info: string;
}; };
@ -49,10 +49,14 @@ const errorMessages = {
const { expiredAt, endpoint } = json; const { expiredAt, endpoint } = json;
return localize('com_error_expired_user_key', endpoint, expiredAt); 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; const { info } = json;
return localize('com_error_input_length', info); return localize('com_error_input_length', info);
}, },
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
const { info } = json;
return info;
},
[ViolationTypes.BAN]: [ViolationTypes.BAN]:
'Your account has been temporarily banned due to violations of our service.', 'Your account has been temporarily banned due to violations of our service.',
invalid_api_key: invalid_api_key:

View file

@ -94,7 +94,7 @@ export default function ExportModal({
<Label htmlFor="type" className="text-left text-sm font-medium"> <Label htmlFor="type" className="text-left text-sm font-medium">
{localize('com_nav_export_type')} {localize('com_nav_export_type')}
</Label> </Label>
<Dropdown value={type} onChange={_setType} options={typeOptions} /> <Dropdown value={type} onChange={_setType} options={typeOptions} portal={false} />
</div> </div>
</div> </div>
<div className="grid w-full gap-6 sm:grid-cols-2"> <div className="grid w-full gap-6 sm:grid-cols-2">

View file

@ -12,7 +12,7 @@ export const useScreenshot = () => {
const { ref } = useContext(ScreenshotContext); const { ref } = useContext(ScreenshotContext);
const { theme } = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
const takeScreenShot = async (node: HTMLElement) => { const takeScreenShot = async (node?: HTMLElement) => {
if (!node) { if (!node) {
throw new Error('You should provide correct html 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; isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
} }
const backgroundColor = isDark ? '#171717' : 'white'; 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 croppedCanvas = document.createElement('canvas');
const croppedCanvasContext = croppedCanvas.getContext('2d') as CanvasRenderingContext2D; const croppedCanvasContext = croppedCanvas.getContext('2d') as CanvasRenderingContext2D;
// init data // init data
@ -35,9 +41,9 @@ export const useScreenshot = () => {
croppedCanvas.height = cropHeight; croppedCanvas.height = cropHeight;
croppedCanvasContext.fillStyle = backgroundColor; 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); const base64Image = croppedCanvas.toDataURL('image/png', 1);

1146
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.698", "version": "0.7.699",
"description": "data services for librechat apps", "description": "data services for librechat apps",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",

View file

@ -699,18 +699,19 @@ export const defaultModels = {
[EModelEndpoint.assistants]: ['chatgpt-4o-latest', ...sharedOpenAIModels], [EModelEndpoint.assistants]: ['chatgpt-4o-latest', ...sharedOpenAIModels],
[EModelEndpoint.agents]: sharedOpenAIModels, // TODO: Add agent models (agentsModels) [EModelEndpoint.agents]: sharedOpenAIModels, // TODO: Add agent models (agentsModels)
[EModelEndpoint.google]: [ [EModelEndpoint.google]: [
'gemini-pro', // Shared Google Models between Vertex AI & Gen AI
'gemini-pro-vision', // Gemini 2.0 Models
'chat-bison', 'gemini-2.0-flash-001',
'chat-bison-32k', 'gemini-2.0-flash-exp',
'codechat-bison', 'gemini-2.0-flash-lite-preview-02-05',
'codechat-bison-32k', 'gemini-2.0-pro-exp-02-05',
'text-bison', // Gemini 1.5 Models
'text-bison-32k', 'gemini-1.5-flash-001',
'text-unicorn', 'gemini-1.5-flash-002',
'code-gecko', 'gemini-1.5-pro-001',
'code-bison', 'gemini-1.5-pro-002',
'code-bison-32k', // Gemini 1.0 Models
'gemini-1.0-pro-001',
], ],
[EModelEndpoint.anthropic]: sharedAnthropicModels, [EModelEndpoint.anthropic]: sharedAnthropicModels,
[EModelEndpoint.openAI]: [ [EModelEndpoint.openAI]: [
@ -1019,6 +1020,10 @@ export enum ErrorTypes {
* Invalid request error, API rejected request * Invalid request error, API rejected request
*/ */
NO_SYSTEM_MESSAGES = 'no_system_messages', NO_SYSTEM_MESSAGES = 'no_system_messages',
/**
* Google provider returned an error
*/
GOOGLE_ERROR = 'google_error',
} }
/** /**

View file

@ -13,8 +13,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('test')).toBe('test'); expect(zodSchema?.parse('test')).toBe('test');
expect(() => zodSchema.parse(123)).toThrow(); expect(() => zodSchema?.parse(123)).toThrow();
}); });
it('should convert string enum schema', () => { it('should convert string enum schema', () => {
@ -24,8 +24,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('foo')).toBe('foo'); expect(zodSchema?.parse('foo')).toBe('foo');
expect(() => zodSchema.parse('invalid')).toThrow(); expect(() => zodSchema?.parse('invalid')).toThrow();
}); });
it('should convert number schema', () => { it('should convert number schema', () => {
@ -34,8 +34,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse(123)).toBe(123); expect(zodSchema?.parse(123)).toBe(123);
expect(() => zodSchema.parse('123')).toThrow(); expect(() => zodSchema?.parse('123')).toThrow();
}); });
it('should convert boolean schema', () => { it('should convert boolean schema', () => {
@ -44,8 +44,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse(true)).toBe(true); expect(zodSchema?.parse(true)).toBe(true);
expect(() => zodSchema.parse('true')).toThrow(); expect(() => zodSchema?.parse('true')).toThrow();
}); });
}); });
@ -57,8 +57,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); expect(zodSchema?.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
expect(() => zodSchema.parse(['a', 123, 'c'])).toThrow(); expect(() => zodSchema?.parse(['a', 123, 'c'])).toThrow();
}); });
it('should convert array of numbers schema', () => { it('should convert array of numbers schema', () => {
@ -68,8 +68,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse([1, 2, 3])).toEqual([1, 2, 3]); expect(zodSchema?.parse([1, 2, 3])).toEqual([1, 2, 3]);
expect(() => zodSchema.parse([1, '2', 3])).toThrow(); expect(() => zodSchema?.parse([1, '2', 3])).toThrow();
}); });
}); });
@ -84,8 +84,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 }); expect(zodSchema?.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 });
expect(() => zodSchema.parse({ name: 123, age: 30 })).toThrow(); expect(() => zodSchema?.parse({ name: 123, age: 30 })).toThrow();
}); });
it('should handle required fields', () => { it('should handle required fields', () => {
@ -99,8 +99,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ name: 'John' })).toEqual({ name: 'John' }); expect(zodSchema?.parse({ name: 'John' })).toEqual({ name: 'John' });
expect(() => zodSchema.parse({})).toThrow(); expect(() => zodSchema?.parse({})).toThrow();
}); });
it('should handle nested objects', () => { it('should handle nested objects', () => {
@ -120,10 +120,10 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); 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 }, 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', () => { it('should handle objects with arrays', () => {
@ -138,8 +138,8 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ names: ['John', 'Jane'] })).toEqual({ names: ['John', 'Jane'] }); expect(zodSchema?.parse({ names: ['John', 'Jane'] })).toEqual({ names: ['John', 'Jane'] });
expect(() => zodSchema.parse({ names: ['John', 123] })).toThrow(); expect(() => zodSchema?.parse({ names: ['John', 123] })).toThrow();
}); });
}); });
@ -151,7 +151,7 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({})).toEqual({}); expect(zodSchema?.parse({})).toEqual({});
}); });
it('should handle unknown types as unknown', () => { it('should handle unknown types as unknown', () => {
@ -160,8 +160,8 @@ describe('convertJsonSchemaToZod', () => {
} as unknown as JsonSchemaType; } as unknown as JsonSchemaType;
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('anything')).toBe('anything'); expect(zodSchema?.parse('anything')).toBe('anything');
expect(zodSchema.parse(123)).toBe(123); expect(zodSchema?.parse(123)).toBe(123);
}); });
it('should handle empty enum arrays as regular strings', () => { it('should handle empty enum arrays as regular strings', () => {
@ -171,7 +171,7 @@ describe('convertJsonSchemaToZod', () => {
}; };
const zodSchema = convertJsonSchemaToZod(schema); 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(zodSchema.parse(validData)).toEqual(validData);
expect(() => expect(() =>
@ -253,7 +256,7 @@ describe('convertJsonSchemaToZod', () => {
}, },
}; };
const zodSchema = convertJsonSchemaToZod(schema); 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', () => { it('should preserve field descriptions', () => {
@ -309,7 +312,7 @@ describe('convertJsonSchemaToZod', () => {
// Type assertions for better type safety // Type assertions for better type safety
const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {}; const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {};
expect(zodSchema.description).toBe('User record'); expect(zodSchema?.description).toBe('User record');
if ('user' in shape) { if ('user' in shape) {
expect(shape.user.description).toBe('User details'); expect(shape.user.description).toBe('User details');
@ -436,7 +439,7 @@ describe('convertJsonSchemaToZod', () => {
const zodSchema = convertJsonSchemaToZod(schema); const zodSchema = convertJsonSchemaToZod(schema);
// Test top-level description // 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 : {}; 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();
});
});
}); });

View file

@ -9,7 +9,24 @@ export type JsonSchemaType = {
description?: string; 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; let zodSchema: z.ZodType;
// Handle primitive types // Handle primitive types
@ -26,13 +43,16 @@ export function convertJsonSchemaToZod(schema: JsonSchemaType): z.ZodType {
zodSchema = z.boolean(); zodSchema = z.boolean();
} else if (schema.type === 'array' && schema.items !== undefined) { } else if (schema.type === 'array' && schema.items !== undefined) {
const itemSchema = convertJsonSchemaToZod(schema.items); const itemSchema = convertJsonSchemaToZod(schema.items);
zodSchema = z.array(itemSchema); zodSchema = z.array(itemSchema as z.ZodType);
} else if (schema.type === 'object') { } else if (schema.type === 'object') {
const shape: Record<string, z.ZodType> = {}; const shape: Record<string, z.ZodType> = {};
const properties = schema.properties ?? {}; const properties = schema.properties ?? {};
for (const [key, value] of Object.entries(properties)) { for (const [key, value] of Object.entries(properties)) {
let fieldSchema = convertJsonSchemaToZod(value); let fieldSchema = convertJsonSchemaToZod(value);
if (!fieldSchema) {
continue;
}
if (value.description != null && value.description !== '') { if (value.description != null && value.description !== '') {
fieldSchema = fieldSchema.describe(value.description); fieldSchema = fieldSchema.describe(value.description);
} }