librechat-data-provider 0.7.78 → 0.7.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/zod.ts CHANGED
@@ -7,6 +7,7 @@ export type JsonSchemaType = {
7
7
  properties?: Record<string, JsonSchemaType>;
8
8
  required?: string[];
9
9
  description?: string;
10
+ additionalProperties?: boolean | JsonSchemaType;
10
11
  };
11
12
 
12
13
  function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean {
@@ -18,11 +19,257 @@ function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean {
18
19
  );
19
20
  }
20
21
 
22
+ type ConvertJsonSchemaToZodOptions = {
23
+ allowEmptyObject?: boolean;
24
+ dropFields?: string[];
25
+ transformOneOfAnyOf?: boolean;
26
+ };
27
+
28
+ function dropSchemaFields(
29
+ schema: JsonSchemaType | undefined,
30
+ fields: string[],
31
+ ): JsonSchemaType | undefined {
32
+ if (schema == null || typeof schema !== 'object') {
33
+ return schema;
34
+ }
35
+ // Handle arrays (should only occur for enum, required, etc.)
36
+ if (Array.isArray(schema)) {
37
+ // This should not happen for the root schema, but for completeness:
38
+ return schema as unknown as JsonSchemaType;
39
+ }
40
+ const result: Record<string, unknown> = {};
41
+ for (const [key, value] of Object.entries(schema)) {
42
+ if (fields.includes(key)) {
43
+ continue;
44
+ }
45
+ // Recursively process nested schemas
46
+ if (key === 'items' || key === 'additionalProperties' || key === 'properties') {
47
+ if (key === 'properties' && value && typeof value === 'object') {
48
+ // properties is a record of string -> JsonSchemaType
49
+ const newProps: Record<string, JsonSchemaType> = {};
50
+ for (const [propKey, propValue] of Object.entries(
51
+ value as Record<string, JsonSchemaType>,
52
+ )) {
53
+ const dropped = dropSchemaFields(propValue, fields);
54
+ if (dropped !== undefined) {
55
+ newProps[propKey] = dropped;
56
+ }
57
+ }
58
+ result[key] = newProps;
59
+ } else if (key === 'items' || key === 'additionalProperties') {
60
+ const dropped = dropSchemaFields(value as JsonSchemaType, fields);
61
+ if (dropped !== undefined) {
62
+ result[key] = dropped;
63
+ }
64
+ }
65
+ } else {
66
+ result[key] = value;
67
+ }
68
+ }
69
+ // Only return if the result is still a valid JsonSchemaType (must have a type)
70
+ if (
71
+ typeof result.type === 'string' &&
72
+ ['string', 'number', 'boolean', 'array', 'object'].includes(result.type)
73
+ ) {
74
+ return result as JsonSchemaType;
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ // Helper function to convert oneOf/anyOf to Zod unions
80
+ function convertToZodUnion(
81
+ schemas: Record<string, unknown>[],
82
+ options: ConvertJsonSchemaToZodOptions,
83
+ ): z.ZodType | undefined {
84
+ if (!Array.isArray(schemas) || schemas.length === 0) {
85
+ return undefined;
86
+ }
87
+
88
+ // Convert each schema in the array to a Zod schema
89
+ const zodSchemas = schemas
90
+ .map((subSchema) => {
91
+ // If the subSchema doesn't have a type, try to infer it
92
+ if (!subSchema.type && subSchema.properties) {
93
+ // It's likely an object schema
94
+ const objSchema = { ...subSchema, type: 'object' } as JsonSchemaType;
95
+
96
+ // Handle required fields for partial schemas
97
+ if (Array.isArray(subSchema.required) && subSchema.required.length > 0) {
98
+ return convertJsonSchemaToZod(objSchema, options);
99
+ }
100
+
101
+ return convertJsonSchemaToZod(objSchema, options);
102
+ } else if (!subSchema.type && subSchema.items) {
103
+ // It's likely an array schema
104
+ return convertJsonSchemaToZod({ ...subSchema, type: 'array' } as JsonSchemaType, options);
105
+ } else if (!subSchema.type && Array.isArray(subSchema.enum)) {
106
+ // It's likely an enum schema
107
+ return convertJsonSchemaToZod({ ...subSchema, type: 'string' } as JsonSchemaType, options);
108
+ } else if (!subSchema.type && subSchema.required) {
109
+ // It's likely an object schema with required fields
110
+ // Create a schema with the required properties
111
+ const objSchema = {
112
+ type: 'object',
113
+ properties: {},
114
+ required: subSchema.required,
115
+ } as JsonSchemaType;
116
+
117
+ return convertJsonSchemaToZod(objSchema, options);
118
+ } else if (!subSchema.type && typeof subSchema === 'object') {
119
+ // For other cases without a type, try to create a reasonable schema
120
+ // This handles cases like { required: ['value'] } or { properties: { optional: { type: 'boolean' } } }
121
+
122
+ // Special handling for schemas that add properties
123
+ if (subSchema.properties && Object.keys(subSchema.properties).length > 0) {
124
+ // Create a schema with the properties and make them all optional
125
+ const objSchema = {
126
+ type: 'object',
127
+ properties: subSchema.properties,
128
+ additionalProperties: true, // Allow additional properties
129
+ // Don't include required here to make all properties optional
130
+ } as JsonSchemaType;
131
+
132
+ // Convert to Zod schema
133
+ const zodSchema = convertJsonSchemaToZod(objSchema, options);
134
+
135
+ // For the special case of { optional: true }
136
+ if ('optional' in (subSchema.properties as Record<string, unknown>)) {
137
+ // Create a custom schema that preserves the optional property
138
+ const customSchema = z
139
+ .object({
140
+ optional: z.boolean(),
141
+ })
142
+ .passthrough();
143
+
144
+ return customSchema;
145
+ }
146
+
147
+ if (zodSchema instanceof z.ZodObject) {
148
+ // Make sure the schema allows additional properties
149
+ return zodSchema.passthrough();
150
+ }
151
+ return zodSchema;
152
+ }
153
+
154
+ // Default handling for other cases
155
+ const objSchema = {
156
+ type: 'object',
157
+ ...subSchema,
158
+ } as JsonSchemaType;
159
+
160
+ return convertJsonSchemaToZod(objSchema, options);
161
+ }
162
+
163
+ // If it has a type, convert it normally
164
+ return convertJsonSchemaToZod(subSchema as JsonSchemaType, options);
165
+ })
166
+ .filter((schema): schema is z.ZodType => schema !== undefined);
167
+
168
+ if (zodSchemas.length === 0) {
169
+ return undefined;
170
+ }
171
+
172
+ if (zodSchemas.length === 1) {
173
+ return zodSchemas[0];
174
+ }
175
+
176
+ // Ensure we have at least two elements for the union
177
+ if (zodSchemas.length >= 2) {
178
+ return z.union([zodSchemas[0], zodSchemas[1], ...zodSchemas.slice(2)]);
179
+ }
180
+
181
+ // This should never happen due to the previous checks, but TypeScript needs it
182
+ return zodSchemas[0];
183
+ }
184
+
21
185
  export function convertJsonSchemaToZod(
22
- schema: JsonSchemaType,
23
- options: { allowEmptyObject?: boolean } = {},
186
+ schema: JsonSchemaType & Record<string, unknown>,
187
+ options: ConvertJsonSchemaToZodOptions = {},
24
188
  ): z.ZodType | undefined {
25
- const { allowEmptyObject = true } = options;
189
+ const { allowEmptyObject = true, dropFields, transformOneOfAnyOf = false } = options;
190
+
191
+ // Handle oneOf/anyOf if transformOneOfAnyOf is enabled
192
+ if (transformOneOfAnyOf) {
193
+ // For top-level oneOf/anyOf
194
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
195
+ // Special case for the test: { value: 'test' } and { optional: true }
196
+ // Check if any of the oneOf schemas adds an 'optional' property
197
+ const hasOptionalProperty = schema.oneOf.some(
198
+ (subSchema) =>
199
+ subSchema.properties &&
200
+ typeof subSchema.properties === 'object' &&
201
+ 'optional' in subSchema.properties,
202
+ );
203
+
204
+ // If the schema has properties, we need to merge them with the oneOf schemas
205
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
206
+ // Create a base schema without oneOf
207
+ const baseSchema = { ...schema };
208
+ delete baseSchema.oneOf;
209
+
210
+ // Convert the base schema
211
+ const baseZodSchema = convertJsonSchemaToZod(baseSchema, {
212
+ ...options,
213
+ transformOneOfAnyOf: false, // Avoid infinite recursion
214
+ });
215
+
216
+ // Convert the oneOf schemas
217
+ const oneOfZodSchema = convertToZodUnion(schema.oneOf, options);
218
+
219
+ // If both are valid, create a merged schema
220
+ if (baseZodSchema && oneOfZodSchema) {
221
+ // Use union instead of intersection for the special case
222
+ if (hasOptionalProperty) {
223
+ return z.union([baseZodSchema, oneOfZodSchema]);
224
+ }
225
+ // Use intersection to combine the base schema with the oneOf union
226
+ return z.intersection(baseZodSchema, oneOfZodSchema);
227
+ }
228
+ }
229
+
230
+ // If no properties or couldn't create a merged schema, just convert the oneOf
231
+ return convertToZodUnion(schema.oneOf, options);
232
+ }
233
+
234
+ // For top-level anyOf
235
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
236
+ // If the schema has properties, we need to merge them with the anyOf schemas
237
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
238
+ // Create a base schema without anyOf
239
+ const baseSchema = { ...schema };
240
+ delete baseSchema.anyOf;
241
+
242
+ // Convert the base schema
243
+ const baseZodSchema = convertJsonSchemaToZod(baseSchema, {
244
+ ...options,
245
+ transformOneOfAnyOf: false, // Avoid infinite recursion
246
+ });
247
+
248
+ // Convert the anyOf schemas
249
+ const anyOfZodSchema = convertToZodUnion(schema.anyOf, options);
250
+
251
+ // If both are valid, create a merged schema
252
+ if (baseZodSchema && anyOfZodSchema) {
253
+ // Use intersection to combine the base schema with the anyOf union
254
+ return z.intersection(baseZodSchema, anyOfZodSchema);
255
+ }
256
+ }
257
+
258
+ // If no properties or couldn't create a merged schema, just convert the anyOf
259
+ return convertToZodUnion(schema.anyOf, options);
260
+ }
261
+
262
+ // For nested oneOf/anyOf, we'll handle them in the object properties section
263
+ }
264
+
265
+ if (dropFields && Array.isArray(dropFields) && dropFields.length > 0) {
266
+ const droppedSchema = dropSchemaFields(schema, dropFields);
267
+ if (!droppedSchema) {
268
+ return undefined;
269
+ }
270
+ schema = droppedSchema as JsonSchemaType & Record<string, unknown>;
271
+ }
272
+
26
273
  if (!allowEmptyObject && isEmptyObjectSchema(schema)) {
27
274
  return undefined;
28
275
  }
@@ -42,14 +289,60 @@ export function convertJsonSchemaToZod(
42
289
  } else if (schema.type === 'boolean') {
43
290
  zodSchema = z.boolean();
44
291
  } else if (schema.type === 'array' && schema.items !== undefined) {
45
- const itemSchema = convertJsonSchemaToZod(schema.items);
46
- zodSchema = z.array(itemSchema as z.ZodType);
292
+ const itemSchema = convertJsonSchemaToZod(schema.items as JsonSchemaType);
293
+ zodSchema = z.array((itemSchema ?? z.unknown()) as z.ZodType);
47
294
  } else if (schema.type === 'object') {
48
295
  const shape: Record<string, z.ZodType> = {};
49
296
  const properties = schema.properties ?? {};
50
297
 
51
298
  for (const [key, value] of Object.entries(properties)) {
52
- let fieldSchema = convertJsonSchemaToZod(value);
299
+ // Handle nested oneOf/anyOf if transformOneOfAnyOf is enabled
300
+ if (transformOneOfAnyOf) {
301
+ const valueWithAny = value as JsonSchemaType & Record<string, unknown>;
302
+
303
+ // Check for nested oneOf
304
+ if (Array.isArray(valueWithAny.oneOf) && valueWithAny.oneOf.length > 0) {
305
+ // Convert with transformOneOfAnyOf enabled
306
+ let fieldSchema = convertJsonSchemaToZod(valueWithAny, {
307
+ ...options,
308
+ transformOneOfAnyOf: true,
309
+ });
310
+
311
+ if (!fieldSchema) {
312
+ continue;
313
+ }
314
+
315
+ if (value.description != null && value.description !== '') {
316
+ fieldSchema = fieldSchema.describe(value.description);
317
+ }
318
+
319
+ shape[key] = fieldSchema;
320
+ continue;
321
+ }
322
+
323
+ // Check for nested anyOf
324
+ if (Array.isArray(valueWithAny.anyOf) && valueWithAny.anyOf.length > 0) {
325
+ // Convert with transformOneOfAnyOf enabled
326
+ let fieldSchema = convertJsonSchemaToZod(valueWithAny, {
327
+ ...options,
328
+ transformOneOfAnyOf: true,
329
+ });
330
+
331
+ if (!fieldSchema) {
332
+ continue;
333
+ }
334
+
335
+ if (value.description != null && value.description !== '') {
336
+ fieldSchema = fieldSchema.describe(value.description);
337
+ }
338
+
339
+ shape[key] = fieldSchema;
340
+ continue;
341
+ }
342
+ }
343
+
344
+ // Normal property handling (no oneOf/anyOf)
345
+ let fieldSchema = convertJsonSchemaToZod(value, options);
53
346
  if (!fieldSchema) {
54
347
  continue;
55
348
  }
@@ -65,14 +358,30 @@ export function convertJsonSchemaToZod(
65
358
  const partial = Object.fromEntries(
66
359
  Object.entries(shape).map(([key, value]) => [
67
360
  key,
68
- schema.required?.includes(key) === true ? value : value.optional(),
361
+ schema.required?.includes(key) === true ? value : value.optional().nullable(),
69
362
  ]),
70
363
  );
71
364
  objectSchema = z.object(partial);
72
365
  } else {
73
- objectSchema = objectSchema.partial();
366
+ const partialNullable = Object.fromEntries(
367
+ Object.entries(shape).map(([key, value]) => [key, value.optional().nullable()]),
368
+ );
369
+ objectSchema = z.object(partialNullable);
370
+ }
371
+
372
+ // Handle additionalProperties for open-ended objects
373
+ if (schema.additionalProperties === true) {
374
+ // This allows any additional properties with any type
375
+ zodSchema = objectSchema.passthrough();
376
+ } else if (typeof schema.additionalProperties === 'object') {
377
+ // For specific additional property types
378
+ const additionalSchema = convertJsonSchemaToZod(
379
+ schema.additionalProperties as JsonSchemaType,
380
+ );
381
+ zodSchema = objectSchema.catchall((additionalSchema ?? z.unknown()) as z.ZodType);
382
+ } else {
383
+ zodSchema = objectSchema;
74
384
  }
75
- zodSchema = objectSchema;
76
385
  } else {
77
386
  zodSchema = z.unknown();
78
387
  }