@tanstack/openai-base 0.9.9 → 0.9.11
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/dist/esm/adapters/chat-completions-text.js +844 -962
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/chat-completions-tool-converter.js +54 -38
- package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
- package/dist/esm/adapters/responses-text.d.ts +22 -8
- package/dist/esm/adapters/responses-text.js +1193 -1319
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/dist/esm/adapters/responses-tool-converter.js +50 -34
- package/dist/esm/adapters/responses-tool-converter.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -43
- package/dist/esm/tools/apply-patch-tool.js +20 -13
- package/dist/esm/tools/apply-patch-tool.js.map +1 -1
- package/dist/esm/tools/code-interpreter-tool.js +26 -18
- package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
- package/dist/esm/tools/computer-use-tool.js +26 -19
- package/dist/esm/tools/computer-use-tool.js.map +1 -1
- package/dist/esm/tools/custom-tool.js +23 -21
- package/dist/esm/tools/custom-tool.js.map +1 -1
- package/dist/esm/tools/file-search-tool.js +30 -30
- package/dist/esm/tools/file-search-tool.js.map +1 -1
- package/dist/esm/tools/function-tool.js +44 -31
- package/dist/esm/tools/function-tool.js.map +1 -1
- package/dist/esm/tools/image-generation-tool.js +30 -23
- package/dist/esm/tools/image-generation-tool.js.map +1 -1
- package/dist/esm/tools/local-shell-tool.js +20 -13
- package/dist/esm/tools/local-shell-tool.js.map +1 -1
- package/dist/esm/tools/mcp-tool.js +31 -25
- package/dist/esm/tools/mcp-tool.js.map +1 -1
- package/dist/esm/tools/shell-tool.js +25 -21
- package/dist/esm/tools/shell-tool.js.map +1 -1
- package/dist/esm/tools/tool-converter.js +37 -46
- package/dist/esm/tools/tool-converter.js.map +1 -1
- package/dist/esm/tools/web-search-preview-tool.js +26 -15
- package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
- package/dist/esm/tools/web-search-tool.js +27 -15
- package/dist/esm/tools/web-search-tool.js.map +1 -1
- package/dist/esm/usage.js +88 -76
- package/dist/esm/usage.js.map +1 -1
- package/dist/esm/utils/request-options.js +20 -9
- package/dist/esm/utils/request-options.js.map +1 -1
- package/dist/esm/utils/schema-converter.js +193 -146
- package/dist/esm/utils/schema-converter.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/responses-text.ts +84 -57
- package/src/index.ts +4 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -1,967 +1,849 @@
|
|
|
1
|
+
import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
|
|
2
|
+
import { buildChatCompletionsUsage } from "../usage.js";
|
|
3
|
+
import { extractRequestOptions } from "../utils/request-options.js";
|
|
4
|
+
import { convertToolsToChatCompletionsFormat } from "./chat-completions-tool-converter.js";
|
|
1
5
|
import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
|
|
2
6
|
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
3
7
|
import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
4
8
|
import { generateId } from "@tanstack/ai-utils";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* Modern OpenAI-compatible Chat Completions APIs support `tools` and
|
|
845
|
-
* `response_format: json_schema` together in a single streaming request
|
|
846
|
-
* (per issue #605). Subclasses can override — Groq, for instance, must
|
|
847
|
-
* return `false` because its API rejects schema + tools + stream with a
|
|
848
|
-
* 400.
|
|
849
|
-
*/
|
|
850
|
-
supportsCombinedToolsAndSchema() {
|
|
851
|
-
return true;
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Converts a single ModelMessage to the Chat Completions API message format.
|
|
855
|
-
* Override this in subclasses to handle provider-specific message formats.
|
|
856
|
-
*/
|
|
857
|
-
convertMessage(message) {
|
|
858
|
-
if (message.role === "tool") {
|
|
859
|
-
return {
|
|
860
|
-
role: "tool",
|
|
861
|
-
tool_call_id: message.toolCallId || "",
|
|
862
|
-
content: typeof message.content === "string" ? message.content : JSON.stringify(message.content)
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
if (message.role === "assistant") {
|
|
866
|
-
const toolCalls = message.toolCalls?.map((tc) => ({
|
|
867
|
-
id: tc.id,
|
|
868
|
-
type: "function",
|
|
869
|
-
function: {
|
|
870
|
-
name: tc.function.name,
|
|
871
|
-
arguments: typeof tc.function.arguments === "string" ? tc.function.arguments : JSON.stringify(tc.function.arguments)
|
|
872
|
-
}
|
|
873
|
-
}));
|
|
874
|
-
const hasToolCalls = !!toolCalls && toolCalls.length > 0;
|
|
875
|
-
const textContent = this.extractTextContent(message.content);
|
|
876
|
-
return {
|
|
877
|
-
role: "assistant",
|
|
878
|
-
content: hasToolCalls && !textContent ? null : textContent,
|
|
879
|
-
...hasToolCalls ? { tool_calls: toolCalls } : {}
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
const contentParts = this.normalizeContent(message.content);
|
|
883
|
-
if (contentParts.length === 1 && contentParts[0]?.type === "text") {
|
|
884
|
-
const text = contentParts[0].content;
|
|
885
|
-
if (text.length === 0) {
|
|
886
|
-
throw new Error(
|
|
887
|
-
`User message for ${this.name} has empty text content. Empty user messages would produce a paid request with no input; provide non-empty content or omit the message.`
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
return {
|
|
891
|
-
role: "user",
|
|
892
|
-
content: text
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
const parts = [];
|
|
896
|
-
for (const part of contentParts) {
|
|
897
|
-
const converted = this.convertContentPart(part);
|
|
898
|
-
if (!converted) {
|
|
899
|
-
throw new Error(
|
|
900
|
-
`Unsupported content part type for ${this.name}: ${part.type}. Override convertContentPart() in a subclass to handle this type, or remove it from the message.`
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
parts.push(converted);
|
|
904
|
-
}
|
|
905
|
-
if (parts.length === 0) {
|
|
906
|
-
throw new Error(
|
|
907
|
-
`User message for ${this.name} has no content parts. Empty user messages would produce a paid request with no input; provide at least one text/image/audio part or omit the message.`
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
return {
|
|
911
|
-
role: "user",
|
|
912
|
-
content: parts
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* Converts a single ContentPart to the Chat Completions API content part format.
|
|
917
|
-
* Override this in subclasses to handle additional content types or provider-specific metadata.
|
|
918
|
-
*/
|
|
919
|
-
convertContentPart(part) {
|
|
920
|
-
if (part.type === "text") {
|
|
921
|
-
return { type: "text", text: part.content };
|
|
922
|
-
}
|
|
923
|
-
if (part.type === "image") {
|
|
924
|
-
const imageMetadata = part.metadata;
|
|
925
|
-
const imageValue = part.source.value;
|
|
926
|
-
const imageMime = part.source.mimeType || "application/octet-stream";
|
|
927
|
-
const imageUrl = part.source.type === "data" && !imageValue.startsWith("data:") ? `data:${imageMime};base64,${imageValue}` : imageValue;
|
|
928
|
-
return {
|
|
929
|
-
type: "image_url",
|
|
930
|
-
image_url: {
|
|
931
|
-
url: imageUrl,
|
|
932
|
-
detail: imageMetadata?.detail || "auto"
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Normalizes message content to an array of ContentPart.
|
|
940
|
-
* Handles backward compatibility with string content.
|
|
941
|
-
*/
|
|
942
|
-
normalizeContent(content) {
|
|
943
|
-
if (content === null) {
|
|
944
|
-
return [];
|
|
945
|
-
}
|
|
946
|
-
if (typeof content === "string") {
|
|
947
|
-
return [{ type: "text", content }];
|
|
948
|
-
}
|
|
949
|
-
return content;
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Extracts text content from a content value that may be string, null, or ContentPart array.
|
|
953
|
-
*/
|
|
954
|
-
extractTextContent(content) {
|
|
955
|
-
if (content === null) {
|
|
956
|
-
return "";
|
|
957
|
-
}
|
|
958
|
-
if (typeof content === "string") {
|
|
959
|
-
return content;
|
|
960
|
-
}
|
|
961
|
-
return content.filter((p) => p.type === "text").map((p) => p.content).join("");
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
export {
|
|
965
|
-
OpenAIBaseChatCompletionsTextAdapter
|
|
9
|
+
//#region src/adapters/chat-completions-text.ts
|
|
10
|
+
/**
|
|
11
|
+
* Shared implementation of the OpenAI Chat Completions API. Holds the
|
|
12
|
+
* stream-accumulator + AG-UI lifecycle logic and calls the OpenAI SDK
|
|
13
|
+
* directly. Subclasses (ai-openai, ai-grok, ai-groq) construct an OpenAI
|
|
14
|
+
* client with their provider-specific `baseURL` / headers and pass it in.
|
|
15
|
+
*/
|
|
16
|
+
var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
|
|
17
|
+
kind = "text";
|
|
18
|
+
name;
|
|
19
|
+
client;
|
|
20
|
+
constructor(model, name, client) {
|
|
21
|
+
super({}, model);
|
|
22
|
+
this.name = name;
|
|
23
|
+
this.client = client;
|
|
24
|
+
}
|
|
25
|
+
async *chatStream(options) {
|
|
26
|
+
const aguiState = {
|
|
27
|
+
runId: generateId(this.name),
|
|
28
|
+
threadId: options.threadId ?? generateId(this.name),
|
|
29
|
+
messageId: generateId(this.name),
|
|
30
|
+
hasEmittedRunStarted: false
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
const requestParams = this.mapOptionsToRequest(options);
|
|
34
|
+
options.logger.request(`activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`, {
|
|
35
|
+
provider: this.name,
|
|
36
|
+
model: this.model
|
|
37
|
+
});
|
|
38
|
+
const stream = await this.client.chat.completions.create({
|
|
39
|
+
...requestParams,
|
|
40
|
+
stream: true,
|
|
41
|
+
stream_options: { include_usage: true }
|
|
42
|
+
}, extractRequestOptions(options.request));
|
|
43
|
+
yield* this.processStreamChunks(stream, options, aguiState);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const errorPayload = toRunErrorPayload(error, `${this.name}.chatStream failed`);
|
|
46
|
+
const rawEvent = toRunErrorRawEvent(error);
|
|
47
|
+
if (!aguiState.hasEmittedRunStarted) {
|
|
48
|
+
aguiState.hasEmittedRunStarted = true;
|
|
49
|
+
yield {
|
|
50
|
+
type: EventType.RUN_STARTED,
|
|
51
|
+
runId: aguiState.runId,
|
|
52
|
+
threadId: aguiState.threadId,
|
|
53
|
+
model: options.model,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
parentRunId: options.parentRunId
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
yield {
|
|
59
|
+
type: EventType.RUN_ERROR,
|
|
60
|
+
model: options.model,
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
message: errorPayload.message,
|
|
63
|
+
code: errorPayload.code,
|
|
64
|
+
...rawEvent !== void 0 && { rawEvent },
|
|
65
|
+
error: {
|
|
66
|
+
message: errorPayload.message,
|
|
67
|
+
code: errorPayload.code
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
options.logger.errors(`${this.name}.chatStream fatal`, {
|
|
71
|
+
error: errorPayload,
|
|
72
|
+
source: `${this.name}.chatStream`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate structured output using the provider's JSON Schema response format.
|
|
78
|
+
* Uses stream: false to get the complete response in one call.
|
|
79
|
+
*
|
|
80
|
+
* OpenAI-compatible APIs have strict requirements for structured output:
|
|
81
|
+
* - All properties must be in the `required` array
|
|
82
|
+
* - Optional fields should have null added to their type union
|
|
83
|
+
* - additionalProperties must be false for all objects
|
|
84
|
+
*
|
|
85
|
+
* The outputSchema is already JSON Schema (converted in the ai layer).
|
|
86
|
+
* We apply provider-specific transformations for structured output compatibility.
|
|
87
|
+
*/
|
|
88
|
+
async structuredOutput(options) {
|
|
89
|
+
const { chatOptions, outputSchema } = options;
|
|
90
|
+
const requestParams = this.mapOptionsToRequest(chatOptions);
|
|
91
|
+
const jsonSchema = this.makeStructuredOutputCompatible(outputSchema, outputSchema.required);
|
|
92
|
+
try {
|
|
93
|
+
const { stream_options: _, stream: __, ...cleanParams } = requestParams;
|
|
94
|
+
chatOptions.logger.request(`activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`, {
|
|
95
|
+
provider: this.name,
|
|
96
|
+
model: this.model
|
|
97
|
+
});
|
|
98
|
+
const rawText = (await this.client.chat.completions.create({
|
|
99
|
+
...cleanParams,
|
|
100
|
+
stream: false,
|
|
101
|
+
response_format: {
|
|
102
|
+
type: "json_schema",
|
|
103
|
+
json_schema: {
|
|
104
|
+
name: "structured_output",
|
|
105
|
+
schema: jsonSchema,
|
|
106
|
+
strict: true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, extractRequestOptions(chatOptions.request))).choices[0]?.message.content;
|
|
110
|
+
if (typeof rawText !== "string" || rawText.length === 0) throw new Error(`${this.name}.structuredOutput: response contained no content`);
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(rawText);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(`Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
data: this.transformStructuredOutput(parsed),
|
|
119
|
+
rawText
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
|
|
123
|
+
error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
|
|
124
|
+
source: `${this.name}.structuredOutput`
|
|
125
|
+
});
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Stream structured output. Single Chat Completions request with
|
|
131
|
+
* `response_format: json_schema` + `stream: true`. Emits the standard
|
|
132
|
+
* AG-UI lifecycle (`RUN_STARTED` → `REASONING_*?` → `TEXT_MESSAGE_*`
|
|
133
|
+
* carrying raw JSON deltas → terminal `CUSTOM 'structured-output.complete'`
|
|
134
|
+
* → `RUN_FINISHED`). Subclasses use the same SDK-call / reasoning /
|
|
135
|
+
* structured-output-transform hooks as `chatStream` / `structuredOutput` —
|
|
136
|
+
* no per-subclass override should be needed.
|
|
137
|
+
*/
|
|
138
|
+
async *structuredOutputStream(options) {
|
|
139
|
+
const { chatOptions, outputSchema } = options;
|
|
140
|
+
const requestParams = this.mapOptionsToRequest(chatOptions);
|
|
141
|
+
const jsonSchema = this.makeStructuredOutputCompatible(outputSchema, outputSchema.required);
|
|
142
|
+
const timestamp = Date.now();
|
|
143
|
+
const aguiState = {
|
|
144
|
+
runId: generateId(this.name),
|
|
145
|
+
threadId: chatOptions.threadId ?? generateId(this.name),
|
|
146
|
+
messageId: generateId(this.name),
|
|
147
|
+
timestamp,
|
|
148
|
+
hasEmittedRunStarted: false
|
|
149
|
+
};
|
|
150
|
+
let accumulatedContent = "";
|
|
151
|
+
let accumulatedReasoning = "";
|
|
152
|
+
let hasEmittedTextMessageStart = false;
|
|
153
|
+
let reasoningMessageId;
|
|
154
|
+
let hasClosedReasoning = false;
|
|
155
|
+
let stepId;
|
|
156
|
+
let lastModel;
|
|
157
|
+
let lastUsage;
|
|
158
|
+
const closeReasoningLifecycle = function* () {
|
|
159
|
+
if (reasoningMessageId && !hasClosedReasoning) {
|
|
160
|
+
hasClosedReasoning = true;
|
|
161
|
+
yield {
|
|
162
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
163
|
+
messageId: reasoningMessageId,
|
|
164
|
+
model: lastModel || chatOptions.model,
|
|
165
|
+
timestamp
|
|
166
|
+
};
|
|
167
|
+
yield {
|
|
168
|
+
type: EventType.REASONING_END,
|
|
169
|
+
messageId: reasoningMessageId,
|
|
170
|
+
model: lastModel || chatOptions.model,
|
|
171
|
+
timestamp
|
|
172
|
+
};
|
|
173
|
+
if (stepId) yield {
|
|
174
|
+
type: EventType.STEP_FINISHED,
|
|
175
|
+
stepName: stepId,
|
|
176
|
+
stepId,
|
|
177
|
+
model: lastModel || chatOptions.model,
|
|
178
|
+
timestamp,
|
|
179
|
+
content: accumulatedReasoning
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}.bind(this);
|
|
183
|
+
try {
|
|
184
|
+
const { stream_options: _so, stream: _s, tools: _t, ...cleanParams } = requestParams;
|
|
185
|
+
chatOptions.logger.request(`activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`, {
|
|
186
|
+
provider: this.name,
|
|
187
|
+
model: this.model
|
|
188
|
+
});
|
|
189
|
+
const stream = await this.client.chat.completions.create({
|
|
190
|
+
...cleanParams,
|
|
191
|
+
stream: true,
|
|
192
|
+
stream_options: { include_usage: true },
|
|
193
|
+
response_format: {
|
|
194
|
+
type: "json_schema",
|
|
195
|
+
json_schema: {
|
|
196
|
+
name: "structured_output",
|
|
197
|
+
schema: jsonSchema,
|
|
198
|
+
strict: true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}, extractRequestOptions(chatOptions.request));
|
|
202
|
+
for await (const chunk of stream) {
|
|
203
|
+
const choiceForLog = chunk.choices[0];
|
|
204
|
+
chatOptions.logger.provider(`provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? "none"} hasContent=${!!choiceForLog?.delta.content} hasUsage=${!!chunk.usage}`, {
|
|
205
|
+
provider: this.name,
|
|
206
|
+
model: chunk.model
|
|
207
|
+
});
|
|
208
|
+
if (chunk.model) lastModel = chunk.model;
|
|
209
|
+
const usage = chunk.usage ?? chunk.x_groq?.usage;
|
|
210
|
+
if (usage) lastUsage = usage;
|
|
211
|
+
if (!aguiState.hasEmittedRunStarted) {
|
|
212
|
+
aguiState.hasEmittedRunStarted = true;
|
|
213
|
+
yield {
|
|
214
|
+
type: EventType.RUN_STARTED,
|
|
215
|
+
runId: aguiState.runId,
|
|
216
|
+
threadId: aguiState.threadId,
|
|
217
|
+
model: chunk.model || chatOptions.model,
|
|
218
|
+
timestamp,
|
|
219
|
+
parentRunId: chatOptions.parentRunId
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const reasoning = this.extractReasoning(chunk);
|
|
223
|
+
if (reasoning && reasoning.text) {
|
|
224
|
+
if (!reasoningMessageId) {
|
|
225
|
+
reasoningMessageId = generateId(this.name);
|
|
226
|
+
stepId = generateId(this.name);
|
|
227
|
+
yield {
|
|
228
|
+
type: EventType.REASONING_START,
|
|
229
|
+
messageId: reasoningMessageId,
|
|
230
|
+
model: chunk.model || chatOptions.model,
|
|
231
|
+
timestamp
|
|
232
|
+
};
|
|
233
|
+
yield {
|
|
234
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
235
|
+
messageId: reasoningMessageId,
|
|
236
|
+
role: "reasoning",
|
|
237
|
+
model: chunk.model || chatOptions.model,
|
|
238
|
+
timestamp
|
|
239
|
+
};
|
|
240
|
+
yield {
|
|
241
|
+
type: EventType.STEP_STARTED,
|
|
242
|
+
stepName: stepId,
|
|
243
|
+
stepId,
|
|
244
|
+
model: chunk.model || chatOptions.model,
|
|
245
|
+
timestamp,
|
|
246
|
+
stepType: "thinking"
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
accumulatedReasoning += reasoning.text;
|
|
250
|
+
yield {
|
|
251
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
252
|
+
messageId: reasoningMessageId,
|
|
253
|
+
delta: reasoning.text,
|
|
254
|
+
model: chunk.model || chatOptions.model,
|
|
255
|
+
timestamp
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const choice = chunk.choices[0];
|
|
259
|
+
if (!choice) continue;
|
|
260
|
+
const deltaContent = choice.delta.content;
|
|
261
|
+
if (deltaContent) {
|
|
262
|
+
yield* closeReasoningLifecycle();
|
|
263
|
+
if (!hasEmittedTextMessageStart) {
|
|
264
|
+
hasEmittedTextMessageStart = true;
|
|
265
|
+
yield {
|
|
266
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
267
|
+
messageId: aguiState.messageId,
|
|
268
|
+
model: chunk.model || chatOptions.model,
|
|
269
|
+
timestamp,
|
|
270
|
+
role: "assistant"
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
accumulatedContent += deltaContent;
|
|
274
|
+
yield {
|
|
275
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
276
|
+
messageId: aguiState.messageId,
|
|
277
|
+
model: chunk.model || chatOptions.model,
|
|
278
|
+
timestamp,
|
|
279
|
+
delta: deltaContent,
|
|
280
|
+
content: accumulatedContent
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
yield* closeReasoningLifecycle();
|
|
285
|
+
if (hasEmittedTextMessageStart) yield {
|
|
286
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
287
|
+
messageId: aguiState.messageId,
|
|
288
|
+
model: lastModel || chatOptions.model,
|
|
289
|
+
timestamp
|
|
290
|
+
};
|
|
291
|
+
if (accumulatedContent.length === 0) {
|
|
292
|
+
yield {
|
|
293
|
+
type: EventType.RUN_ERROR,
|
|
294
|
+
runId: aguiState.runId,
|
|
295
|
+
model: lastModel || chatOptions.model,
|
|
296
|
+
timestamp,
|
|
297
|
+
message: `${this.name}.structuredOutputStream: response contained no content`,
|
|
298
|
+
code: "empty-response",
|
|
299
|
+
error: {
|
|
300
|
+
message: `${this.name}.structuredOutputStream: response contained no content`,
|
|
301
|
+
code: "empty-response"
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let parsed;
|
|
307
|
+
try {
|
|
308
|
+
parsed = JSON.parse(accumulatedContent);
|
|
309
|
+
} catch {
|
|
310
|
+
yield {
|
|
311
|
+
type: EventType.RUN_ERROR,
|
|
312
|
+
runId: aguiState.runId,
|
|
313
|
+
model: lastModel || chatOptions.model,
|
|
314
|
+
timestamp,
|
|
315
|
+
message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? "..." : ""}`,
|
|
316
|
+
code: "parse-error",
|
|
317
|
+
error: {
|
|
318
|
+
message: "Failed to parse structured output as JSON",
|
|
319
|
+
code: "parse-error"
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const transformed = this.transformStructuredOutput(parsed);
|
|
325
|
+
yield {
|
|
326
|
+
type: EventType.CUSTOM,
|
|
327
|
+
name: "structured-output.complete",
|
|
328
|
+
value: {
|
|
329
|
+
object: transformed,
|
|
330
|
+
raw: accumulatedContent,
|
|
331
|
+
...accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}
|
|
332
|
+
},
|
|
333
|
+
model: lastModel || chatOptions.model,
|
|
334
|
+
timestamp
|
|
335
|
+
};
|
|
336
|
+
yield {
|
|
337
|
+
type: EventType.RUN_FINISHED,
|
|
338
|
+
runId: aguiState.runId,
|
|
339
|
+
threadId: aguiState.threadId,
|
|
340
|
+
model: lastModel || chatOptions.model,
|
|
341
|
+
timestamp,
|
|
342
|
+
finishReason: "stop",
|
|
343
|
+
...lastUsage && { usage: buildChatCompletionsUsage(lastUsage) }
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!aguiState.hasEmittedRunStarted) {
|
|
347
|
+
aguiState.hasEmittedRunStarted = true;
|
|
348
|
+
yield {
|
|
349
|
+
type: EventType.RUN_STARTED,
|
|
350
|
+
runId: aguiState.runId,
|
|
351
|
+
threadId: aguiState.threadId,
|
|
352
|
+
model: chatOptions.model,
|
|
353
|
+
timestamp,
|
|
354
|
+
parentRunId: chatOptions.parentRunId
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const isAbort = this.isAbortError(error);
|
|
358
|
+
const errorPayload = toRunErrorPayload(error, `${this.name}.structuredOutputStream failed`);
|
|
359
|
+
const resolvedCode = isAbort ? "aborted" : errorPayload.code;
|
|
360
|
+
const rawEvent = isAbort ? void 0 : toRunErrorRawEvent(error);
|
|
361
|
+
yield {
|
|
362
|
+
type: EventType.RUN_ERROR,
|
|
363
|
+
runId: aguiState.runId,
|
|
364
|
+
model: lastModel || chatOptions.model,
|
|
365
|
+
timestamp,
|
|
366
|
+
message: errorPayload.message,
|
|
367
|
+
...resolvedCode !== void 0 && { code: resolvedCode },
|
|
368
|
+
...rawEvent !== void 0 && { rawEvent },
|
|
369
|
+
error: {
|
|
370
|
+
message: errorPayload.message,
|
|
371
|
+
...resolvedCode !== void 0 && { code: resolvedCode }
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
|
|
375
|
+
error: errorPayload,
|
|
376
|
+
source: `${this.name}.structuredOutputStream`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Cross-SDK abort detection for `structuredOutputStream`. Default duck-types
|
|
382
|
+
* on `name === 'APIUserAbortError'` (OpenAI SDK), `code === 'ERR_CANCELED'`,
|
|
383
|
+
* and standard `AbortError`s. Subclasses with proprietary error types (e.g.
|
|
384
|
+
* `@openrouter/sdk`'s `RequestAbortedError`) override to extend the check.
|
|
385
|
+
*/
|
|
386
|
+
isAbortError(error) {
|
|
387
|
+
if (!error || typeof error !== "object") return false;
|
|
388
|
+
const e = error;
|
|
389
|
+
return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Applies provider-specific transformations for structured output compatibility.
|
|
393
|
+
* Override this in subclasses to handle provider-specific quirks.
|
|
394
|
+
*/
|
|
395
|
+
makeStructuredOutputCompatible(schema, originalRequired) {
|
|
396
|
+
return makeStructuredOutputCompatible(schema, originalRequired);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Extract reasoning content from a stream chunk. Default returns
|
|
400
|
+
* `undefined` because the OpenAI Chat Completions chunk shape doesn't
|
|
401
|
+
* carry reasoning. The chunk param is typed `unknown` so an override can
|
|
402
|
+
* narrow to its own SDK chunk type without an `as` dance — the base only
|
|
403
|
+
* passes through `processStreamChunks`'s structurally-iterated chunk.
|
|
404
|
+
*/
|
|
405
|
+
extractReasoning(_chunk) {}
|
|
406
|
+
/**
|
|
407
|
+
* Final shaping pass applied to parsed structured-output JSON before it is
|
|
408
|
+
* returned to the caller. Default is a passthrough.
|
|
409
|
+
*
|
|
410
|
+
* Provider `null`s are no longer stripped here: strict-mode null-widening is
|
|
411
|
+
* now undone precisely by the engine (`undoNullWidening`, driven by the
|
|
412
|
+
* schema's null-widening map) the moment the result is captured, so a blind
|
|
413
|
+
* `transformNullsToUndefined` at the adapter would only destroy genuine
|
|
414
|
+
* `.nullable()` nulls. Subclasses may still override to remap or reshape the
|
|
415
|
+
* provider's structured output.
|
|
416
|
+
*/
|
|
417
|
+
transformStructuredOutput(parsed) {
|
|
418
|
+
return parsed;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Processes streamed chunks from the Chat Completions API and yields AG-UI events.
|
|
422
|
+
* Override this in subclasses to handle provider-specific stream behavior.
|
|
423
|
+
*/
|
|
424
|
+
async *processStreamChunks(stream, options, aguiState) {
|
|
425
|
+
let accumulatedContent = "";
|
|
426
|
+
let hasEmittedTextMessageStart = false;
|
|
427
|
+
let lastModel;
|
|
428
|
+
let lastUsage;
|
|
429
|
+
let pendingFinishReason;
|
|
430
|
+
const toolCallsInProgress = /* @__PURE__ */ new Map();
|
|
431
|
+
let reasoningMessageId;
|
|
432
|
+
let hasClosedReasoning = false;
|
|
433
|
+
let stepId;
|
|
434
|
+
let accumulatedReasoning = "";
|
|
435
|
+
let emittedAnyToolCallEnd = false;
|
|
436
|
+
try {
|
|
437
|
+
for await (const chunk of stream) {
|
|
438
|
+
const choiceForLog = chunk.choices[0];
|
|
439
|
+
options.logger.provider(`provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? "none"} hasContent=${!!choiceForLog?.delta.content} hasToolCalls=${!!choiceForLog?.delta.tool_calls} hasUsage=${!!chunk.usage}`, {
|
|
440
|
+
provider: this.name,
|
|
441
|
+
model: chunk.model
|
|
442
|
+
});
|
|
443
|
+
if (chunk.usage) lastUsage = chunk.usage;
|
|
444
|
+
if (chunk.model) lastModel = chunk.model;
|
|
445
|
+
if (!aguiState.hasEmittedRunStarted) {
|
|
446
|
+
aguiState.hasEmittedRunStarted = true;
|
|
447
|
+
yield {
|
|
448
|
+
type: EventType.RUN_STARTED,
|
|
449
|
+
runId: aguiState.runId,
|
|
450
|
+
threadId: aguiState.threadId,
|
|
451
|
+
model: chunk.model || options.model,
|
|
452
|
+
timestamp: Date.now(),
|
|
453
|
+
parentRunId: options.parentRunId
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const reasoning = this.extractReasoning(chunk);
|
|
457
|
+
if (reasoning && reasoning.text) {
|
|
458
|
+
if (!reasoningMessageId) {
|
|
459
|
+
reasoningMessageId = generateId(this.name);
|
|
460
|
+
stepId = generateId(this.name);
|
|
461
|
+
yield {
|
|
462
|
+
type: EventType.REASONING_START,
|
|
463
|
+
messageId: reasoningMessageId,
|
|
464
|
+
model: chunk.model || options.model,
|
|
465
|
+
timestamp: Date.now()
|
|
466
|
+
};
|
|
467
|
+
yield {
|
|
468
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
469
|
+
messageId: reasoningMessageId,
|
|
470
|
+
role: "reasoning",
|
|
471
|
+
model: chunk.model || options.model,
|
|
472
|
+
timestamp: Date.now()
|
|
473
|
+
};
|
|
474
|
+
yield {
|
|
475
|
+
type: EventType.STEP_STARTED,
|
|
476
|
+
stepName: stepId,
|
|
477
|
+
stepId,
|
|
478
|
+
model: chunk.model || options.model,
|
|
479
|
+
timestamp: Date.now(),
|
|
480
|
+
stepType: "thinking"
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
accumulatedReasoning += reasoning.text;
|
|
484
|
+
yield {
|
|
485
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
486
|
+
messageId: reasoningMessageId,
|
|
487
|
+
delta: reasoning.text,
|
|
488
|
+
model: chunk.model || options.model,
|
|
489
|
+
timestamp: Date.now()
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const choice = chunk.choices[0];
|
|
493
|
+
if (!choice) continue;
|
|
494
|
+
const delta = choice.delta;
|
|
495
|
+
const deltaContent = delta.content;
|
|
496
|
+
const deltaToolCalls = delta.tool_calls;
|
|
497
|
+
if (deltaContent) {
|
|
498
|
+
if (reasoningMessageId && !hasClosedReasoning) {
|
|
499
|
+
hasClosedReasoning = true;
|
|
500
|
+
yield {
|
|
501
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
502
|
+
messageId: reasoningMessageId,
|
|
503
|
+
model: chunk.model || options.model,
|
|
504
|
+
timestamp: Date.now()
|
|
505
|
+
};
|
|
506
|
+
yield {
|
|
507
|
+
type: EventType.REASONING_END,
|
|
508
|
+
messageId: reasoningMessageId,
|
|
509
|
+
model: chunk.model || options.model,
|
|
510
|
+
timestamp: Date.now()
|
|
511
|
+
};
|
|
512
|
+
if (stepId) yield {
|
|
513
|
+
type: EventType.STEP_FINISHED,
|
|
514
|
+
stepName: stepId,
|
|
515
|
+
stepId,
|
|
516
|
+
model: chunk.model || options.model,
|
|
517
|
+
timestamp: Date.now(),
|
|
518
|
+
content: accumulatedReasoning
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (!hasEmittedTextMessageStart) {
|
|
522
|
+
hasEmittedTextMessageStart = true;
|
|
523
|
+
yield {
|
|
524
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
525
|
+
messageId: aguiState.messageId,
|
|
526
|
+
model: chunk.model || options.model,
|
|
527
|
+
timestamp: Date.now(),
|
|
528
|
+
role: "assistant"
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
accumulatedContent += deltaContent;
|
|
532
|
+
yield {
|
|
533
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
534
|
+
messageId: aguiState.messageId,
|
|
535
|
+
model: chunk.model || options.model,
|
|
536
|
+
timestamp: Date.now(),
|
|
537
|
+
delta: deltaContent,
|
|
538
|
+
content: accumulatedContent
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (deltaToolCalls) for (const toolCallDelta of deltaToolCalls) {
|
|
542
|
+
const index = toolCallDelta.index;
|
|
543
|
+
let toolCall = toolCallsInProgress.get(index);
|
|
544
|
+
if (!toolCall) {
|
|
545
|
+
toolCall = {
|
|
546
|
+
id: toolCallDelta.id || "",
|
|
547
|
+
name: toolCallDelta.function?.name || "",
|
|
548
|
+
arguments: "",
|
|
549
|
+
started: false
|
|
550
|
+
};
|
|
551
|
+
toolCallsInProgress.set(index, toolCall);
|
|
552
|
+
}
|
|
553
|
+
if (toolCallDelta.id) toolCall.id = toolCallDelta.id;
|
|
554
|
+
if (toolCallDelta.function?.name) toolCall.name = toolCallDelta.function.name;
|
|
555
|
+
if (toolCallDelta.function?.arguments) toolCall.arguments += toolCallDelta.function.arguments;
|
|
556
|
+
if (toolCall.id && toolCall.name && !toolCall.started) {
|
|
557
|
+
toolCall.started = true;
|
|
558
|
+
yield {
|
|
559
|
+
type: EventType.TOOL_CALL_START,
|
|
560
|
+
toolCallId: toolCall.id,
|
|
561
|
+
toolCallName: toolCall.name,
|
|
562
|
+
toolName: toolCall.name,
|
|
563
|
+
parentMessageId: aguiState.messageId,
|
|
564
|
+
model: chunk.model || options.model,
|
|
565
|
+
timestamp: Date.now(),
|
|
566
|
+
index
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (toolCallDelta.function?.arguments && toolCall.started) yield {
|
|
570
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
571
|
+
toolCallId: toolCall.id,
|
|
572
|
+
model: chunk.model || options.model,
|
|
573
|
+
timestamp: Date.now(),
|
|
574
|
+
delta: toolCallDelta.function.arguments
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (choice.finish_reason) {
|
|
578
|
+
if (choice.finish_reason === "tool_calls" || toolCallsInProgress.size > 0) {
|
|
579
|
+
for (const [, toolCall] of toolCallsInProgress) {
|
|
580
|
+
if (!toolCall.started) continue;
|
|
581
|
+
let parsedInput = {};
|
|
582
|
+
if (toolCall.arguments) try {
|
|
583
|
+
const parsed = JSON.parse(toolCall.arguments);
|
|
584
|
+
parsedInput = parsed && typeof parsed === "object" ? parsed : {};
|
|
585
|
+
} catch (parseError) {
|
|
586
|
+
options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed`, {
|
|
587
|
+
error: toRunErrorPayload(parseError, `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`),
|
|
588
|
+
source: `${this.name}.processStreamChunks`,
|
|
589
|
+
toolCallId: toolCall.id,
|
|
590
|
+
toolName: toolCall.name,
|
|
591
|
+
rawArguments: toolCall.arguments
|
|
592
|
+
});
|
|
593
|
+
parsedInput = {};
|
|
594
|
+
}
|
|
595
|
+
yield {
|
|
596
|
+
type: EventType.TOOL_CALL_END,
|
|
597
|
+
toolCallId: toolCall.id,
|
|
598
|
+
toolCallName: toolCall.name,
|
|
599
|
+
toolName: toolCall.name,
|
|
600
|
+
model: chunk.model || options.model,
|
|
601
|
+
timestamp: Date.now(),
|
|
602
|
+
input: parsedInput
|
|
603
|
+
};
|
|
604
|
+
emittedAnyToolCallEnd = true;
|
|
605
|
+
}
|
|
606
|
+
toolCallsInProgress.clear();
|
|
607
|
+
}
|
|
608
|
+
if (hasEmittedTextMessageStart) {
|
|
609
|
+
yield {
|
|
610
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
611
|
+
messageId: aguiState.messageId,
|
|
612
|
+
model: chunk.model || options.model,
|
|
613
|
+
timestamp: Date.now()
|
|
614
|
+
};
|
|
615
|
+
hasEmittedTextMessageStart = false;
|
|
616
|
+
}
|
|
617
|
+
pendingFinishReason = choice.finish_reason;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (aguiState.hasEmittedRunStarted) {
|
|
621
|
+
let pendingToolCount = 0;
|
|
622
|
+
for (const [, toolCall] of toolCallsInProgress) {
|
|
623
|
+
if (!toolCall.started) continue;
|
|
624
|
+
let parsedInput = {};
|
|
625
|
+
if (toolCall.arguments) try {
|
|
626
|
+
const parsed = JSON.parse(toolCall.arguments);
|
|
627
|
+
parsedInput = parsed && typeof parsed === "object" ? parsed : {};
|
|
628
|
+
} catch (parseError) {
|
|
629
|
+
options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed (drain)`, {
|
|
630
|
+
error: toRunErrorPayload(parseError, `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`),
|
|
631
|
+
source: `${this.name}.processStreamChunks`,
|
|
632
|
+
toolCallId: toolCall.id,
|
|
633
|
+
toolName: toolCall.name,
|
|
634
|
+
rawArguments: toolCall.arguments
|
|
635
|
+
});
|
|
636
|
+
parsedInput = {};
|
|
637
|
+
}
|
|
638
|
+
yield {
|
|
639
|
+
type: EventType.TOOL_CALL_END,
|
|
640
|
+
toolCallId: toolCall.id,
|
|
641
|
+
toolCallName: toolCall.name,
|
|
642
|
+
toolName: toolCall.name,
|
|
643
|
+
model: lastModel || options.model,
|
|
644
|
+
timestamp: Date.now(),
|
|
645
|
+
input: parsedInput
|
|
646
|
+
};
|
|
647
|
+
pendingToolCount += 1;
|
|
648
|
+
emittedAnyToolCallEnd = true;
|
|
649
|
+
}
|
|
650
|
+
toolCallsInProgress.clear();
|
|
651
|
+
if (hasEmittedTextMessageStart) yield {
|
|
652
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
653
|
+
messageId: aguiState.messageId,
|
|
654
|
+
model: lastModel || options.model,
|
|
655
|
+
timestamp: Date.now()
|
|
656
|
+
};
|
|
657
|
+
if (reasoningMessageId && !hasClosedReasoning) {
|
|
658
|
+
hasClosedReasoning = true;
|
|
659
|
+
yield {
|
|
660
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
661
|
+
messageId: reasoningMessageId,
|
|
662
|
+
model: lastModel || options.model,
|
|
663
|
+
timestamp: Date.now()
|
|
664
|
+
};
|
|
665
|
+
yield {
|
|
666
|
+
type: EventType.REASONING_END,
|
|
667
|
+
messageId: reasoningMessageId,
|
|
668
|
+
model: lastModel || options.model,
|
|
669
|
+
timestamp: Date.now()
|
|
670
|
+
};
|
|
671
|
+
if (stepId) yield {
|
|
672
|
+
type: EventType.STEP_FINISHED,
|
|
673
|
+
stepName: stepId,
|
|
674
|
+
stepId,
|
|
675
|
+
model: lastModel || options.model,
|
|
676
|
+
timestamp: Date.now(),
|
|
677
|
+
content: accumulatedReasoning
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const finishReason = emittedAnyToolCallEnd ? "tool_calls" : pendingFinishReason === "tool_calls" ? "stop" : pendingFinishReason === "function_call" ? "tool_calls" : pendingFinishReason ?? "stop";
|
|
681
|
+
yield {
|
|
682
|
+
type: EventType.RUN_FINISHED,
|
|
683
|
+
runId: aguiState.runId,
|
|
684
|
+
threadId: aguiState.threadId,
|
|
685
|
+
model: lastModel || options.model,
|
|
686
|
+
timestamp: Date.now(),
|
|
687
|
+
...lastUsage && { usage: buildChatCompletionsUsage(lastUsage) },
|
|
688
|
+
finishReason
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
} catch (error) {
|
|
692
|
+
const errorPayload = toRunErrorPayload(error, `${this.name}.processStreamChunks failed`);
|
|
693
|
+
const rawEvent = toRunErrorRawEvent(error);
|
|
694
|
+
options.logger.errors(`${this.name}.processStreamChunks fatal`, {
|
|
695
|
+
error: errorPayload,
|
|
696
|
+
source: `${this.name}.processStreamChunks`
|
|
697
|
+
});
|
|
698
|
+
yield {
|
|
699
|
+
type: EventType.RUN_ERROR,
|
|
700
|
+
model: options.model,
|
|
701
|
+
timestamp: Date.now(),
|
|
702
|
+
message: errorPayload.message,
|
|
703
|
+
...errorPayload.code !== void 0 && { code: errorPayload.code },
|
|
704
|
+
...rawEvent !== void 0 && { rawEvent },
|
|
705
|
+
error: {
|
|
706
|
+
message: errorPayload.message,
|
|
707
|
+
...errorPayload.code !== void 0 && { code: errorPayload.code }
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Maps common TextOptions to Chat Completions API request format.
|
|
714
|
+
* Override this in subclasses to add provider-specific options.
|
|
715
|
+
*/
|
|
716
|
+
mapOptionsToRequest(options) {
|
|
717
|
+
const tools = options.tools ? convertToolsToChatCompletionsFormat(options.tools, this.makeStructuredOutputCompatible.bind(this)) : void 0;
|
|
718
|
+
const messages = [];
|
|
719
|
+
const systemPrompts = normalizeSystemPrompts(options.systemPrompts);
|
|
720
|
+
if (systemPrompts.length > 0) messages.push({
|
|
721
|
+
role: "system",
|
|
722
|
+
content: systemPrompts.map((p) => p.content).join("\n")
|
|
723
|
+
});
|
|
724
|
+
for (const message of options.messages) messages.push(this.convertMessage(message));
|
|
725
|
+
const modelOptions = options.modelOptions;
|
|
726
|
+
const combinedSchema = options.outputSchema;
|
|
727
|
+
const responseFormat = combinedSchema ? { response_format: {
|
|
728
|
+
type: "json_schema",
|
|
729
|
+
json_schema: {
|
|
730
|
+
name: "structured_output",
|
|
731
|
+
schema: this.makeStructuredOutputCompatible(combinedSchema, Array.isArray(combinedSchema.required) ? combinedSchema.required : void 0),
|
|
732
|
+
strict: true
|
|
733
|
+
}
|
|
734
|
+
} } : void 0;
|
|
735
|
+
return {
|
|
736
|
+
...modelOptions,
|
|
737
|
+
model: options.model,
|
|
738
|
+
messages,
|
|
739
|
+
...tools && tools.length > 0 && { tools },
|
|
740
|
+
...responseFormat ?? {},
|
|
741
|
+
stream: true
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Modern OpenAI-compatible Chat Completions APIs support `tools` and
|
|
746
|
+
* `response_format: json_schema` together in a single streaming request
|
|
747
|
+
* (per issue #605). Subclasses can override — Groq, for instance, must
|
|
748
|
+
* return `false` because its API rejects schema + tools + stream with a
|
|
749
|
+
* 400.
|
|
750
|
+
*/
|
|
751
|
+
supportsCombinedToolsAndSchema() {
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Converts a single ModelMessage to the Chat Completions API message format.
|
|
756
|
+
* Override this in subclasses to handle provider-specific message formats.
|
|
757
|
+
*/
|
|
758
|
+
convertMessage(message) {
|
|
759
|
+
if (message.role === "tool") return {
|
|
760
|
+
role: "tool",
|
|
761
|
+
tool_call_id: message.toolCallId || "",
|
|
762
|
+
content: typeof message.content === "string" ? message.content : JSON.stringify(message.content)
|
|
763
|
+
};
|
|
764
|
+
if (message.role === "assistant") {
|
|
765
|
+
const toolCalls = message.toolCalls?.map((tc) => ({
|
|
766
|
+
id: tc.id,
|
|
767
|
+
type: "function",
|
|
768
|
+
function: {
|
|
769
|
+
name: tc.function.name,
|
|
770
|
+
arguments: typeof tc.function.arguments === "string" ? tc.function.arguments : JSON.stringify(tc.function.arguments)
|
|
771
|
+
}
|
|
772
|
+
}));
|
|
773
|
+
const hasToolCalls = !!toolCalls && toolCalls.length > 0;
|
|
774
|
+
const textContent = this.extractTextContent(message.content);
|
|
775
|
+
return {
|
|
776
|
+
role: "assistant",
|
|
777
|
+
content: hasToolCalls && !textContent ? null : textContent,
|
|
778
|
+
...hasToolCalls ? { tool_calls: toolCalls } : {}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const contentParts = this.normalizeContent(message.content);
|
|
782
|
+
if (contentParts.length === 1 && contentParts[0]?.type === "text") {
|
|
783
|
+
const text = contentParts[0].content;
|
|
784
|
+
if (text.length === 0) throw new Error(`User message for ${this.name} has empty text content. Empty user messages would produce a paid request with no input; provide non-empty content or omit the message.`);
|
|
785
|
+
return {
|
|
786
|
+
role: "user",
|
|
787
|
+
content: text
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const parts = [];
|
|
791
|
+
for (const part of contentParts) {
|
|
792
|
+
const converted = this.convertContentPart(part);
|
|
793
|
+
if (!converted) throw new Error(`Unsupported content part type for ${this.name}: ${part.type}. Override convertContentPart() in a subclass to handle this type, or remove it from the message.`);
|
|
794
|
+
parts.push(converted);
|
|
795
|
+
}
|
|
796
|
+
if (parts.length === 0) throw new Error(`User message for ${this.name} has no content parts. Empty user messages would produce a paid request with no input; provide at least one text/image/audio part or omit the message.`);
|
|
797
|
+
return {
|
|
798
|
+
role: "user",
|
|
799
|
+
content: parts
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Converts a single ContentPart to the Chat Completions API content part format.
|
|
804
|
+
* Override this in subclasses to handle additional content types or provider-specific metadata.
|
|
805
|
+
*/
|
|
806
|
+
convertContentPart(part) {
|
|
807
|
+
if (part.type === "text") return {
|
|
808
|
+
type: "text",
|
|
809
|
+
text: part.content
|
|
810
|
+
};
|
|
811
|
+
if (part.type === "image") {
|
|
812
|
+
const imageMetadata = part.metadata;
|
|
813
|
+
const imageValue = part.source.value;
|
|
814
|
+
const imageMime = part.source.mimeType || "application/octet-stream";
|
|
815
|
+
return {
|
|
816
|
+
type: "image_url",
|
|
817
|
+
image_url: {
|
|
818
|
+
url: part.source.type === "data" && !imageValue.startsWith("data:") ? `data:${imageMime};base64,${imageValue}` : imageValue,
|
|
819
|
+
detail: imageMetadata?.detail || "auto"
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Normalizes message content to an array of ContentPart.
|
|
827
|
+
* Handles backward compatibility with string content.
|
|
828
|
+
*/
|
|
829
|
+
normalizeContent(content) {
|
|
830
|
+
if (content === null) return [];
|
|
831
|
+
if (typeof content === "string") return [{
|
|
832
|
+
type: "text",
|
|
833
|
+
content
|
|
834
|
+
}];
|
|
835
|
+
return content;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Extracts text content from a content value that may be string, null, or ContentPart array.
|
|
839
|
+
*/
|
|
840
|
+
extractTextContent(content) {
|
|
841
|
+
if (content === null) return "";
|
|
842
|
+
if (typeof content === "string") return content;
|
|
843
|
+
return content.filter((p) => p.type === "text").map((p) => p.content).join("");
|
|
844
|
+
}
|
|
966
845
|
};
|
|
967
|
-
//#
|
|
846
|
+
//#endregion
|
|
847
|
+
export { OpenAIBaseChatCompletionsTextAdapter };
|
|
848
|
+
|
|
849
|
+
//# sourceMappingURL=chat-completions-text.js.map
|