🚀 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

@ -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',
}
/**

View file

@ -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();
});
});
});

View file

@ -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<string, z.ZodType> = {};
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);
}