@ai-sdk/openai 0.0.0-85f9a635-20240518005312 → 0.0.0-98261322-20260122142521

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 (123) hide show
  1. package/CHANGELOG.md +2910 -0
  2. package/README.md +11 -173
  3. package/dist/index.d.mts +870 -187
  4. package/dist/index.d.ts +870 -187
  5. package/dist/index.js +5467 -617
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +5555 -619
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/internal/index.d.mts +1067 -0
  10. package/dist/internal/index.d.ts +1067 -0
  11. package/dist/internal/index.js +5742 -0
  12. package/dist/internal/index.js.map +1 -0
  13. package/dist/internal/index.mjs +5788 -0
  14. package/dist/internal/index.mjs.map +1 -0
  15. package/docs/03-openai.mdx +2018 -0
  16. package/internal.d.ts +1 -0
  17. package/package.json +30 -18
  18. package/src/chat/__fixtures__/azure-model-router.1.chunks.txt +8 -0
  19. package/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap +88 -0
  20. package/src/chat/convert-openai-chat-usage.ts +57 -0
  21. package/src/chat/convert-to-openai-chat-messages.test.ts +516 -0
  22. package/src/chat/convert-to-openai-chat-messages.ts +225 -0
  23. package/src/chat/get-response-metadata.ts +15 -0
  24. package/src/chat/map-openai-finish-reason.ts +19 -0
  25. package/src/chat/openai-chat-api.ts +198 -0
  26. package/src/chat/openai-chat-language-model.test.ts +3496 -0
  27. package/src/chat/openai-chat-language-model.ts +700 -0
  28. package/src/chat/openai-chat-options.ts +186 -0
  29. package/src/chat/openai-chat-prepare-tools.test.ts +322 -0
  30. package/src/chat/openai-chat-prepare-tools.ts +84 -0
  31. package/src/chat/openai-chat-prompt.ts +70 -0
  32. package/src/completion/convert-openai-completion-usage.ts +46 -0
  33. package/src/completion/convert-to-openai-completion-prompt.ts +93 -0
  34. package/src/completion/get-response-metadata.ts +15 -0
  35. package/src/completion/map-openai-finish-reason.ts +19 -0
  36. package/src/completion/openai-completion-api.ts +81 -0
  37. package/src/completion/openai-completion-language-model.test.ts +752 -0
  38. package/src/completion/openai-completion-language-model.ts +336 -0
  39. package/src/completion/openai-completion-options.ts +58 -0
  40. package/src/embedding/__snapshots__/openai-embedding-model.test.ts.snap +43 -0
  41. package/src/embedding/openai-embedding-api.ts +13 -0
  42. package/src/embedding/openai-embedding-model.test.ts +146 -0
  43. package/src/embedding/openai-embedding-model.ts +95 -0
  44. package/src/embedding/openai-embedding-options.ts +30 -0
  45. package/src/image/openai-image-api.ts +35 -0
  46. package/src/image/openai-image-model.test.ts +722 -0
  47. package/src/image/openai-image-model.ts +305 -0
  48. package/src/image/openai-image-options.ts +28 -0
  49. package/src/index.ts +9 -0
  50. package/src/internal/index.ts +19 -0
  51. package/src/openai-config.ts +18 -0
  52. package/src/openai-error.test.ts +34 -0
  53. package/src/openai-error.ts +22 -0
  54. package/src/openai-language-model-capabilities.test.ts +93 -0
  55. package/src/openai-language-model-capabilities.ts +54 -0
  56. package/src/openai-provider.test.ts +98 -0
  57. package/src/openai-provider.ts +270 -0
  58. package/src/openai-tools.ts +114 -0
  59. package/src/responses/__fixtures__/openai-apply-patch-tool-delete.1.chunks.txt +5 -0
  60. package/src/responses/__fixtures__/openai-apply-patch-tool.1.chunks.txt +38 -0
  61. package/src/responses/__fixtures__/openai-apply-patch-tool.1.json +69 -0
  62. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.chunks.txt +393 -0
  63. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.json +137 -0
  64. package/src/responses/__fixtures__/openai-error.1.chunks.txt +4 -0
  65. package/src/responses/__fixtures__/openai-error.1.json +8 -0
  66. package/src/responses/__fixtures__/openai-file-search-tool.1.chunks.txt +94 -0
  67. package/src/responses/__fixtures__/openai-file-search-tool.1.json +89 -0
  68. package/src/responses/__fixtures__/openai-file-search-tool.2.chunks.txt +93 -0
  69. package/src/responses/__fixtures__/openai-file-search-tool.2.json +112 -0
  70. package/src/responses/__fixtures__/openai-image-generation-tool.1.chunks.txt +16 -0
  71. package/src/responses/__fixtures__/openai-image-generation-tool.1.json +96 -0
  72. package/src/responses/__fixtures__/openai-local-shell-tool.1.chunks.txt +7 -0
  73. package/src/responses/__fixtures__/openai-local-shell-tool.1.json +70 -0
  74. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.chunks.txt +11 -0
  75. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.json +169 -0
  76. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.chunks.txt +123 -0
  77. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.json +176 -0
  78. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.chunks.txt +11 -0
  79. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.json +169 -0
  80. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.chunks.txt +84 -0
  81. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.json +182 -0
  82. package/src/responses/__fixtures__/openai-mcp-tool.1.chunks.txt +373 -0
  83. package/src/responses/__fixtures__/openai-mcp-tool.1.json +159 -0
  84. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.chunks.txt +110 -0
  85. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.json +117 -0
  86. package/src/responses/__fixtures__/openai-shell-tool.1.chunks.txt +182 -0
  87. package/src/responses/__fixtures__/openai-shell-tool.1.json +73 -0
  88. package/src/responses/__fixtures__/openai-web-search-tool.1.chunks.txt +185 -0
  89. package/src/responses/__fixtures__/openai-web-search-tool.1.json +266 -0
  90. package/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap +10955 -0
  91. package/src/responses/convert-openai-responses-usage.ts +53 -0
  92. package/src/responses/convert-to-openai-responses-input.test.ts +2976 -0
  93. package/src/responses/convert-to-openai-responses-input.ts +578 -0
  94. package/src/responses/map-openai-responses-finish-reason.ts +22 -0
  95. package/src/responses/openai-responses-api.test.ts +89 -0
  96. package/src/responses/openai-responses-api.ts +1086 -0
  97. package/src/responses/openai-responses-language-model.test.ts +6927 -0
  98. package/src/responses/openai-responses-language-model.ts +1932 -0
  99. package/src/responses/openai-responses-options.ts +312 -0
  100. package/src/responses/openai-responses-prepare-tools.test.ts +924 -0
  101. package/src/responses/openai-responses-prepare-tools.ts +264 -0
  102. package/src/responses/openai-responses-provider-metadata.ts +39 -0
  103. package/src/speech/openai-speech-api.ts +38 -0
  104. package/src/speech/openai-speech-model.test.ts +202 -0
  105. package/src/speech/openai-speech-model.ts +137 -0
  106. package/src/speech/openai-speech-options.ts +22 -0
  107. package/src/tool/apply-patch.ts +141 -0
  108. package/src/tool/code-interpreter.ts +104 -0
  109. package/src/tool/file-search.ts +145 -0
  110. package/src/tool/image-generation.ts +126 -0
  111. package/src/tool/local-shell.test-d.ts +20 -0
  112. package/src/tool/local-shell.ts +72 -0
  113. package/src/tool/mcp.ts +125 -0
  114. package/src/tool/shell.ts +85 -0
  115. package/src/tool/web-search-preview.ts +139 -0
  116. package/src/tool/web-search.test-d.ts +13 -0
  117. package/src/tool/web-search.ts +179 -0
  118. package/src/transcription/openai-transcription-api.ts +37 -0
  119. package/src/transcription/openai-transcription-model.test.ts +507 -0
  120. package/src/transcription/openai-transcription-model.ts +232 -0
  121. package/src/transcription/openai-transcription-options.ts +50 -0
  122. package/src/transcription/transcription-test.mp3 +0 -0
  123. package/src/version.ts +6 -0
@@ -0,0 +1,2976 @@
1
+ import { ToolNameMapping } from '../../../provider-utils/src/create-tool-name-mapping';
2
+ import { convertToOpenAIResponsesInput } from './convert-to-openai-responses-input';
3
+ import { describe, it, expect } from 'vitest';
4
+
5
+ const testToolNameMapping: ToolNameMapping = {
6
+ toProviderToolName: (customToolName: string) => customToolName,
7
+ toCustomToolName: (providerToolName: string) => providerToolName,
8
+ };
9
+
10
+ describe('convertToOpenAIResponsesInput', () => {
11
+ describe('system messages', () => {
12
+ it('should convert system messages to system role', async () => {
13
+ const result = await convertToOpenAIResponsesInput({
14
+ prompt: [{ role: 'system', content: 'Hello' }],
15
+ toolNameMapping: testToolNameMapping,
16
+ systemMessageMode: 'system',
17
+ providerOptionsName: 'openai',
18
+ store: true,
19
+ });
20
+
21
+ expect(result.input).toEqual([{ role: 'system', content: 'Hello' }]);
22
+ });
23
+
24
+ it('should convert system messages to developer role', async () => {
25
+ const result = await convertToOpenAIResponsesInput({
26
+ prompt: [{ role: 'system', content: 'Hello' }],
27
+ toolNameMapping: testToolNameMapping,
28
+ systemMessageMode: 'developer',
29
+ providerOptionsName: 'openai',
30
+ store: true,
31
+ });
32
+
33
+ expect(result.input).toEqual([{ role: 'developer', content: 'Hello' }]);
34
+ });
35
+
36
+ it('should remove system messages', async () => {
37
+ const result = await convertToOpenAIResponsesInput({
38
+ prompt: [{ role: 'system', content: 'Hello' }],
39
+ toolNameMapping: testToolNameMapping,
40
+ systemMessageMode: 'remove',
41
+ providerOptionsName: 'openai',
42
+ store: true,
43
+ });
44
+
45
+ expect(result.input).toEqual([]);
46
+ });
47
+ });
48
+
49
+ describe('user messages', () => {
50
+ it('should convert messages with only a text part to a string content', async () => {
51
+ const result = await convertToOpenAIResponsesInput({
52
+ prompt: [
53
+ {
54
+ role: 'user',
55
+ content: [{ type: 'text', text: 'Hello' }],
56
+ },
57
+ ],
58
+ toolNameMapping: testToolNameMapping,
59
+ systemMessageMode: 'system',
60
+ providerOptionsName: 'openai',
61
+ store: true,
62
+ });
63
+
64
+ expect(result.input).toEqual([
65
+ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] },
66
+ ]);
67
+ });
68
+
69
+ it('should convert messages with image parts using URL', async () => {
70
+ const result = await convertToOpenAIResponsesInput({
71
+ prompt: [
72
+ {
73
+ role: 'user',
74
+ content: [
75
+ { type: 'text', text: 'Hello' },
76
+ {
77
+ type: 'file',
78
+ mediaType: 'image/*',
79
+ data: new URL('https://example.com/image.jpg'),
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ toolNameMapping: testToolNameMapping,
85
+ systemMessageMode: 'system',
86
+ providerOptionsName: 'openai',
87
+ store: true,
88
+ });
89
+
90
+ expect(result.input).toEqual([
91
+ {
92
+ role: 'user',
93
+ content: [
94
+ { type: 'input_text', text: 'Hello' },
95
+ {
96
+ type: 'input_image',
97
+ image_url: 'https://example.com/image.jpg',
98
+ },
99
+ ],
100
+ },
101
+ ]);
102
+ });
103
+
104
+ it('should convert messages with image parts using binary data', async () => {
105
+ const result = await convertToOpenAIResponsesInput({
106
+ prompt: [
107
+ {
108
+ role: 'user',
109
+ content: [
110
+ {
111
+ type: 'file',
112
+ mediaType: 'image/png',
113
+ data: Buffer.from([0, 1, 2, 3]).toString('base64'),
114
+ },
115
+ ],
116
+ },
117
+ ],
118
+ toolNameMapping: testToolNameMapping,
119
+ systemMessageMode: 'system',
120
+ providerOptionsName: 'openai',
121
+ store: true,
122
+ });
123
+
124
+ expect(result.input).toEqual([
125
+ {
126
+ role: 'user',
127
+ content: [
128
+ {
129
+ type: 'input_image',
130
+ image_url: 'data:image/png;base64,AAECAw==',
131
+ },
132
+ ],
133
+ },
134
+ ]);
135
+ });
136
+
137
+ it('should convert messages with image parts using Uint8Array', async () => {
138
+ const result = await convertToOpenAIResponsesInput({
139
+ prompt: [
140
+ {
141
+ role: 'user',
142
+ content: [
143
+ {
144
+ type: 'file',
145
+ mediaType: 'image/png',
146
+ data: new Uint8Array([0, 1, 2, 3]),
147
+ },
148
+ ],
149
+ },
150
+ ],
151
+ toolNameMapping: testToolNameMapping,
152
+ systemMessageMode: 'system',
153
+ providerOptionsName: 'openai',
154
+ store: true,
155
+ });
156
+
157
+ expect(result.input).toEqual([
158
+ {
159
+ role: 'user',
160
+ content: [
161
+ {
162
+ type: 'input_image',
163
+ image_url: 'data:image/png;base64,AAECAw==',
164
+ },
165
+ ],
166
+ },
167
+ ]);
168
+ });
169
+
170
+ it('should convert messages with image parts using file_id', async () => {
171
+ const result = await convertToOpenAIResponsesInput({
172
+ prompt: [
173
+ {
174
+ role: 'user',
175
+ content: [
176
+ {
177
+ type: 'file',
178
+ mediaType: 'image/png',
179
+ data: 'file-12345',
180
+ },
181
+ ],
182
+ },
183
+ ],
184
+ toolNameMapping: testToolNameMapping,
185
+ systemMessageMode: 'system',
186
+ providerOptionsName: 'openai',
187
+ fileIdPrefixes: ['file-'],
188
+ store: true,
189
+ });
190
+
191
+ expect(result.input).toEqual([
192
+ {
193
+ role: 'user',
194
+ content: [
195
+ {
196
+ type: 'input_image',
197
+ file_id: 'file-12345',
198
+ },
199
+ ],
200
+ },
201
+ ]);
202
+ });
203
+
204
+ it('should use default mime type for binary images', async () => {
205
+ const result = await convertToOpenAIResponsesInput({
206
+ prompt: [
207
+ {
208
+ role: 'user',
209
+ content: [
210
+ {
211
+ type: 'file',
212
+ mediaType: 'image/*',
213
+ data: Buffer.from([0, 1, 2, 3]).toString('base64'),
214
+ },
215
+ ],
216
+ },
217
+ ],
218
+ toolNameMapping: testToolNameMapping,
219
+ systemMessageMode: 'system',
220
+ providerOptionsName: 'openai',
221
+ store: true,
222
+ });
223
+
224
+ expect(result.input).toEqual([
225
+ {
226
+ role: 'user',
227
+ content: [
228
+ {
229
+ type: 'input_image',
230
+ image_url: 'data:image/jpeg;base64,AAECAw==',
231
+ },
232
+ ],
233
+ },
234
+ ]);
235
+ });
236
+
237
+ it('should add image detail when specified through extension', async () => {
238
+ const result = await convertToOpenAIResponsesInput({
239
+ prompt: [
240
+ {
241
+ role: 'user',
242
+ content: [
243
+ {
244
+ type: 'file',
245
+ mediaType: 'image/png',
246
+ data: Buffer.from([0, 1, 2, 3]).toString('base64'),
247
+ providerOptions: {
248
+ openai: {
249
+ imageDetail: 'low',
250
+ },
251
+ },
252
+ },
253
+ ],
254
+ },
255
+ ],
256
+ toolNameMapping: testToolNameMapping,
257
+ systemMessageMode: 'system',
258
+ providerOptionsName: 'openai',
259
+ store: true,
260
+ });
261
+
262
+ expect(result.input).toEqual([
263
+ {
264
+ role: 'user',
265
+ content: [
266
+ {
267
+ type: 'input_image',
268
+ image_url: 'data:image/png;base64,AAECAw==',
269
+ detail: 'low',
270
+ },
271
+ ],
272
+ },
273
+ ]);
274
+ });
275
+
276
+ it('should read image detail from providerOptions when providerOptionsName is azure', async () => {
277
+ const result = await convertToOpenAIResponsesInput({
278
+ prompt: [
279
+ {
280
+ role: 'user',
281
+ content: [
282
+ {
283
+ type: 'file',
284
+ mediaType: 'image/png',
285
+ data: Buffer.from([0, 1, 2, 3]).toString('base64'),
286
+ providerOptions: {
287
+ azure: {
288
+ imageDetail: 'low',
289
+ },
290
+ },
291
+ },
292
+ ],
293
+ },
294
+ ],
295
+ toolNameMapping: testToolNameMapping,
296
+ systemMessageMode: 'system',
297
+ providerOptionsName: 'azure',
298
+ store: true,
299
+ });
300
+
301
+ expect(result.input).toEqual([
302
+ {
303
+ role: 'user',
304
+ content: [
305
+ {
306
+ type: 'input_image',
307
+ image_url: 'data:image/png;base64,AAECAw==',
308
+ detail: 'low',
309
+ },
310
+ ],
311
+ },
312
+ ]);
313
+ });
314
+
315
+ it('should convert messages with PDF file parts', async () => {
316
+ const base64Data = 'AQIDBAU='; // Base64 encoding of pdfData
317
+
318
+ const result = await convertToOpenAIResponsesInput({
319
+ prompt: [
320
+ {
321
+ role: 'user',
322
+ content: [
323
+ {
324
+ type: 'file',
325
+ mediaType: 'application/pdf',
326
+ data: base64Data,
327
+ filename: 'document.pdf',
328
+ },
329
+ ],
330
+ },
331
+ ],
332
+ toolNameMapping: testToolNameMapping,
333
+ systemMessageMode: 'system',
334
+ providerOptionsName: 'openai',
335
+ store: true,
336
+ });
337
+
338
+ expect(result.input).toEqual([
339
+ {
340
+ role: 'user',
341
+ content: [
342
+ {
343
+ type: 'input_file',
344
+ filename: 'document.pdf',
345
+ file_data: 'data:application/pdf;base64,AQIDBAU=',
346
+ },
347
+ ],
348
+ },
349
+ ]);
350
+ });
351
+
352
+ it('should convert messages with PDF file parts using file_id', async () => {
353
+ const result = await convertToOpenAIResponsesInput({
354
+ prompt: [
355
+ {
356
+ role: 'user',
357
+ content: [
358
+ {
359
+ type: 'file',
360
+ mediaType: 'application/pdf',
361
+ data: 'file-pdf-12345',
362
+ },
363
+ ],
364
+ },
365
+ ],
366
+ toolNameMapping: testToolNameMapping,
367
+ systemMessageMode: 'system',
368
+ providerOptionsName: 'openai',
369
+ fileIdPrefixes: ['file-'],
370
+ store: true,
371
+ });
372
+
373
+ expect(result.input).toEqual([
374
+ {
375
+ role: 'user',
376
+ content: [
377
+ {
378
+ type: 'input_file',
379
+ file_id: 'file-pdf-12345',
380
+ },
381
+ ],
382
+ },
383
+ ]);
384
+ });
385
+
386
+ it('should use default filename for PDF file parts when not provided', async () => {
387
+ const base64Data = 'AQIDBAU=';
388
+
389
+ const result = await convertToOpenAIResponsesInput({
390
+ prompt: [
391
+ {
392
+ role: 'user',
393
+ content: [
394
+ {
395
+ type: 'file',
396
+ mediaType: 'application/pdf',
397
+ data: base64Data,
398
+ },
399
+ ],
400
+ },
401
+ ],
402
+ toolNameMapping: testToolNameMapping,
403
+ systemMessageMode: 'system',
404
+ providerOptionsName: 'openai',
405
+ store: true,
406
+ });
407
+
408
+ expect(result.input).toEqual([
409
+ {
410
+ role: 'user',
411
+ content: [
412
+ {
413
+ type: 'input_file',
414
+ filename: 'part-0.pdf',
415
+ file_data: 'data:application/pdf;base64,AQIDBAU=',
416
+ },
417
+ ],
418
+ },
419
+ ]);
420
+ });
421
+
422
+ it('should throw error for unsupported file types', async () => {
423
+ const base64Data = 'AQIDBAU=';
424
+
425
+ await expect(
426
+ convertToOpenAIResponsesInput({
427
+ prompt: [
428
+ {
429
+ role: 'user',
430
+ content: [
431
+ {
432
+ type: 'file',
433
+ mediaType: 'text/plain',
434
+ data: base64Data,
435
+ },
436
+ ],
437
+ },
438
+ ],
439
+ toolNameMapping: testToolNameMapping,
440
+ systemMessageMode: 'system',
441
+ providerOptionsName: 'openai',
442
+ store: true,
443
+ }),
444
+ ).rejects.toThrow('file part media type text/plain');
445
+ });
446
+
447
+ it('should convert PDF file parts with URL to input_file with file_url', async () => {
448
+ const result = await convertToOpenAIResponsesInput({
449
+ prompt: [
450
+ {
451
+ role: 'user',
452
+ content: [
453
+ {
454
+ type: 'file',
455
+ mediaType: 'application/pdf',
456
+ data: new URL('https://example.com/document.pdf'),
457
+ },
458
+ ],
459
+ },
460
+ ],
461
+ toolNameMapping: testToolNameMapping,
462
+ systemMessageMode: 'system',
463
+ providerOptionsName: 'openai',
464
+ store: true,
465
+ });
466
+
467
+ expect(result.input).toEqual([
468
+ {
469
+ role: 'user',
470
+ content: [
471
+ {
472
+ type: 'input_file',
473
+ file_url: 'https://example.com/document.pdf',
474
+ },
475
+ ],
476
+ },
477
+ ]);
478
+ });
479
+
480
+ describe('Azure OpenAI file ID support', () => {
481
+ it('should convert image parts with assistant- prefix', async () => {
482
+ const result = await convertToOpenAIResponsesInput({
483
+ toolNameMapping: testToolNameMapping,
484
+ prompt: [
485
+ {
486
+ role: 'user',
487
+ content: [
488
+ {
489
+ type: 'file',
490
+ mediaType: 'image/png',
491
+ data: 'assistant-img-abc123',
492
+ },
493
+ ],
494
+ },
495
+ ],
496
+ systemMessageMode: 'system',
497
+ providerOptionsName: 'openai',
498
+ fileIdPrefixes: ['assistant-'],
499
+ store: true,
500
+ });
501
+
502
+ expect(result.input).toEqual([
503
+ {
504
+ role: 'user',
505
+ content: [
506
+ {
507
+ type: 'input_image',
508
+ file_id: 'assistant-img-abc123',
509
+ },
510
+ ],
511
+ },
512
+ ]);
513
+ });
514
+
515
+ it('should convert PDF parts with assistant- prefix', async () => {
516
+ const result = await convertToOpenAIResponsesInput({
517
+ prompt: [
518
+ {
519
+ role: 'user',
520
+ content: [
521
+ {
522
+ type: 'file',
523
+ mediaType: 'application/pdf',
524
+ data: 'assistant-pdf-abc123',
525
+ },
526
+ ],
527
+ },
528
+ ],
529
+ toolNameMapping: testToolNameMapping,
530
+ systemMessageMode: 'system',
531
+ providerOptionsName: 'openai',
532
+ fileIdPrefixes: ['assistant-'],
533
+ store: true,
534
+ });
535
+
536
+ expect(result.input).toEqual([
537
+ {
538
+ role: 'user',
539
+ content: [
540
+ {
541
+ type: 'input_file',
542
+ file_id: 'assistant-pdf-abc123',
543
+ },
544
+ ],
545
+ },
546
+ ]);
547
+ });
548
+
549
+ it('should support multiple file ID prefixes', async () => {
550
+ const result = await convertToOpenAIResponsesInput({
551
+ prompt: [
552
+ {
553
+ role: 'user',
554
+ content: [
555
+ {
556
+ type: 'file',
557
+ mediaType: 'image/png',
558
+ data: 'assistant-img-abc123',
559
+ },
560
+ {
561
+ type: 'file',
562
+ mediaType: 'application/pdf',
563
+ data: 'file-pdf-xyz789',
564
+ },
565
+ ],
566
+ },
567
+ ],
568
+ toolNameMapping: testToolNameMapping,
569
+ systemMessageMode: 'system',
570
+ providerOptionsName: 'openai',
571
+ fileIdPrefixes: ['assistant-', 'file-'],
572
+ store: true,
573
+ });
574
+
575
+ expect(result.input).toEqual([
576
+ {
577
+ role: 'user',
578
+ content: [
579
+ {
580
+ type: 'input_image',
581
+ file_id: 'assistant-img-abc123',
582
+ },
583
+ {
584
+ type: 'input_file',
585
+ file_id: 'file-pdf-xyz789',
586
+ },
587
+ ],
588
+ },
589
+ ]);
590
+ });
591
+ });
592
+
593
+ describe('fileIdPrefixes undefined behavior', () => {
594
+ it('should treat all file data as base64 when fileIdPrefixes is undefined', async () => {
595
+ const result = await convertToOpenAIResponsesInput({
596
+ prompt: [
597
+ {
598
+ role: 'user',
599
+ content: [
600
+ {
601
+ type: 'file',
602
+ mediaType: 'image/png',
603
+ data: 'file-12345', // Looks like file ID but should be treated as base64
604
+ },
605
+ {
606
+ type: 'file',
607
+ mediaType: 'application/pdf',
608
+ data: 'assistant-abc123', // Looks like file ID but should be treated as base64
609
+ filename: 'test.pdf',
610
+ },
611
+ ],
612
+ },
613
+ ],
614
+ toolNameMapping: testToolNameMapping,
615
+ systemMessageMode: 'system',
616
+ providerOptionsName: 'openai',
617
+ // fileIdPrefixes intentionally omitted
618
+ store: true,
619
+ });
620
+
621
+ expect(result.input).toEqual([
622
+ {
623
+ role: 'user',
624
+ content: [
625
+ {
626
+ type: 'input_image',
627
+ image_url: 'data:image/png;base64,file-12345',
628
+ },
629
+ {
630
+ type: 'input_file',
631
+ filename: 'test.pdf',
632
+ file_data: 'data:application/pdf;base64,assistant-abc123',
633
+ },
634
+ ],
635
+ },
636
+ ]);
637
+ });
638
+
639
+ it('should handle empty fileIdPrefixes array', async () => {
640
+ const result = await convertToOpenAIResponsesInput({
641
+ prompt: [
642
+ {
643
+ role: 'user',
644
+ content: [
645
+ {
646
+ type: 'file',
647
+ mediaType: 'image/png',
648
+ data: 'file-12345',
649
+ },
650
+ ],
651
+ },
652
+ ],
653
+ toolNameMapping: testToolNameMapping,
654
+ systemMessageMode: 'system',
655
+ providerOptionsName: 'openai',
656
+ fileIdPrefixes: [], // Empty array should disable file ID detection
657
+ store: true,
658
+ });
659
+
660
+ expect(result.input).toEqual([
661
+ {
662
+ role: 'user',
663
+ content: [
664
+ {
665
+ type: 'input_image',
666
+ image_url: 'data:image/png;base64,file-12345',
667
+ },
668
+ ],
669
+ },
670
+ ]);
671
+ });
672
+ });
673
+ });
674
+
675
+ describe('assistant messages', () => {
676
+ it('should convert messages with only a text part to a string content', async () => {
677
+ const result = await convertToOpenAIResponsesInput({
678
+ prompt: [
679
+ { role: 'assistant', content: [{ type: 'text', text: 'Hello' }] },
680
+ ],
681
+ toolNameMapping: testToolNameMapping,
682
+ systemMessageMode: 'system',
683
+ providerOptionsName: 'openai',
684
+ store: true,
685
+ });
686
+
687
+ expect(result.input).toEqual([
688
+ {
689
+ role: 'assistant',
690
+ content: [{ type: 'output_text', text: 'Hello' }],
691
+ },
692
+ ]);
693
+ });
694
+
695
+ it('should convert messages with tool call parts', async () => {
696
+ const result = await convertToOpenAIResponsesInput({
697
+ toolNameMapping: testToolNameMapping,
698
+ prompt: [
699
+ {
700
+ role: 'assistant',
701
+ content: [
702
+ { type: 'text', text: 'I will search for that information.' },
703
+ {
704
+ type: 'tool-call',
705
+ toolCallId: 'call_123',
706
+ toolName: 'search',
707
+ input: { query: 'weather in San Francisco' },
708
+ },
709
+ ],
710
+ },
711
+ ],
712
+ systemMessageMode: 'system',
713
+ providerOptionsName: 'openai',
714
+ store: true,
715
+ });
716
+
717
+ expect(result.input).toEqual([
718
+ {
719
+ role: 'assistant',
720
+ content: [
721
+ {
722
+ type: 'output_text',
723
+ text: 'I will search for that information.',
724
+ },
725
+ ],
726
+ },
727
+ {
728
+ type: 'function_call',
729
+ call_id: 'call_123',
730
+ name: 'search',
731
+ arguments: JSON.stringify({ query: 'weather in San Francisco' }),
732
+ },
733
+ ]);
734
+ });
735
+
736
+ it('should convert messages with tool call parts that have ids', async () => {
737
+ const result = await convertToOpenAIResponsesInput({
738
+ toolNameMapping: testToolNameMapping,
739
+ prompt: [
740
+ {
741
+ role: 'assistant',
742
+ content: [
743
+ {
744
+ type: 'text',
745
+ text: 'I will search for that information.',
746
+ providerOptions: {
747
+ openai: {
748
+ itemId: 'id_123',
749
+ },
750
+ },
751
+ },
752
+ {
753
+ type: 'tool-call',
754
+ toolCallId: 'call_123',
755
+ toolName: 'search',
756
+ input: { query: 'weather in San Francisco' },
757
+ providerOptions: {
758
+ openai: {
759
+ itemId: 'id_456',
760
+ },
761
+ },
762
+ },
763
+ ],
764
+ },
765
+ ],
766
+ systemMessageMode: 'system',
767
+ providerOptionsName: 'openai',
768
+ store: true,
769
+ });
770
+
771
+ expect(result.input).toMatchInlineSnapshot(`
772
+ [
773
+ {
774
+ "id": "id_123",
775
+ "type": "item_reference",
776
+ },
777
+ {
778
+ "id": "id_456",
779
+ "type": "item_reference",
780
+ },
781
+ ]
782
+ `);
783
+ });
784
+
785
+ it('should convert multiple tool call parts in a single message', async () => {
786
+ const result = await convertToOpenAIResponsesInput({
787
+ toolNameMapping: testToolNameMapping,
788
+ prompt: [
789
+ {
790
+ role: 'assistant',
791
+ content: [
792
+ {
793
+ type: 'tool-call',
794
+ toolCallId: 'call_123',
795
+ toolName: 'search',
796
+ input: { query: 'weather in San Francisco' },
797
+ },
798
+ {
799
+ type: 'tool-call',
800
+ toolCallId: 'call_456',
801
+ toolName: 'calculator',
802
+ input: { expression: '2 + 2' },
803
+ },
804
+ ],
805
+ },
806
+ ],
807
+ systemMessageMode: 'system',
808
+ providerOptionsName: 'openai',
809
+ store: true,
810
+ });
811
+
812
+ expect(result.input).toEqual([
813
+ {
814
+ type: 'function_call',
815
+ call_id: 'call_123',
816
+ name: 'search',
817
+ arguments: JSON.stringify({ query: 'weather in San Francisco' }),
818
+ },
819
+ {
820
+ type: 'function_call',
821
+ call_id: 'call_456',
822
+ name: 'calculator',
823
+ arguments: JSON.stringify({ expression: '2 + 2' }),
824
+ },
825
+ ]);
826
+ });
827
+
828
+ describe('reasoning messages (store: false)', () => {
829
+ describe('single summary part', () => {
830
+ it('should convert single reasoning part with text', async () => {
831
+ const result = await convertToOpenAIResponsesInput({
832
+ toolNameMapping: testToolNameMapping,
833
+ prompt: [
834
+ {
835
+ role: 'assistant',
836
+ content: [
837
+ {
838
+ type: 'reasoning',
839
+ text: 'Analyzing the problem step by step',
840
+ providerOptions: {
841
+ openai: {
842
+ itemId: 'reasoning_001',
843
+ },
844
+ },
845
+ },
846
+ ],
847
+ },
848
+ ],
849
+ systemMessageMode: 'system',
850
+ providerOptionsName: 'openai',
851
+ store: false,
852
+ });
853
+
854
+ expect(result.input).toEqual([
855
+ {
856
+ type: 'reasoning',
857
+ id: 'reasoning_001',
858
+ encrypted_content: undefined,
859
+ summary: [
860
+ {
861
+ type: 'summary_text',
862
+ text: 'Analyzing the problem step by step',
863
+ },
864
+ ],
865
+ },
866
+ ]);
867
+
868
+ expect(result.warnings).toHaveLength(0);
869
+ });
870
+
871
+ it('should convert single reasoning part with encrypted content', async () => {
872
+ const result = await convertToOpenAIResponsesInput({
873
+ toolNameMapping: testToolNameMapping,
874
+ prompt: [
875
+ {
876
+ role: 'assistant',
877
+ content: [
878
+ {
879
+ type: 'reasoning',
880
+ text: 'Analyzing the problem step by step',
881
+ providerOptions: {
882
+ openai: {
883
+ itemId: 'reasoning_001',
884
+ reasoningEncryptedContent: 'encrypted_content_001',
885
+ },
886
+ },
887
+ },
888
+ ],
889
+ },
890
+ ],
891
+ systemMessageMode: 'system',
892
+ providerOptionsName: 'openai',
893
+ store: false,
894
+ });
895
+
896
+ expect(result.input).toEqual([
897
+ {
898
+ type: 'reasoning',
899
+ id: 'reasoning_001',
900
+ encrypted_content: 'encrypted_content_001',
901
+ summary: [
902
+ {
903
+ type: 'summary_text',
904
+ text: 'Analyzing the problem step by step',
905
+ },
906
+ ],
907
+ },
908
+ ]);
909
+
910
+ expect(result.warnings).toHaveLength(0);
911
+ });
912
+
913
+ it('should convert single reasoning part with null encrypted content', async () => {
914
+ const result = await convertToOpenAIResponsesInput({
915
+ toolNameMapping: testToolNameMapping,
916
+ prompt: [
917
+ {
918
+ role: 'assistant',
919
+ content: [
920
+ {
921
+ type: 'reasoning',
922
+ text: 'Analyzing the problem step by step',
923
+ providerOptions: {
924
+ openai: {
925
+ itemId: 'reasoning_001',
926
+ reasoningEncryptedContent: null,
927
+ },
928
+ },
929
+ },
930
+ ],
931
+ },
932
+ ],
933
+ systemMessageMode: 'system',
934
+ providerOptionsName: 'openai',
935
+ store: false,
936
+ });
937
+
938
+ expect(result.input).toEqual([
939
+ {
940
+ type: 'reasoning',
941
+ id: 'reasoning_001',
942
+ encrypted_content: null,
943
+ summary: [
944
+ {
945
+ type: 'summary_text',
946
+ text: 'Analyzing the problem step by step',
947
+ },
948
+ ],
949
+ },
950
+ ]);
951
+
952
+ expect(result.warnings).toHaveLength(0);
953
+ });
954
+ });
955
+
956
+ describe('single summary part with empty text', () => {
957
+ it('should create empty summary for initial empty text', async () => {
958
+ const result = await convertToOpenAIResponsesInput({
959
+ toolNameMapping: testToolNameMapping,
960
+ prompt: [
961
+ {
962
+ role: 'assistant',
963
+ content: [
964
+ {
965
+ type: 'reasoning',
966
+ text: '', // Empty text should NOT generate warning when it's the first reasoning part
967
+ providerOptions: {
968
+ openai: {
969
+ itemId: 'reasoning_001',
970
+ },
971
+ },
972
+ },
973
+ ],
974
+ },
975
+ ],
976
+ systemMessageMode: 'system',
977
+ providerOptionsName: 'openai',
978
+ store: false,
979
+ });
980
+
981
+ expect(result.input).toEqual([
982
+ {
983
+ type: 'reasoning',
984
+ id: 'reasoning_001',
985
+ encrypted_content: undefined,
986
+ summary: [],
987
+ },
988
+ ]);
989
+
990
+ expect(result.warnings).toHaveLength(0);
991
+ });
992
+
993
+ it('should create empty summary for initial empty text with encrypted content', async () => {
994
+ const result = await convertToOpenAIResponsesInput({
995
+ toolNameMapping: testToolNameMapping,
996
+ prompt: [
997
+ {
998
+ role: 'assistant',
999
+ content: [
1000
+ {
1001
+ type: 'reasoning',
1002
+ text: '', // Empty text should NOT generate warning when it's the first reasoning part
1003
+ providerOptions: {
1004
+ openai: {
1005
+ itemId: 'reasoning_001',
1006
+ reasoningEncryptedContent: 'encrypted_content_001',
1007
+ },
1008
+ },
1009
+ },
1010
+ ],
1011
+ },
1012
+ ],
1013
+ systemMessageMode: 'system',
1014
+ providerOptionsName: 'openai',
1015
+ store: false,
1016
+ });
1017
+
1018
+ expect(result.input).toEqual([
1019
+ {
1020
+ type: 'reasoning',
1021
+ id: 'reasoning_001',
1022
+ encrypted_content: 'encrypted_content_001',
1023
+ summary: [],
1024
+ },
1025
+ ]);
1026
+
1027
+ expect(result.warnings).toHaveLength(0);
1028
+ });
1029
+
1030
+ it('should warn when appending empty text to existing sequence', async () => {
1031
+ const result = await convertToOpenAIResponsesInput({
1032
+ toolNameMapping: testToolNameMapping,
1033
+ prompt: [
1034
+ {
1035
+ role: 'assistant',
1036
+ content: [
1037
+ {
1038
+ type: 'reasoning',
1039
+ text: 'First reasoning step',
1040
+ providerOptions: {
1041
+ openai: {
1042
+ itemId: 'reasoning_001',
1043
+ },
1044
+ },
1045
+ },
1046
+ {
1047
+ type: 'reasoning',
1048
+ text: '', // Empty text should generate warning when appending to existing reasoning sequence
1049
+ providerOptions: {
1050
+ openai: {
1051
+ itemId: 'reasoning_001',
1052
+ },
1053
+ },
1054
+ },
1055
+ ],
1056
+ },
1057
+ ],
1058
+ systemMessageMode: 'system',
1059
+ providerOptionsName: 'openai',
1060
+ store: false,
1061
+ });
1062
+
1063
+ expect(result.input).toEqual([
1064
+ {
1065
+ type: 'reasoning',
1066
+ id: 'reasoning_001',
1067
+ encrypted_content: undefined,
1068
+ summary: [
1069
+ {
1070
+ type: 'summary_text',
1071
+ text: 'First reasoning step',
1072
+ },
1073
+ ],
1074
+ },
1075
+ ]);
1076
+
1077
+ expect(result.warnings).toMatchInlineSnapshot(`
1078
+ [
1079
+ {
1080
+ "message": "Cannot append empty reasoning part to existing reasoning sequence. Skipping reasoning part: {"type":"reasoning","text":"","providerOptions":{"openai":{"itemId":"reasoning_001"}}}.",
1081
+ "type": "other",
1082
+ },
1083
+ ]
1084
+ `);
1085
+ });
1086
+ });
1087
+
1088
+ describe('merging and sequencing', () => {
1089
+ it('should merge consecutive parts with same reasoning ID', async () => {
1090
+ const result = await convertToOpenAIResponsesInput({
1091
+ toolNameMapping: testToolNameMapping,
1092
+ prompt: [
1093
+ {
1094
+ role: 'assistant',
1095
+ content: [
1096
+ {
1097
+ type: 'reasoning',
1098
+ text: 'First reasoning step',
1099
+ providerOptions: {
1100
+ openai: {
1101
+ itemId: 'reasoning_001',
1102
+ },
1103
+ },
1104
+ },
1105
+ {
1106
+ type: 'reasoning',
1107
+ text: 'Second reasoning step',
1108
+ providerOptions: {
1109
+ openai: {
1110
+ itemId: 'reasoning_001',
1111
+ // encrypted content is stored in the last summary part
1112
+ reasoningEncryptedContent: 'encrypted_content_001',
1113
+ },
1114
+ },
1115
+ },
1116
+ ],
1117
+ },
1118
+ ],
1119
+ systemMessageMode: 'system',
1120
+ providerOptionsName: 'openai',
1121
+ store: false,
1122
+ });
1123
+
1124
+ expect(result.input).toMatchInlineSnapshot(`
1125
+ [
1126
+ {
1127
+ "encrypted_content": "encrypted_content_001",
1128
+ "id": "reasoning_001",
1129
+ "summary": [
1130
+ {
1131
+ "text": "First reasoning step",
1132
+ "type": "summary_text",
1133
+ },
1134
+ {
1135
+ "text": "Second reasoning step",
1136
+ "type": "summary_text",
1137
+ },
1138
+ ],
1139
+ "type": "reasoning",
1140
+ },
1141
+ ]
1142
+ `);
1143
+
1144
+ expect(result.warnings).toHaveLength(0);
1145
+ });
1146
+
1147
+ it('should create separate messages for different reasoning IDs', async () => {
1148
+ const result = await convertToOpenAIResponsesInput({
1149
+ toolNameMapping: testToolNameMapping,
1150
+ prompt: [
1151
+ {
1152
+ role: 'assistant',
1153
+ content: [
1154
+ {
1155
+ type: 'reasoning',
1156
+ text: 'First reasoning block',
1157
+ providerOptions: {
1158
+ openai: {
1159
+ itemId: 'reasoning_001',
1160
+ },
1161
+ },
1162
+ },
1163
+ {
1164
+ type: 'reasoning',
1165
+ text: 'Second reasoning block',
1166
+ providerOptions: {
1167
+ openai: {
1168
+ itemId: 'reasoning_002',
1169
+ },
1170
+ },
1171
+ },
1172
+ ],
1173
+ },
1174
+ ],
1175
+ systemMessageMode: 'system',
1176
+ providerOptionsName: 'openai',
1177
+ store: false,
1178
+ });
1179
+
1180
+ expect(result.input).toEqual([
1181
+ {
1182
+ type: 'reasoning',
1183
+ id: 'reasoning_001',
1184
+ encrypted_content: undefined,
1185
+ summary: [
1186
+ {
1187
+ type: 'summary_text',
1188
+ text: 'First reasoning block',
1189
+ },
1190
+ ],
1191
+ },
1192
+ {
1193
+ type: 'reasoning',
1194
+ id: 'reasoning_002',
1195
+ encrypted_content: undefined,
1196
+ summary: [
1197
+ {
1198
+ type: 'summary_text',
1199
+ text: 'Second reasoning block',
1200
+ },
1201
+ ],
1202
+ },
1203
+ ]);
1204
+
1205
+ expect(result.warnings).toHaveLength(0);
1206
+ });
1207
+
1208
+ it('should handle reasoning across multiple assistant messages', async () => {
1209
+ const result = await convertToOpenAIResponsesInput({
1210
+ toolNameMapping: testToolNameMapping,
1211
+ prompt: [
1212
+ {
1213
+ role: 'user',
1214
+ content: [{ type: 'text', text: 'First user question' }],
1215
+ },
1216
+ {
1217
+ role: 'assistant',
1218
+ content: [
1219
+ {
1220
+ type: 'reasoning',
1221
+ text: 'First reasoning step (message 1)',
1222
+ providerOptions: {
1223
+ openai: {
1224
+ itemId: 'reasoning_001',
1225
+ },
1226
+ },
1227
+ },
1228
+ {
1229
+ type: 'reasoning',
1230
+ text: 'Second reasoning step (message 1)',
1231
+ providerOptions: {
1232
+ openai: {
1233
+ itemId: 'reasoning_001',
1234
+ },
1235
+ },
1236
+ },
1237
+ { type: 'text', text: 'First response' },
1238
+ ],
1239
+ },
1240
+ {
1241
+ role: 'user',
1242
+ content: [{ type: 'text', text: 'Second user question' }],
1243
+ },
1244
+ {
1245
+ role: 'assistant',
1246
+ content: [
1247
+ {
1248
+ type: 'reasoning',
1249
+ text: 'First reasoning step (message 2)',
1250
+ providerOptions: {
1251
+ openai: {
1252
+ itemId: 'reasoning_002',
1253
+ },
1254
+ },
1255
+ },
1256
+ { type: 'text', text: 'Second response' },
1257
+ ],
1258
+ },
1259
+ ],
1260
+ systemMessageMode: 'system',
1261
+ providerOptionsName: 'openai',
1262
+ store: true,
1263
+ });
1264
+
1265
+ expect(result.input).toMatchInlineSnapshot(`
1266
+ [
1267
+ {
1268
+ "content": [
1269
+ {
1270
+ "text": "First user question",
1271
+ "type": "input_text",
1272
+ },
1273
+ ],
1274
+ "role": "user",
1275
+ },
1276
+ {
1277
+ "id": "reasoning_001",
1278
+ "type": "item_reference",
1279
+ },
1280
+ {
1281
+ "content": [
1282
+ {
1283
+ "text": "First response",
1284
+ "type": "output_text",
1285
+ },
1286
+ ],
1287
+ "id": undefined,
1288
+ "role": "assistant",
1289
+ },
1290
+ {
1291
+ "content": [
1292
+ {
1293
+ "text": "Second user question",
1294
+ "type": "input_text",
1295
+ },
1296
+ ],
1297
+ "role": "user",
1298
+ },
1299
+ {
1300
+ "id": "reasoning_002",
1301
+ "type": "item_reference",
1302
+ },
1303
+ {
1304
+ "content": [
1305
+ {
1306
+ "text": "Second response",
1307
+ "type": "output_text",
1308
+ },
1309
+ ],
1310
+ "id": undefined,
1311
+ "role": "assistant",
1312
+ },
1313
+ ]
1314
+ `);
1315
+
1316
+ expect(result.warnings).toMatchInlineSnapshot(`[]`);
1317
+ });
1318
+
1319
+ it('should handle reasoning across multiple assistant messages', async () => {
1320
+ const result = await convertToOpenAIResponsesInput({
1321
+ toolNameMapping: testToolNameMapping,
1322
+ prompt: [
1323
+ {
1324
+ role: 'user',
1325
+ content: [{ type: 'text', text: 'First user question' }],
1326
+ },
1327
+ {
1328
+ role: 'assistant',
1329
+ content: [
1330
+ {
1331
+ type: 'reasoning',
1332
+ text: 'First reasoning step (message 1)',
1333
+ providerOptions: {
1334
+ openai: {
1335
+ itemId: 'reasoning_001',
1336
+ },
1337
+ },
1338
+ },
1339
+ {
1340
+ type: 'reasoning',
1341
+ text: 'Second reasoning step (message 1)',
1342
+ providerOptions: {
1343
+ openai: {
1344
+ itemId: 'reasoning_001',
1345
+ },
1346
+ },
1347
+ },
1348
+ { type: 'text', text: 'First response' },
1349
+ ],
1350
+ },
1351
+ {
1352
+ role: 'user',
1353
+ content: [{ type: 'text', text: 'Second user question' }],
1354
+ },
1355
+ {
1356
+ role: 'assistant',
1357
+ content: [
1358
+ {
1359
+ type: 'reasoning',
1360
+ text: 'First reasoning step (message 2)',
1361
+ providerOptions: {
1362
+ openai: {
1363
+ itemId: 'reasoning_002',
1364
+ },
1365
+ },
1366
+ },
1367
+ { type: 'text', text: 'Second response' },
1368
+ ],
1369
+ },
1370
+ ],
1371
+ systemMessageMode: 'system',
1372
+ providerOptionsName: 'openai',
1373
+ store: false,
1374
+ });
1375
+
1376
+ expect(result.input).toEqual([
1377
+ {
1378
+ role: 'user',
1379
+ content: [{ type: 'input_text', text: 'First user question' }],
1380
+ },
1381
+ {
1382
+ type: 'reasoning',
1383
+ id: 'reasoning_001',
1384
+ encrypted_content: undefined,
1385
+ summary: [
1386
+ {
1387
+ type: 'summary_text',
1388
+ text: 'First reasoning step (message 1)',
1389
+ },
1390
+ {
1391
+ type: 'summary_text',
1392
+ text: 'Second reasoning step (message 1)',
1393
+ },
1394
+ ],
1395
+ },
1396
+ {
1397
+ role: 'assistant',
1398
+ content: [{ type: 'output_text', text: 'First response' }],
1399
+ },
1400
+ {
1401
+ role: 'user',
1402
+ content: [{ type: 'input_text', text: 'Second user question' }],
1403
+ },
1404
+ {
1405
+ type: 'reasoning',
1406
+ id: 'reasoning_002',
1407
+ encrypted_content: undefined,
1408
+ summary: [
1409
+ {
1410
+ type: 'summary_text',
1411
+ text: 'First reasoning step (message 2)',
1412
+ },
1413
+ ],
1414
+ },
1415
+ {
1416
+ role: 'assistant',
1417
+ content: [{ type: 'output_text', text: 'Second response' }],
1418
+ },
1419
+ ]);
1420
+
1421
+ expect(result.warnings).toHaveLength(0);
1422
+ });
1423
+
1424
+ it('should handle complex reasoning sequences with tool interactions', async () => {
1425
+ const result = await convertToOpenAIResponsesInput({
1426
+ toolNameMapping: testToolNameMapping,
1427
+ prompt: [
1428
+ {
1429
+ role: 'assistant',
1430
+ content: [
1431
+ // First reasoning block: reasoning → reasoning
1432
+ {
1433
+ type: 'reasoning',
1434
+ text: 'Initial analysis step 1',
1435
+ providerOptions: {
1436
+ openai: {
1437
+ itemId: 'reasoning_001',
1438
+ reasoningEncryptedContent: 'encrypted_content_001',
1439
+ },
1440
+ },
1441
+ },
1442
+ {
1443
+ type: 'reasoning',
1444
+ text: 'Initial analysis step 2',
1445
+ providerOptions: {
1446
+ openai: {
1447
+ itemId: 'reasoning_001',
1448
+ reasoningEncryptedContent: 'encrypted_content_001',
1449
+ },
1450
+ },
1451
+ },
1452
+ // First tool interaction: tool-call
1453
+ {
1454
+ type: 'tool-call',
1455
+ toolCallId: 'call_001',
1456
+ toolName: 'search',
1457
+ input: { query: 'initial search' },
1458
+ },
1459
+ ],
1460
+ },
1461
+ // Tool result comes as separate message
1462
+ {
1463
+ role: 'tool',
1464
+ content: [
1465
+ {
1466
+ type: 'tool-result',
1467
+ toolCallId: 'call_001',
1468
+ toolName: 'search',
1469
+ output: {
1470
+ type: 'json',
1471
+ value: { results: ['result1', 'result2'] },
1472
+ },
1473
+ },
1474
+ ],
1475
+ },
1476
+ {
1477
+ role: 'assistant',
1478
+ content: [
1479
+ // Second reasoning block: reasoning → reasoning → reasoning
1480
+ {
1481
+ type: 'reasoning',
1482
+ text: 'Processing results step 1',
1483
+ providerOptions: {
1484
+ openai: {
1485
+ itemId: 'reasoning_002',
1486
+ reasoningEncryptedContent: 'encrypted_content_002',
1487
+ },
1488
+ },
1489
+ },
1490
+ {
1491
+ type: 'reasoning',
1492
+ text: 'Processing results step 2',
1493
+ providerOptions: {
1494
+ openai: {
1495
+ itemId: 'reasoning_002',
1496
+ reasoningEncryptedContent: 'encrypted_content_002',
1497
+ },
1498
+ },
1499
+ },
1500
+ {
1501
+ type: 'reasoning',
1502
+ text: 'Processing results step 3',
1503
+ providerOptions: {
1504
+ openai: {
1505
+ itemId: 'reasoning_002',
1506
+ reasoningEncryptedContent: 'encrypted_content_002',
1507
+ },
1508
+ },
1509
+ },
1510
+ // Second tool interaction: tool-call
1511
+ {
1512
+ type: 'tool-call',
1513
+ toolCallId: 'call_002',
1514
+ toolName: 'calculator',
1515
+ input: { expression: '2 + 2' },
1516
+ },
1517
+ ],
1518
+ },
1519
+ // Second tool result
1520
+ {
1521
+ role: 'tool',
1522
+ content: [
1523
+ {
1524
+ type: 'tool-result',
1525
+ toolCallId: 'call_002',
1526
+ toolName: 'calculator',
1527
+ output: {
1528
+ type: 'json',
1529
+ value: { result: 4 },
1530
+ },
1531
+ },
1532
+ ],
1533
+ },
1534
+ {
1535
+ role: 'assistant',
1536
+ content: [
1537
+ // Final text output
1538
+ {
1539
+ type: 'text',
1540
+ text: 'Based on my analysis and calculations, here is the final answer.',
1541
+ },
1542
+ ],
1543
+ },
1544
+ ],
1545
+ systemMessageMode: 'system',
1546
+ providerOptionsName: 'openai',
1547
+ store: false,
1548
+ });
1549
+
1550
+ expect(result.input).toEqual([
1551
+ // First reasoning block (2 parts merged)
1552
+ {
1553
+ type: 'reasoning',
1554
+ id: 'reasoning_001',
1555
+ encrypted_content: 'encrypted_content_001',
1556
+ summary: [
1557
+ {
1558
+ type: 'summary_text',
1559
+ text: 'Initial analysis step 1',
1560
+ },
1561
+ {
1562
+ type: 'summary_text',
1563
+ text: 'Initial analysis step 2',
1564
+ },
1565
+ ],
1566
+ },
1567
+ // First tool call
1568
+ {
1569
+ type: 'function_call',
1570
+ call_id: 'call_001',
1571
+ name: 'search',
1572
+ arguments: JSON.stringify({ query: 'initial search' }),
1573
+ },
1574
+ // First tool result
1575
+ {
1576
+ type: 'function_call_output',
1577
+ call_id: 'call_001',
1578
+ output: JSON.stringify({ results: ['result1', 'result2'] }),
1579
+ },
1580
+ // Second reasoning block (3 parts merged)
1581
+ {
1582
+ type: 'reasoning',
1583
+ id: 'reasoning_002',
1584
+ encrypted_content: 'encrypted_content_002',
1585
+ summary: [
1586
+ {
1587
+ type: 'summary_text',
1588
+ text: 'Processing results step 1',
1589
+ },
1590
+ {
1591
+ type: 'summary_text',
1592
+ text: 'Processing results step 2',
1593
+ },
1594
+ {
1595
+ type: 'summary_text',
1596
+ text: 'Processing results step 3',
1597
+ },
1598
+ ],
1599
+ },
1600
+ // Second tool call
1601
+ {
1602
+ type: 'function_call',
1603
+ call_id: 'call_002',
1604
+ name: 'calculator',
1605
+ arguments: JSON.stringify({ expression: '2 + 2' }),
1606
+ },
1607
+ // Second tool result
1608
+ {
1609
+ type: 'function_call_output',
1610
+ call_id: 'call_002',
1611
+ output: JSON.stringify({ result: 4 }),
1612
+ },
1613
+ // Final text output
1614
+ {
1615
+ role: 'assistant',
1616
+ content: [
1617
+ {
1618
+ type: 'output_text',
1619
+ text: 'Based on my analysis and calculations, here is the final answer.',
1620
+ },
1621
+ ],
1622
+ },
1623
+ ]);
1624
+
1625
+ expect(result.warnings).toHaveLength(0);
1626
+ });
1627
+ });
1628
+
1629
+ describe('error handling', () => {
1630
+ it('should warn when reasoning part has no provider options', async () => {
1631
+ const result = await convertToOpenAIResponsesInput({
1632
+ toolNameMapping: testToolNameMapping,
1633
+ prompt: [
1634
+ {
1635
+ role: 'assistant',
1636
+ content: [
1637
+ {
1638
+ type: 'reasoning',
1639
+ text: 'This is a reasoning part without any provider options',
1640
+ },
1641
+ ],
1642
+ },
1643
+ ],
1644
+ systemMessageMode: 'system',
1645
+ providerOptionsName: 'openai',
1646
+ store: false,
1647
+ });
1648
+
1649
+ expect(result.input).toHaveLength(0);
1650
+
1651
+ expect(result.warnings).toMatchInlineSnapshot(`
1652
+ [
1653
+ {
1654
+ "message": "Non-OpenAI reasoning parts are not supported. Skipping reasoning part: {"type":"reasoning","text":"This is a reasoning part without any provider options"}.",
1655
+ "type": "other",
1656
+ },
1657
+ ]
1658
+ `);
1659
+ });
1660
+
1661
+ it('should warn when reasoning part lacks OpenAI-specific reasoning ID provider options', async () => {
1662
+ const result = await convertToOpenAIResponsesInput({
1663
+ toolNameMapping: testToolNameMapping,
1664
+ prompt: [
1665
+ {
1666
+ role: 'assistant',
1667
+ content: [
1668
+ {
1669
+ type: 'reasoning',
1670
+ text: 'This is a reasoning part without OpenAI-specific reasoning id provider options',
1671
+ providerOptions: {
1672
+ openai: {
1673
+ reasoning: {
1674
+ encryptedContent: 'encrypted_content_001',
1675
+ },
1676
+ },
1677
+ },
1678
+ },
1679
+ ],
1680
+ },
1681
+ ],
1682
+ systemMessageMode: 'system',
1683
+ providerOptionsName: 'openai',
1684
+ store: false,
1685
+ });
1686
+
1687
+ expect(result.input).toHaveLength(0);
1688
+
1689
+ expect(result.warnings).toMatchInlineSnapshot(`
1690
+ [
1691
+ {
1692
+ "message": "Non-OpenAI reasoning parts are not supported. Skipping reasoning part: {"type":"reasoning","text":"This is a reasoning part without OpenAI-specific reasoning id provider options","providerOptions":{"openai":{"reasoning":{"encryptedContent":"encrypted_content_001"}}}}.",
1693
+ "type": "other",
1694
+ },
1695
+ ]
1696
+ `);
1697
+ });
1698
+ });
1699
+ });
1700
+ });
1701
+
1702
+ describe('tool messages', () => {
1703
+ it('should convert single tool result part with json value', async () => {
1704
+ const result = await convertToOpenAIResponsesInput({
1705
+ toolNameMapping: testToolNameMapping,
1706
+ prompt: [
1707
+ {
1708
+ role: 'tool',
1709
+ content: [
1710
+ {
1711
+ type: 'tool-result',
1712
+ toolCallId: 'call_123',
1713
+ toolName: 'search',
1714
+ output: {
1715
+ type: 'json',
1716
+ value: { temperature: '72°F', condition: 'Sunny' },
1717
+ },
1718
+ },
1719
+ ],
1720
+ },
1721
+ ],
1722
+ systemMessageMode: 'system',
1723
+ providerOptionsName: 'openai',
1724
+ store: true,
1725
+ });
1726
+
1727
+ expect(result.input).toMatchInlineSnapshot(`
1728
+ [
1729
+ {
1730
+ "call_id": "call_123",
1731
+ "output": "{"temperature":"72°F","condition":"Sunny"}",
1732
+ "type": "function_call_output",
1733
+ },
1734
+ ]
1735
+ `);
1736
+ });
1737
+
1738
+ it('should convert single tool result part with text value', async () => {
1739
+ const result = await convertToOpenAIResponsesInput({
1740
+ toolNameMapping: testToolNameMapping,
1741
+ prompt: [
1742
+ {
1743
+ role: 'tool',
1744
+ content: [
1745
+ {
1746
+ type: 'tool-result',
1747
+ toolCallId: 'call_123',
1748
+ toolName: 'search',
1749
+ output: {
1750
+ type: 'text',
1751
+ value: 'The weather in San Francisco is 72°F',
1752
+ },
1753
+ },
1754
+ ],
1755
+ },
1756
+ ],
1757
+ systemMessageMode: 'system',
1758
+ providerOptionsName: 'openai',
1759
+ store: true,
1760
+ });
1761
+
1762
+ expect(result.input).toMatchInlineSnapshot(`
1763
+ [
1764
+ {
1765
+ "call_id": "call_123",
1766
+ "output": "The weather in San Francisco is 72°F",
1767
+ "type": "function_call_output",
1768
+ },
1769
+ ]
1770
+ `);
1771
+ });
1772
+
1773
+ it('should convert single tool result part with multipart that contains text', async () => {
1774
+ const result = await convertToOpenAIResponsesInput({
1775
+ toolNameMapping: testToolNameMapping,
1776
+ prompt: [
1777
+ {
1778
+ role: 'tool',
1779
+ content: [
1780
+ {
1781
+ type: 'tool-result',
1782
+ toolCallId: 'call_123',
1783
+ toolName: 'search',
1784
+ output: {
1785
+ type: 'content',
1786
+ value: [
1787
+ {
1788
+ type: 'text',
1789
+ text: 'The weather in San Francisco is 72°F',
1790
+ },
1791
+ ],
1792
+ },
1793
+ },
1794
+ ],
1795
+ },
1796
+ ],
1797
+ systemMessageMode: 'system',
1798
+ providerOptionsName: 'openai',
1799
+ store: true,
1800
+ });
1801
+
1802
+ expect(result.input).toMatchInlineSnapshot(`
1803
+ [
1804
+ {
1805
+ "call_id": "call_123",
1806
+ "output": [
1807
+ {
1808
+ "text": "The weather in San Francisco is 72°F",
1809
+ "type": "input_text",
1810
+ },
1811
+ ],
1812
+ "type": "function_call_output",
1813
+ },
1814
+ ]
1815
+ `);
1816
+ });
1817
+
1818
+ it('should convert single tool result part with multipart that contains image', async () => {
1819
+ const result = await convertToOpenAIResponsesInput({
1820
+ toolNameMapping: testToolNameMapping,
1821
+ prompt: [
1822
+ {
1823
+ role: 'tool',
1824
+ content: [
1825
+ {
1826
+ type: 'tool-result',
1827
+ toolCallId: 'call_123',
1828
+ toolName: 'search',
1829
+ output: {
1830
+ type: 'content',
1831
+ value: [
1832
+ {
1833
+ type: 'image-data',
1834
+ mediaType: 'image/png',
1835
+ data: 'base64_data',
1836
+ },
1837
+ ],
1838
+ },
1839
+ },
1840
+ ],
1841
+ },
1842
+ ],
1843
+ systemMessageMode: 'system',
1844
+ providerOptionsName: 'openai',
1845
+ store: true,
1846
+ });
1847
+
1848
+ expect(result.input).toMatchInlineSnapshot(`
1849
+ [
1850
+ {
1851
+ "call_id": "call_123",
1852
+ "output": [
1853
+ {
1854
+ "image_url": "data:image/png;base64,base64_data",
1855
+ "type": "input_image",
1856
+ },
1857
+ ],
1858
+ "type": "function_call_output",
1859
+ },
1860
+ ]
1861
+ `);
1862
+ });
1863
+
1864
+ it('should convert single tool result part with multipart that contains image URL', async () => {
1865
+ const result = await convertToOpenAIResponsesInput({
1866
+ toolNameMapping: testToolNameMapping,
1867
+ prompt: [
1868
+ {
1869
+ role: 'tool',
1870
+ content: [
1871
+ {
1872
+ type: 'tool-result',
1873
+ toolCallId: 'call_123',
1874
+ toolName: 'screenshot',
1875
+ output: {
1876
+ type: 'content',
1877
+ value: [
1878
+ {
1879
+ type: 'image-url',
1880
+ url: 'https://example.com/screenshot.png',
1881
+ },
1882
+ ],
1883
+ },
1884
+ },
1885
+ ],
1886
+ },
1887
+ ],
1888
+ systemMessageMode: 'system',
1889
+ providerOptionsName: 'openai',
1890
+ store: true,
1891
+ });
1892
+
1893
+ expect(result.input).toMatchInlineSnapshot(`
1894
+ [
1895
+ {
1896
+ "call_id": "call_123",
1897
+ "output": [
1898
+ {
1899
+ "image_url": "https://example.com/screenshot.png",
1900
+ "type": "input_image",
1901
+ },
1902
+ ],
1903
+ "type": "function_call_output",
1904
+ },
1905
+ ]
1906
+ `);
1907
+ });
1908
+
1909
+ it('should convert single tool result part with multipart that contains file (PDF)', async () => {
1910
+ const base64Data = 'AQIDBAU=';
1911
+ const result = await convertToOpenAIResponsesInput({
1912
+ toolNameMapping: testToolNameMapping,
1913
+ prompt: [
1914
+ {
1915
+ role: 'tool',
1916
+ content: [
1917
+ {
1918
+ type: 'tool-result',
1919
+ toolCallId: 'call_123',
1920
+ toolName: 'search',
1921
+ output: {
1922
+ type: 'content',
1923
+ value: [
1924
+ {
1925
+ type: 'file-data',
1926
+ mediaType: 'application/pdf',
1927
+ data: base64Data,
1928
+ filename: 'document.pdf',
1929
+ },
1930
+ ],
1931
+ },
1932
+ },
1933
+ ],
1934
+ },
1935
+ ],
1936
+ systemMessageMode: 'system',
1937
+ providerOptionsName: 'openai',
1938
+ store: true,
1939
+ });
1940
+
1941
+ expect(result.input).toMatchInlineSnapshot(`
1942
+ [
1943
+ {
1944
+ "call_id": "call_123",
1945
+ "output": [
1946
+ {
1947
+ "file_data": "data:application/pdf;base64,AQIDBAU=",
1948
+ "filename": "document.pdf",
1949
+ "type": "input_file",
1950
+ },
1951
+ ],
1952
+ "type": "function_call_output",
1953
+ },
1954
+ ]
1955
+ `);
1956
+ });
1957
+
1958
+ it('should convert single tool result part with multipart with mixed content (text, image, file)', async () => {
1959
+ const base64Data = 'AQIDBAU=';
1960
+ const result = await convertToOpenAIResponsesInput({
1961
+ toolNameMapping: testToolNameMapping,
1962
+ prompt: [
1963
+ {
1964
+ role: 'tool',
1965
+ content: [
1966
+ {
1967
+ type: 'tool-result',
1968
+ toolCallId: 'call_123',
1969
+ toolName: 'search',
1970
+ output: {
1971
+ type: 'content',
1972
+ value: [
1973
+ {
1974
+ type: 'text',
1975
+ text: 'The weather in San Francisco is 72°F',
1976
+ },
1977
+ {
1978
+ type: 'image-data',
1979
+ mediaType: 'image/png',
1980
+ data: 'base64_data',
1981
+ },
1982
+ {
1983
+ type: 'file-data',
1984
+ mediaType: 'application/pdf',
1985
+ data: base64Data,
1986
+ },
1987
+ ],
1988
+ },
1989
+ },
1990
+ ],
1991
+ },
1992
+ ],
1993
+ systemMessageMode: 'system',
1994
+ providerOptionsName: 'openai',
1995
+ store: true,
1996
+ });
1997
+
1998
+ expect(result.input).toMatchInlineSnapshot(`
1999
+ [
2000
+ {
2001
+ "call_id": "call_123",
2002
+ "output": [
2003
+ {
2004
+ "text": "The weather in San Francisco is 72°F",
2005
+ "type": "input_text",
2006
+ },
2007
+ {
2008
+ "image_url": "data:image/png;base64,base64_data",
2009
+ "type": "input_image",
2010
+ },
2011
+ {
2012
+ "file_data": "data:application/pdf;base64,AQIDBAU=",
2013
+ "filename": "data",
2014
+ "type": "input_file",
2015
+ },
2016
+ ],
2017
+ "type": "function_call_output",
2018
+ },
2019
+ ]
2020
+ `);
2021
+ });
2022
+
2023
+ it('should convert multiple tool result parts in a single message', async () => {
2024
+ const result = await convertToOpenAIResponsesInput({
2025
+ toolNameMapping: testToolNameMapping,
2026
+ prompt: [
2027
+ {
2028
+ role: 'tool',
2029
+ content: [
2030
+ {
2031
+ type: 'tool-result',
2032
+ toolCallId: 'call_123',
2033
+ toolName: 'search',
2034
+ output: {
2035
+ type: 'json',
2036
+ value: { temperature: '72°F', condition: 'Sunny' },
2037
+ },
2038
+ },
2039
+ {
2040
+ type: 'tool-result',
2041
+ toolCallId: 'call_456',
2042
+ toolName: 'calculator',
2043
+ output: { type: 'json', value: 4 },
2044
+ },
2045
+ ],
2046
+ },
2047
+ ],
2048
+ systemMessageMode: 'system',
2049
+ providerOptionsName: 'openai',
2050
+ store: true,
2051
+ });
2052
+
2053
+ expect(result.input).toEqual([
2054
+ {
2055
+ type: 'function_call_output',
2056
+ call_id: 'call_123',
2057
+ output: JSON.stringify({ temperature: '72°F', condition: 'Sunny' }),
2058
+ },
2059
+ {
2060
+ type: 'function_call_output',
2061
+ call_id: 'call_456',
2062
+ output: JSON.stringify(4),
2063
+ },
2064
+ ]);
2065
+ });
2066
+ });
2067
+
2068
+ describe('provider-defined tools', () => {
2069
+ it('should convert single provider-executed tool call and result into item reference with store: true', async () => {
2070
+ const result = await convertToOpenAIResponsesInput({
2071
+ toolNameMapping: testToolNameMapping,
2072
+ prompt: [
2073
+ {
2074
+ role: 'assistant',
2075
+ content: [
2076
+ {
2077
+ input: { code: 'example code', containerId: 'container_123' },
2078
+ providerExecuted: true,
2079
+ toolCallId:
2080
+ 'ci_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f',
2081
+ toolName: 'code_interpreter',
2082
+ type: 'tool-call',
2083
+ },
2084
+ {
2085
+ output: {
2086
+ type: 'json',
2087
+ value: {
2088
+ outputs: [{ type: 'logs', logs: 'example logs' }],
2089
+ },
2090
+ },
2091
+ toolCallId:
2092
+ 'ci_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f',
2093
+ toolName: 'code_interpreter',
2094
+ type: 'tool-result',
2095
+ },
2096
+ ],
2097
+ },
2098
+ ],
2099
+ systemMessageMode: 'system',
2100
+ providerOptionsName: 'openai',
2101
+ store: true,
2102
+ });
2103
+
2104
+ expect(result.input).toMatchInlineSnapshot(`
2105
+ [
2106
+ {
2107
+ "id": "ci_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f",
2108
+ "type": "item_reference",
2109
+ },
2110
+ ]
2111
+ `);
2112
+ });
2113
+
2114
+ it('should exclude provider-executed tool calls and results from prompt with store: false', async () => {
2115
+ const result = await convertToOpenAIResponsesInput({
2116
+ toolNameMapping: testToolNameMapping,
2117
+ prompt: [
2118
+ {
2119
+ role: 'assistant',
2120
+ content: [
2121
+ {
2122
+ type: 'text',
2123
+ text: 'Let me search for recent news from San Francisco.',
2124
+ },
2125
+ {
2126
+ type: 'tool-call',
2127
+ toolCallId: 'ws_67cf2b3051e88190b006770db6fdb13d',
2128
+ toolName: 'web_search',
2129
+ input: {
2130
+ query: 'San Francisco major news events June 22 2025',
2131
+ },
2132
+ providerExecuted: true,
2133
+ },
2134
+ {
2135
+ type: 'tool-result',
2136
+ toolCallId: 'ws_67cf2b3051e88190b006770db6fdb13d',
2137
+ toolName: 'web_search',
2138
+ output: {
2139
+ type: 'json',
2140
+ value: {
2141
+ action: {
2142
+ type: 'search',
2143
+ query: 'San Francisco major news events June 22 2025',
2144
+ },
2145
+ sources: [
2146
+ {
2147
+ type: 'url',
2148
+ url: 'https://patch.com/california/san-francisco/calendar',
2149
+ },
2150
+ ],
2151
+ },
2152
+ },
2153
+ },
2154
+ {
2155
+ type: 'text',
2156
+ text: 'Based on the search results, several significant events took place in San Francisco yesterday (June 22, 2025).',
2157
+ },
2158
+ ],
2159
+ },
2160
+ ],
2161
+ systemMessageMode: 'system',
2162
+ providerOptionsName: 'openai',
2163
+ store: false,
2164
+ });
2165
+
2166
+ expect(result).toMatchInlineSnapshot(`
2167
+ {
2168
+ "input": [
2169
+ {
2170
+ "content": [
2171
+ {
2172
+ "text": "Let me search for recent news from San Francisco.",
2173
+ "type": "output_text",
2174
+ },
2175
+ ],
2176
+ "id": undefined,
2177
+ "role": "assistant",
2178
+ },
2179
+ {
2180
+ "content": [
2181
+ {
2182
+ "text": "Based on the search results, several significant events took place in San Francisco yesterday (June 22, 2025).",
2183
+ "type": "output_text",
2184
+ },
2185
+ ],
2186
+ "id": undefined,
2187
+ "role": "assistant",
2188
+ },
2189
+ ],
2190
+ "warnings": [
2191
+ {
2192
+ "message": "Results for OpenAI tool web_search are not sent to the API when store is false",
2193
+ "type": "other",
2194
+ },
2195
+ ],
2196
+ }
2197
+ `);
2198
+ });
2199
+
2200
+ describe('local shell', () => {
2201
+ it('should convert local shell tool call and result into item reference with store: true', async () => {
2202
+ const result = await convertToOpenAIResponsesInput({
2203
+ toolNameMapping: testToolNameMapping,
2204
+ prompt: [
2205
+ {
2206
+ role: 'assistant',
2207
+ content: [
2208
+ {
2209
+ type: 'tool-call',
2210
+ toolCallId: 'call_XWgeTylovOiS8xLNz2TONOgO',
2211
+ toolName: 'local_shell',
2212
+ input: { action: { type: 'exec', command: ['ls'] } },
2213
+ providerOptions: {
2214
+ openai: {
2215
+ itemId:
2216
+ 'lsh_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f',
2217
+ },
2218
+ },
2219
+ },
2220
+ ],
2221
+ },
2222
+ {
2223
+ role: 'tool',
2224
+ content: [
2225
+ {
2226
+ type: 'tool-result',
2227
+ toolCallId: 'call_XWgeTylovOiS8xLNz2TONOgO',
2228
+ toolName: 'local_shell',
2229
+ output: { type: 'json', value: { output: 'example output' } },
2230
+ },
2231
+ ],
2232
+ },
2233
+ ],
2234
+ systemMessageMode: 'system',
2235
+ providerOptionsName: 'openai',
2236
+ store: true,
2237
+ hasLocalShellTool: true,
2238
+ });
2239
+
2240
+ expect(result.input).toMatchInlineSnapshot(`
2241
+ [
2242
+ {
2243
+ "id": "lsh_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f",
2244
+ "type": "item_reference",
2245
+ },
2246
+ {
2247
+ "call_id": "call_XWgeTylovOiS8xLNz2TONOgO",
2248
+ "output": "example output",
2249
+ "type": "local_shell_call_output",
2250
+ },
2251
+ ]
2252
+ `);
2253
+ });
2254
+
2255
+ it('should convert local shell tool call and result into item reference with store: false', async () => {
2256
+ const result = await convertToOpenAIResponsesInput({
2257
+ toolNameMapping: testToolNameMapping,
2258
+ prompt: [
2259
+ {
2260
+ role: 'assistant',
2261
+ content: [
2262
+ {
2263
+ type: 'tool-call',
2264
+ toolCallId: 'call_XWgeTylovOiS8xLNz2TONOgO',
2265
+ toolName: 'local_shell',
2266
+ input: { action: { type: 'exec', command: ['ls'] } },
2267
+ providerOptions: {
2268
+ openai: {
2269
+ itemId:
2270
+ 'lsh_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f',
2271
+ },
2272
+ },
2273
+ },
2274
+ ],
2275
+ },
2276
+ {
2277
+ role: 'tool',
2278
+ content: [
2279
+ {
2280
+ type: 'tool-result',
2281
+ toolCallId: 'call_XWgeTylovOiS8xLNz2TONOgO',
2282
+ toolName: 'local_shell',
2283
+ output: { type: 'json', value: { output: 'example output' } },
2284
+ },
2285
+ ],
2286
+ },
2287
+ ],
2288
+ systemMessageMode: 'system',
2289
+ providerOptionsName: 'openai',
2290
+ store: false,
2291
+ hasLocalShellTool: true,
2292
+ });
2293
+
2294
+ expect(result.input).toMatchInlineSnapshot(`
2295
+ [
2296
+ {
2297
+ "action": {
2298
+ "command": [
2299
+ "ls",
2300
+ ],
2301
+ "env": undefined,
2302
+ "timeout_ms": undefined,
2303
+ "type": "exec",
2304
+ "user": undefined,
2305
+ "working_directory": undefined,
2306
+ },
2307
+ "call_id": "call_XWgeTylovOiS8xLNz2TONOgO",
2308
+ "id": "lsh_68c2e2cf522c81908f3e2c1bccd1493b0b24aae9c6c01e4f",
2309
+ "type": "local_shell_call",
2310
+ },
2311
+ {
2312
+ "call_id": "call_XWgeTylovOiS8xLNz2TONOgO",
2313
+ "output": "example output",
2314
+ "type": "local_shell_call_output",
2315
+ },
2316
+ ]
2317
+ `);
2318
+ });
2319
+ });
2320
+ });
2321
+
2322
+ describe('provider tool outputs', () => {
2323
+ it('should include apply_patch output when multiple tool results are present', async () => {
2324
+ const result = await convertToOpenAIResponsesInput({
2325
+ toolNameMapping: testToolNameMapping,
2326
+ prompt: [
2327
+ {
2328
+ role: 'tool',
2329
+ content: [
2330
+ {
2331
+ type: 'tool-result',
2332
+ toolCallId: 'call-shell',
2333
+ toolName: 'shell',
2334
+ output: {
2335
+ type: 'json',
2336
+ value: {
2337
+ output: [
2338
+ {
2339
+ stdout: 'hi\n',
2340
+ stderr: '',
2341
+ outcome: { type: 'exit', exitCode: 0 },
2342
+ },
2343
+ ],
2344
+ },
2345
+ },
2346
+ },
2347
+ {
2348
+ type: 'tool-result',
2349
+ toolCallId: 'call-apply',
2350
+ toolName: 'apply_patch',
2351
+ output: {
2352
+ type: 'json',
2353
+ value: {
2354
+ status: 'completed',
2355
+ output: 'patched',
2356
+ },
2357
+ },
2358
+ },
2359
+ ],
2360
+ },
2361
+ ],
2362
+ systemMessageMode: 'system',
2363
+ providerOptionsName: 'openai',
2364
+ store: true,
2365
+ hasShellTool: true,
2366
+ hasApplyPatchTool: true,
2367
+ });
2368
+
2369
+ expect(result.input).toEqual([
2370
+ {
2371
+ type: 'shell_call_output',
2372
+ call_id: 'call-shell',
2373
+ output: [
2374
+ {
2375
+ stdout: 'hi\n',
2376
+ stderr: '',
2377
+ outcome: { type: 'exit', exit_code: 0 },
2378
+ },
2379
+ ],
2380
+ },
2381
+ {
2382
+ type: 'apply_patch_call_output',
2383
+ call_id: 'call-apply',
2384
+ status: 'completed',
2385
+ output: 'patched',
2386
+ },
2387
+ ]);
2388
+ });
2389
+ });
2390
+
2391
+ describe('function tools', () => {
2392
+ it('should include client-side tool calls in prompt', async () => {
2393
+ const result = await convertToOpenAIResponsesInput({
2394
+ prompt: [
2395
+ {
2396
+ role: 'assistant',
2397
+ content: [
2398
+ {
2399
+ type: 'tool-call',
2400
+ toolCallId: 'call-1',
2401
+ toolName: 'calculator',
2402
+ input: { a: 1, b: 2 },
2403
+ providerExecuted: false,
2404
+ },
2405
+ ],
2406
+ },
2407
+ ],
2408
+ toolNameMapping: testToolNameMapping,
2409
+ systemMessageMode: 'system',
2410
+ providerOptionsName: 'openai',
2411
+ store: true,
2412
+ });
2413
+
2414
+ expect(result).toMatchInlineSnapshot(`
2415
+ {
2416
+ "input": [
2417
+ {
2418
+ "arguments": "{"a":1,"b":2}",
2419
+ "call_id": "call-1",
2420
+ "id": undefined,
2421
+ "name": "calculator",
2422
+ "type": "function_call",
2423
+ },
2424
+ ],
2425
+ "warnings": [],
2426
+ }
2427
+ `);
2428
+ });
2429
+ });
2430
+
2431
+ describe('MCP tool approval responses', () => {
2432
+ it('should convert approved tool-approval-response to mcp_approval_response with store: true', async () => {
2433
+ const result = await convertToOpenAIResponsesInput({
2434
+ toolNameMapping: testToolNameMapping,
2435
+ prompt: [
2436
+ {
2437
+ role: 'tool',
2438
+ content: [
2439
+ {
2440
+ type: 'tool-approval-response',
2441
+ approvalId: 'mcp-approval-123',
2442
+ approved: true,
2443
+ },
2444
+ ],
2445
+ },
2446
+ ],
2447
+ systemMessageMode: 'system',
2448
+ providerOptionsName: 'openai',
2449
+ store: true,
2450
+ });
2451
+
2452
+ expect(result.input).toMatchInlineSnapshot(`
2453
+ [
2454
+ {
2455
+ "id": "mcp-approval-123",
2456
+ "type": "item_reference",
2457
+ },
2458
+ {
2459
+ "approval_request_id": "mcp-approval-123",
2460
+ "approve": true,
2461
+ "type": "mcp_approval_response",
2462
+ },
2463
+ ]
2464
+ `);
2465
+ });
2466
+
2467
+ it('should convert denied tool-approval-response to mcp_approval_response with store: true', async () => {
2468
+ const result = await convertToOpenAIResponsesInput({
2469
+ toolNameMapping: testToolNameMapping,
2470
+ prompt: [
2471
+ {
2472
+ role: 'tool',
2473
+ content: [
2474
+ {
2475
+ type: 'tool-approval-response',
2476
+ approvalId: 'mcp-approval-456',
2477
+ approved: false,
2478
+ },
2479
+ ],
2480
+ },
2481
+ ],
2482
+ systemMessageMode: 'system',
2483
+ providerOptionsName: 'openai',
2484
+ store: true,
2485
+ });
2486
+
2487
+ expect(result.input).toMatchInlineSnapshot(`
2488
+ [
2489
+ {
2490
+ "id": "mcp-approval-456",
2491
+ "type": "item_reference",
2492
+ },
2493
+ {
2494
+ "approval_request_id": "mcp-approval-456",
2495
+ "approve": false,
2496
+ "type": "mcp_approval_response",
2497
+ },
2498
+ ]
2499
+ `);
2500
+ });
2501
+
2502
+ it('should convert tool-approval-response to mcp_approval_response without item_reference when store: false', async () => {
2503
+ const result = await convertToOpenAIResponsesInput({
2504
+ toolNameMapping: testToolNameMapping,
2505
+ prompt: [
2506
+ {
2507
+ role: 'tool',
2508
+ content: [
2509
+ {
2510
+ type: 'tool-approval-response',
2511
+ approvalId: 'mcp-approval-789',
2512
+ approved: true,
2513
+ },
2514
+ ],
2515
+ },
2516
+ ],
2517
+ systemMessageMode: 'system',
2518
+ providerOptionsName: 'openai',
2519
+ store: false,
2520
+ });
2521
+
2522
+ expect(result.input).toMatchInlineSnapshot(`
2523
+ [
2524
+ {
2525
+ "approval_request_id": "mcp-approval-789",
2526
+ "approve": true,
2527
+ "type": "mcp_approval_response",
2528
+ },
2529
+ ]
2530
+ `);
2531
+ });
2532
+
2533
+ it('should skip duplicate tool-approval-response with same approvalId', async () => {
2534
+ const result = await convertToOpenAIResponsesInput({
2535
+ toolNameMapping: testToolNameMapping,
2536
+ prompt: [
2537
+ {
2538
+ role: 'tool',
2539
+ content: [
2540
+ {
2541
+ type: 'tool-approval-response',
2542
+ approvalId: 'duplicate-approval',
2543
+ approved: true,
2544
+ },
2545
+ {
2546
+ type: 'tool-approval-response',
2547
+ approvalId: 'duplicate-approval',
2548
+ approved: true,
2549
+ },
2550
+ ],
2551
+ },
2552
+ ],
2553
+ systemMessageMode: 'system',
2554
+ providerOptionsName: 'openai',
2555
+ store: true,
2556
+ });
2557
+
2558
+ expect(result.input).toMatchInlineSnapshot(`
2559
+ [
2560
+ {
2561
+ "id": "duplicate-approval",
2562
+ "type": "item_reference",
2563
+ },
2564
+ {
2565
+ "approval_request_id": "duplicate-approval",
2566
+ "approve": true,
2567
+ "type": "mcp_approval_response",
2568
+ },
2569
+ ]
2570
+ `);
2571
+ });
2572
+
2573
+ it('should handle multiple different tool-approval-responses', async () => {
2574
+ const result = await convertToOpenAIResponsesInput({
2575
+ toolNameMapping: testToolNameMapping,
2576
+ prompt: [
2577
+ {
2578
+ role: 'tool',
2579
+ content: [
2580
+ {
2581
+ type: 'tool-approval-response',
2582
+ approvalId: 'approval-1',
2583
+ approved: true,
2584
+ },
2585
+ {
2586
+ type: 'tool-approval-response',
2587
+ approvalId: 'approval-2',
2588
+ approved: false,
2589
+ },
2590
+ ],
2591
+ },
2592
+ ],
2593
+ systemMessageMode: 'system',
2594
+ providerOptionsName: 'openai',
2595
+ store: true,
2596
+ });
2597
+
2598
+ expect(result.input).toMatchInlineSnapshot(`
2599
+ [
2600
+ {
2601
+ "id": "approval-1",
2602
+ "type": "item_reference",
2603
+ },
2604
+ {
2605
+ "approval_request_id": "approval-1",
2606
+ "approve": true,
2607
+ "type": "mcp_approval_response",
2608
+ },
2609
+ {
2610
+ "id": "approval-2",
2611
+ "type": "item_reference",
2612
+ },
2613
+ {
2614
+ "approval_request_id": "approval-2",
2615
+ "approve": false,
2616
+ "type": "mcp_approval_response",
2617
+ },
2618
+ ]
2619
+ `);
2620
+ });
2621
+
2622
+ it('should skip execution-denied output when it has approvalId in providerOptions', async () => {
2623
+ const result = await convertToOpenAIResponsesInput({
2624
+ toolNameMapping: testToolNameMapping,
2625
+ prompt: [
2626
+ {
2627
+ role: 'tool',
2628
+ content: [
2629
+ {
2630
+ type: 'tool-approval-response',
2631
+ approvalId: 'denied-approval',
2632
+ approved: false,
2633
+ },
2634
+ {
2635
+ type: 'tool-result',
2636
+ toolCallId: 'call-123',
2637
+ toolName: 'mcp_tool',
2638
+ output: {
2639
+ type: 'execution-denied',
2640
+ reason: 'User denied the tool execution',
2641
+ providerOptions: {
2642
+ openai: {
2643
+ approvalId: 'denied-approval',
2644
+ },
2645
+ },
2646
+ },
2647
+ },
2648
+ ],
2649
+ },
2650
+ ],
2651
+ systemMessageMode: 'system',
2652
+ providerOptionsName: 'openai',
2653
+ store: true,
2654
+ });
2655
+
2656
+ // Only the mcp_approval_response should be present, not a function_call_output
2657
+ expect(result.input).toMatchInlineSnapshot(`
2658
+ [
2659
+ {
2660
+ "id": "denied-approval",
2661
+ "type": "item_reference",
2662
+ },
2663
+ {
2664
+ "approval_request_id": "denied-approval",
2665
+ "approve": false,
2666
+ "type": "mcp_approval_response",
2667
+ },
2668
+ ]
2669
+ `);
2670
+ });
2671
+
2672
+ it('should handle tool-approval-response mixed with regular tool results', async () => {
2673
+ const result = await convertToOpenAIResponsesInput({
2674
+ toolNameMapping: testToolNameMapping,
2675
+ prompt: [
2676
+ {
2677
+ role: 'tool',
2678
+ content: [
2679
+ {
2680
+ type: 'tool-approval-response',
2681
+ approvalId: 'approval-for-mcp',
2682
+ approved: true,
2683
+ },
2684
+ {
2685
+ type: 'tool-result',
2686
+ toolCallId: 'regular-call-1',
2687
+ toolName: 'calculator',
2688
+ output: {
2689
+ type: 'json',
2690
+ value: { result: 42 },
2691
+ },
2692
+ },
2693
+ ],
2694
+ },
2695
+ ],
2696
+ systemMessageMode: 'system',
2697
+ providerOptionsName: 'openai',
2698
+ store: true,
2699
+ });
2700
+
2701
+ expect(result.input).toMatchInlineSnapshot(`
2702
+ [
2703
+ {
2704
+ "id": "approval-for-mcp",
2705
+ "type": "item_reference",
2706
+ },
2707
+ {
2708
+ "approval_request_id": "approval-for-mcp",
2709
+ "approve": true,
2710
+ "type": "mcp_approval_response",
2711
+ },
2712
+ {
2713
+ "call_id": "regular-call-1",
2714
+ "output": "{"result":42}",
2715
+ "type": "function_call_output",
2716
+ },
2717
+ ]
2718
+ `);
2719
+ });
2720
+ });
2721
+
2722
+ describe('hasConversation', () => {
2723
+ it('should skip assistant text messages with item IDs when hasConversation is true', async () => {
2724
+ const result = await convertToOpenAIResponsesInput({
2725
+ toolNameMapping: testToolNameMapping,
2726
+ prompt: [
2727
+ {
2728
+ role: 'user',
2729
+ content: [{ type: 'text', text: 'Hello' }],
2730
+ },
2731
+ {
2732
+ role: 'assistant',
2733
+ content: [
2734
+ {
2735
+ type: 'text',
2736
+ text: 'Hi there!',
2737
+ providerOptions: { openai: { itemId: 'msg_existing_123' } },
2738
+ },
2739
+ ],
2740
+ },
2741
+ {
2742
+ role: 'user',
2743
+ content: [{ type: 'text', text: 'What is the weather?' }],
2744
+ },
2745
+ ],
2746
+ systemMessageMode: 'system',
2747
+ providerOptionsName: 'openai',
2748
+ store: true,
2749
+ hasConversation: true,
2750
+ });
2751
+
2752
+ expect(result.input).toMatchInlineSnapshot(`
2753
+ [
2754
+ {
2755
+ "content": [
2756
+ {
2757
+ "text": "Hello",
2758
+ "type": "input_text",
2759
+ },
2760
+ ],
2761
+ "role": "user",
2762
+ },
2763
+ {
2764
+ "content": [
2765
+ {
2766
+ "text": "What is the weather?",
2767
+ "type": "input_text",
2768
+ },
2769
+ ],
2770
+ "role": "user",
2771
+ },
2772
+ ]
2773
+ `);
2774
+ });
2775
+
2776
+ it('should skip assistant tool-call messages with item IDs when hasConversation is true', async () => {
2777
+ const result = await convertToOpenAIResponsesInput({
2778
+ toolNameMapping: testToolNameMapping,
2779
+ prompt: [
2780
+ {
2781
+ role: 'user',
2782
+ content: [{ type: 'text', text: 'What is the weather?' }],
2783
+ },
2784
+ {
2785
+ role: 'assistant',
2786
+ content: [
2787
+ {
2788
+ type: 'tool-call',
2789
+ toolCallId: 'call_123',
2790
+ toolName: 'getWeather',
2791
+ input: { location: 'San Francisco' },
2792
+ providerOptions: {
2793
+ openai: { itemId: 'fc_existing_456' },
2794
+ },
2795
+ },
2796
+ ],
2797
+ },
2798
+ {
2799
+ role: 'tool',
2800
+ content: [
2801
+ {
2802
+ type: 'tool-result',
2803
+ toolCallId: 'call_123',
2804
+ toolName: 'getWeather',
2805
+ output: { type: 'json', value: { temp: 72 } },
2806
+ },
2807
+ ],
2808
+ },
2809
+ ],
2810
+ systemMessageMode: 'system',
2811
+ providerOptionsName: 'openai',
2812
+ store: true,
2813
+ hasConversation: true,
2814
+ });
2815
+
2816
+ // Tool call with itemId should be skipped, but tool output should remain
2817
+ expect(result.input).toMatchInlineSnapshot(`
2818
+ [
2819
+ {
2820
+ "content": [
2821
+ {
2822
+ "text": "What is the weather?",
2823
+ "type": "input_text",
2824
+ },
2825
+ ],
2826
+ "role": "user",
2827
+ },
2828
+ {
2829
+ "call_id": "call_123",
2830
+ "output": "{"temp":72}",
2831
+ "type": "function_call_output",
2832
+ },
2833
+ ]
2834
+ `);
2835
+ });
2836
+
2837
+ it('should include assistant messages without item IDs when hasConversation is true', async () => {
2838
+ const result = await convertToOpenAIResponsesInput({
2839
+ toolNameMapping: testToolNameMapping,
2840
+ prompt: [
2841
+ {
2842
+ role: 'user',
2843
+ content: [{ type: 'text', text: 'Hello' }],
2844
+ },
2845
+ {
2846
+ role: 'assistant',
2847
+ content: [
2848
+ {
2849
+ type: 'text',
2850
+ text: 'Hi there!',
2851
+ // No itemId - this is a new message
2852
+ },
2853
+ ],
2854
+ },
2855
+ ],
2856
+ systemMessageMode: 'system',
2857
+ providerOptionsName: 'openai',
2858
+ store: true,
2859
+ hasConversation: true,
2860
+ });
2861
+
2862
+ // Assistant message without itemId should be included
2863
+ expect(result.input).toMatchInlineSnapshot(`
2864
+ [
2865
+ {
2866
+ "content": [
2867
+ {
2868
+ "text": "Hello",
2869
+ "type": "input_text",
2870
+ },
2871
+ ],
2872
+ "role": "user",
2873
+ },
2874
+ {
2875
+ "content": [
2876
+ {
2877
+ "text": "Hi there!",
2878
+ "type": "output_text",
2879
+ },
2880
+ ],
2881
+ "id": undefined,
2882
+ "role": "assistant",
2883
+ },
2884
+ ]
2885
+ `);
2886
+ });
2887
+
2888
+ it('should include assistant messages with item IDs when hasConversation is false', async () => {
2889
+ const result = await convertToOpenAIResponsesInput({
2890
+ toolNameMapping: testToolNameMapping,
2891
+ prompt: [
2892
+ {
2893
+ role: 'user',
2894
+ content: [{ type: 'text', text: 'Hello' }],
2895
+ },
2896
+ {
2897
+ role: 'assistant',
2898
+ content: [
2899
+ {
2900
+ type: 'text',
2901
+ text: 'Hi there!',
2902
+ providerOptions: { openai: { itemId: 'msg_existing_123' } },
2903
+ },
2904
+ ],
2905
+ },
2906
+ ],
2907
+ systemMessageMode: 'system',
2908
+ providerOptionsName: 'openai',
2909
+ store: true,
2910
+ hasConversation: false,
2911
+ });
2912
+
2913
+ // With hasConversation false, should use item_reference
2914
+ expect(result.input).toMatchInlineSnapshot(`
2915
+ [
2916
+ {
2917
+ "content": [
2918
+ {
2919
+ "text": "Hello",
2920
+ "type": "input_text",
2921
+ },
2922
+ ],
2923
+ "role": "user",
2924
+ },
2925
+ {
2926
+ "id": "msg_existing_123",
2927
+ "type": "item_reference",
2928
+ },
2929
+ ]
2930
+ `);
2931
+ });
2932
+
2933
+ it('should skip reasoning parts with item IDs when hasConversation is true', async () => {
2934
+ const result = await convertToOpenAIResponsesInput({
2935
+ toolNameMapping: testToolNameMapping,
2936
+ prompt: [
2937
+ {
2938
+ role: 'user',
2939
+ content: [{ type: 'text', text: 'Hello' }],
2940
+ },
2941
+ {
2942
+ role: 'assistant',
2943
+ content: [
2944
+ {
2945
+ type: 'reasoning',
2946
+ text: 'Let me think...',
2947
+ providerOptions: {
2948
+ openai: { itemId: 'reasoning_existing_789' },
2949
+ },
2950
+ },
2951
+ ],
2952
+ },
2953
+ ],
2954
+ systemMessageMode: 'system',
2955
+ providerOptionsName: 'openai',
2956
+ store: true,
2957
+ hasConversation: true,
2958
+ });
2959
+
2960
+ // Reasoning with itemId should be skipped
2961
+ expect(result.input).toMatchInlineSnapshot(`
2962
+ [
2963
+ {
2964
+ "content": [
2965
+ {
2966
+ "text": "Hello",
2967
+ "type": "input_text",
2968
+ },
2969
+ ],
2970
+ "role": "user",
2971
+ },
2972
+ ]
2973
+ `);
2974
+ });
2975
+ });
2976
+ });