mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🚀 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:
parent
33e60c379b
commit
63afb317c6
19 changed files with 939 additions and 720 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
1146
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue