librechat-data-provider 0.7.78 → 0.7.82

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librechat-data-provider",
3
- "version": "0.7.78",
3
+ "version": "0.7.82",
4
4
  "description": "data services for librechat apps",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.es.js",
@@ -40,6 +40,7 @@
40
40
  "homepage": "https://librechat.ai",
41
41
  "dependencies": {
42
42
  "axios": "^1.8.2",
43
+ "dayjs": "^1.11.13",
43
44
  "js-yaml": "^4.1.0",
44
45
  "zod": "^3.22.4"
45
46
  },
@@ -206,6 +206,244 @@ describe('ActionRequest', () => {
206
206
  },
207
207
  });
208
208
  });
209
+
210
+ it('handles GET requests with header and query parameters', async () => {
211
+ mockedAxios.get.mockResolvedValue({ data: { success: true } });
212
+
213
+ const data: Record<string, unknown> = {
214
+ 'api-version': '2025-01-01',
215
+ 'some-header': 'header-var',
216
+ };
217
+
218
+ const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
219
+ 'api-version': 'query',
220
+ 'some-header': 'header',
221
+ };
222
+
223
+ const actionRequest = new ActionRequest(
224
+ 'https://example.com',
225
+ '/get',
226
+ 'GET',
227
+ 'testGET',
228
+ false,
229
+ '',
230
+ loc,
231
+ );
232
+ const executer = actionRequest.setParams(data);
233
+ const response = await executer.execute();
234
+ expect(mockedAxios.get).toHaveBeenCalled();
235
+
236
+ const [url, config] = mockedAxios.get.mock.calls[0];
237
+ expect(url).toBe('https://example.com/get');
238
+ expect(config?.headers).toEqual({
239
+ 'some-header': 'header-var',
240
+ });
241
+ expect(config?.params).toEqual({
242
+ 'api-version': '2025-01-01',
243
+ });
244
+ expect(response.data.success).toBe(true);
245
+ });
246
+
247
+ it('handles GET requests with header and path parameters', async () => {
248
+ mockedAxios.get.mockResolvedValue({ data: { success: true } });
249
+
250
+ const data: Record<string, unknown> = {
251
+ 'user-id': '1',
252
+ 'some-header': 'header-var',
253
+ };
254
+
255
+ const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
256
+ 'user-id': 'path',
257
+ 'some-header': 'header',
258
+ };
259
+
260
+ const actionRequest = new ActionRequest(
261
+ 'https://example.com',
262
+ '/getwithpath/{user-id}',
263
+ 'GET',
264
+ 'testGETwithpath',
265
+ false,
266
+ '',
267
+ loc,
268
+ );
269
+ const executer = actionRequest.setParams(data);
270
+ const response = await executer.execute();
271
+ expect(mockedAxios.get).toHaveBeenCalled();
272
+
273
+ const [url, config] = mockedAxios.get.mock.calls[0];
274
+ expect(url).toBe('https://example.com/getwithpath/1');
275
+ expect(config?.headers).toEqual({
276
+ 'some-header': 'header-var',
277
+ });
278
+ expect(config?.params).toEqual({
279
+ });
280
+ expect(response.data.success).toBe(true);
281
+ });
282
+
283
+ it('handles POST requests with body, header and query parameters', async () => {
284
+ mockedAxios.post.mockResolvedValue({ data: { success: true } });
285
+
286
+ const data: Record<string, unknown> = {
287
+ 'api-version': '2025-01-01',
288
+ 'message': 'a body parameter',
289
+ 'some-header': 'header-var',
290
+ };
291
+
292
+ const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
293
+ 'api-version': 'query',
294
+ 'message': 'body',
295
+ 'some-header': 'header',
296
+ };
297
+
298
+ const actionRequest = new ActionRequest(
299
+ 'https://example.com',
300
+ '/post',
301
+ 'POST',
302
+ 'testPost',
303
+ false,
304
+ 'application/json',
305
+ loc,
306
+ );
307
+ const executer = actionRequest.setParams(data);
308
+ const response = await executer.execute();
309
+ expect(mockedAxios.post).toHaveBeenCalled();
310
+
311
+ const [url, body, config] = mockedAxios.post.mock.calls[0];
312
+ expect(url).toBe('https://example.com/post');
313
+ expect(body).toEqual({ message: 'a body parameter' });
314
+ expect(config?.headers).toEqual({
315
+ 'some-header': 'header-var',
316
+ 'Content-Type': 'application/json',
317
+ });
318
+ expect(config?.params).toEqual({
319
+ 'api-version': '2025-01-01',
320
+ });
321
+ expect(response.data.success).toBe(true);
322
+ });
323
+
324
+ it('handles PUT requests with body, header and query parameters', async () => {
325
+ mockedAxios.put.mockResolvedValue({ data: { success: true } });
326
+
327
+ const data: Record<string, unknown> = {
328
+ 'api-version': '2025-01-01',
329
+ 'message': 'a body parameter',
330
+ 'some-header': 'header-var',
331
+ };
332
+
333
+ const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
334
+ 'api-version': 'query',
335
+ 'message': 'body',
336
+ 'some-header': 'header',
337
+ };
338
+
339
+ const actionRequest = new ActionRequest(
340
+ 'https://example.com',
341
+ '/put',
342
+ 'PUT',
343
+ 'testPut',
344
+ false,
345
+ 'application/json',
346
+ loc,
347
+ );
348
+ const executer = actionRequest.setParams(data);
349
+ const response = await executer.execute();
350
+ expect(mockedAxios.put).toHaveBeenCalled();
351
+
352
+ const [url, body, config] = mockedAxios.put.mock.calls[0];
353
+ expect(url).toBe('https://example.com/put');
354
+ expect(body).toEqual({ message: 'a body parameter' });
355
+ expect(config?.headers).toEqual({
356
+ 'some-header': 'header-var',
357
+ 'Content-Type': 'application/json',
358
+ });
359
+ expect(config?.params).toEqual({
360
+ 'api-version': '2025-01-01',
361
+ });
362
+ expect(response.data.success).toBe(true);
363
+ });
364
+
365
+ it('handles PATCH requests with body, header and query parameters', async () => {
366
+ mockedAxios.patch.mockResolvedValue({ data: { success: true } });
367
+
368
+ const data: Record<string, unknown> = {
369
+ 'api-version': '2025-01-01',
370
+ 'message': 'a body parameter',
371
+ 'some-header': 'header-var',
372
+ };
373
+
374
+ const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
375
+ 'api-version': 'query',
376
+ 'message': 'body',
377
+ 'some-header': 'header',
378
+ };
379
+
380
+ const actionRequest = new ActionRequest(
381
+ 'https://example.com',
382
+ '/patch',
383
+ 'PATCH',
384
+ 'testPatch',
385
+ false,
386
+ 'application/json',
387
+ loc,
388
+ );
389
+ const executer = actionRequest.setParams(data);
390
+ const response = await executer.execute();
391
+ expect(mockedAxios.patch).toHaveBeenCalled();
392
+
393
+ const [url, body, config] = mockedAxios.patch.mock.calls[0];
394
+ expect(url).toBe('https://example.com/patch');
395
+ expect(body).toEqual({ message: 'a body parameter' });
396
+ expect(config?.headers).toEqual({
397
+ 'some-header': 'header-var',
398
+ 'Content-Type': 'application/json',
399
+ });
400
+ expect(config?.params).toEqual({
401
+ 'api-version': '2025-01-01',
402
+ });
403
+ expect(response.data.success).toBe(true);
404
+ });
405
+
406
+ it('handles DELETE requests with body, header and query parameters', async () => {
407
+ mockedAxios.delete.mockResolvedValue({ data: { success: true } });
408
+
409
+ const data: Record<string, unknown> = {
410
+ 'api-version': '2025-01-01',
411
+ 'message-id': '1',
412
+ 'some-header': 'header-var',
413
+ };
414
+
415
+ const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
416
+ 'api-version': 'query',
417
+ 'message-id': 'body',
418
+ 'some-header': 'header',
419
+ };
420
+
421
+ const actionRequest = new ActionRequest(
422
+ 'https://example.com',
423
+ '/delete',
424
+ 'DELETE',
425
+ 'testDelete',
426
+ false,
427
+ 'application/json',
428
+ loc,
429
+ );
430
+ const executer = actionRequest.setParams(data);
431
+ const response = await executer.execute();
432
+ expect(mockedAxios.delete).toHaveBeenCalled();
433
+
434
+ const [url, config] = mockedAxios.delete.mock.calls[0];
435
+ expect(url).toBe('https://example.com/delete');
436
+ expect(config?.data).toEqual({ 'message-id': '1' });
437
+ expect(config?.headers).toEqual({
438
+ 'some-header': 'header-var',
439
+ 'Content-Type': 'application/json',
440
+ });
441
+ expect(config?.params).toEqual({
442
+ 'api-version': '2025-01-01',
443
+ });
444
+ expect(response.data.success).toBe(true);
445
+ });
446
+
209
447
  });
210
448
 
211
449
  it('throws an error for unsupported HTTP method', async () => {
package/specs/mcp.spec.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { StdioOptionsSchema } from '../src/mcp';
1
+ import { StdioOptionsSchema, processMCPEnv, MCPOptions } from '../src/mcp';
2
2
 
3
3
  describe('Environment Variable Extraction (MCP)', () => {
4
4
  const originalEnv = process.env;
@@ -49,4 +49,129 @@ describe('Environment Variable Extraction (MCP)', () => {
49
49
  expect(result.env).toBeUndefined();
50
50
  });
51
51
  });
52
+
53
+ describe('processMCPEnv', () => {
54
+ it('should create a deep clone of the input object', () => {
55
+ const originalObj: MCPOptions = {
56
+ command: 'node',
57
+ args: ['server.js'],
58
+ env: {
59
+ API_KEY: '${TEST_API_KEY}',
60
+ PLAIN_VALUE: 'plain-value',
61
+ },
62
+ };
63
+
64
+ const result = processMCPEnv(originalObj);
65
+
66
+ // Verify it's not the same object reference
67
+ expect(result).not.toBe(originalObj);
68
+
69
+ // Modify the result and ensure original is unchanged
70
+ if ('env' in result && result.env) {
71
+ result.env.API_KEY = 'modified-value';
72
+ }
73
+
74
+ expect(originalObj.env?.API_KEY).toBe('${TEST_API_KEY}');
75
+ });
76
+
77
+ it('should process environment variables in env field', () => {
78
+ const obj: MCPOptions = {
79
+ command: 'node',
80
+ args: ['server.js'],
81
+ env: {
82
+ API_KEY: '${TEST_API_KEY}',
83
+ ANOTHER_KEY: '${ANOTHER_SECRET}',
84
+ PLAIN_VALUE: 'plain-value',
85
+ NON_EXISTENT: '${NON_EXISTENT_VAR}',
86
+ },
87
+ };
88
+
89
+ const result = processMCPEnv(obj);
90
+
91
+ expect('env' in result && result.env).toEqual({
92
+ API_KEY: 'test-api-key-value',
93
+ ANOTHER_KEY: 'another-secret-value',
94
+ PLAIN_VALUE: 'plain-value',
95
+ NON_EXISTENT: '${NON_EXISTENT_VAR}',
96
+ });
97
+ });
98
+
99
+ it('should process user ID in headers field', () => {
100
+ const userId = 'test-user-123';
101
+ const obj: MCPOptions = {
102
+ type: 'sse',
103
+ url: 'https://example.com',
104
+ headers: {
105
+ Authorization: '${TEST_API_KEY}',
106
+ 'User-Id': '{{LIBRECHAT_USER_ID}}',
107
+ 'Content-Type': 'application/json',
108
+ },
109
+ };
110
+
111
+ const result = processMCPEnv(obj, userId);
112
+
113
+ expect('headers' in result && result.headers).toEqual({
114
+ Authorization: 'test-api-key-value',
115
+ 'User-Id': 'test-user-123',
116
+ 'Content-Type': 'application/json',
117
+ });
118
+ });
119
+
120
+ it('should handle null or undefined input', () => {
121
+ // @ts-ignore - Testing null/undefined handling
122
+ expect(processMCPEnv(null)).toBeNull();
123
+ // @ts-ignore - Testing null/undefined handling
124
+ expect(processMCPEnv(undefined)).toBeUndefined();
125
+ });
126
+
127
+ it('should not modify objects without env or headers', () => {
128
+ const obj: MCPOptions = {
129
+ command: 'node',
130
+ args: ['server.js'],
131
+ timeout: 5000,
132
+ };
133
+
134
+ const result = processMCPEnv(obj);
135
+
136
+ expect(result).toEqual(obj);
137
+ expect(result).not.toBe(obj); // Still a different object (deep clone)
138
+ });
139
+
140
+ it('should ensure different users with same starting config get separate values', () => {
141
+ // Create a single base configuration
142
+ const baseConfig: MCPOptions = {
143
+ type: 'sse',
144
+ url: 'https://example.com',
145
+ headers: {
146
+ 'User-Id': '{{LIBRECHAT_USER_ID}}',
147
+ 'API-Key': '${TEST_API_KEY}',
148
+ },
149
+ };
150
+
151
+ // Process for two different users
152
+ const user1Id = 'user-123';
153
+ const user2Id = 'user-456';
154
+
155
+ const resultUser1 = processMCPEnv(baseConfig, user1Id);
156
+ const resultUser2 = processMCPEnv(baseConfig, user2Id);
157
+
158
+ // Verify each has the correct user ID
159
+ expect('headers' in resultUser1 && resultUser1.headers?.['User-Id']).toBe(user1Id);
160
+ expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe(user2Id);
161
+
162
+ // Verify they're different objects
163
+ expect(resultUser1).not.toBe(resultUser2);
164
+
165
+ // Modify one result and ensure it doesn't affect the other
166
+ if ('headers' in resultUser1 && resultUser1.headers) {
167
+ resultUser1.headers['User-Id'] = 'modified-user';
168
+ }
169
+
170
+ // Original config should be unchanged
171
+ expect(baseConfig.headers?.['User-Id']).toBe('{{LIBRECHAT_USER_ID}}');
172
+
173
+ // Second user's config should be unchanged
174
+ expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe(user2Id);
175
+ });
176
+ });
52
177
  });
@@ -0,0 +1,125 @@
1
+ import { replaceSpecialVars } from '../src/parsers';
2
+ import { specialVariables } from '../src/config';
3
+ import type { TUser } from '../src/types';
4
+
5
+ // Mock dayjs module with consistent date/time values regardless of environment
6
+ jest.mock('dayjs', () => {
7
+ // Create a mock implementation that returns fixed values
8
+ const mockDayjs = () => ({
9
+ format: (format: string) => {
10
+ if (format === 'YYYY-MM-DD') {
11
+ return '2024-04-29';
12
+ }
13
+ if (format === 'YYYY-MM-DD HH:mm:ss') {
14
+ return '2024-04-29 12:34:56';
15
+ }
16
+ return format; // fallback
17
+ },
18
+ day: () => 1, // 1 = Monday
19
+ toISOString: () => '2024-04-29T16:34:56.000Z',
20
+ });
21
+
22
+ // Add any static methods needed
23
+ mockDayjs.extend = jest.fn();
24
+
25
+ return mockDayjs;
26
+ });
27
+
28
+ describe('replaceSpecialVars', () => {
29
+ // Create a partial user object for testing
30
+ const mockUser = {
31
+ name: 'Test User',
32
+ id: 'user123',
33
+ } as TUser;
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ });
38
+
39
+ test('should return the original text if text is empty', () => {
40
+ expect(replaceSpecialVars({ text: '' })).toBe('');
41
+ expect(replaceSpecialVars({ text: null as unknown as string })).toBe(null);
42
+ expect(replaceSpecialVars({ text: undefined as unknown as string })).toBe(undefined);
43
+ });
44
+
45
+ test('should replace {{current_date}} with the current date', () => {
46
+ const result = replaceSpecialVars({ text: 'Today is {{current_date}}' });
47
+ // dayjs().day() returns 1 for Monday (April 29, 2024 is a Monday)
48
+ expect(result).toBe('Today is 2024-04-29 (1)');
49
+ });
50
+
51
+ test('should replace {{current_datetime}} with the current datetime', () => {
52
+ const result = replaceSpecialVars({ text: 'Now is {{current_datetime}}' });
53
+ expect(result).toBe('Now is 2024-04-29 12:34:56 (1)');
54
+ });
55
+
56
+ test('should replace {{iso_datetime}} with the ISO datetime', () => {
57
+ const result = replaceSpecialVars({ text: 'ISO time: {{iso_datetime}}' });
58
+ expect(result).toBe('ISO time: 2024-04-29T16:34:56.000Z');
59
+ });
60
+
61
+ test('should replace {{current_user}} with the user name if provided', () => {
62
+ const result = replaceSpecialVars({
63
+ text: 'Hello {{current_user}}!',
64
+ user: mockUser,
65
+ });
66
+ expect(result).toBe('Hello Test User!');
67
+ });
68
+
69
+ test('should not replace {{current_user}} if user is not provided', () => {
70
+ const result = replaceSpecialVars({
71
+ text: 'Hello {{current_user}}!',
72
+ });
73
+ expect(result).toBe('Hello {{current_user}}!');
74
+ });
75
+
76
+ test('should not replace {{current_user}} if user has no name', () => {
77
+ const result = replaceSpecialVars({
78
+ text: 'Hello {{current_user}}!',
79
+ user: { id: 'user123' } as TUser,
80
+ });
81
+ expect(result).toBe('Hello {{current_user}}!');
82
+ });
83
+
84
+ test('should handle multiple replacements in the same text', () => {
85
+ const result = replaceSpecialVars({
86
+ text: 'Hello {{current_user}}! Today is {{current_date}} and the time is {{current_datetime}}. ISO: {{iso_datetime}}',
87
+ user: mockUser,
88
+ });
89
+ expect(result).toBe(
90
+ 'Hello Test User! Today is 2024-04-29 (1) and the time is 2024-04-29 12:34:56 (1). ISO: 2024-04-29T16:34:56.000Z',
91
+ );
92
+ });
93
+
94
+ test('should be case-insensitive when replacing variables', () => {
95
+ const result = replaceSpecialVars({
96
+ text: 'Date: {{CURRENT_DATE}}, User: {{Current_User}}',
97
+ user: mockUser,
98
+ });
99
+ expect(result).toBe('Date: 2024-04-29 (1), User: Test User');
100
+ });
101
+
102
+ test('should confirm all specialVariables from config.ts get parsed', () => {
103
+ // Create a text that includes all special variables
104
+ const specialVarsText = Object.keys(specialVariables)
105
+ .map((key) => `{{${key}}}`)
106
+ .join(' ');
107
+
108
+ const result = replaceSpecialVars({
109
+ text: specialVarsText,
110
+ user: mockUser,
111
+ });
112
+
113
+ // Verify none of the original variable placeholders remain in the result
114
+ Object.keys(specialVariables).forEach((key) => {
115
+ const placeholder = `{{${key}}}`;
116
+ expect(result).not.toContain(placeholder);
117
+ });
118
+
119
+ // Verify the expected replacements
120
+ expect(result).toContain('2024-04-29 (1)'); // current_date
121
+ expect(result).toContain('2024-04-29 12:34:56 (1)'); // current_datetime
122
+ expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime
123
+ expect(result).toContain('Test User'); // current_user
124
+ });
125
+ });
package/src/actions.ts CHANGED
@@ -167,12 +167,13 @@ class RequestConfig {
167
167
  readonly operation: string,
168
168
  readonly isConsequential: boolean,
169
169
  readonly contentType: string,
170
+ readonly parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
170
171
  ) {}
171
172
  }
172
173
 
173
174
  class RequestExecutor {
174
175
  path: string;
175
- params?: object;
176
+ params?: Record<string, unknown>;
176
177
  private operationHash?: string;
177
178
  private authHeaders: Record<string, string> = {};
178
179
  private authToken?: string;
@@ -181,15 +182,28 @@ class RequestExecutor {
181
182
  this.path = config.basePath;
182
183
  }
183
184
 
184
- setParams(params: object) {
185
+ setParams(params: Record<string, unknown>) {
185
186
  this.operationHash = sha1(JSON.stringify(params));
186
- this.params = Object.assign({}, params);
187
-
188
- for (const [key, value] of Object.entries(params)) {
189
- const paramPattern = `{${key}}`;
190
- if (this.path.includes(paramPattern)) {
191
- this.path = this.path.replace(paramPattern, encodeURIComponent(value as string));
192
- delete (this.params as Record<string, unknown>)[key];
187
+ this.params = { ...params } as Record<string, unknown>;
188
+ if (this.config.parameterLocations) {
189
+ //Substituting “Path” Parameters:
190
+ for (const [key, value] of Object.entries(params)) {
191
+ if (this.config.parameterLocations[key] === 'path') {
192
+ const paramPattern = `{${key}}`;
193
+ if (this.path.includes(paramPattern)) {
194
+ this.path = this.path.replace(paramPattern, encodeURIComponent(String(value)));
195
+ delete this.params[key];
196
+ }
197
+ }
198
+ }
199
+ } else {
200
+ // Fallback: if no locations are defined, perform path substitution for all keys.
201
+ for (const [key, value] of Object.entries(params)) {
202
+ const paramPattern = `{${key}}`;
203
+ if (this.path.includes(paramPattern)) {
204
+ this.path = this.path.replace(paramPattern, encodeURIComponent(String(value)));
205
+ delete this.params[key];
206
+ }
193
207
  }
194
208
  }
195
209
  return this;
@@ -275,23 +289,46 @@ class RequestExecutor {
275
289
 
276
290
  async execute() {
277
291
  const url = createURL(this.config.domain, this.path);
278
- const headers = {
292
+ const headers: Record<string, string> = {
279
293
  ...this.authHeaders,
280
- 'Content-Type': this.config.contentType,
294
+ ...(this.config.contentType ? { 'Content-Type': this.config.contentType } : {}),
281
295
  };
282
-
283
296
  const method = this.config.method.toLowerCase();
284
297
  const axios = _axios.create();
298
+
299
+ // Initialize separate containers for query and body parameters.
300
+ const queryParams: Record<string, unknown> = {};
301
+ const bodyParams: Record<string, unknown> = {};
302
+
303
+ if (this.config.parameterLocations && this.params) {
304
+ for (const key of Object.keys(this.params)) {
305
+ // Determine parameter placement; default to "query" for GET and "body" for others.
306
+ const loc: 'query' | 'path' | 'header' | 'body' = this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
307
+
308
+ const val = this.params[key];
309
+ if (loc === 'query') {
310
+ queryParams[key] = val;
311
+ } else if (loc === 'header') {
312
+ headers[key] = String(val);
313
+ } else if (loc === 'body') {
314
+ bodyParams[key] = val;
315
+ }
316
+ }
317
+ } else if (this.params) {
318
+ Object.assign(queryParams, this.params);
319
+ Object.assign(bodyParams, this.params);
320
+ }
321
+
285
322
  if (method === 'get') {
286
- return axios.get(url, { headers, params: this.params });
323
+ return axios.get(url, { headers, params: queryParams });
287
324
  } else if (method === 'post') {
288
- return axios.post(url, this.params, { headers });
325
+ return axios.post(url, bodyParams, { headers, params: queryParams });
289
326
  } else if (method === 'put') {
290
- return axios.put(url, this.params, { headers });
327
+ return axios.put(url, bodyParams, { headers, params: queryParams });
291
328
  } else if (method === 'delete') {
292
- return axios.delete(url, { headers, data: this.params });
329
+ return axios.delete(url, { headers, data: bodyParams, params: queryParams });
293
330
  } else if (method === 'patch') {
294
- return axios.patch(url, this.params, { headers });
331
+ return axios.patch(url, bodyParams, { headers, params: queryParams });
295
332
  } else {
296
333
  throw new Error(`Unsupported HTTP method: ${method}`);
297
334
  }
@@ -312,8 +349,9 @@ export class ActionRequest {
312
349
  operation: string,
313
350
  isConsequential: boolean,
314
351
  contentType: string,
352
+ parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
315
353
  ) {
316
- this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType);
354
+ this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType, parameterLocations);
317
355
  }
318
356
 
319
357
  // Add getters to maintain backward compatibility
@@ -341,7 +379,7 @@ export class ActionRequest {
341
379
  }
342
380
 
343
381
  // Maintain backward compatibility by delegating to a new executor
344
- setParams(params: object) {
382
+ setParams(params: Record<string, unknown>) {
345
383
  const executor = this.createExecutor();
346
384
  executor.setParams(params);
347
385
  return executor;
@@ -406,6 +444,7 @@ export function openapiToFunction(
406
444
  // Iterate over each path and method in the OpenAPI spec
407
445
  for (const [path, methods] of Object.entries(openapiSpec.paths)) {
408
446
  for (const [method, operation] of Object.entries(methods as OpenAPIV3.PathsObject)) {
447
+ const paramLocations: Record<string, 'query' | 'path' | 'header' | 'body'> = {};
409
448
  const operationObj = operation as OpenAPIV3.OperationObject & {
410
449
  'x-openai-isConsequential'?: boolean;
411
450
  } & {
@@ -445,6 +484,14 @@ export function openapiToFunction(
445
484
  if (resolvedParam.required) {
446
485
  parametersSchema.required.push(paramName);
447
486
  }
487
+ // Record the parameter location from the OpenAPI "in" field.
488
+ paramLocations[paramName] =
489
+ (resolvedParam.in === 'query' ||
490
+ resolvedParam.in === 'path' ||
491
+ resolvedParam.in === 'header' ||
492
+ resolvedParam.in === 'body')
493
+ ? resolvedParam.in
494
+ : 'query';
448
495
  }
449
496
  }
450
497
 
@@ -464,6 +511,12 @@ export function openapiToFunction(
464
511
  if (resolvedSchema.required) {
465
512
  parametersSchema.required.push(...resolvedSchema.required);
466
513
  }
514
+ // Mark requestBody properties as belonging to the "body"
515
+ if (resolvedSchema.properties) {
516
+ for (const key in resolvedSchema.properties) {
517
+ paramLocations[key] = 'body';
518
+ }
519
+ }
467
520
  }
468
521
 
469
522
  const functionSignature = new FunctionSignature(
@@ -481,6 +534,7 @@ export function openapiToFunction(
481
534
  operationId,
482
535
  !!(operationObj['x-openai-isConsequential'] ?? false),
483
536
  operationObj.requestBody ? 'application/json' : '',
537
+ paramLocations,
484
538
  );
485
539
 
486
540
  requestBuilders[operationId] = actionRequest;