librechat-data-provider 0.7.4 → 0.7.7

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.
Files changed (49) hide show
  1. package/check_updates.sh +1 -0
  2. package/dist/index.es.js +1 -1
  3. package/dist/index.es.js.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/react-query/index.es.js +1 -1
  7. package/dist/react-query/index.es.js.map +1 -1
  8. package/dist/react-query/package.json +1 -1
  9. package/package.json +6 -6
  10. package/react-query/package.json +1 -1
  11. package/server-rollup.config.js +3 -3
  12. package/specs/actions.spec.ts +700 -36
  13. package/specs/azure.spec.ts +8 -5
  14. package/specs/filetypes.spec.ts +1 -7
  15. package/specs/mcp.spec.ts +52 -0
  16. package/specs/openapiSpecs.ts +127 -0
  17. package/specs/utils.spec.ts +129 -0
  18. package/src/actions.ts +311 -101
  19. package/src/api-endpoints.ts +70 -13
  20. package/src/artifacts.ts +3104 -0
  21. package/src/azure.ts +40 -33
  22. package/src/bedrock.ts +227 -0
  23. package/src/config.ts +344 -78
  24. package/src/createPayload.ts +3 -1
  25. package/src/data-service.ts +353 -90
  26. package/src/file-config.ts +13 -2
  27. package/src/generate.ts +31 -2
  28. package/src/index.ts +12 -4
  29. package/src/keys.ts +17 -0
  30. package/src/mcp.ts +87 -0
  31. package/src/models.ts +1 -1
  32. package/src/parsers.ts +118 -60
  33. package/src/react-query/react-query-service.ts +54 -115
  34. package/src/request.ts +31 -7
  35. package/src/roles.ts +91 -2
  36. package/src/schemas.ts +513 -340
  37. package/src/types/agents.ts +276 -0
  38. package/src/types/assistants.ts +181 -27
  39. package/src/types/files.ts +6 -0
  40. package/src/types/mutations.ts +170 -7
  41. package/src/types/queries.ts +43 -21
  42. package/src/types/runs.ts +23 -0
  43. package/src/types.ts +132 -67
  44. package/src/utils.ts +44 -0
  45. package/src/zod.spec.ts +526 -0
  46. package/src/zod.ts +86 -0
  47. package/tsconfig.json +1 -2
  48. package/specs/parsers.spec.ts +0 -48
  49. package/src/sse.js +0 -242
@@ -1,4 +1,5 @@
1
1
  import axios from 'axios';
2
+ import { z } from 'zod';
2
3
  import { OpenAPIV3 } from 'openapi-types';
3
4
  import {
4
5
  createURL,
@@ -8,13 +9,19 @@ import {
8
9
  FunctionSignature,
9
10
  validateAndParseOpenAPISpec,
10
11
  } from '../src/actions';
11
- import { getWeatherOpenapiSpec, whimsicalOpenapiSpec, scholarAIOpenapiSpec } from './openapiSpecs';
12
+ import {
13
+ getWeatherOpenapiSpec,
14
+ whimsicalOpenapiSpec,
15
+ scholarAIOpenapiSpec,
16
+ swapidev,
17
+ } from './openapiSpecs';
12
18
  import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/assistants';
13
19
  import type { FlowchartSchema } from './openapiSpecs';
14
20
  import type { ParametersSchema } from '../src/actions';
15
21
 
16
22
  jest.mock('axios');
17
23
  const mockedAxios = axios as jest.Mocked<typeof axios>;
24
+ mockedAxios.create.mockReturnValue(mockedAxios);
18
25
 
19
26
  describe('FunctionSignature', () => {
20
27
  it('creates a function signature and converts to JSON tool', () => {
@@ -59,7 +66,7 @@ describe('ActionRequest', () => {
59
66
  false,
60
67
  'application/json',
61
68
  );
62
- await actionRequest.setParams({ param1: 'value1' });
69
+ actionRequest.setParams({ param1: 'value1' });
63
70
  const response = await actionRequest.execute();
64
71
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', expect.anything());
65
72
  expect(response.data).toEqual({ success: true, method: 'GET' });
@@ -84,7 +91,7 @@ describe('ActionRequest', () => {
84
91
  false,
85
92
  'application/json',
86
93
  );
87
- await actionRequest.setParams({ param: 'test' });
94
+ actionRequest.setParams({ param: 'test' });
88
95
  const response = await actionRequest.execute();
89
96
  expect(mockedAxios.get).toHaveBeenCalled();
90
97
  expect(response.data.success).toBe(true);
@@ -100,7 +107,7 @@ describe('ActionRequest', () => {
100
107
  false,
101
108
  'application/json',
102
109
  );
103
- await actionRequest.setParams({ param: 'test' });
110
+ actionRequest.setParams({ param: 'test' });
104
111
  const response = await actionRequest.execute();
105
112
  expect(mockedAxios.post).toHaveBeenCalled();
106
113
  expect(response.data.success).toBe(true);
@@ -116,7 +123,7 @@ describe('ActionRequest', () => {
116
123
  false,
117
124
  'application/json',
118
125
  );
119
- await actionRequest.setParams({ param: 'test' });
126
+ actionRequest.setParams({ param: 'test' });
120
127
  const response = await actionRequest.execute();
121
128
  expect(mockedAxios.put).toHaveBeenCalled();
122
129
  expect(response.data.success).toBe(true);
@@ -132,7 +139,7 @@ describe('ActionRequest', () => {
132
139
  false,
133
140
  'application/json',
134
141
  );
135
- await actionRequest.setParams({ param: 'test' });
142
+ actionRequest.setParams({ param: 'test' });
136
143
  const response = await actionRequest.execute();
137
144
  expect(mockedAxios.delete).toHaveBeenCalled();
138
145
  expect(response.data.success).toBe(true);
@@ -148,7 +155,7 @@ describe('ActionRequest', () => {
148
155
  false,
149
156
  'application/json',
150
157
  );
151
- await actionRequest.setParams({ param: 'test' });
158
+ actionRequest.setParams({ param: 'test' });
152
159
  const response = await actionRequest.execute();
153
160
  expect(mockedAxios.patch).toHaveBeenCalled();
154
161
  expect(response.data.success).toBe(true);
@@ -163,7 +170,7 @@ describe('ActionRequest', () => {
163
170
  false,
164
171
  'application/json',
165
172
  );
166
- await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
173
+ await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: invalid');
167
174
  });
168
175
 
169
176
  it('replaces path parameters with values from toolInput', async () => {
@@ -176,20 +183,21 @@ describe('ActionRequest', () => {
176
183
  'application/json',
177
184
  );
178
185
 
179
- await actionRequest.setParams({
186
+ const executor = actionRequest.createExecutor();
187
+ executor.setParams({
180
188
  stocksTicker: 'AAPL',
181
189
  multiplier: 5,
182
190
  startDate: '2023-01-01',
183
191
  endDate: '2023-12-31',
184
192
  });
185
193
 
186
- expect(actionRequest.path).toBe('/stocks/AAPL/bars/5');
187
- expect(actionRequest.params).toEqual({
194
+ expect(executor.path).toBe('/stocks/AAPL/bars/5');
195
+ expect(executor.params).toEqual({
188
196
  startDate: '2023-01-01',
189
197
  endDate: '2023-12-31',
190
198
  });
191
199
 
192
- await actionRequest.execute();
200
+ await executor.execute();
193
201
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/stocks/AAPL/bars/5', {
194
202
  headers: expect.anything(),
195
203
  params: {
@@ -209,7 +217,271 @@ describe('ActionRequest', () => {
209
217
  false,
210
218
  'application/json',
211
219
  );
212
- await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
220
+ await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: invalid');
221
+ });
222
+
223
+ describe('ActionRequest Concurrent Execution', () => {
224
+ beforeEach(() => {
225
+ jest.clearAllMocks();
226
+ mockedAxios.get.mockImplementation(async (url, config) => ({
227
+ data: { url, params: config?.params, headers: config?.headers },
228
+ }));
229
+ });
230
+
231
+ it('maintains isolated state between concurrent executions with different parameters', async () => {
232
+ const actionRequest = new ActionRequest(
233
+ 'https://example.com',
234
+ '/math/sqrt/{number}',
235
+ 'GET',
236
+ 'getSqrt',
237
+ false,
238
+ 'application/json',
239
+ );
240
+
241
+ // Simulate concurrent requests with different numbers
242
+ const numbers = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30];
243
+ const requests = numbers.map((num) => ({
244
+ number: num.toString(),
245
+ precision: '2',
246
+ }));
247
+
248
+ const responses = await Promise.all(
249
+ requests.map((params) => {
250
+ const executor = actionRequest.createExecutor();
251
+ return executor.setParams(params).execute();
252
+ }),
253
+ );
254
+
255
+ // Verify each response used the correct path parameter
256
+ responses.forEach((response, index) => {
257
+ const expectedUrl = `https://example.com/math/sqrt/${numbers[index]}`;
258
+ expect(response.data.url).toBe(expectedUrl);
259
+ expect(response.data.params).toEqual({ precision: '2' });
260
+ });
261
+
262
+ // Verify the correct number of calls were made
263
+ expect(mockedAxios.get).toHaveBeenCalledTimes(numbers.length);
264
+ });
265
+
266
+ it('maintains isolated authentication state between concurrent executions', async () => {
267
+ const actionRequest = new ActionRequest(
268
+ 'https://example.com',
269
+ '/secure/resource/{id}',
270
+ 'GET',
271
+ 'getResource',
272
+ false,
273
+ 'application/json',
274
+ );
275
+
276
+ const requests = [
277
+ {
278
+ params: { id: '1' },
279
+ auth: {
280
+ auth: {
281
+ type: AuthTypeEnum.ServiceHttp,
282
+ authorization_type: AuthorizationTypeEnum.Bearer,
283
+ },
284
+ api_key: 'token1',
285
+ },
286
+ },
287
+ {
288
+ params: { id: '2' },
289
+ auth: {
290
+ auth: {
291
+ type: AuthTypeEnum.ServiceHttp,
292
+ authorization_type: AuthorizationTypeEnum.Bearer,
293
+ },
294
+ api_key: 'token2',
295
+ },
296
+ },
297
+ ];
298
+
299
+ const responses = await Promise.all(
300
+ requests.map(async ({ params, auth }) => {
301
+ const executor = actionRequest.createExecutor();
302
+ return (await executor.setParams(params).setAuth(auth)).execute();
303
+ }),
304
+ );
305
+
306
+ // Verify each response had its own auth token
307
+ responses.forEach((response, index) => {
308
+ const expectedUrl = `https://example.com/secure/resource/${index + 1}`;
309
+ expect(response.data.url).toBe(expectedUrl);
310
+ expect(response.data.headers).toMatchObject({
311
+ Authorization: `Bearer token${index + 1}`,
312
+ });
313
+ });
314
+ });
315
+
316
+ it('handles mixed authentication types concurrently', async () => {
317
+ const actionRequest = new ActionRequest(
318
+ 'https://example.com',
319
+ '/api/{version}/data',
320
+ 'GET',
321
+ 'getData',
322
+ false,
323
+ 'application/json',
324
+ );
325
+
326
+ const requests = [
327
+ {
328
+ params: { version: 'v1' },
329
+ auth: {
330
+ auth: {
331
+ type: AuthTypeEnum.ServiceHttp,
332
+ authorization_type: AuthorizationTypeEnum.Bearer,
333
+ },
334
+ api_key: 'bearer_token',
335
+ },
336
+ },
337
+ {
338
+ params: { version: 'v2' },
339
+ auth: {
340
+ auth: {
341
+ type: AuthTypeEnum.ServiceHttp,
342
+ authorization_type: AuthorizationTypeEnum.Basic,
343
+ },
344
+ api_key: 'basic:auth',
345
+ },
346
+ },
347
+ {
348
+ params: { version: 'v3' },
349
+ auth: {
350
+ auth: {
351
+ type: AuthTypeEnum.ServiceHttp,
352
+ authorization_type: AuthorizationTypeEnum.Custom,
353
+ custom_auth_header: 'X-API-Key',
354
+ },
355
+ api_key: 'custom_key',
356
+ },
357
+ },
358
+ ];
359
+
360
+ const responses = await Promise.all(
361
+ requests.map(async ({ params, auth }) => {
362
+ const executor = actionRequest.createExecutor();
363
+ return (await executor.setParams(params).setAuth(auth)).execute();
364
+ }),
365
+ );
366
+
367
+ // Verify each response had the correct auth type and headers
368
+ expect(responses[0].data.headers).toMatchObject({
369
+ Authorization: 'Bearer bearer_token',
370
+ });
371
+
372
+ expect(responses[1].data.headers).toMatchObject({
373
+ Authorization: `Basic ${Buffer.from('basic:auth').toString('base64')}`,
374
+ });
375
+
376
+ expect(responses[2].data.headers).toMatchObject({
377
+ 'X-API-Key': 'custom_key',
378
+ });
379
+ });
380
+
381
+ it('maintains parameter integrity during concurrent path parameter replacement', async () => {
382
+ const actionRequest = new ActionRequest(
383
+ 'https://example.com',
384
+ '/users/{userId}/posts/{postId}',
385
+ 'GET',
386
+ 'getUserPost',
387
+ false,
388
+ 'application/json',
389
+ );
390
+
391
+ const requests = [
392
+ { userId: '1', postId: 'a', filter: 'recent' },
393
+ { userId: '2', postId: 'b', filter: 'popular' },
394
+ { userId: '3', postId: 'c', filter: 'trending' },
395
+ ];
396
+
397
+ const responses = await Promise.all(
398
+ requests.map((params) => {
399
+ const executor = actionRequest.createExecutor();
400
+ return executor.setParams(params).execute();
401
+ }),
402
+ );
403
+
404
+ responses.forEach((response, index) => {
405
+ const expectedUrl = `https://example.com/users/${requests[index].userId}/posts/${requests[index].postId}`;
406
+ expect(response.data.url).toBe(expectedUrl);
407
+ expect(response.data.params).toEqual({ filter: requests[index].filter });
408
+ });
409
+ });
410
+
411
+ it('preserves original ActionRequest state after multiple executions', async () => {
412
+ const actionRequest = new ActionRequest(
413
+ 'https://example.com',
414
+ '/original/{param}',
415
+ 'GET',
416
+ 'testOp',
417
+ false,
418
+ 'application/json',
419
+ );
420
+
421
+ // Store original values
422
+ const originalPath = actionRequest.path;
423
+ const originalDomain = actionRequest.domain;
424
+ const originalMethod = actionRequest.method;
425
+
426
+ // Perform multiple concurrent executions
427
+ await Promise.all([
428
+ actionRequest.createExecutor().setParams({ param: '1' }).execute(),
429
+ actionRequest.createExecutor().setParams({ param: '2' }).execute(),
430
+ actionRequest.createExecutor().setParams({ param: '3' }).execute(),
431
+ ]);
432
+
433
+ // Verify original ActionRequest remains unchanged
434
+ expect(actionRequest.path).toBe(originalPath);
435
+ expect(actionRequest.domain).toBe(originalDomain);
436
+ expect(actionRequest.method).toBe(originalMethod);
437
+ });
438
+
439
+ it('shares immutable configuration between executors from the same ActionRequest', () => {
440
+ const actionRequest = new ActionRequest(
441
+ 'https://example.com',
442
+ '/api/{version}/data',
443
+ 'GET',
444
+ 'getData',
445
+ false,
446
+ 'application/json',
447
+ );
448
+
449
+ // Create multiple executors
450
+ const executor1 = actionRequest.createExecutor();
451
+ const executor2 = actionRequest.createExecutor();
452
+ const executor3 = actionRequest.createExecutor();
453
+
454
+ // Test that the configuration properties are shared
455
+ [executor1, executor2, executor3].forEach((executor) => {
456
+ expect(executor.getConfig()).toBeDefined();
457
+ expect(executor.getConfig()).toEqual({
458
+ domain: 'https://example.com',
459
+ basePath: '/api/{version}/data',
460
+ method: 'GET',
461
+ operation: 'getData',
462
+ isConsequential: false,
463
+ contentType: 'application/json',
464
+ });
465
+ });
466
+
467
+ // Verify that config objects are the exact same instance (shared reference)
468
+ expect(executor1.getConfig()).toBe(executor2.getConfig());
469
+ expect(executor2.getConfig()).toBe(executor3.getConfig());
470
+
471
+ // Verify that modifying mutable state doesn't affect other executors
472
+ executor1.setParams({ version: 'v1' });
473
+ executor2.setParams({ version: 'v2' });
474
+ executor3.setParams({ version: 'v3' });
475
+
476
+ expect(executor1.path).toBe('/api/v1/data');
477
+ expect(executor2.path).toBe('/api/v2/data');
478
+ expect(executor3.path).toBe('/api/v3/data');
479
+
480
+ // Verify that the original config remains unchanged
481
+ expect(executor1.getConfig().basePath).toBe('/api/{version}/data');
482
+ expect(executor2.getConfig().basePath).toBe('/api/{version}/data');
483
+ expect(executor3.getConfig().basePath).toBe('/api/{version}/data');
484
+ });
213
485
  });
214
486
  });
215
487
 
@@ -227,7 +499,8 @@ describe('Authentication Handling', () => {
227
499
  const api_key = 'user:pass';
228
500
  const encodedCredentials = Buffer.from('user:pass').toString('base64');
229
501
 
230
- actionRequest.setAuth({
502
+ const executor = actionRequest.createExecutor();
503
+ await executor.setParams({ param1: 'value1' }).setAuth({
231
504
  auth: {
232
505
  type: AuthTypeEnum.ServiceHttp,
233
506
  authorization_type: AuthorizationTypeEnum.Basic,
@@ -235,13 +508,13 @@ describe('Authentication Handling', () => {
235
508
  api_key,
236
509
  });
237
510
 
238
- await actionRequest.setParams({ param1: 'value1' });
239
- await actionRequest.execute();
511
+ await executor.execute();
240
512
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
241
513
  headers: expect.objectContaining({
242
514
  Authorization: `Basic ${encodedCredentials}`,
515
+ 'Content-Type': 'application/json',
243
516
  }),
244
- params: expect.anything(),
517
+ params: { param1: 'value1' },
245
518
  });
246
519
  });
247
520
 
@@ -254,20 +527,23 @@ describe('Authentication Handling', () => {
254
527
  false,
255
528
  'application/json',
256
529
  );
257
- actionRequest.setAuth({
530
+
531
+ const executor = actionRequest.createExecutor();
532
+ await executor.setParams({ param1: 'value1' }).setAuth({
258
533
  auth: {
259
534
  type: AuthTypeEnum.ServiceHttp,
260
535
  authorization_type: AuthorizationTypeEnum.Bearer,
261
536
  },
262
537
  api_key: 'token123',
263
538
  });
264
- await actionRequest.setParams({ param1: 'value1' });
265
- await actionRequest.execute();
539
+
540
+ await executor.execute();
266
541
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
267
542
  headers: expect.objectContaining({
268
543
  Authorization: 'Bearer token123',
544
+ 'Content-Type': 'application/json',
269
545
  }),
270
- params: expect.anything(),
546
+ params: { param1: 'value1' },
271
547
  });
272
548
  });
273
549
 
@@ -280,22 +556,24 @@ describe('Authentication Handling', () => {
280
556
  false,
281
557
  'application/json',
282
558
  );
283
- // Updated to match ActionMetadata structure
284
- actionRequest.setAuth({
559
+
560
+ const executor = actionRequest.createExecutor();
561
+ await executor.setParams({ param1: 'value1' }).setAuth({
285
562
  auth: {
286
- type: AuthTypeEnum.ServiceHttp, // Assuming this is a valid enum or value for your context
287
- authorization_type: AuthorizationTypeEnum.Custom, // Assuming Custom means using a custom header
563
+ type: AuthTypeEnum.ServiceHttp,
564
+ authorization_type: AuthorizationTypeEnum.Custom,
288
565
  custom_auth_header: 'X-API-KEY',
289
566
  },
290
567
  api_key: 'abc123',
291
568
  });
292
- await actionRequest.setParams({ param1: 'value1' });
293
- await actionRequest.execute();
569
+
570
+ await executor.execute();
294
571
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
295
572
  headers: expect.objectContaining({
296
573
  'X-API-KEY': 'abc123',
574
+ 'Content-Type': 'application/json',
297
575
  }),
298
- params: expect.anything(),
576
+ params: { param1: 'value1' },
299
577
  });
300
578
  });
301
579
  });
@@ -306,22 +584,100 @@ describe('resolveRef', () => {
306
584
  const flowchartRequestRef = (
307
585
  openapiSpec.paths['/ai.chatgpt.render-flowchart']?.post
308
586
  ?.requestBody as OpenAPIV3.RequestBodyObject
309
- )?.content['application/json'].schema;
587
+ ).content['application/json'].schema;
588
+
310
589
  expect(flowchartRequestRef).toBeDefined();
311
- const resolvedFlowchartRequest = resolveRef(
312
- flowchartRequestRef as OpenAPIV3.RequestBodyObject,
590
+
591
+ const resolvedSchemaObject = resolveRef(
592
+ flowchartRequestRef as OpenAPIV3.ReferenceObject,
313
593
  openapiSpec.components,
314
- );
594
+ ) as OpenAPIV3.SchemaObject;
595
+
596
+ expect(resolvedSchemaObject).toBeDefined();
597
+ expect(resolvedSchemaObject.type).toBe('object');
598
+ expect(resolvedSchemaObject.properties).toBeDefined();
315
599
 
316
- expect(resolvedFlowchartRequest).toBeDefined();
317
- expect(resolvedFlowchartRequest.type).toBe('object');
318
- const properties = resolvedFlowchartRequest.properties as FlowchartSchema;
319
- expect(properties).toBeDefined();
600
+ const properties = resolvedSchemaObject.properties as FlowchartSchema;
320
601
  expect(properties.mermaid).toBeDefined();
321
602
  expect(properties.mermaid.type).toBe('string');
322
603
  });
323
604
  });
324
605
 
606
+ describe('resolveRef general cases', () => {
607
+ const spec = {
608
+ openapi: '3.0.0',
609
+ info: { title: 'TestSpec', version: '1.0.0' },
610
+ paths: {},
611
+ components: {
612
+ schemas: {
613
+ TestSchema: { type: 'string' },
614
+ },
615
+ parameters: {
616
+ TestParam: {
617
+ name: 'myParam',
618
+ in: 'query',
619
+ required: false,
620
+ schema: { $ref: '#/components/schemas/TestSchema' },
621
+ },
622
+ },
623
+ requestBodies: {
624
+ TestRequestBody: {
625
+ content: {
626
+ 'application/json': {
627
+ schema: { $ref: '#/components/schemas/TestSchema' },
628
+ },
629
+ },
630
+ },
631
+ },
632
+ },
633
+ } satisfies OpenAPIV3.Document;
634
+
635
+ it('resolves schema refs correctly', () => {
636
+ const schemaRef: OpenAPIV3.ReferenceObject = { $ref: '#/components/schemas/TestSchema' };
637
+ const resolvedSchema = resolveRef<OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject>(
638
+ schemaRef,
639
+ spec.components,
640
+ );
641
+ expect(resolvedSchema.type).toEqual('string');
642
+ });
643
+
644
+ it('resolves parameter refs correctly, then schema within parameter', () => {
645
+ const paramRef: OpenAPIV3.ReferenceObject = { $ref: '#/components/parameters/TestParam' };
646
+ const resolvedParam = resolveRef<OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject>(
647
+ paramRef,
648
+ spec.components,
649
+ );
650
+ expect(resolvedParam.name).toEqual('myParam');
651
+ expect(resolvedParam.in).toEqual('query');
652
+ expect(resolvedParam.required).toBe(false);
653
+
654
+ const paramSchema = resolveRef<OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject>(
655
+ resolvedParam.schema as OpenAPIV3.ReferenceObject,
656
+ spec.components,
657
+ );
658
+ expect(paramSchema.type).toEqual('string');
659
+ });
660
+
661
+ it('resolves requestBody refs correctly, then schema within requestBody', () => {
662
+ const requestBodyRef: OpenAPIV3.ReferenceObject = {
663
+ $ref: '#/components/requestBodies/TestRequestBody',
664
+ };
665
+ const resolvedRequestBody = resolveRef<OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject>(
666
+ requestBodyRef,
667
+ spec.components,
668
+ );
669
+
670
+ expect(resolvedRequestBody.content['application/json']).toBeDefined();
671
+
672
+ const schemaInRequestBody = resolveRef<OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject>(
673
+ resolvedRequestBody.content['application/json'].schema as OpenAPIV3.ReferenceObject,
674
+ spec.components,
675
+ );
676
+
677
+ expect(schemaInRequestBody.type).toEqual('string');
678
+ });
679
+ });
680
+
325
681
  describe('openapiToFunction', () => {
326
682
  it('converts OpenAPI spec to function signatures and request builders', () => {
327
683
  const { functionSignatures, requestBuilders } = openapiToFunction(getWeatherOpenapiSpec);
@@ -548,4 +904,312 @@ describe('createURL', () => {
548
904
  'https://example.com/subdirectory/api/v1/users',
549
905
  );
550
906
  });
907
+
908
+ describe('openapiToFunction zodSchemas', () => {
909
+ describe('getWeatherOpenapiSpec', () => {
910
+ const { zodSchemas } = openapiToFunction(getWeatherOpenapiSpec, true);
911
+
912
+ it('generates correct Zod schema for GetCurrentWeather', () => {
913
+ expect(zodSchemas).toBeDefined();
914
+ expect(zodSchemas?.GetCurrentWeather).toBeDefined();
915
+
916
+ const GetCurrentWeatherSchema = zodSchemas?.GetCurrentWeather;
917
+
918
+ expect(GetCurrentWeatherSchema instanceof z.ZodObject).toBe(true);
919
+
920
+ if (!(GetCurrentWeatherSchema instanceof z.ZodObject)) {
921
+ throw new Error('GetCurrentWeatherSchema is not a ZodObject');
922
+ }
923
+
924
+ const shape = GetCurrentWeatherSchema.shape;
925
+ expect(shape.location instanceof z.ZodString).toBe(true);
926
+
927
+ // Check locations property
928
+ expect(shape.locations).toBeDefined();
929
+ expect(shape.locations instanceof z.ZodOptional).toBe(true);
930
+
931
+ if (!(shape.locations instanceof z.ZodOptional)) {
932
+ throw new Error('locations is not a ZodOptional');
933
+ }
934
+
935
+ const locationsInnerType = shape.locations._def.innerType;
936
+ expect(locationsInnerType instanceof z.ZodArray).toBe(true);
937
+
938
+ if (!(locationsInnerType instanceof z.ZodArray)) {
939
+ throw new Error('locationsInnerType is not a ZodArray');
940
+ }
941
+
942
+ const locationsItemSchema = locationsInnerType.element;
943
+ expect(locationsItemSchema instanceof z.ZodObject).toBe(true);
944
+
945
+ if (!(locationsItemSchema instanceof z.ZodObject)) {
946
+ throw new Error('locationsItemSchema is not a ZodObject');
947
+ }
948
+
949
+ // Validate the structure of locationsItemSchema
950
+ expect(locationsItemSchema.shape.city instanceof z.ZodString).toBe(true);
951
+ expect(locationsItemSchema.shape.state instanceof z.ZodString).toBe(true);
952
+ expect(locationsItemSchema.shape.countryCode instanceof z.ZodString).toBe(true);
953
+
954
+ // Check if time is optional
955
+ const timeSchema = locationsItemSchema.shape.time;
956
+ expect(timeSchema instanceof z.ZodOptional).toBe(true);
957
+
958
+ if (!(timeSchema instanceof z.ZodOptional)) {
959
+ throw new Error('timeSchema is not a ZodOptional');
960
+ }
961
+
962
+ expect(timeSchema._def.innerType instanceof z.ZodString).toBe(true);
963
+
964
+ // Check the description
965
+ expect(shape.locations._def.description).toBe(
966
+ 'A list of locations to retrieve the weather for.',
967
+ );
968
+ });
969
+
970
+ it('validates correct data for GetCurrentWeather', () => {
971
+ const GetCurrentWeatherSchema = zodSchemas?.GetCurrentWeather as z.ZodTypeAny;
972
+ const validData = {
973
+ location: 'New York',
974
+ locations: [
975
+ { city: 'New York', state: 'NY', countryCode: 'US', time: '2023-12-04T14:00:00Z' },
976
+ ],
977
+ };
978
+ expect(() => GetCurrentWeatherSchema.parse(validData)).not.toThrow();
979
+ });
980
+
981
+ it('throws error for invalid data for GetCurrentWeather', () => {
982
+ const GetCurrentWeatherSchema = zodSchemas?.GetCurrentWeather as z.ZodTypeAny;
983
+ const invalidData = {
984
+ location: 123,
985
+ locations: [{ city: 'New York', state: 'NY', countryCode: 'US', time: 'invalid-time' }],
986
+ };
987
+ expect(() => GetCurrentWeatherSchema.parse(invalidData)).toThrow();
988
+ });
989
+ });
990
+
991
+ describe('whimsicalOpenapiSpec', () => {
992
+ const { zodSchemas } = openapiToFunction(whimsicalOpenapiSpec, true);
993
+
994
+ it('generates correct Zod schema for postRenderFlowchart', () => {
995
+ expect(zodSchemas).toBeDefined();
996
+ expect(zodSchemas?.postRenderFlowchart).toBeDefined();
997
+
998
+ const PostRenderFlowchartSchema = zodSchemas?.postRenderFlowchart;
999
+ expect(PostRenderFlowchartSchema).toBeInstanceOf(z.ZodObject);
1000
+
1001
+ if (!(PostRenderFlowchartSchema instanceof z.ZodObject)) {
1002
+ return;
1003
+ }
1004
+
1005
+ const shape = PostRenderFlowchartSchema.shape;
1006
+ expect(shape.mermaid).toBeInstanceOf(z.ZodString);
1007
+ expect(shape.title).toBeInstanceOf(z.ZodOptional);
1008
+ expect((shape.title as z.ZodOptional<z.ZodString>)._def.innerType).toBeInstanceOf(
1009
+ z.ZodString,
1010
+ );
1011
+ });
1012
+
1013
+ it('validates correct data for postRenderFlowchart', () => {
1014
+ const PostRenderFlowchartSchema = zodSchemas?.postRenderFlowchart;
1015
+ const validData = {
1016
+ mermaid: 'graph TD; A-->B; B-->C; C-->D;',
1017
+ title: 'Test Flowchart',
1018
+ };
1019
+ expect(() => PostRenderFlowchartSchema?.parse(validData)).not.toThrow();
1020
+ });
1021
+
1022
+ it('throws error for invalid data for postRenderFlowchart', () => {
1023
+ const PostRenderFlowchartSchema = zodSchemas?.postRenderFlowchart;
1024
+ const invalidData = {
1025
+ mermaid: 123,
1026
+ title: 42,
1027
+ };
1028
+ expect(() => PostRenderFlowchartSchema?.parse(invalidData)).toThrow();
1029
+ });
1030
+ });
1031
+
1032
+ describe('scholarAIOpenapiSpec', () => {
1033
+ const result = validateAndParseOpenAPISpec(scholarAIOpenapiSpec);
1034
+ const spec = result.spec as OpenAPIV3.Document;
1035
+ const { zodSchemas } = openapiToFunction(spec, true);
1036
+
1037
+ it('generates correct Zod schema for searchAbstracts', () => {
1038
+ expect(zodSchemas).toBeDefined();
1039
+ expect(zodSchemas?.searchAbstracts).toBeDefined();
1040
+
1041
+ const SearchAbstractsSchema = zodSchemas?.searchAbstracts;
1042
+ expect(SearchAbstractsSchema).toBeInstanceOf(z.ZodObject);
1043
+
1044
+ if (!(SearchAbstractsSchema instanceof z.ZodObject)) {
1045
+ return;
1046
+ }
1047
+
1048
+ const shape = SearchAbstractsSchema.shape;
1049
+ expect(shape.keywords).toBeInstanceOf(z.ZodString);
1050
+ expect(shape.sort).toBeInstanceOf(z.ZodOptional);
1051
+ expect(
1052
+ (shape.sort as z.ZodOptional<z.ZodEnum<[string, ...string[]]>>)._def.innerType,
1053
+ ).toBeInstanceOf(z.ZodEnum);
1054
+ expect(shape.query).toBeInstanceOf(z.ZodString);
1055
+ expect(shape.peer_reviewed_only).toBeInstanceOf(z.ZodOptional);
1056
+ expect(shape.start_year).toBeInstanceOf(z.ZodOptional);
1057
+ expect(shape.end_year).toBeInstanceOf(z.ZodOptional);
1058
+ expect(shape.offset).toBeInstanceOf(z.ZodOptional);
1059
+ });
1060
+
1061
+ it('validates correct data for searchAbstracts', () => {
1062
+ const SearchAbstractsSchema = zodSchemas?.searchAbstracts;
1063
+ const validData = {
1064
+ keywords: 'machine learning',
1065
+ sort: 'cited_by_count',
1066
+ query: 'AI applications',
1067
+ peer_reviewed_only: 'true',
1068
+ start_year: '2020',
1069
+ end_year: '2023',
1070
+ offset: '0',
1071
+ };
1072
+ expect(() => SearchAbstractsSchema?.parse(validData)).not.toThrow();
1073
+ });
1074
+
1075
+ it('throws error for invalid data for searchAbstracts', () => {
1076
+ const SearchAbstractsSchema = zodSchemas?.searchAbstracts;
1077
+ const invalidData = {
1078
+ keywords: 123,
1079
+ sort: 'invalid_sort',
1080
+ query: 42,
1081
+ peer_reviewed_only: 'maybe',
1082
+ start_year: 2020,
1083
+ end_year: 2023,
1084
+ offset: 0,
1085
+ };
1086
+ expect(() => SearchAbstractsSchema?.parse(invalidData)).toThrow();
1087
+ });
1088
+
1089
+ it('generates correct Zod schema for getFullText', () => {
1090
+ expect(zodSchemas?.getFullText).toBeDefined();
1091
+
1092
+ const GetFullTextSchema = zodSchemas?.getFullText;
1093
+ expect(GetFullTextSchema).toBeInstanceOf(z.ZodObject);
1094
+
1095
+ if (!(GetFullTextSchema instanceof z.ZodObject)) {
1096
+ return;
1097
+ }
1098
+
1099
+ const shape = GetFullTextSchema.shape;
1100
+ expect(shape.pdf_url).toBeInstanceOf(z.ZodString);
1101
+ expect(shape.chunk).toBeInstanceOf(z.ZodOptional);
1102
+ expect((shape.chunk as z.ZodOptional<z.ZodNumber>)._def.innerType).toBeInstanceOf(
1103
+ z.ZodNumber,
1104
+ );
1105
+ });
1106
+
1107
+ it('generates correct Zod schema for saveCitation', () => {
1108
+ expect(zodSchemas?.saveCitation).toBeDefined();
1109
+
1110
+ const SaveCitationSchema = zodSchemas?.saveCitation;
1111
+ expect(SaveCitationSchema).toBeInstanceOf(z.ZodObject);
1112
+
1113
+ if (!(SaveCitationSchema instanceof z.ZodObject)) {
1114
+ return;
1115
+ }
1116
+
1117
+ const shape = SaveCitationSchema.shape;
1118
+ expect(shape.doi).toBeInstanceOf(z.ZodString);
1119
+ expect(shape.zotero_user_id).toBeInstanceOf(z.ZodString);
1120
+ expect(shape.zotero_api_key).toBeInstanceOf(z.ZodString);
1121
+ });
1122
+ });
1123
+ });
1124
+
1125
+ describe('openapiToFunction zodSchemas for SWAPI', () => {
1126
+ const result = validateAndParseOpenAPISpec(swapidev);
1127
+ const spec = result.spec as OpenAPIV3.Document;
1128
+ const { zodSchemas } = openapiToFunction(spec, true);
1129
+
1130
+ describe('getPeople schema', () => {
1131
+ it('does not generate Zod schema for getPeople (no parameters)', () => {
1132
+ expect(zodSchemas).toBeDefined();
1133
+ expect(zodSchemas?.getPeople).toBeUndefined();
1134
+ });
1135
+
1136
+ it('validates correct data for getPeople', () => {
1137
+ const GetPeopleSchema = zodSchemas?.getPeople;
1138
+ expect(GetPeopleSchema).toBeUndefined();
1139
+ });
1140
+
1141
+ it('does not throw for invalid data for getPeople', () => {
1142
+ const GetPeopleSchema = zodSchemas?.getPeople;
1143
+ expect(GetPeopleSchema).toBeUndefined();
1144
+ });
1145
+ });
1146
+
1147
+ describe('getPersonById schema', () => {
1148
+ it('generates correct Zod schema for getPersonById', () => {
1149
+ expect(zodSchemas).toBeDefined();
1150
+ expect(zodSchemas?.getPersonById).toBeDefined();
1151
+
1152
+ const GetPersonByIdSchema = zodSchemas?.getPersonById;
1153
+ expect(GetPersonByIdSchema).toBeInstanceOf(z.ZodObject);
1154
+
1155
+ if (!(GetPersonByIdSchema instanceof z.ZodObject)) {
1156
+ return;
1157
+ }
1158
+
1159
+ const shape = GetPersonByIdSchema.shape;
1160
+ expect(shape.id).toBeInstanceOf(z.ZodString);
1161
+ });
1162
+
1163
+ it('validates correct data for getPersonById', () => {
1164
+ const GetPersonByIdSchema = zodSchemas?.getPersonById;
1165
+ const validData = { id: '1' };
1166
+ expect(() => GetPersonByIdSchema?.parse(validData)).not.toThrow();
1167
+ });
1168
+
1169
+ it('throws error for invalid data for getPersonById', () => {
1170
+ const GetPersonByIdSchema = zodSchemas?.getPersonById;
1171
+ const invalidData = { id: 1 }; // should be string
1172
+ expect(() => GetPersonByIdSchema?.parse(invalidData)).toThrow();
1173
+ });
1174
+ });
1175
+ });
1176
+
1177
+ describe('openapiToFunction parameter refs resolution', () => {
1178
+ const weatherSpec = {
1179
+ openapi: '3.0.0',
1180
+ info: { title: 'Weather', version: '1.0.0' },
1181
+ servers: [{ url: 'https://api.weather.gov' }],
1182
+ paths: {
1183
+ '/points/{point}': {
1184
+ get: {
1185
+ operationId: 'getPoint',
1186
+ parameters: [{ $ref: '#/components/parameters/PathPoint' }],
1187
+ responses: { '200': { description: 'ok' } },
1188
+ },
1189
+ },
1190
+ },
1191
+ components: {
1192
+ parameters: {
1193
+ PathPoint: {
1194
+ name: 'point',
1195
+ in: 'path',
1196
+ required: true,
1197
+ schema: { type: 'string', pattern: '^(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)$' },
1198
+ },
1199
+ },
1200
+ },
1201
+ } satisfies OpenAPIV3.Document;
1202
+
1203
+ it('correctly resolves $ref for parameters', () => {
1204
+ const { functionSignatures } = openapiToFunction(weatherSpec, true);
1205
+ const func = functionSignatures.find((sig) => sig.name === 'getPoint');
1206
+ expect(func).toBeDefined();
1207
+ expect(func?.parameters.properties).toHaveProperty('point');
1208
+ expect(func?.parameters.required).toContain('point');
1209
+
1210
+ const paramSchema = func?.parameters.properties['point'] as OpenAPIV3.SchemaObject;
1211
+ expect(paramSchema.type).toEqual('string');
1212
+ expect(paramSchema.pattern).toEqual('^(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)$');
1213
+ });
1214
+ });
551
1215
  });