@eum-agent/llm-provider-chatgpt 0.0.0-stage → 1.1.0

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/index.js ADDED
@@ -0,0 +1,2926 @@
1
+ import { z } from "zod";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { setImmediate, setTimeout } from "node:timers/promises";
4
+ import { createServer } from "node:http";
5
+ //#endregion
6
+ //#region src/catalog.ts
7
+ /** This extension's endpoint declarations and offline catalogue seed. */
8
+ const catalog = {
9
+ vendors: [{
10
+ kind: "vendor",
11
+ id: "chatgpt",
12
+ logo: "data:image/svg+xml,%3Csvg%20fill%3D%22currentColor%22%20fill-rule%3D%22evenodd%22%20height%3D%2224%22%20style%3D%22flex%3Anone%3Bline-height%3A1%22%20viewBox%3D%220%200%2024%2024%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EOpenAI%3C%2Ftitle%3E%3Cpath%20d%3D%22M9.205%208.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357%201.356-.523%202.117-.523%202.854%200%204.662%202.212%204.662%204.566%200%20.167%200%20.357-.024.547l-4.71-2.759a.797.797%200%2000-.856%200l-5.97%203.473zm10.609%208.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473%201.95-1.118a.433.433%200%2001.476%200l4.543%202.617c1.309.76%202.189%202.378%202.189%203.948%200%201.808-1.07%203.473-2.76%204.163zM7.802%2012.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545%201.95-4.472%204.591-4.472%201%200%201.927.333%202.712.928L8.23%205.067c-.285.166-.428.404-.428.737v6.898zM12%2015.128l-2.795-1.57v-3.33L12%208.658l2.795%201.57v3.33L12%2015.128zm1.796%207.23c-1%200-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974%201.142c.167.095.238.238.238.428v5.233c0%202.545-1.974%204.472-4.614%204.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482%204.482%200%20014.21%206.327v5.423c0%20.333.143.571.428.738l5.947%203.449-1.95%201.118a.432.432%200%2001-.476%200zm-.262%203.9c-2.688%200-4.662-2.021-4.662-4.519%200-.19.024-.38.047-.57l4.686%202.71c.286.167.571.167.856%200l5.97-3.448v2.26c0%20.19-.07.333-.237.428l-4.543%202.616c-.619.357-1.356.523-2.117.523zm5.899%202.83a5.947%205.947%200%20005.827-4.756C22.287%2018.339%2024%2015.84%2024%2013.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498%200-3.401-2.759-5.947-5.946-5.947-.642%200-1.26.095-1.88.31A5.962%205.962%200%200010.205%200a5.947%205.947%200%2000-5.827%204.757C1.713%205.447%200%207.945%200%2010.49c0%201.666.713%203.283%201.998%204.448-.119.5-.19%201-.19%201.499%200%203.401%202.759%205.946%205.946%205.946.642%200%201.26-.095%201.88-.309a5.96%205.96%200%20004.162%201.713z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E",
13
+ name: "ChatGPT",
14
+ region: null,
15
+ market: "international",
16
+ catalogKey: "openai",
17
+ canListModels: true,
18
+ endpoints: [{
19
+ type: "chatgpt",
20
+ endpointId: "subscription",
21
+ protocolIds: ["chatgpt-subscription"],
22
+ baseUrl: "https://chatgpt.com/backend-api/codex"
23
+ }]
24
+ }],
25
+ snapshot: { "chatgpt": {
26
+ "gpt-5-nano": {
27
+ "id": "gpt-5-nano",
28
+ "name": "GPT-5 Nano",
29
+ "family": "gpt-nano",
30
+ "reasoning": true,
31
+ "reasoning_options": [{
32
+ "type": "effort",
33
+ "values": [
34
+ "minimal",
35
+ "low",
36
+ "medium",
37
+ "high"
38
+ ]
39
+ }],
40
+ "temperature": false,
41
+ "tool_call": true,
42
+ "attachment": true,
43
+ "modalities": {
44
+ "input": ["text", "image"],
45
+ "output": ["text"]
46
+ },
47
+ "limit": {
48
+ "context": 4e5,
49
+ "output": 128e3
50
+ },
51
+ "release_date": "2025-08-07"
52
+ },
53
+ "gpt-4.1-nano": {
54
+ "id": "gpt-4.1-nano",
55
+ "name": "GPT-4.1 nano",
56
+ "family": "gpt-nano",
57
+ "reasoning": false,
58
+ "temperature": true,
59
+ "tool_call": true,
60
+ "attachment": true,
61
+ "modalities": {
62
+ "input": ["text", "image"],
63
+ "output": ["text"]
64
+ },
65
+ "limit": {
66
+ "context": 1047576,
67
+ "output": 32768
68
+ },
69
+ "release_date": "2025-04-14"
70
+ },
71
+ "gpt-4o-2024-05-13": {
72
+ "id": "gpt-4o-2024-05-13",
73
+ "name": "GPT-4o (2024-05-13)",
74
+ "family": "gpt",
75
+ "reasoning": false,
76
+ "temperature": true,
77
+ "tool_call": true,
78
+ "attachment": true,
79
+ "modalities": {
80
+ "input": ["text", "image"],
81
+ "output": ["text"]
82
+ },
83
+ "limit": {
84
+ "context": 128e3,
85
+ "output": 4096
86
+ },
87
+ "release_date": "2024-05-13"
88
+ },
89
+ "gpt-5-pro": {
90
+ "id": "gpt-5-pro",
91
+ "name": "GPT-5 Pro",
92
+ "family": "gpt-pro",
93
+ "reasoning": true,
94
+ "reasoning_options": [{
95
+ "type": "effort",
96
+ "values": ["high"]
97
+ }],
98
+ "temperature": false,
99
+ "tool_call": true,
100
+ "attachment": true,
101
+ "modalities": {
102
+ "input": ["text", "image"],
103
+ "output": ["text"]
104
+ },
105
+ "limit": {
106
+ "context": 4e5,
107
+ "output": 272e3
108
+ },
109
+ "release_date": "2025-10-06"
110
+ },
111
+ "chatgpt-image-latest": {
112
+ "id": "chatgpt-image-latest",
113
+ "name": "chatgpt-image-latest",
114
+ "family": "gpt-image",
115
+ "reasoning": false,
116
+ "temperature": false,
117
+ "tool_call": false,
118
+ "attachment": true,
119
+ "modalities": {
120
+ "input": ["text", "image"],
121
+ "output": ["text", "image"]
122
+ },
123
+ "limit": {
124
+ "context": 0,
125
+ "output": 0
126
+ },
127
+ "release_date": "2025-12-16"
128
+ },
129
+ "gpt-5.6-sol": {
130
+ "id": "gpt-5.6-sol",
131
+ "name": "GPT-5.6 Sol",
132
+ "family": "gpt-sol",
133
+ "reasoning": true,
134
+ "reasoning_options": [{
135
+ "type": "effort",
136
+ "values": [
137
+ "none",
138
+ "low",
139
+ "medium",
140
+ "high",
141
+ "xhigh",
142
+ "max"
143
+ ]
144
+ }],
145
+ "temperature": false,
146
+ "tool_call": true,
147
+ "attachment": true,
148
+ "modalities": {
149
+ "input": [
150
+ "text",
151
+ "image",
152
+ "pdf"
153
+ ],
154
+ "output": ["text"]
155
+ },
156
+ "limit": {
157
+ "context": 105e4,
158
+ "output": 128e3
159
+ },
160
+ "release_date": "2026-07-09"
161
+ },
162
+ "gpt-4o-2024-08-06": {
163
+ "id": "gpt-4o-2024-08-06",
164
+ "name": "GPT-4o (2024-08-06)",
165
+ "family": "gpt",
166
+ "reasoning": false,
167
+ "temperature": true,
168
+ "tool_call": true,
169
+ "attachment": true,
170
+ "modalities": {
171
+ "input": ["text", "image"],
172
+ "output": ["text"]
173
+ },
174
+ "limit": {
175
+ "context": 128e3,
176
+ "output": 16384
177
+ },
178
+ "release_date": "2024-08-06"
179
+ },
180
+ "gpt-6-astra": {
181
+ "id": "gpt-6-astra",
182
+ "name": "GPT-6 Astra",
183
+ "family": "gpt-astra",
184
+ "reasoning": true,
185
+ "reasoning_options": [{
186
+ "type": "effort",
187
+ "values": [
188
+ "low",
189
+ "medium",
190
+ "high",
191
+ "xhigh",
192
+ "max"
193
+ ]
194
+ }],
195
+ "temperature": false,
196
+ "tool_call": true,
197
+ "attachment": true,
198
+ "modalities": {
199
+ "input": [
200
+ "text",
201
+ "image",
202
+ "pdf"
203
+ ],
204
+ "output": ["text"]
205
+ },
206
+ "limit": {
207
+ "context": 105e4,
208
+ "output": 128e3
209
+ },
210
+ "release_date": "2026-09-04"
211
+ },
212
+ "gpt-5.2-pro": {
213
+ "id": "gpt-5.2-pro",
214
+ "name": "GPT-5.2 Pro",
215
+ "family": "gpt-pro",
216
+ "reasoning": true,
217
+ "reasoning_options": [{
218
+ "type": "effort",
219
+ "values": [
220
+ "medium",
221
+ "high",
222
+ "xhigh"
223
+ ]
224
+ }],
225
+ "temperature": false,
226
+ "tool_call": true,
227
+ "attachment": true,
228
+ "modalities": {
229
+ "input": ["text", "image"],
230
+ "output": ["text"]
231
+ },
232
+ "limit": {
233
+ "context": 4e5,
234
+ "output": 128e3
235
+ },
236
+ "release_date": "2025-12-11"
237
+ },
238
+ "gpt-5.3-codex-spark": {
239
+ "id": "gpt-5.3-codex-spark",
240
+ "name": "GPT-5.3 Codex Spark",
241
+ "family": "gpt-codex-spark",
242
+ "reasoning": true,
243
+ "reasoning_options": [{
244
+ "type": "effort",
245
+ "values": [
246
+ "none",
247
+ "low",
248
+ "medium",
249
+ "high",
250
+ "xhigh"
251
+ ]
252
+ }],
253
+ "temperature": false,
254
+ "tool_call": true,
255
+ "attachment": true,
256
+ "modalities": {
257
+ "input": [
258
+ "text",
259
+ "image",
260
+ "pdf"
261
+ ],
262
+ "output": ["text"]
263
+ },
264
+ "limit": {
265
+ "context": 128e3,
266
+ "output": 32e3
267
+ },
268
+ "release_date": "2026-02-05"
269
+ },
270
+ "gpt-4.1-mini": {
271
+ "id": "gpt-4.1-mini",
272
+ "name": "GPT-4.1 mini",
273
+ "family": "gpt-mini",
274
+ "reasoning": false,
275
+ "temperature": true,
276
+ "tool_call": true,
277
+ "attachment": true,
278
+ "modalities": {
279
+ "input": [
280
+ "text",
281
+ "image",
282
+ "pdf"
283
+ ],
284
+ "output": ["text"]
285
+ },
286
+ "limit": {
287
+ "context": 1047576,
288
+ "output": 32768
289
+ },
290
+ "release_date": "2025-04-14"
291
+ },
292
+ "gpt-5.4": {
293
+ "id": "gpt-5.4",
294
+ "name": "GPT-5.4",
295
+ "family": "gpt",
296
+ "reasoning": true,
297
+ "reasoning_options": [{
298
+ "type": "effort",
299
+ "values": [
300
+ "none",
301
+ "low",
302
+ "medium",
303
+ "high",
304
+ "xhigh"
305
+ ]
306
+ }],
307
+ "temperature": true,
308
+ "tool_call": true,
309
+ "attachment": true,
310
+ "modalities": {
311
+ "input": [
312
+ "text",
313
+ "image",
314
+ "pdf"
315
+ ],
316
+ "output": ["text"]
317
+ },
318
+ "limit": {
319
+ "context": 105e4,
320
+ "output": 128e3
321
+ },
322
+ "release_date": "2026-03-05"
323
+ },
324
+ "gpt-4-turbo": {
325
+ "id": "gpt-4-turbo",
326
+ "name": "GPT-4 Turbo",
327
+ "family": "gpt",
328
+ "reasoning": false,
329
+ "temperature": true,
330
+ "tool_call": true,
331
+ "attachment": true,
332
+ "modalities": {
333
+ "input": ["text", "image"],
334
+ "output": ["text"]
335
+ },
336
+ "limit": {
337
+ "context": 128e3,
338
+ "output": 4096
339
+ },
340
+ "release_date": "2023-11-06"
341
+ },
342
+ "gpt-5.1": {
343
+ "id": "gpt-5.1",
344
+ "name": "GPT-5.1",
345
+ "family": "gpt",
346
+ "reasoning": true,
347
+ "reasoning_options": [{
348
+ "type": "effort",
349
+ "values": [
350
+ "none",
351
+ "low",
352
+ "medium",
353
+ "high"
354
+ ]
355
+ }],
356
+ "temperature": true,
357
+ "tool_call": true,
358
+ "attachment": true,
359
+ "modalities": {
360
+ "input": ["text", "image"],
361
+ "output": ["text"]
362
+ },
363
+ "limit": {
364
+ "context": 4e5,
365
+ "output": 128e3
366
+ },
367
+ "release_date": "2025-11-13"
368
+ },
369
+ "o1": {
370
+ "id": "o1",
371
+ "name": "o1",
372
+ "family": "o",
373
+ "reasoning": true,
374
+ "reasoning_options": [{
375
+ "type": "effort",
376
+ "values": [
377
+ "low",
378
+ "medium",
379
+ "high"
380
+ ]
381
+ }],
382
+ "temperature": false,
383
+ "tool_call": true,
384
+ "attachment": true,
385
+ "modalities": {
386
+ "input": [
387
+ "text",
388
+ "image",
389
+ "pdf"
390
+ ],
391
+ "output": ["text"]
392
+ },
393
+ "limit": {
394
+ "context": 2e5,
395
+ "output": 1e5
396
+ },
397
+ "release_date": "2024-12-05"
398
+ },
399
+ "gpt-4o": {
400
+ "id": "gpt-4o",
401
+ "name": "GPT-4o",
402
+ "family": "gpt",
403
+ "reasoning": false,
404
+ "temperature": true,
405
+ "tool_call": true,
406
+ "attachment": true,
407
+ "modalities": {
408
+ "input": [
409
+ "text",
410
+ "image",
411
+ "pdf"
412
+ ],
413
+ "output": ["text"]
414
+ },
415
+ "limit": {
416
+ "context": 128e3,
417
+ "output": 16384
418
+ },
419
+ "release_date": "2024-05-13"
420
+ },
421
+ "gpt-5.6-luna": {
422
+ "id": "gpt-5.6-luna",
423
+ "name": "GPT-5.6 Luna",
424
+ "family": "gpt-luna",
425
+ "reasoning": true,
426
+ "reasoning_options": [{
427
+ "type": "effort",
428
+ "values": [
429
+ "none",
430
+ "low",
431
+ "medium",
432
+ "high",
433
+ "xhigh",
434
+ "max"
435
+ ]
436
+ }],
437
+ "temperature": false,
438
+ "tool_call": true,
439
+ "attachment": true,
440
+ "modalities": {
441
+ "input": [
442
+ "text",
443
+ "image",
444
+ "pdf"
445
+ ],
446
+ "output": ["text"]
447
+ },
448
+ "limit": {
449
+ "context": 105e4,
450
+ "output": 128e3
451
+ },
452
+ "release_date": "2026-07-09"
453
+ },
454
+ "gpt-5.3-codex": {
455
+ "id": "gpt-5.3-codex",
456
+ "name": "GPT-5.3 Codex",
457
+ "family": "gpt-codex",
458
+ "reasoning": true,
459
+ "reasoning_options": [{
460
+ "type": "effort",
461
+ "values": [
462
+ "none",
463
+ "low",
464
+ "medium",
465
+ "high",
466
+ "xhigh"
467
+ ]
468
+ }],
469
+ "temperature": true,
470
+ "tool_call": true,
471
+ "attachment": true,
472
+ "modalities": {
473
+ "input": [
474
+ "text",
475
+ "image",
476
+ "pdf"
477
+ ],
478
+ "output": ["text"]
479
+ },
480
+ "limit": {
481
+ "context": 4e5,
482
+ "output": 128e3
483
+ },
484
+ "release_date": "2026-02-05"
485
+ },
486
+ "gpt-4o-mini": {
487
+ "id": "gpt-4o-mini",
488
+ "name": "GPT-4o mini",
489
+ "family": "gpt-mini",
490
+ "reasoning": false,
491
+ "temperature": true,
492
+ "tool_call": true,
493
+ "attachment": true,
494
+ "modalities": {
495
+ "input": [
496
+ "text",
497
+ "image",
498
+ "pdf"
499
+ ],
500
+ "output": ["text"]
501
+ },
502
+ "limit": {
503
+ "context": 128e3,
504
+ "output": 16384
505
+ },
506
+ "release_date": "2024-07-18"
507
+ },
508
+ "gpt-image-1.5": {
509
+ "id": "gpt-image-1.5",
510
+ "name": "gpt-image-1.5",
511
+ "family": "gpt-image",
512
+ "reasoning": false,
513
+ "temperature": false,
514
+ "tool_call": false,
515
+ "attachment": true,
516
+ "modalities": {
517
+ "input": ["text", "image"],
518
+ "output": ["text", "image"]
519
+ },
520
+ "limit": {
521
+ "context": 0,
522
+ "output": 0
523
+ },
524
+ "release_date": "2025-11-25"
525
+ },
526
+ "o1-pro": {
527
+ "id": "o1-pro",
528
+ "name": "o1-pro",
529
+ "family": "o-pro",
530
+ "reasoning": true,
531
+ "reasoning_options": [{
532
+ "type": "effort",
533
+ "values": [
534
+ "low",
535
+ "medium",
536
+ "high"
537
+ ]
538
+ }],
539
+ "temperature": false,
540
+ "tool_call": true,
541
+ "attachment": true,
542
+ "modalities": {
543
+ "input": ["text", "image"],
544
+ "output": ["text"]
545
+ },
546
+ "limit": {
547
+ "context": 2e5,
548
+ "output": 1e5
549
+ },
550
+ "release_date": "2025-03-19"
551
+ },
552
+ "gpt-4.1": {
553
+ "id": "gpt-4.1",
554
+ "name": "GPT-4.1",
555
+ "family": "gpt",
556
+ "reasoning": false,
557
+ "temperature": true,
558
+ "tool_call": true,
559
+ "attachment": true,
560
+ "modalities": {
561
+ "input": [
562
+ "text",
563
+ "image",
564
+ "pdf"
565
+ ],
566
+ "output": ["text"]
567
+ },
568
+ "limit": {
569
+ "context": 1047576,
570
+ "output": 32768
571
+ },
572
+ "release_date": "2025-04-14"
573
+ },
574
+ "text-embedding-ada-002": {
575
+ "id": "text-embedding-ada-002",
576
+ "name": "text-embedding-ada-002",
577
+ "family": "text-embedding",
578
+ "reasoning": false,
579
+ "temperature": false,
580
+ "tool_call": false,
581
+ "attachment": false,
582
+ "modalities": {
583
+ "input": ["text"],
584
+ "output": ["text"]
585
+ },
586
+ "limit": {
587
+ "context": 8192,
588
+ "output": 1536
589
+ },
590
+ "release_date": "2022-12-15"
591
+ },
592
+ "gpt-image-1": {
593
+ "id": "gpt-image-1",
594
+ "name": "gpt-image-1",
595
+ "family": "gpt-image",
596
+ "reasoning": false,
597
+ "temperature": false,
598
+ "tool_call": false,
599
+ "attachment": true,
600
+ "modalities": {
601
+ "input": ["text", "image"],
602
+ "output": ["image"]
603
+ },
604
+ "limit": {
605
+ "context": 0,
606
+ "output": 0
607
+ },
608
+ "release_date": "2025-04-24"
609
+ },
610
+ "gpt-5.4-nano": {
611
+ "id": "gpt-5.4-nano",
612
+ "name": "GPT-5.4 nano",
613
+ "family": "gpt-nano",
614
+ "reasoning": true,
615
+ "reasoning_options": [{
616
+ "type": "effort",
617
+ "values": [
618
+ "none",
619
+ "low",
620
+ "medium",
621
+ "high",
622
+ "xhigh"
623
+ ]
624
+ }],
625
+ "temperature": true,
626
+ "tool_call": true,
627
+ "attachment": true,
628
+ "modalities": {
629
+ "input": ["text", "image"],
630
+ "output": ["text"]
631
+ },
632
+ "limit": {
633
+ "context": 4e5,
634
+ "output": 128e3
635
+ },
636
+ "release_date": "2026-03-17"
637
+ },
638
+ "gpt-5.5-pro": {
639
+ "id": "gpt-5.5-pro",
640
+ "name": "GPT-5.5 Pro",
641
+ "family": "gpt-pro",
642
+ "reasoning": true,
643
+ "reasoning_options": [{
644
+ "type": "effort",
645
+ "values": [
646
+ "medium",
647
+ "high",
648
+ "xhigh"
649
+ ]
650
+ }],
651
+ "temperature": false,
652
+ "tool_call": true,
653
+ "attachment": true,
654
+ "modalities": {
655
+ "input": [
656
+ "text",
657
+ "image",
658
+ "pdf"
659
+ ],
660
+ "output": ["text"]
661
+ },
662
+ "limit": {
663
+ "context": 105e4,
664
+ "output": 128e3
665
+ },
666
+ "release_date": "2026-04-23"
667
+ },
668
+ "gpt-image-1-mini": {
669
+ "id": "gpt-image-1-mini",
670
+ "name": "gpt-image-1-mini",
671
+ "family": "gpt-image",
672
+ "reasoning": false,
673
+ "temperature": false,
674
+ "tool_call": false,
675
+ "attachment": true,
676
+ "modalities": {
677
+ "input": ["text", "image"],
678
+ "output": ["text", "image"]
679
+ },
680
+ "limit": {
681
+ "context": 0,
682
+ "output": 0
683
+ },
684
+ "release_date": "2025-09-26"
685
+ },
686
+ "gpt-5.4-mini": {
687
+ "id": "gpt-5.4-mini",
688
+ "name": "GPT-5.4 mini",
689
+ "family": "gpt-mini",
690
+ "reasoning": true,
691
+ "reasoning_options": [{
692
+ "type": "effort",
693
+ "values": [
694
+ "none",
695
+ "low",
696
+ "medium",
697
+ "high",
698
+ "xhigh"
699
+ ]
700
+ }],
701
+ "temperature": true,
702
+ "tool_call": true,
703
+ "attachment": true,
704
+ "modalities": {
705
+ "input": ["text", "image"],
706
+ "output": ["text"]
707
+ },
708
+ "limit": {
709
+ "context": 4e5,
710
+ "output": 128e3
711
+ },
712
+ "release_date": "2026-03-17"
713
+ },
714
+ "gpt-image-2": {
715
+ "id": "gpt-image-2",
716
+ "name": "gpt-image-2",
717
+ "family": "gpt-image",
718
+ "reasoning": false,
719
+ "temperature": false,
720
+ "tool_call": false,
721
+ "attachment": true,
722
+ "modalities": {
723
+ "input": ["text", "image"],
724
+ "output": ["image"]
725
+ },
726
+ "limit": {
727
+ "context": 0,
728
+ "output": 0
729
+ },
730
+ "release_date": "2026-04-21"
731
+ },
732
+ "gpt-3.5-turbo": {
733
+ "id": "gpt-3.5-turbo",
734
+ "name": "GPT-3.5-turbo",
735
+ "family": "gpt",
736
+ "reasoning": false,
737
+ "temperature": true,
738
+ "tool_call": false,
739
+ "attachment": false,
740
+ "modalities": {
741
+ "input": ["text"],
742
+ "output": ["text"]
743
+ },
744
+ "limit": {
745
+ "context": 16385,
746
+ "output": 4096
747
+ },
748
+ "release_date": "2023-03-01"
749
+ },
750
+ "gpt-5.6": {
751
+ "id": "gpt-5.6",
752
+ "name": "GPT-5.6",
753
+ "family": "gpt-sol",
754
+ "reasoning": true,
755
+ "reasoning_options": [{
756
+ "type": "effort",
757
+ "values": [
758
+ "none",
759
+ "low",
760
+ "medium",
761
+ "high",
762
+ "xhigh",
763
+ "max"
764
+ ]
765
+ }],
766
+ "temperature": false,
767
+ "tool_call": true,
768
+ "attachment": true,
769
+ "modalities": {
770
+ "input": [
771
+ "text",
772
+ "image",
773
+ "pdf"
774
+ ],
775
+ "output": ["text"]
776
+ },
777
+ "limit": {
778
+ "context": 105e4,
779
+ "output": 128e3
780
+ },
781
+ "release_date": "2026-07-09"
782
+ },
783
+ "text-embedding-3-small": {
784
+ "id": "text-embedding-3-small",
785
+ "name": "text-embedding-3-small",
786
+ "family": "text-embedding",
787
+ "reasoning": false,
788
+ "temperature": false,
789
+ "tool_call": false,
790
+ "attachment": false,
791
+ "modalities": {
792
+ "input": ["text"],
793
+ "output": ["text"]
794
+ },
795
+ "limit": {
796
+ "context": 8191,
797
+ "output": 1536
798
+ },
799
+ "release_date": "2024-01-25"
800
+ },
801
+ "gpt-5-mini": {
802
+ "id": "gpt-5-mini",
803
+ "name": "GPT-5 Mini",
804
+ "family": "gpt-mini",
805
+ "reasoning": true,
806
+ "reasoning_options": [{
807
+ "type": "effort",
808
+ "values": [
809
+ "minimal",
810
+ "low",
811
+ "medium",
812
+ "high"
813
+ ]
814
+ }],
815
+ "temperature": false,
816
+ "tool_call": true,
817
+ "attachment": true,
818
+ "modalities": {
819
+ "input": ["text", "image"],
820
+ "output": ["text"]
821
+ },
822
+ "limit": {
823
+ "context": 4e5,
824
+ "output": 128e3
825
+ },
826
+ "release_date": "2025-08-07"
827
+ },
828
+ "gpt-5.4-pro": {
829
+ "id": "gpt-5.4-pro",
830
+ "name": "GPT-5.4 Pro",
831
+ "family": "gpt-pro",
832
+ "reasoning": true,
833
+ "reasoning_options": [{
834
+ "type": "effort",
835
+ "values": [
836
+ "medium",
837
+ "high",
838
+ "xhigh"
839
+ ]
840
+ }],
841
+ "temperature": false,
842
+ "tool_call": true,
843
+ "attachment": true,
844
+ "modalities": {
845
+ "input": ["text", "image"],
846
+ "output": ["text"]
847
+ },
848
+ "limit": {
849
+ "context": 105e4,
850
+ "output": 128e3
851
+ },
852
+ "release_date": "2026-03-05"
853
+ },
854
+ "text-embedding-3-large": {
855
+ "id": "text-embedding-3-large",
856
+ "name": "text-embedding-3-large",
857
+ "family": "text-embedding",
858
+ "reasoning": false,
859
+ "temperature": false,
860
+ "tool_call": false,
861
+ "attachment": false,
862
+ "modalities": {
863
+ "input": ["text"],
864
+ "output": ["text"]
865
+ },
866
+ "limit": {
867
+ "context": 8191,
868
+ "output": 3072
869
+ },
870
+ "release_date": "2024-01-25"
871
+ },
872
+ "gpt-5.6-terra": {
873
+ "id": "gpt-5.6-terra",
874
+ "name": "GPT-5.6 Terra",
875
+ "family": "gpt-terra",
876
+ "reasoning": true,
877
+ "reasoning_options": [{
878
+ "type": "effort",
879
+ "values": [
880
+ "none",
881
+ "low",
882
+ "medium",
883
+ "high",
884
+ "xhigh",
885
+ "max"
886
+ ]
887
+ }],
888
+ "temperature": false,
889
+ "tool_call": true,
890
+ "attachment": true,
891
+ "modalities": {
892
+ "input": [
893
+ "text",
894
+ "image",
895
+ "pdf"
896
+ ],
897
+ "output": ["text"]
898
+ },
899
+ "limit": {
900
+ "context": 105e4,
901
+ "output": 128e3
902
+ },
903
+ "release_date": "2026-07-09"
904
+ },
905
+ "gpt-4": {
906
+ "id": "gpt-4",
907
+ "name": "GPT-4",
908
+ "family": "gpt",
909
+ "reasoning": false,
910
+ "temperature": true,
911
+ "tool_call": true,
912
+ "attachment": true,
913
+ "modalities": {
914
+ "input": ["text"],
915
+ "output": ["text"]
916
+ },
917
+ "limit": {
918
+ "context": 8192,
919
+ "output": 8192
920
+ },
921
+ "release_date": "2023-11-06"
922
+ },
923
+ "gpt-5.2": {
924
+ "id": "gpt-5.2",
925
+ "name": "GPT-5.2",
926
+ "family": "gpt",
927
+ "reasoning": true,
928
+ "reasoning_options": [{
929
+ "type": "effort",
930
+ "values": [
931
+ "none",
932
+ "low",
933
+ "medium",
934
+ "high",
935
+ "xhigh"
936
+ ]
937
+ }],
938
+ "temperature": true,
939
+ "tool_call": true,
940
+ "attachment": true,
941
+ "modalities": {
942
+ "input": ["text", "image"],
943
+ "output": ["text"]
944
+ },
945
+ "limit": {
946
+ "context": 4e5,
947
+ "output": 128e3
948
+ },
949
+ "release_date": "2025-12-11"
950
+ },
951
+ "gpt-5": {
952
+ "id": "gpt-5",
953
+ "name": "GPT-5",
954
+ "family": "gpt",
955
+ "reasoning": true,
956
+ "reasoning_options": [{
957
+ "type": "effort",
958
+ "values": [
959
+ "minimal",
960
+ "low",
961
+ "medium",
962
+ "high"
963
+ ]
964
+ }],
965
+ "temperature": false,
966
+ "tool_call": true,
967
+ "attachment": true,
968
+ "modalities": {
969
+ "input": ["text", "image"],
970
+ "output": ["text"]
971
+ },
972
+ "limit": {
973
+ "context": 4e5,
974
+ "output": 128e3
975
+ },
976
+ "release_date": "2025-08-07"
977
+ },
978
+ "gpt-5.2-chat-latest": {
979
+ "id": "gpt-5.2-chat-latest",
980
+ "name": "GPT-5.2 Chat",
981
+ "family": "gpt-codex",
982
+ "reasoning": true,
983
+ "reasoning_options": [{
984
+ "type": "effort",
985
+ "values": ["medium"]
986
+ }],
987
+ "temperature": false,
988
+ "tool_call": true,
989
+ "attachment": true,
990
+ "modalities": {
991
+ "input": ["text", "image"],
992
+ "output": ["text"]
993
+ },
994
+ "limit": {
995
+ "context": 128e3,
996
+ "output": 16384
997
+ },
998
+ "release_date": "2025-12-11"
999
+ },
1000
+ "o4-mini": {
1001
+ "id": "o4-mini",
1002
+ "name": "o4-mini",
1003
+ "family": "o-mini",
1004
+ "reasoning": true,
1005
+ "reasoning_options": [{
1006
+ "type": "effort",
1007
+ "values": [
1008
+ "low",
1009
+ "medium",
1010
+ "high"
1011
+ ]
1012
+ }],
1013
+ "temperature": false,
1014
+ "tool_call": true,
1015
+ "attachment": true,
1016
+ "modalities": {
1017
+ "input": ["text", "image"],
1018
+ "output": ["text"]
1019
+ },
1020
+ "limit": {
1021
+ "context": 2e5,
1022
+ "output": 1e5
1023
+ },
1024
+ "release_date": "2025-04-16"
1025
+ },
1026
+ "gpt-realtime-2.1": {
1027
+ "id": "gpt-realtime-2.1",
1028
+ "name": "GPT-Realtime-2.1",
1029
+ "family": "gpt",
1030
+ "reasoning": true,
1031
+ "reasoning_options": [{
1032
+ "type": "effort",
1033
+ "values": [
1034
+ "minimal",
1035
+ "low",
1036
+ "medium",
1037
+ "high",
1038
+ "xhigh"
1039
+ ]
1040
+ }],
1041
+ "temperature": false,
1042
+ "tool_call": true,
1043
+ "attachment": true,
1044
+ "modalities": {
1045
+ "input": [
1046
+ "text",
1047
+ "audio",
1048
+ "image"
1049
+ ],
1050
+ "output": ["text", "audio"]
1051
+ },
1052
+ "limit": {
1053
+ "context": 128e3,
1054
+ "output": 32e3
1055
+ },
1056
+ "release_date": "2026-07-06"
1057
+ },
1058
+ "o3-mini": {
1059
+ "id": "o3-mini",
1060
+ "name": "o3-mini",
1061
+ "family": "o-mini",
1062
+ "reasoning": true,
1063
+ "reasoning_options": [{
1064
+ "type": "effort",
1065
+ "values": [
1066
+ "low",
1067
+ "medium",
1068
+ "high"
1069
+ ]
1070
+ }],
1071
+ "temperature": false,
1072
+ "tool_call": true,
1073
+ "attachment": false,
1074
+ "modalities": {
1075
+ "input": ["text"],
1076
+ "output": ["text"]
1077
+ },
1078
+ "limit": {
1079
+ "context": 2e5,
1080
+ "output": 1e5
1081
+ },
1082
+ "release_date": "2024-12-20"
1083
+ },
1084
+ "o3": {
1085
+ "id": "o3",
1086
+ "name": "o3",
1087
+ "family": "o",
1088
+ "reasoning": true,
1089
+ "reasoning_options": [{
1090
+ "type": "effort",
1091
+ "values": [
1092
+ "low",
1093
+ "medium",
1094
+ "high"
1095
+ ]
1096
+ }],
1097
+ "temperature": false,
1098
+ "tool_call": true,
1099
+ "attachment": true,
1100
+ "modalities": {
1101
+ "input": [
1102
+ "text",
1103
+ "image",
1104
+ "pdf"
1105
+ ],
1106
+ "output": ["text"]
1107
+ },
1108
+ "limit": {
1109
+ "context": 2e5,
1110
+ "output": 1e5
1111
+ },
1112
+ "release_date": "2025-04-16"
1113
+ },
1114
+ "o3-pro": {
1115
+ "id": "o3-pro",
1116
+ "name": "o3-pro",
1117
+ "family": "o-pro",
1118
+ "reasoning": true,
1119
+ "reasoning_options": [{
1120
+ "type": "effort",
1121
+ "values": [
1122
+ "low",
1123
+ "medium",
1124
+ "high"
1125
+ ]
1126
+ }],
1127
+ "temperature": false,
1128
+ "tool_call": true,
1129
+ "attachment": true,
1130
+ "modalities": {
1131
+ "input": ["text", "image"],
1132
+ "output": ["text"]
1133
+ },
1134
+ "limit": {
1135
+ "context": 2e5,
1136
+ "output": 1e5
1137
+ },
1138
+ "release_date": "2025-06-10"
1139
+ },
1140
+ "gpt-5.3-chat-latest": {
1141
+ "id": "gpt-5.3-chat-latest",
1142
+ "name": "GPT-5.3 Chat (latest)",
1143
+ "family": "gpt",
1144
+ "reasoning": false,
1145
+ "temperature": true,
1146
+ "tool_call": true,
1147
+ "attachment": true,
1148
+ "modalities": {
1149
+ "input": ["text", "image"],
1150
+ "output": ["text"]
1151
+ },
1152
+ "limit": {
1153
+ "context": 128e3,
1154
+ "output": 16384
1155
+ },
1156
+ "release_date": "2026-03-03"
1157
+ },
1158
+ "gpt-5.5": {
1159
+ "id": "gpt-5.5",
1160
+ "name": "GPT-5.5",
1161
+ "family": "gpt",
1162
+ "reasoning": true,
1163
+ "reasoning_options": [{
1164
+ "type": "effort",
1165
+ "values": [
1166
+ "none",
1167
+ "low",
1168
+ "medium",
1169
+ "high",
1170
+ "xhigh"
1171
+ ]
1172
+ }],
1173
+ "temperature": false,
1174
+ "tool_call": true,
1175
+ "attachment": true,
1176
+ "modalities": {
1177
+ "input": [
1178
+ "text",
1179
+ "image",
1180
+ "pdf"
1181
+ ],
1182
+ "output": ["text"]
1183
+ },
1184
+ "limit": {
1185
+ "context": 105e4,
1186
+ "output": 128e3
1187
+ },
1188
+ "release_date": "2026-04-23"
1189
+ },
1190
+ "gpt-4o-2024-11-20": {
1191
+ "id": "gpt-4o-2024-11-20",
1192
+ "name": "GPT-4o (2024-11-20)",
1193
+ "family": "gpt",
1194
+ "reasoning": false,
1195
+ "temperature": true,
1196
+ "tool_call": true,
1197
+ "attachment": true,
1198
+ "modalities": {
1199
+ "input": ["text", "image"],
1200
+ "output": ["text"]
1201
+ },
1202
+ "limit": {
1203
+ "context": 128e3,
1204
+ "output": 16384
1205
+ },
1206
+ "release_date": "2024-11-20"
1207
+ }
1208
+ } },
1209
+ snapshotAt: Date.parse("2026-09-17")
1210
+ };
1211
+ //#endregion
1212
+ //#region src/facts.ts
1213
+ /** Parameter ranges and request limits for this provider wire. */
1214
+ const facts = {
1215
+ outputLimit: { support: "unsupported" },
1216
+ temperature: null,
1217
+ thinkingSwitch: false,
1218
+ limits: {
1219
+ requestBytes: 50331648,
1220
+ images: 100
1221
+ }
1222
+ };
1223
+ //#endregion
1224
+ //#region src/image-generation/model.ts
1225
+ /** The fixed subscription image model. */
1226
+ const IMAGE_MODEL = "gpt-image-2";
1227
+ /** Strict generation parameters; opaque output is the default. */
1228
+ const IMAGE_PARAMS_SCHEMA = z.object({ transparent_background: z.boolean().default(false) }).strict();
1229
+ /** Declares the image model's executable operations. */
1230
+ const IMAGE_GENERATION_MODEL = {
1231
+ id: IMAGE_MODEL,
1232
+ label: IMAGE_MODEL,
1233
+ outputKinds: ["image"],
1234
+ operations: [{
1235
+ id: "create",
1236
+ inputKinds: [],
1237
+ minInputs: 0,
1238
+ maxInputs: 0,
1239
+ paramsSchema: IMAGE_PARAMS_SCHEMA
1240
+ }, {
1241
+ id: "edit",
1242
+ inputKinds: ["image"],
1243
+ minInputs: 1,
1244
+ maxInputs: 5,
1245
+ paramsSchema: IMAGE_PARAMS_SCHEMA
1246
+ }]
1247
+ };
1248
+ //#endregion
1249
+ //#region src/failure.ts
1250
+ var LoginFailure = class extends Error {
1251
+ name = "LoginFailure";
1252
+ code;
1253
+ detail;
1254
+ constructor(code, detail = null) {
1255
+ super(detail === null ? `sign-in ${code}` : `sign-in ${code} (${detail})`);
1256
+ this.code = code;
1257
+ this.detail = detail;
1258
+ }
1259
+ };
1260
+ //#endregion
1261
+ //#region src/oauth.ts
1262
+ /**
1263
+ * Signing a ChatGPT account in, and keeping its tokens fresh.
1264
+ *
1265
+ * The client is the Codex CLI's public one: it has no secret, so what stops
1266
+ * an intercepted authorization code from being spent is PKCE alone. The
1267
+ * addresses and the request shapes are the ones the Codex CLI and opencode
1268
+ * use; none of them is a published contract, and each was verified against a
1269
+ * Pro account on 2026-09-24.
1270
+ *
1271
+ * Every function here takes the addresses it reaches as an argument, so a
1272
+ * test points them at a server of its own.
1273
+ */
1274
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1275
+ /** Who this client says it is, on the authorization and on every call. */
1276
+ const ORIGINATOR = "yaoguang";
1277
+ const CALLBACK_PATH = "/auth/callback";
1278
+ /** The listing answers only models whose minimal client version this reaches. */
1279
+ const CODEX_CLIENT_VERSION = "1.0.0";
1280
+ /** The reasoning efforts the endpoint accepts. The listing names more
1281
+ * (`ultra`), which the endpoint refuses. */
1282
+ const ACCEPTED_EFFORTS = [
1283
+ "none",
1284
+ "minimal",
1285
+ "low",
1286
+ "medium",
1287
+ "high",
1288
+ "xhigh",
1289
+ "max"
1290
+ ];
1291
+ const SCOPE = "openid profile email offline_access";
1292
+ const REQUEST_TIMEOUT_MS = 3e4;
1293
+ /** How long before expiry a token without `earliest_refresh_at` is refreshed. */
1294
+ const REFRESH_AHEAD_MS = 6e4;
1295
+ const ENDPOINTS = {
1296
+ issuer: "https://auth.openai.com",
1297
+ codex: "https://chatgpt.com/backend-api/codex"
1298
+ };
1299
+ function pkce() {
1300
+ const verifier = randomBytes(48).toString("base64url");
1301
+ return {
1302
+ verifier,
1303
+ challenge: createHash("sha256").update(verifier).digest("base64url")
1304
+ };
1305
+ }
1306
+ function newState() {
1307
+ return randomBytes(32).toString("base64url");
1308
+ }
1309
+ function authorizeUrl(endpoints, redirectUri, codes, state) {
1310
+ const params = new URLSearchParams({
1311
+ response_type: "code",
1312
+ client_id: CLIENT_ID,
1313
+ redirect_uri: redirectUri,
1314
+ scope: SCOPE,
1315
+ code_challenge: codes.challenge,
1316
+ code_challenge_method: "S256",
1317
+ id_token_add_organizations: "true",
1318
+ codex_cli_simplified_flow: "true",
1319
+ state,
1320
+ originator: ORIGINATOR
1321
+ });
1322
+ return `${endpoints.issuer}/oauth/authorize?${params.toString()}`;
1323
+ }
1324
+ function claimsOf(jwt) {
1325
+ if (typeof jwt !== "string") return void 0;
1326
+ const payload = jwt.split(".")[1];
1327
+ if (payload === void 0) return void 0;
1328
+ try {
1329
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
1330
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
1331
+ } catch {
1332
+ return;
1333
+ }
1334
+ }
1335
+ function authClaim(claims, key) {
1336
+ const nested = claims?.["https://api.openai.com/auth"];
1337
+ const value = (typeof nested === "object" && nested !== null ? nested[key] : void 0) ?? claims?.[key];
1338
+ return typeof value === "string" && value !== "" ? value : void 0;
1339
+ }
1340
+ /** Who signed in, as a person reads it: the email, else the account id's
1341
+ * last four characters. */
1342
+ function accountLabel(email, accountId) {
1343
+ return email ?? `····${accountId.slice(-4)}`;
1344
+ }
1345
+ /**
1346
+ * The tokens an exchange or a refresh answered, as they are stored. `before`
1347
+ * is the credential being refreshed: a refresh that answers no id token keeps
1348
+ * the account it had.
1349
+ */
1350
+ function storedFrom(body, now, before) {
1351
+ const access = body.access_token;
1352
+ const refresh = body.refresh_token ?? before?.refresh;
1353
+ if (typeof access !== "string" || typeof refresh !== "string") return void 0;
1354
+ const id = claimsOf(body.id_token);
1355
+ const accessClaims = claimsOf(access);
1356
+ const accountId = authClaim(id, "chatgpt_account_id") ?? authClaim(accessClaims, "chatgpt_account_id") ?? before?.accountId;
1357
+ if (accountId === void 0) return void 0;
1358
+ const email = typeof id?.email === "string" ? id.email : void 0;
1359
+ const expiresIn = typeof body.expires_in === "number" ? body.expires_in : 3600;
1360
+ const earliest = typeof body.earliest_refresh_at === "number" ? body.earliest_refresh_at * 1e3 : void 0;
1361
+ const residency = authClaim(accessClaims, "chatgpt_compute_residency");
1362
+ return {
1363
+ access,
1364
+ refresh,
1365
+ expiresAt: String(now + expiresIn * 1e3),
1366
+ ...earliest === void 0 ? {} : { earliestRefreshAt: String(earliest) },
1367
+ accountId,
1368
+ account: email === void 0 && before !== void 0 ? before.account : accountLabel(email, accountId),
1369
+ ...residency === void 0 || residency === "no_constraint" ? {} : { residency }
1370
+ };
1371
+ }
1372
+ function parseStored(credential) {
1373
+ if (credential === void 0) return void 0;
1374
+ try {
1375
+ const parsed = JSON.parse(credential);
1376
+ return typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expiresAt === "string" && typeof parsed.accountId === "string" && typeof parsed.account === "string" ? parsed : void 0;
1377
+ } catch {
1378
+ return;
1379
+ }
1380
+ }
1381
+ /** UTC milliseconds when the host should refresh this credential. */
1382
+ function refreshAfterOf(tokens) {
1383
+ const expiresAt = Number(tokens.expiresAt) - REFRESH_AHEAD_MS;
1384
+ const earliest = tokens.earliestRefreshAt === void 0 ? expiresAt : Number(tokens.earliestRefreshAt);
1385
+ return Math.min(expiresAt, earliest);
1386
+ }
1387
+ async function postForm(fetch, url, form, signal) {
1388
+ return fetch(url, {
1389
+ method: "POST",
1390
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1391
+ body: new URLSearchParams(form).toString(),
1392
+ signal: signal === void 0 ? AbortSignal.timeout(REQUEST_TIMEOUT_MS) : AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
1393
+ });
1394
+ }
1395
+ /** Spends an authorization code. Rejects with `exchange-failed`. */
1396
+ async function exchangeCode(fetch, endpoints, input, now, signal) {
1397
+ let response;
1398
+ try {
1399
+ response = await postForm(fetch, `${endpoints.issuer}/oauth/token`, {
1400
+ grant_type: "authorization_code",
1401
+ code: input.code,
1402
+ redirect_uri: input.redirectUri,
1403
+ client_id: CLIENT_ID,
1404
+ code_verifier: input.verifier
1405
+ }, signal);
1406
+ } catch {
1407
+ throw new LoginFailure("exchange-failed", "unreachable");
1408
+ }
1409
+ if (!response.ok) throw new LoginFailure("exchange-failed", `HTTP ${String(response.status)}`);
1410
+ const stored = storedFrom(await response.json().catch(() => ({})), now());
1411
+ if (stored === void 0) throw new LoginFailure("exchange-failed", "unreadable answer");
1412
+ return stored;
1413
+ }
1414
+ async function refreshTokens(fetch, endpoints, before, now, signal) {
1415
+ let response;
1416
+ try {
1417
+ response = await postForm(fetch, `${endpoints.issuer}/oauth/token`, {
1418
+ grant_type: "refresh_token",
1419
+ refresh_token: before.refresh,
1420
+ client_id: CLIENT_ID
1421
+ }, signal);
1422
+ } catch {
1423
+ if (signal?.aborted) throw new LoginFailure("cancelled");
1424
+ return {
1425
+ kind: "failed",
1426
+ detail: "unreachable"
1427
+ };
1428
+ }
1429
+ if (response.status === 400 || response.status === 401) return { kind: "refused" };
1430
+ if (!response.ok) return {
1431
+ kind: "failed",
1432
+ detail: `HTTP ${String(response.status)}`
1433
+ };
1434
+ const tokens = storedFrom(await response.json().catch(() => ({})), now(), before);
1435
+ return tokens === void 0 ? {
1436
+ kind: "failed",
1437
+ detail: "unreadable answer"
1438
+ } : {
1439
+ kind: "ok",
1440
+ tokens
1441
+ };
1442
+ }
1443
+ //#endregion
1444
+ //#region src/image-generation/client.ts
1445
+ const responseSchema = z.object({
1446
+ data: z.array(z.unknown()).min(1),
1447
+ background: z.enum(["opaque", "transparent"]).optional()
1448
+ });
1449
+ const itemSchema = z.object({ b64_json: z.string() });
1450
+ const RESPONSE_LIMIT = 41943040;
1451
+ const PNG_SIGNATURE = Buffer.from([
1452
+ 137,
1453
+ 80,
1454
+ 78,
1455
+ 71,
1456
+ 13,
1457
+ 10,
1458
+ 26,
1459
+ 10
1460
+ ]);
1461
+ const errorSchema = z.object({ error: z.object({
1462
+ type: z.string().optional(),
1463
+ code: z.string().optional(),
1464
+ limit_id: z.string().optional(),
1465
+ resets_at: z.number().int().optional()
1466
+ }) });
1467
+ /** A body-only provider failure with explicit submission and delivery knowledge. */
1468
+ var ImageGenerationFailure = class extends Error {
1469
+ /** Public failure discriminator. */
1470
+ name = "GenerationFailure";
1471
+ /** Normalized failure category. */
1472
+ code;
1473
+ /** Remote submission and local delivery knowledge. */
1474
+ resultState;
1475
+ /** Provider response status, when available. */
1476
+ status;
1477
+ /** Explicit provider reset time in UTC epoch milliseconds. */
1478
+ retryAt;
1479
+ /** Bounded provider body with known access and refresh tokens removed. */
1480
+ providerText;
1481
+ /** Creates a failure without copying HTTP headers. */
1482
+ constructor(code, message, resultState, metadata = {}) {
1483
+ super(message);
1484
+ this.code = code;
1485
+ this.resultState = resultState;
1486
+ this.status = metadata.status;
1487
+ this.retryAt = metadata.retryAt;
1488
+ this.providerText = metadata.providerText;
1489
+ }
1490
+ };
1491
+ /** Sends exactly one JSON POST and decodes the first inline image. */
1492
+ async function submitImage(options, submission, signal) {
1493
+ if (signal.aborted) await withImageSignal(async () => {}, signal, "not-submitted");
1494
+ let response;
1495
+ try {
1496
+ response = await withImageSignal(() => options.fetch(`${options.baseUrl.replace(/\/$/, "")}/images/${submission.operation === "create" ? "generations" : "edits"}`, {
1497
+ method: "POST",
1498
+ signal,
1499
+ body: submission.body,
1500
+ headers: {
1501
+ "content-type": "application/json",
1502
+ authorization: `Bearer ${submission.tokens.access}`,
1503
+ ...submission.tokens.accountId ? { "ChatGPT-Account-ID": submission.tokens.accountId } : {},
1504
+ "x-codex-image-turn-id": submission.callId,
1505
+ originator: ORIGINATOR,
1506
+ "User-Agent": options.userAgent
1507
+ }
1508
+ }).then((response) => {
1509
+ if (signal.aborted) response.body?.cancel().catch(() => {});
1510
+ return response;
1511
+ }), signal, "unknown");
1512
+ } catch (error) {
1513
+ if (error instanceof ImageGenerationFailure) throw error;
1514
+ throw new ImageGenerationFailure("network", "ChatGPT image request failed; its remote result is unknown", "unknown");
1515
+ }
1516
+ if (response.status === 401) {
1517
+ response.body?.cancel().catch(() => {});
1518
+ throw new ImageGenerationFailure("expired", "ChatGPT sign-in expired; sign in again in model settings (status 401)", "unknown", { status: 401 });
1519
+ }
1520
+ const text = await readResponse(response, signal);
1521
+ if (!response.ok) {
1522
+ let parsedError;
1523
+ try {
1524
+ const result = errorSchema.safeParse(JSON.parse(text));
1525
+ if (result.success) parsedError = result.data;
1526
+ } catch {}
1527
+ const reportedCode = parsedError?.error.code ?? parsedError?.error.type;
1528
+ const quota = reportedCode === "usage_limit_reached" || reportedCode === "insufficient_quota" || parsedError?.error.limit_id === "image_gen";
1529
+ const code = response.status === 401 ? "expired" : response.status === 403 ? "forbidden" : response.status === 429 || quota ? "quota" : "provider";
1530
+ const seconds = parsedError?.error.resets_at;
1531
+ const retryAt = code === "quota" && seconds !== void 0 && seconds > 0 && seconds * 1e3 <= 864e13 ? seconds * 1e3 : void 0;
1532
+ const message = code === "expired" ? "ChatGPT sign-in expired; sign in again in model settings" : code === "forbidden" ? "ChatGPT image generation is forbidden for this account" : code === "quota" ? parsedError?.error.limit_id === "image_gen" ? "ChatGPT image generation quota reached" : "ChatGPT quota reached" : "ChatGPT image generation failed";
1533
+ let providerText = text;
1534
+ for (const token of [submission.tokens.access, submission.tokens.refresh]) if (token) providerText = providerText.split(token).join("****");
1535
+ throw new ImageGenerationFailure(code, `${message} (status ${response.status})`, "unknown", {
1536
+ status: response.status,
1537
+ ...retryAt === void 0 ? {} : { retryAt },
1538
+ providerText: providerText.slice(0, 512)
1539
+ });
1540
+ }
1541
+ let parsed;
1542
+ let item;
1543
+ try {
1544
+ parsed = responseSchema.parse(JSON.parse(text));
1545
+ item = itemSchema.parse(parsed.data[0]);
1546
+ } catch {
1547
+ throw new ImageGenerationFailure("invalid-output", "ChatGPT returned an invalid image response", "not-delivered");
1548
+ }
1549
+ const encoded = item.b64_json;
1550
+ if (encoded.length > Math.ceil(20971520 / 3) * 4) throw new ImageGenerationFailure("too-large", "Generated PNG exceeds 20 MiB", "not-delivered");
1551
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length === 0 || encoded.length % 4 !== 0) throw new ImageGenerationFailure("invalid-output", "ChatGPT returned invalid image base64", "not-delivered");
1552
+ const bytes = Buffer.from(encoded, "base64");
1553
+ if (bytes.byteLength > 20971520) throw new ImageGenerationFailure("too-large", "Generated PNG exceeds 20 MiB", "not-delivered");
1554
+ if (bytes.toString("base64") !== encoded || !bytes.subarray(0, 8).equals(PNG_SIGNATURE)) throw new ImageGenerationFailure("invalid-output", "ChatGPT did not return a valid PNG", "not-delivered");
1555
+ return {
1556
+ bytes,
1557
+ ...parsed.background === void 0 ? {} : { background: parsed.background }
1558
+ };
1559
+ }
1560
+ async function readResponse(response, signal) {
1561
+ const length = response.headers.get("content-length");
1562
+ if (length !== null && Number(length) > RESPONSE_LIMIT) {
1563
+ response.body?.cancel().catch(() => {});
1564
+ throw new ImageGenerationFailure("too-large", "Image response exceeds 40 MiB", "not-delivered");
1565
+ }
1566
+ if (!response.body) return "";
1567
+ const reader = response.body.getReader();
1568
+ const cancel = () => {
1569
+ reader.cancel().catch(() => {});
1570
+ };
1571
+ signal.addEventListener("abort", cancel, { once: true });
1572
+ const chunks = [];
1573
+ let size = 0;
1574
+ try {
1575
+ while (true) {
1576
+ const next = await withImageSignal(() => reader.read(), signal, "unknown");
1577
+ if (next.done) break;
1578
+ size += next.value.byteLength;
1579
+ if (size > RESPONSE_LIMIT) throw new ImageGenerationFailure("too-large", "Image response exceeds 40 MiB", "not-delivered");
1580
+ chunks.push(Buffer.from(next.value));
1581
+ }
1582
+ return Buffer.concat(chunks).toString("utf8");
1583
+ } catch (error) {
1584
+ reader.cancel().catch(() => {});
1585
+ if (error instanceof ImageGenerationFailure) throw error;
1586
+ throw new ImageGenerationFailure("network", "ChatGPT image response was interrupted; its remote result is unknown", "unknown");
1587
+ } finally {
1588
+ signal.removeEventListener("abort", cancel);
1589
+ reader.releaseLock();
1590
+ }
1591
+ }
1592
+ /** Ends a provider wait promptly on abort without claiming that its remote work stopped. */
1593
+ async function withImageSignal(work, signal, resultState) {
1594
+ const aborted = () => new ImageGenerationFailure(signal.reason instanceof Error && signal.reason.name === "TimeoutError" ? "timeout" : "cancelled", signal.reason instanceof Error && signal.reason.name === "TimeoutError" ? "Image generation timed out" : "Image generation was cancelled", resultState);
1595
+ if (signal.aborted) throw aborted();
1596
+ let onAbort;
1597
+ try {
1598
+ return await new Promise((resolve, reject) => {
1599
+ onAbort = () => reject(aborted());
1600
+ signal.addEventListener("abort", onAbort, { once: true });
1601
+ work().then((value) => signal.aborted ? reject(aborted()) : resolve(value), reject);
1602
+ });
1603
+ } finally {
1604
+ if (onAbort) signal.removeEventListener("abort", onAbort);
1605
+ }
1606
+ }
1607
+ //#endregion
1608
+ //#region src/image-generation/port.ts
1609
+ /** Creates an image port without network activity or retaining call credentials. */
1610
+ function createImageGenerationPort(options) {
1611
+ return { async generate(request, signal) {
1612
+ await withImageSignal(async () => {}, signal, "not-submitted");
1613
+ const operation = request.operation;
1614
+ if (request.outputKind !== "image" || operation !== "create" && operation !== "edit") throw new ImageGenerationFailure("unsupported", "Unsupported image operation", "not-submitted");
1615
+ if (operation === "create" && request.inputs.length !== 0 || operation === "edit" && (request.inputs.length < 1 || request.inputs.length > 5)) throw new ImageGenerationFailure("invalid-input", "Image edits require one to five references; creation requires none", "not-submitted");
1616
+ const prompt = request.prompt.trim();
1617
+ if ([...prompt].length < 1 || [...prompt].length > 32e3) throw new ImageGenerationFailure("invalid-input", "Image prompt must contain 1 to 32000 characters", "not-submitted");
1618
+ const parsed = IMAGE_PARAMS_SCHEMA.safeParse(request.params);
1619
+ if (!parsed.success) throw new ImageGenerationFailure("invalid-params", "Invalid image generation parameters", "not-submitted");
1620
+ const params = parsed.data;
1621
+ const login = options.login;
1622
+ if (!login) throw new ImageGenerationFailure("not-signed-in", "Sign in to ChatGPT in model settings", "not-submitted");
1623
+ let credential;
1624
+ try {
1625
+ credential = await withImageSignal(() => login.get(signal), signal, "not-submitted");
1626
+ } catch (error) {
1627
+ if (error instanceof ImageGenerationFailure) throw error;
1628
+ throw new ImageGenerationFailure(typeof error === "object" && error !== null && "name" in error && error.name === "CredentialUnavailableError" ? "credential-unavailable" : "not-signed-in", "ChatGPT sign-in is unavailable; sign in again in model settings", "not-submitted");
1629
+ }
1630
+ const tokens = parseStored(credential.credential);
1631
+ if (!tokens?.access.trim()) throw new ImageGenerationFailure("not-signed-in", "Sign in to ChatGPT in model settings", "not-submitted");
1632
+ const images = [];
1633
+ for (const input of request.inputs) {
1634
+ if (![
1635
+ "image/png",
1636
+ "image/jpeg",
1637
+ "image/webp",
1638
+ "image/gif"
1639
+ ].includes(input.mediaType) || !Number.isSafeInteger(input.size) || input.size <= 0) throw new ImageGenerationFailure("invalid-input", "Unsupported reference image", "not-submitted");
1640
+ if (input.size > 20971520) throw new ImageGenerationFailure("too-large", "Reference image exceeds 20 MiB", "not-submitted");
1641
+ const chunks = [];
1642
+ let length = 0;
1643
+ const iterator = input.read(signal)[Symbol.asyncIterator]();
1644
+ try {
1645
+ while (true) {
1646
+ const next = await withImageSignal(() => iterator.next(), signal, "not-submitted");
1647
+ if (next.done) break;
1648
+ const chunk = next.value;
1649
+ length += chunk.byteLength;
1650
+ if (length > 20971520) throw new ImageGenerationFailure("too-large", "Reference image exceeds 20 MiB", "not-submitted");
1651
+ if (length > input.size) throw new ImageGenerationFailure("invalid-input", "Reference image size changed", "not-submitted");
1652
+ chunks.push(Buffer.from(chunk));
1653
+ }
1654
+ } catch (error) {
1655
+ if (error instanceof ImageGenerationFailure) throw error;
1656
+ throw new ImageGenerationFailure("invalid-input", "Reference image could not be read", "not-submitted");
1657
+ } finally {
1658
+ iterator.return?.().catch(() => {});
1659
+ }
1660
+ if (length !== input.size) throw new ImageGenerationFailure("invalid-input", "Reference image size changed", "not-submitted");
1661
+ images.push({ image_url: `data:${input.mediaType};base64,${Buffer.concat(chunks).toString("base64")}` });
1662
+ }
1663
+ const body = JSON.stringify({
1664
+ model: IMAGE_MODEL,
1665
+ prompt,
1666
+ background: params.transparent_background ? "transparent" : "opaque",
1667
+ quality: "auto",
1668
+ size: "auto",
1669
+ ...request.operation === "edit" ? { images } : {}
1670
+ });
1671
+ if (Buffer.byteLength(body, "utf8") > 50331648) throw new ImageGenerationFailure("too-large", "Encoded image request exceeds 48 MiB", "not-submitted");
1672
+ let image;
1673
+ try {
1674
+ image = await submitImage(options, {
1675
+ operation,
1676
+ body,
1677
+ callId: request.identity.callId,
1678
+ tokens
1679
+ }, signal);
1680
+ } catch (error) {
1681
+ if (error instanceof ImageGenerationFailure && error.code === "expired") login.expire(credential.credential);
1682
+ throw error;
1683
+ }
1684
+ const { bytes, background } = image;
1685
+ const expectedBackground = params.transparent_background ? "transparent" : "opaque";
1686
+ if (background !== void 0 && background !== expectedBackground) throw new ImageGenerationFailure("invalid-output", "ChatGPT did not satisfy the requested background", "not-delivered");
1687
+ let info;
1688
+ try {
1689
+ info = await withImageSignal(() => options.imageInfo(bytes, signal), signal, "unknown");
1690
+ } catch (error) {
1691
+ if (error instanceof ImageGenerationFailure) throw error;
1692
+ throw new ImageGenerationFailure("invalid-output", "ChatGPT returned an undecodable PNG", "not-delivered");
1693
+ }
1694
+ if (!Number.isSafeInteger(info.width) || !Number.isSafeInteger(info.height) || info.width <= 0 || info.height <= 0 || info.width > 1e4 || info.height > 1e4 || info.mediaType !== "image/png" || info.hasTransparentPixels !== params.transparent_background || params.transparent_background && !info.hasAlpha) throw new ImageGenerationFailure("invalid-output", "ChatGPT did not satisfy the requested background", "not-delivered");
1695
+ return { assets: [{
1696
+ mediaType: "image/png",
1697
+ name: "generated-image.png",
1698
+ body: (async function* () {
1699
+ yield bytes;
1700
+ })()
1701
+ }] };
1702
+ } };
1703
+ }
1704
+ //#endregion
1705
+ //#region src/errors.ts
1706
+ const failureKindOf = (status) => status === 413 ? "request-too-large" : void 0;
1707
+ /** Same shape as the other adapters' port error, and `authored`: every
1708
+ * message this module builds is written here, from fixed words, a status
1709
+ * and a code matched against a fixed spelling, so the runtime may show it
1710
+ * as it stands. */
1711
+ var ModelCallError = class extends Error {
1712
+ /** Stable fact selected by this local producer, independent of display text. */
1713
+ failureCode;
1714
+ status;
1715
+ retryable;
1716
+ authored = true;
1717
+ /** The endpoint's own error body, unredacted, on a refusal this module did
1718
+ * not recognise. The runtime's guard removes every credential of the
1719
+ * endpoint from it before it reaches a reason. */
1720
+ providerText;
1721
+ /** How long the endpoint asked to wait before trying again, when it said. */
1722
+ retryAfterMs;
1723
+ /** The endpoint's error code, matched against a fixed spelling. */
1724
+ code;
1725
+ /** What the status alone says the refusal is (`failureKindOf`). */
1726
+ kind;
1727
+ constructor(message, options) {
1728
+ super(message);
1729
+ this.name = "ModelCallError";
1730
+ this.failureCode = options.failureCode;
1731
+ this.status = options.status;
1732
+ this.retryable = options.retryable;
1733
+ this.providerText = options.providerText;
1734
+ this.retryAfterMs = options.retryAfterMs;
1735
+ this.code = options.code;
1736
+ this.kind = failureKindOf(options.status);
1737
+ }
1738
+ };
1739
+ const NOT_SIGNED_IN = "not signed in to ChatGPT; sign in again in settings";
1740
+ const SIGN_IN_EXPIRED = "ChatGPT sign-in expired; sign in again in settings";
1741
+ function notSignedIn() {
1742
+ return new ModelCallError(NOT_SIGNED_IN, {
1743
+ retryable: false,
1744
+ failureCode: "authentication-required"
1745
+ });
1746
+ }
1747
+ function signInExpired(status) {
1748
+ return new ModelCallError(SIGN_IN_EXPIRED, {
1749
+ failureCode: "authentication-expired",
1750
+ ...status === void 0 ? {} : { status },
1751
+ retryable: false
1752
+ });
1753
+ }
1754
+ const SAFE_CODES = /* @__PURE__ */ new Set([
1755
+ "usage_not_included",
1756
+ "usage_limit_reached",
1757
+ "insufficient_quota",
1758
+ "context_length_exceeded",
1759
+ "rate_limit_exceeded",
1760
+ "overloaded_error",
1761
+ "api_error",
1762
+ "rate_limit_error",
1763
+ "cyber_policy",
1764
+ "invalid_prompt",
1765
+ "bio_policy",
1766
+ "misalignment_policy_violation"
1767
+ ]);
1768
+ function codeOf(body) {
1769
+ const code = body.error?.code ?? body.error?.type;
1770
+ return typeof code === "string" && SAFE_CODES.has(code) ? code : void 0;
1771
+ }
1772
+ function resetOf(body) {
1773
+ const at = Number(body.error?.resets_at);
1774
+ return Number.isFinite(at) && at > 0 ? `; resets ${(/* @__PURE__ */ new Date(at * 1e3)).toISOString()}` : "";
1775
+ }
1776
+ /** A refused HTTP answer, read into a port error. */
1777
+ function toPortError(status, text) {
1778
+ let body = {};
1779
+ try {
1780
+ const parsed = JSON.parse(text);
1781
+ if (typeof parsed === "object" && parsed !== null) body = parsed;
1782
+ } catch {}
1783
+ const code = codeOf(body);
1784
+ const detail = typeof body.detail === "string" ? body.detail : "";
1785
+ const at = `(status ${String(status)})`;
1786
+ if (code === "usage_not_included") return new ModelCallError(`your ChatGPT plan does not include Codex ${at}`, {
1787
+ failureCode: "model-unavailable",
1788
+ code,
1789
+ status,
1790
+ retryable: false
1791
+ });
1792
+ if (code === "usage_limit_reached" || code === "insufficient_quota") return new ModelCallError(`ChatGPT usage limit reached ${at}${resetOf(body)}`, {
1793
+ failureCode: "quota-exceeded",
1794
+ code,
1795
+ status,
1796
+ retryable: false
1797
+ });
1798
+ if (code === "context_length_exceeded") return new ModelCallError(`input exceeds the model's context window ${at}`, {
1799
+ failureCode: "context-limit-exceeded",
1800
+ code,
1801
+ status,
1802
+ retryable: false
1803
+ });
1804
+ if (detail.includes("is not supported when using Codex with a ChatGPT account")) return new ModelCallError(`this model is not available with a ChatGPT subscription ${at}`, {
1805
+ failureCode: "model-unavailable",
1806
+ status,
1807
+ retryable: false
1808
+ });
1809
+ return new ModelCallError(`ChatGPT API error (status ${String(status)}${code === void 0 ? "" : `, code ${code}`})`, {
1810
+ status,
1811
+ failureCode: status === 413 ? "request-too-large" : "http-error",
1812
+ ...code === void 0 ? {} : { code },
1813
+ retryable: status === 429 || status >= 500,
1814
+ ...text.trim() === "" ? {} : { providerText: text }
1815
+ });
1816
+ }
1817
+ /** A failure that is not an HTTP answer: a dropped connection, a body that
1818
+ * could not be read — worth a second attempt. Its message comes from this
1819
+ * side, never the network. */
1820
+ function toCallError(error) {
1821
+ if (error instanceof ModelCallError) return error;
1822
+ const name = error instanceof Error ? error.name : "";
1823
+ return new ModelCallError(`ChatGPT call failed${name === "" ? "" : ` (${name})`}`, { retryable: true });
1824
+ }
1825
+ /** Codes a `response.failed` carries that a second attempt cannot change. */
1826
+ const FINAL_CODES = /* @__PURE__ */ new Set([
1827
+ "context_length_exceeded",
1828
+ "insufficient_quota",
1829
+ "usage_not_included",
1830
+ "usage_limit_reached",
1831
+ "cyber_policy",
1832
+ "invalid_prompt",
1833
+ "bio_policy",
1834
+ "misalignment_policy_violation"
1835
+ ]);
1836
+ /** `try again in 1.5s` / `in 250ms`: the wait a rate-limit message names. */
1837
+ function waitIn(message) {
1838
+ if (typeof message !== "string") return void 0;
1839
+ const match = /try again in\s*(\d+(?:\.\d+)?)\s*(s|ms|seconds?)\b/i.exec(message);
1840
+ if (match === null) return void 0;
1841
+ const value = Number(match[1]);
1842
+ const milliseconds = match[2]?.toLowerCase() === "ms" ? value : value * 1e3;
1843
+ return Number.isFinite(milliseconds) && milliseconds >= 0 ? milliseconds : void 0;
1844
+ }
1845
+ /**
1846
+ * A `response.failed` event, read into a port error: rate limits, overload
1847
+ * and unclassified server errors are retried — a rate limit after the wait
1848
+ * its message names — while an oversized input, a spent quota and a policy
1849
+ * refusal are final. Only the code travels; the message is read for the wait.
1850
+ */
1851
+ function toFailedError(failure) {
1852
+ const code = typeof failure.code === "string" && SAFE_CODES.has(failure.code) ? failure.code : void 0;
1853
+ const retryAfterMs = code === "rate_limit_exceeded" ? waitIn(failure.message) : void 0;
1854
+ return new ModelCallError(`ChatGPT response failed${code === void 0 ? "" : ` (code ${code})`}`, {
1855
+ failureCode: code === "context_length_exceeded" ? "context-limit-exceeded" : code === "insufficient_quota" || code === "usage_limit_reached" ? "quota-exceeded" : code === "usage_not_included" ? "model-unavailable" : "provider-error",
1856
+ retryable: code === void 0 || !FINAL_CODES.has(code),
1857
+ ...code === void 0 ? {} : { code },
1858
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs }
1859
+ });
1860
+ }
1861
+ /** Converts locally identified failures to host candidates without copying provider text. */
1862
+ function toModelFailure(error, failures) {
1863
+ if (failures.owns(error)) return error;
1864
+ const converted = toCallError(error);
1865
+ const known = error instanceof ModelCallError;
1866
+ const code = converted.code;
1867
+ const diagnosis = !known ? "unknown" : converted.failureCode ?? (converted.status === void 0 ? "response-invalid" : "http-error");
1868
+ const providerCodes = /* @__PURE__ */ new Set([
1869
+ "usage_not_included",
1870
+ "usage_limit_reached",
1871
+ "insufficient_quota",
1872
+ "context_length_exceeded",
1873
+ "rate_limit_exceeded",
1874
+ "overloaded_error",
1875
+ "api_error",
1876
+ "rate_limit_error",
1877
+ "cyber_policy",
1878
+ "invalid_prompt",
1879
+ "bio_policy",
1880
+ "misalignment_policy_violation"
1881
+ ]);
1882
+ const status = Number.isInteger(converted.status) && Number(converted.status) >= 100 && Number(converted.status) <= 599 ? converted.status : void 0;
1883
+ return failures.create({
1884
+ code: diagnosis,
1885
+ stage: diagnosis === "unknown" ? "unknown" : diagnosis.startsWith("authentication") || diagnosis === "request-too-large" ? "request" : status !== void 0 ? "transport" : diagnosis === "response-invalid" ? "response-validation" : "stream",
1886
+ category: diagnosis === "unknown" ? "unknown" : diagnosis === "stream-interrupted" ? "network" : diagnosis === "response-invalid" ? "protocol" : diagnosis === "request-too-large" ? "request-too-large" : "provider",
1887
+ source: known ? "protocol" : "unknown",
1888
+ retryable: converted.retryable,
1889
+ ...status === void 0 ? {} : { status },
1890
+ ...code !== void 0 && providerCodes.has(code) ? { providerCode: code } : {}
1891
+ });
1892
+ }
1893
+ //#endregion
1894
+ //#region src/stream.ts
1895
+ /**
1896
+ * A Codex Responses event stream → `ModelChunk`s.
1897
+ *
1898
+ * Deltas are forwarded as they arrive. The final message is assembled from
1899
+ * the `response.output_item.done` events, in order — the `response.completed`
1900
+ * event's own `output` arrives empty from this endpoint — and a reasoning
1901
+ * item becomes a thinking part whose `signature` is its encrypted content,
1902
+ * attributed to the model that produced it so the request side can decide
1903
+ * whether it may go back.
1904
+ */
1905
+ const EVENT_LIMIT = 8388608;
1906
+ const SLAB_SIZE = 65536;
1907
+ /**
1908
+ * Reads JSON SSE events across fragmented LF, CRLF or CR separators.
1909
+ * Yields after 64 KiB of work, counting each incoming byte and each read, including empty reads.
1910
+ * Rejects bodies above 8 MiB before decoding; ignores non-JSON and unfinished EOF events.
1911
+ */
1912
+ async function* sseEvents(body) {
1913
+ const decoder = new TextDecoder();
1914
+ let slabs = [];
1915
+ let slab;
1916
+ let used = 0;
1917
+ let size = 0;
1918
+ let suffix = 0;
1919
+ let pendingDelimiter = 0;
1920
+ let work = 0;
1921
+ const oversized = () => new ModelCallError("SSE event exceeds 8 MiB", { retryable: false });
1922
+ function completed(delimiter) {
1923
+ const length = size - delimiter;
1924
+ if (length > EVENT_LIMIT) throw oversized();
1925
+ const block = decoder.decode(Buffer.concat(slabs, length));
1926
+ slabs = [];
1927
+ slab = void 0;
1928
+ size = 0;
1929
+ used = 0;
1930
+ suffix = 0;
1931
+ pendingDelimiter = 0;
1932
+ const data = block.split(/\r\n|\n|\r/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
1933
+ if (data !== "" && data !== "[DONE]") try {
1934
+ return JSON.parse(data);
1935
+ } catch {}
1936
+ }
1937
+ for await (const bytes of body) {
1938
+ work += 1;
1939
+ if (work >= SLAB_SIZE) {
1940
+ work = 0;
1941
+ await setImmediate();
1942
+ }
1943
+ if (bytes.length === 0) continue;
1944
+ if (pendingDelimiter !== 0) {
1945
+ const event = completed(pendingDelimiter);
1946
+ if (event !== void 0) yield event;
1947
+ }
1948
+ for (let index = 0; index < bytes.length; index += 1) {
1949
+ if (size >= 8388612) throw oversized();
1950
+ if (slab === void 0 || used === SLAB_SIZE) {
1951
+ slab = Buffer.allocUnsafe(SLAB_SIZE);
1952
+ slabs.push(slab);
1953
+ used = 0;
1954
+ }
1955
+ const byte = bytes[index];
1956
+ slab[used++] = byte;
1957
+ size += 1;
1958
+ suffix = (suffix << 8 | byte) >>> 0;
1959
+ const delimiter = (suffix & 65535) === 2570 || (suffix & 65535) === 3341 ? 2 : suffix === 218762506 ? 4 : 0;
1960
+ if (delimiter !== 0) {
1961
+ if (byte === 13 && index === bytes.length - 1) pendingDelimiter = delimiter;
1962
+ else {
1963
+ const event = completed(delimiter);
1964
+ if (event !== void 0) yield event;
1965
+ }
1966
+ }
1967
+ work += 1;
1968
+ if (work >= SLAB_SIZE) {
1969
+ work = 0;
1970
+ await setImmediate();
1971
+ }
1972
+ }
1973
+ }
1974
+ if (pendingDelimiter !== 0) {
1975
+ const event = completed(pendingDelimiter);
1976
+ if (event !== void 0) yield event;
1977
+ } else if (size > EVENT_LIMIT) throw oversized();
1978
+ }
1979
+ const str = (value) => typeof value === "string" ? value : "";
1980
+ const num = (value) => typeof value === "number" && value >= 0 ? value : 0;
1981
+ function partsOf(item, model) {
1982
+ if (item.type === "reasoning") {
1983
+ const text = (Array.isArray(item.summary) ? item.summary : []).map((one) => str(one.text)).join("\n\n");
1984
+ const encrypted = str(item.encrypted_content);
1985
+ return [{
1986
+ type: "thinking",
1987
+ text,
1988
+ ...encrypted === "" ? {} : {
1989
+ signature: encrypted,
1990
+ originModel: model
1991
+ }
1992
+ }];
1993
+ }
1994
+ if (item.type === "message") {
1995
+ const text = (Array.isArray(item.content) ? item.content : []).map((one) => {
1996
+ const part = one;
1997
+ return part.type === "output_text" ? str(part.text) : part.type === "refusal" ? str(part.refusal) : "";
1998
+ }).join("");
1999
+ return text === "" ? [] : [{
2000
+ type: "text",
2001
+ text
2002
+ }];
2003
+ }
2004
+ if (item.type === "function_call") {
2005
+ const id = str(item.call_id);
2006
+ const name = str(item.name);
2007
+ if (id.trim() === "" || name.trim() === "") throw new ModelCallError("invalid model function_call: missing call_id or name", { retryable: false });
2008
+ let input;
2009
+ try {
2010
+ input = JSON.parse(str(item.arguments));
2011
+ } catch {
2012
+ throw new ModelCallError("invalid model function_call: arguments are not complete JSON", { retryable: false });
2013
+ }
2014
+ return [{
2015
+ type: "tool-use",
2016
+ id,
2017
+ name,
2018
+ input
2019
+ }];
2020
+ }
2021
+ return [];
2022
+ }
2023
+ /** UTF-8 bytes over four: rough, and only ever used as a ceiling. */
2024
+ const tokensOf = (text) => Math.ceil(Buffer.byteLength(text, "utf8") / 4);
2025
+ /**
2026
+ * Forwards deltas immediately and assembles terminal output from completed items.
2027
+ * Completed unusable replies and malformed tool calls fail without retry; refusals remain visible.
2028
+ */
2029
+ async function* mapResponsesStream(events, options) {
2030
+ const items = [];
2031
+ const callIds = /* @__PURE__ */ new Map();
2032
+ const pendingCalls = /* @__PURE__ */ new Map();
2033
+ let openThinking = "";
2034
+ let written = 0;
2035
+ let capped = false;
2036
+ let refused = false;
2037
+ let open = "";
2038
+ for await (const event of events) {
2039
+ switch (event.type) {
2040
+ case "response.output_text.delta":
2041
+ case "response.refusal.delta": {
2042
+ if (event.type === "response.refusal.delta") refused = true;
2043
+ const text = str(event.delta);
2044
+ if (text === "") break;
2045
+ yield {
2046
+ kind: "text-delta",
2047
+ text
2048
+ };
2049
+ open += text;
2050
+ written += tokensOf(text);
2051
+ if (options.outputCap !== void 0 && written >= options.outputCap) capped = true;
2052
+ break;
2053
+ }
2054
+ case "response.reasoning_summary_text.delta": {
2055
+ const text = str(event.delta);
2056
+ if (text !== "") {
2057
+ openThinking += text;
2058
+ yield {
2059
+ kind: "thinking-delta",
2060
+ text
2061
+ };
2062
+ }
2063
+ break;
2064
+ }
2065
+ case "response.output_item.added": {
2066
+ const item = event.item;
2067
+ if (item?.type === "function_call") {
2068
+ const callId = str(item.call_id);
2069
+ callIds.set(str(item.id), callId);
2070
+ pendingCalls.set(str(item.id), {
2071
+ id: callId,
2072
+ ...str(item.name) === "" ? {} : { name: str(item.name) }
2073
+ });
2074
+ yield {
2075
+ kind: "tool-call-delta",
2076
+ id: callId,
2077
+ name: str(item.name),
2078
+ inputDelta: ""
2079
+ };
2080
+ }
2081
+ break;
2082
+ }
2083
+ case "response.function_call_arguments.delta": {
2084
+ const callId = callIds.get(str(event.item_id));
2085
+ if (callId !== void 0) yield {
2086
+ kind: "tool-call-delta",
2087
+ id: callId,
2088
+ inputDelta: str(event.delta)
2089
+ };
2090
+ break;
2091
+ }
2092
+ case "response.output_item.done": {
2093
+ const item = event.item;
2094
+ if (item !== void 0) items.push(item);
2095
+ if (item?.type === "function_call") {
2096
+ pendingCalls.delete(str(item.id));
2097
+ for (const [key, call] of pendingCalls) if (call.id === str(item.call_id)) pendingCalls.delete(key);
2098
+ }
2099
+ if (item?.type === "reasoning") openThinking = "";
2100
+ if (item?.type === "message" && Array.isArray(item.content)) refused ||= item.content.some((part) => part.type === "refusal");
2101
+ if (item?.type === "message") open = "";
2102
+ break;
2103
+ }
2104
+ case "response.completed":
2105
+ case "response.incomplete": {
2106
+ const response = event.response ?? {};
2107
+ const usage = response.usage ?? {};
2108
+ const details = usage.input_tokens_details ?? {};
2109
+ const cache = typeof details.cached_tokens === "number" && details.cached_tokens >= 0 ? { cacheReadTokens: details.cached_tokens } : {};
2110
+ const truncated = event.type === "response.incomplete" && response.incomplete_details?.reason === "max_output_tokens";
2111
+ if (event.type === "response.incomplete" && !truncated && !refused) throw new ModelCallError(`incomplete model reply (${event.type})`, { retryable: false });
2112
+ const incompleteToolCalls = [...pendingCalls.values()];
2113
+ const content = items.flatMap((item) => {
2114
+ try {
2115
+ return partsOf(item, options.model);
2116
+ } catch (error) {
2117
+ if (!truncated || item.type !== "function_call" || str(item.call_id) === "") throw error;
2118
+ incompleteToolCalls.push({
2119
+ id: str(item.call_id),
2120
+ ...str(item.name) === "" ? {} : { name: str(item.name) }
2121
+ });
2122
+ return [];
2123
+ }
2124
+ });
2125
+ if (truncated && open !== "") content.push({
2126
+ type: "text",
2127
+ text: open
2128
+ });
2129
+ if (truncated && openThinking !== "") content.unshift({
2130
+ type: "thinking",
2131
+ text: openThinking
2132
+ });
2133
+ if (!truncated && !refused && !content.some((part) => part.type === "text" || part.type === "tool-use")) throw new ModelCallError(`incomplete model reply (${event.type})`, {
2134
+ retryable: false,
2135
+ failureCode: "response-empty"
2136
+ });
2137
+ const finishReason = refused ? "refusal" : truncated ? "max-tokens" : content.some((part) => part.type === "tool-use") ? "tool-calls" : "stop";
2138
+ yield {
2139
+ kind: "final",
2140
+ finishReason,
2141
+ ...finishReason === "max-tokens" ? { outputTruncation: {
2142
+ incompleteToolCalls,
2143
+ ...openThinking !== "" ? { incompleteThinking: true } : {}
2144
+ } } : {},
2145
+ usage: {
2146
+ inputTokens: num(usage.input_tokens),
2147
+ outputTokens: num(usage.output_tokens),
2148
+ ...cache
2149
+ },
2150
+ message: {
2151
+ role: "assistant",
2152
+ content
2153
+ }
2154
+ };
2155
+ return;
2156
+ }
2157
+ case "response.failed":
2158
+ case "error": throw toFailedError(event.response?.error ?? event.error ?? event);
2159
+ }
2160
+ if (capped) {
2161
+ options.stop?.();
2162
+ const content = items.flatMap((item) => partsOf(item, options.model));
2163
+ if (open !== "") content.push({
2164
+ type: "text",
2165
+ text: open
2166
+ });
2167
+ if (openThinking !== "") content.unshift({
2168
+ type: "thinking",
2169
+ text: openThinking
2170
+ });
2171
+ yield {
2172
+ kind: "final",
2173
+ finishReason: "max-tokens",
2174
+ outputTruncation: {
2175
+ incompleteToolCalls: [...pendingCalls.values()],
2176
+ ...openThinking !== "" ? { incompleteThinking: true } : {}
2177
+ },
2178
+ usage: {
2179
+ inputTokens: 0,
2180
+ outputTokens: written
2181
+ },
2182
+ message: {
2183
+ role: "assistant",
2184
+ content
2185
+ }
2186
+ };
2187
+ return;
2188
+ }
2189
+ }
2190
+ throw new ModelCallError("model stream interrupted", {
2191
+ retryable: true,
2192
+ failureCode: "stream-interrupted"
2193
+ });
2194
+ }
2195
+ //#endregion
2196
+ //#region src/port.ts
2197
+ /** Requires one host-owned transport identity before any network request. */
2198
+ function transportIdOf(request) {
2199
+ const identity = request.sessionId ?? request.callId;
2200
+ if (!identity || request.sessionId !== void 0 && request.callId !== void 0) throw new Error("a model request requires exactly one session or call identity");
2201
+ return identity;
2202
+ }
2203
+ /** Builds authenticated headers without exposing the token in request metadata. */
2204
+ function headersOf(tokens, sessionId, userAgent) {
2205
+ return {
2206
+ authorization: `Bearer ${tokens.access}`,
2207
+ "chatgpt-account-id": tokens.accountId,
2208
+ originator: ORIGINATOR,
2209
+ "user-agent": userAgent,
2210
+ "session-id": sessionId,
2211
+ "content-type": "application/json",
2212
+ accept: "text/event-stream",
2213
+ ...tokens.residency === void 0 ? {} : { "x-openai-internal-codex-residency": tokens.residency }
2214
+ };
2215
+ }
2216
+ /** Creates a port whose retries preserve the host-selected transport identity. */
2217
+ function createChatGPTPort(options) {
2218
+ function build(request, inspection = false) {
2219
+ const plan = options.params(request.params ?? {});
2220
+ const media = options.media?.(request.params ?? {}) ?? {
2221
+ acceptsImages: true,
2222
+ limits: void 0
2223
+ };
2224
+ const replay = options.support.planEffortCache({
2225
+ request,
2226
+ inspection,
2227
+ effort: plan.effort,
2228
+ capability: plan.thinking === false || plan.effort === "none" ? {
2229
+ status: "unsupported",
2230
+ reason: "reasoning-required"
2231
+ } : { status: "supported" },
2232
+ bindingFingerprint: options.support.inspectionFingerprint({
2233
+ connectionFingerprint: options.connectionFingerprint,
2234
+ providerId: options.providerId,
2235
+ endpoint: options.endpoints.codex,
2236
+ model: options.model,
2237
+ system: request.system,
2238
+ tools: request.tools,
2239
+ thinking: plan.thinking
2240
+ })
2241
+ });
2242
+ const body = options.codec.render(request, {
2243
+ model: options.model,
2244
+ endpoint: options.providerId,
2245
+ effort: replay.baseEffort,
2246
+ effortUpdates: replay.updates,
2247
+ media
2248
+ }, options.support.planMedia(request, media));
2249
+ const sessionId = transportIdOf(request);
2250
+ return {
2251
+ body: {
2252
+ ...body,
2253
+ prompt_cache_key: sessionId
2254
+ },
2255
+ sessionId,
2256
+ media,
2257
+ replay
2258
+ };
2259
+ }
2260
+ async function send(body, sessionId, signal, login, reportProgress) {
2261
+ const envelope = await login.get(signal);
2262
+ const tokens = parseStored(envelope.credential);
2263
+ if (tokens === void 0) throw notSignedIn();
2264
+ const response = await options.fetch(`${options.endpoints.codex}/responses`, {
2265
+ method: "POST",
2266
+ headers: headersOf(tokens, sessionId, options.userAgent),
2267
+ body: JSON.stringify(body),
2268
+ signal
2269
+ });
2270
+ return {
2271
+ response: options.support.observeResponseProgress(response, reportProgress),
2272
+ credential: envelope.credential
2273
+ };
2274
+ }
2275
+ return {
2276
+ inspect(request) {
2277
+ const { body, media } = build(request, true);
2278
+ const capacity = options.catalogue()?.limit;
2279
+ return options.support.inspect({
2280
+ body,
2281
+ textTokens: options.support.measureText(options.support.requestText("responses", body)),
2282
+ tokenizer: "o200k_base-protocol-text-v2",
2283
+ hiddenInputTokens: options.codec.hiddenInputTokens(request, {
2284
+ model: options.model,
2285
+ endpoint: options.providerId
2286
+ }),
2287
+ request,
2288
+ capacity,
2289
+ outputReserve: capacity?.output ?? 0,
2290
+ outputReserveSource: "capacity",
2291
+ mediaTokens: options.support.planMedia(request, media).tokens,
2292
+ fingerprint: {
2293
+ rendering: "chatgpt-codex-responses-v2",
2294
+ endpoint: options.providerId,
2295
+ model: options.model,
2296
+ purpose: request.purpose,
2297
+ contextWindow: request.params?.contextWindow,
2298
+ capacity,
2299
+ settings: { effort: body.reasoning?.effort }
2300
+ }
2301
+ });
2302
+ },
2303
+ stream(request, signal, observer) {
2304
+ signal.throwIfAborted();
2305
+ const login = options.login;
2306
+ const { body, sessionId, replay } = build(request);
2307
+ const cap = request.purpose === "summary" && request.summaryMaxTokens !== void 0 ? request.summaryMaxTokens : void 0;
2308
+ return options.support.withChatAttempts({
2309
+ signal,
2310
+ ...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries },
2311
+ ...options.sleep === void 0 ? {} : { sleep: options.sleep },
2312
+ ...options.idleMs === void 0 ? {} : { idleMs: options.idleMs },
2313
+ async *attempt(attemptSignal, reportProgress, attempt) {
2314
+ const observe = (event) => {
2315
+ try {
2316
+ observer?.observe?.(event);
2317
+ } catch {}
2318
+ };
2319
+ const attemptCount = attempt + 1;
2320
+ let outcome = "unknown";
2321
+ let firstResponseFinished = false;
2322
+ observe({
2323
+ stage: "attempt",
2324
+ phase: "start",
2325
+ attempt: attemptCount
2326
+ });
2327
+ observe({
2328
+ stage: "first-response",
2329
+ phase: "start",
2330
+ attempt: attemptCount
2331
+ });
2332
+ try {
2333
+ attemptSignal.throwIfAborted();
2334
+ if (login === void 0) throw notSignedIn();
2335
+ const { response, credential } = await send(body, sessionId, attemptSignal, login, reportProgress);
2336
+ firstResponseFinished = true;
2337
+ observe({
2338
+ stage: "first-response",
2339
+ phase: "end",
2340
+ attempt: attemptCount,
2341
+ outcome: "ok"
2342
+ });
2343
+ if (response.status === 401) {
2344
+ await response.body?.cancel();
2345
+ login.expire(credential);
2346
+ throw signInExpired(401);
2347
+ }
2348
+ if (!response.ok || response.body === null) {
2349
+ const refused = toPortError(response.status, await response.text().catch(() => ""));
2350
+ const wait = Number(response.headers.get("retry-after"));
2351
+ throw Number.isFinite(wait * 1e3) && wait > 0 && refused.retryable ? new ModelCallError(refused.message, {
2352
+ failureCode: refused.failureCode,
2353
+ code: refused.code,
2354
+ ...refused.status === void 0 ? {} : { status: refused.status },
2355
+ retryable: true,
2356
+ retryAfterMs: wait * 1e3,
2357
+ ...refused.providerText === void 0 ? {} : { providerText: refused.providerText }
2358
+ }) : refused;
2359
+ }
2360
+ attemptSignal.throwIfAborted();
2361
+ try {
2362
+ observer?.dispatched({ attempt });
2363
+ } catch {}
2364
+ attemptSignal.throwIfAborted();
2365
+ const stream = response.body;
2366
+ for await (const chunk of mapResponsesStream(sseEvents(stream), {
2367
+ model: options.model,
2368
+ ...cap === void 0 ? {} : { outputCap: cap },
2369
+ stop: () => void stream.cancel().catch(() => {})
2370
+ })) {
2371
+ if (chunk.kind === "final") outcome = "ok";
2372
+ yield chunk.kind === "final" && replay.replayData ? {
2373
+ ...chunk,
2374
+ replayData: replay.replayData
2375
+ } : chunk;
2376
+ }
2377
+ } catch (error) {
2378
+ outcome = attemptSignal.aborted ? "cancelled" : "error";
2379
+ throw error;
2380
+ } finally {
2381
+ if (!firstResponseFinished) observe({
2382
+ stage: "first-response",
2383
+ phase: "end",
2384
+ attempt: attemptCount,
2385
+ outcome
2386
+ });
2387
+ observe({
2388
+ stage: "attempt",
2389
+ phase: "end",
2390
+ attempt: attemptCount,
2391
+ outcome
2392
+ });
2393
+ }
2394
+ },
2395
+ classify(error) {
2396
+ const portError = toCallError(error);
2397
+ return {
2398
+ error: toModelFailure(error, options.support.failures),
2399
+ retryable: portError.retryable,
2400
+ retryAfterMs: portError.retryAfterMs
2401
+ };
2402
+ }
2403
+ });
2404
+ }
2405
+ };
2406
+ }
2407
+ //#endregion
2408
+ //#region src/probe.ts
2409
+ function effortsOf(levels) {
2410
+ if (!Array.isArray(levels)) return void 0;
2411
+ const named = levels.map((one) => typeof one === "string" ? one : one?.effort).filter((one) => typeof one === "string");
2412
+ const accepted = ACCEPTED_EFFORTS.filter((effort) => named.includes(effort));
2413
+ return accepted.length === 0 ? void 0 : accepted;
2414
+ }
2415
+ function offerOf(model) {
2416
+ if (typeof model.slug !== "string" || model.visibility !== "list") return void 0;
2417
+ const efforts = effortsOf(model.supported_reasoning_levels);
2418
+ return {
2419
+ model: model.slug,
2420
+ ...typeof model.display_name === "string" ? { name: model.display_name } : {},
2421
+ ...Number.isInteger(model.context_window) && Number(model.context_window) > 0 ? { contextWindow: Number(model.context_window) } : {},
2422
+ ...efforts === void 0 ? {} : { efforts }
2423
+ };
2424
+ }
2425
+ /**
2426
+ * The secret tokens this probe could have carried: the ones it called with
2427
+ * and the ones stored now. A refresh during the probe writes tokens after the
2428
+ * host collected its redaction set, so the probe checks the endpoint's words
2429
+ * against them itself.
2430
+ */
2431
+ function heldTokens(used, stored) {
2432
+ const now = parseStored(stored);
2433
+ return [
2434
+ used.access,
2435
+ used.refresh,
2436
+ now?.access,
2437
+ now?.refresh
2438
+ ].filter((one) => one !== void 0 && one !== "");
2439
+ }
2440
+ function createProbes(deps) {
2441
+ async function listModels(probe) {
2442
+ const login = probe.login;
2443
+ if (login === void 0) throw new Error("not signed in");
2444
+ const envelope = await login.get(probe.signal);
2445
+ const tokens = parseStored(envelope.credential);
2446
+ if (tokens === void 0) throw new Error("not signed in");
2447
+ const response = await deps.fetch(`${deps.endpoints.codex}/models?client_version=${CODEX_CLIENT_VERSION}`, {
2448
+ method: "GET",
2449
+ headers: {
2450
+ authorization: `Bearer ${tokens.access}`,
2451
+ "chatgpt-account-id": tokens.accountId,
2452
+ originator: ORIGINATOR,
2453
+ "user-agent": deps.userAgent,
2454
+ accept: "application/json"
2455
+ },
2456
+ signal: probe.signal
2457
+ });
2458
+ if (!response.ok) {
2459
+ const body = await response.text().catch(() => "");
2460
+ const echoes = heldTokens(tokens, envelope.credential).some((token) => body.includes(token));
2461
+ throw Object.assign(new Error(body === "" || echoes ? response.statusText : body), { status: response.status });
2462
+ }
2463
+ const body = await response.json();
2464
+ return (Array.isArray(body.models) ? body.models : []).map(offerOf).filter((one) => one !== void 0);
2465
+ }
2466
+ return {
2467
+ listModels,
2468
+ async testConnection(probe) {
2469
+ await listModels(probe);
2470
+ }
2471
+ };
2472
+ }
2473
+ //#endregion
2474
+ //#region src/support.ts
2475
+ /** Normalizes official facts without requiring an installed Chat capability. */
2476
+ function normalizeOffers(input) {
2477
+ return Object.fromEntries(input.offers.map((offer) => [offer.model, {
2478
+ id: offer.model,
2479
+ ...offer.name === void 0 ? {} : { name: offer.name },
2480
+ ...offer.reasoning === void 0 && !offer.efforts?.length ? {} : { reasoning: offer.efforts?.length ? true : offer.reasoning },
2481
+ ...offer.efforts?.length ? { reasoning_options: [{
2482
+ type: "effort",
2483
+ values: [...offer.efforts]
2484
+ }] } : {},
2485
+ ...offer.temperature === void 0 ? {} : { temperature: offer.temperature },
2486
+ ...offer.toolCall === void 0 ? {} : { tool_call: offer.toolCall },
2487
+ ...offer.input === void 0 ? {} : { attachment: offer.input.some((one) => one !== "text") },
2488
+ ...offer.input === void 0 && offer.output === void 0 ? {} : { modalities: {
2489
+ ...offer.input === void 0 ? {} : { input: [...offer.input] },
2490
+ ...offer.output === void 0 ? {} : { output: [...offer.output] }
2491
+ } },
2492
+ ...!offer.contextWindow && !offer.maxOutput ? {} : { limit: {
2493
+ ...offer.contextWindow && offer.contextWindow > 0 ? { context: offer.contextWindow } : {},
2494
+ ...offer.maxOutput && offer.maxOutput > 0 ? { output: offer.maxOutput } : {}
2495
+ } },
2496
+ ...offer.releaseDate === void 0 ? {} : { release_date: offer.releaseDate }
2497
+ }]));
2498
+ }
2499
+ /** The owner-qualified accessor is read per operation; withdrawal never exposes an old service. */
2500
+ function lazySupport(read) {
2501
+ return new Proxy({}, { get(_target, key) {
2502
+ if (key === "normalizeListing") return normalizeOffers;
2503
+ const service = read();
2504
+ if (!service) throw new Error("Chat model support is unavailable");
2505
+ const value = Reflect.get(service, key);
2506
+ return typeof value === "function" ? value.bind(service) : value;
2507
+ } });
2508
+ }
2509
+ //#endregion
2510
+ //#region src/protocol.ts
2511
+ const USER_AGENT = "yaoguang";
2512
+ /** Builds the subscription provider using host-owned planning, routing and login credentials. */
2513
+ function chatgptProtocolType(support, fetch, codec, options = {}, imageInfo = (bytes, signal) => support.imageInfo(bytes, signal)) {
2514
+ const endpoints = options.endpoints ?? ENDPOINTS;
2515
+ const probes = createProbes({
2516
+ fetch,
2517
+ endpoints,
2518
+ userAgent: USER_AGENT
2519
+ });
2520
+ return {
2521
+ type: "chatgpt-subscription",
2522
+ label: "ChatGPT Subscription",
2523
+ logo: catalog.vendors[0]?.logo,
2524
+ fields: [],
2525
+ userConfigurable: false,
2526
+ optionsSchema: z.strictObject({}),
2527
+ facts,
2528
+ effortCacheCapability(input) {
2529
+ return input.params.thinking === false || input.params.effort === "none" ? {
2530
+ status: "unsupported",
2531
+ reason: "reasoning-required"
2532
+ } : { status: "supported" };
2533
+ },
2534
+ prepareParams: (input) => support.prepareParams(input, facts),
2535
+ adapters: {
2536
+ chat: {
2537
+ fileKinds: ["image"],
2538
+ supportsModel: (model) => !/^gpt-image(?:-|$)/.test(model),
2539
+ createPort: (call) => createChatGPTPort({
2540
+ fetch,
2541
+ codec,
2542
+ connectionFingerprint: call.connectionFingerprint,
2543
+ endpoints,
2544
+ model: call.model,
2545
+ providerId: call.providerId,
2546
+ login: call.login,
2547
+ support,
2548
+ userAgent: USER_AGENT,
2549
+ catalogue: () => support.catalogue(call.providerId, call.model),
2550
+ media: (stored) => support.mediaContextOf({
2551
+ entry: support.catalogue(call.providerId, call.model),
2552
+ stored,
2553
+ facts,
2554
+ endpoint: support.endpoint(call.providerId)
2555
+ }),
2556
+ params: (stored) => support.resolveParams({
2557
+ facts,
2558
+ model: call.model,
2559
+ endpoint: support.endpoint(call.providerId),
2560
+ entry: support.catalogue(call.providerId, call.model),
2561
+ stored
2562
+ }),
2563
+ ...options.sleep === void 0 ? {} : { sleep: options.sleep }
2564
+ })
2565
+ },
2566
+ image: {
2567
+ models: () => [IMAGE_GENERATION_MODEL],
2568
+ createPort: (call) => createImageGenerationPort({
2569
+ fetch,
2570
+ baseUrl: endpoints.codex,
2571
+ login: call.login,
2572
+ imageInfo,
2573
+ userAgent: USER_AGENT
2574
+ })
2575
+ }
2576
+ },
2577
+ normalizeListing: (input) => normalizeOffers(input),
2578
+ async listModels(probe) {
2579
+ const offers = await probes.listModels(probe);
2580
+ if (offers.some((offer) => offer.model === IMAGE_GENERATION_MODEL.id)) return { offers };
2581
+ return { offers: [...offers, {
2582
+ model: IMAGE_GENERATION_MODEL.id,
2583
+ name: IMAGE_GENERATION_MODEL.label,
2584
+ input: ["text", "image"],
2585
+ output: [...IMAGE_GENERATION_MODEL.outputKinds]
2586
+ }] };
2587
+ },
2588
+ testConnection: (probe) => probes.testConnection(probe)
2589
+ };
2590
+ }
2591
+ //#endregion
2592
+ //#region src/callback.ts
2593
+ /**
2594
+ * The loopback listener a browser sign-in returns to.
2595
+ *
2596
+ * The redirect address is fixed by the client's registration —
2597
+ * `http://localhost:1455/auth/callback` — and a browser may resolve
2598
+ * `localhost` to either loopback address, while Node's `listen('localhost')`
2599
+ * binds only the first one it resolves. So the same port is opened on both
2600
+ * `127.0.0.1` and `::1`, one flow behind the two; a machine with no IPv6
2601
+ * loopback serves IPv4 alone. Either address being taken refuses the whole
2602
+ * sign-in: a browser landing on the other program would hand it the code.
2603
+ */
2604
+ /** The listener is in use by something else. */
2605
+ var CallbackPortInUse = class extends Error {
2606
+ constructor(port) {
2607
+ super(`port ${String(port)} is in use`);
2608
+ this.name = "CallbackPortInUse";
2609
+ }
2610
+ };
2611
+ function listen(port, host) {
2612
+ return new Promise((resolve, reject) => {
2613
+ const server = createServer();
2614
+ server.once("error", reject);
2615
+ server.listen({
2616
+ port,
2617
+ host,
2618
+ exclusive: true
2619
+ }, () => {
2620
+ server.off("error", reject);
2621
+ resolve(server);
2622
+ });
2623
+ });
2624
+ }
2625
+ function page(ok) {
2626
+ return `<!doctype html><html><head><meta charset="utf-8"><title>YaoGuang</title></head><body style="font-family:system-ui;margin:3rem"><p>${ok ? "Sign-in complete. You can close this page. 登录完成,可以关闭此页。" : "Sign-in was not completed. You can close this page. 登录未完成,可以关闭此页。"}</p><script>setTimeout(function(){window.close()},300)<\/script></body></html>`;
2627
+ }
2628
+ function respond(res, status, ok) {
2629
+ res.writeHead(status, {
2630
+ "content-type": "text/html; charset=utf-8",
2631
+ "cache-control": "no-store"
2632
+ });
2633
+ res.end(page(ok));
2634
+ }
2635
+ /**
2636
+ * Opens the listener pair for one flow. Rejects with `CallbackPortInUse`
2637
+ * when either address is taken, closing whichever one had opened.
2638
+ */
2639
+ async function openCallback(port, state) {
2640
+ const servers = [];
2641
+ let bound = port;
2642
+ try {
2643
+ const v4 = await listen(port, "127.0.0.1");
2644
+ servers.push(v4);
2645
+ bound = v4.address().port;
2646
+ try {
2647
+ servers.push(await listen(bound, "::1"));
2648
+ } catch (error) {
2649
+ const code = error.code;
2650
+ if (code !== "EADDRNOTAVAIL" && code !== "EAFNOSUPPORT") throw error;
2651
+ }
2652
+ } catch (error) {
2653
+ for (const server of servers) server.close();
2654
+ if (error.code === "EADDRINUSE") throw new CallbackPortInUse(bound);
2655
+ throw error;
2656
+ }
2657
+ let settle = () => {};
2658
+ const result = new Promise((resolve) => {
2659
+ settle = resolve;
2660
+ });
2661
+ let waiting;
2662
+ let taken = false;
2663
+ const handle = (req, res) => {
2664
+ const url = new URL(req.url ?? "/", `http://localhost:${String(bound)}`);
2665
+ if (req.method !== "GET" || url.pathname !== "/auth/callback") {
2666
+ res.writeHead(404).end();
2667
+ return;
2668
+ }
2669
+ if (url.searchParams.get("state") !== state || taken) {
2670
+ respond(res, 400, false);
2671
+ return;
2672
+ }
2673
+ taken = true;
2674
+ const error = url.searchParams.get("error");
2675
+ const code = url.searchParams.get("code");
2676
+ if (error !== null || code === null || code === "") {
2677
+ respond(res, 200, false);
2678
+ settle({
2679
+ kind: "error",
2680
+ error: error !== null && /^[a-z_]{1,64}$/.test(error) ? error : "error"
2681
+ });
2682
+ return;
2683
+ }
2684
+ waiting = res;
2685
+ settle({
2686
+ kind: "code",
2687
+ code
2688
+ });
2689
+ };
2690
+ for (const server of servers) server.on("request", handle);
2691
+ let closed = false;
2692
+ return {
2693
+ port: bound,
2694
+ result,
2695
+ finish(ok) {
2696
+ if (closed) return;
2697
+ closed = true;
2698
+ if (waiting !== void 0) respond(waiting, 200, ok);
2699
+ for (const server of servers) {
2700
+ server.close();
2701
+ server.closeAllConnections();
2702
+ }
2703
+ }
2704
+ };
2705
+ }
2706
+ //#endregion
2707
+ //#region src/login.ts
2708
+ /**
2709
+ * The two ways a ChatGPT account signs in.
2710
+ *
2711
+ * Browser: a loopback listener waits for the authorization page to send the
2712
+ * person back with a code. Device code: the person types a code at an
2713
+ * address on any device, and this side polls until the account confirms it.
2714
+ * Both end in the same token exchange and the same stored shape.
2715
+ *
2716
+ * The host owns each flow's life — its deadline, its cancellation, where the
2717
+ * result goes — and hands that over as the `signal`. What this module owns is
2718
+ * the one process-wide listener: its port is fixed, so a second browser
2719
+ * sign-in while one waits is refused rather than queued.
2720
+ */
2721
+ function resultOf(tokens) {
2722
+ return {
2723
+ credential: JSON.stringify(tokens),
2724
+ account: tokens.account,
2725
+ refreshAfter: refreshAfterOf(tokens)
2726
+ };
2727
+ }
2728
+ function aborted(signal) {
2729
+ return new Promise((_, reject) => {
2730
+ const fail = () => reject(new LoginFailure("cancelled"));
2731
+ if (signal.aborted) fail();
2732
+ else signal.addEventListener("abort", fail, { once: true });
2733
+ });
2734
+ }
2735
+ function createLogin(deps) {
2736
+ let listening = false;
2737
+ async function browser(signal) {
2738
+ if (listening) throw new Error("another ChatGPT sign-in is in progress; finish or cancel it, or use device code sign-in");
2739
+ const codes = pkce();
2740
+ const state = newState();
2741
+ listening = true;
2742
+ let callback;
2743
+ try {
2744
+ callback = await openCallback(deps.callbackPort, state);
2745
+ } catch (error) {
2746
+ listening = false;
2747
+ if (error instanceof CallbackPortInUse) throw new Error(`port ${String(deps.callbackPort)} is in use (another ChatGPT sign-in, such as the Codex CLI, may be running); use device code sign-in instead`);
2748
+ throw error;
2749
+ }
2750
+ const redirectUri = `http://localhost:${String(callback.port)}${CALLBACK_PATH}`;
2751
+ const completion = (async () => {
2752
+ try {
2753
+ const answer = await Promise.race([callback.result, aborted(signal)]);
2754
+ if (answer.kind === "error") throw new LoginFailure("declined", answer.error);
2755
+ const tokens = await exchangeCode(deps.fetch, deps.endpoints, {
2756
+ code: answer.code,
2757
+ verifier: codes.verifier,
2758
+ redirectUri
2759
+ }, deps.now, signal);
2760
+ callback.finish(true);
2761
+ return resultOf(tokens);
2762
+ } catch (error) {
2763
+ callback.finish(false);
2764
+ throw error;
2765
+ } finally {
2766
+ listening = false;
2767
+ }
2768
+ })();
2769
+ completion.catch(() => {});
2770
+ return {
2771
+ url: authorizeUrl(deps.endpoints, redirectUri, codes, state),
2772
+ completion
2773
+ };
2774
+ }
2775
+ async function device(signal) {
2776
+ const headers = {
2777
+ "content-type": "application/json",
2778
+ "user-agent": deps.userAgent
2779
+ };
2780
+ let response;
2781
+ try {
2782
+ response = await deps.fetch(`${deps.endpoints.issuer}/api/accounts/deviceauth/usercode`, {
2783
+ method: "POST",
2784
+ headers,
2785
+ body: JSON.stringify({ client_id: CLIENT_ID }),
2786
+ signal
2787
+ });
2788
+ } catch {
2789
+ throw new Error("could not start device code sign-in (unreachable)");
2790
+ }
2791
+ if (!response.ok) throw new Error(`could not start device code sign-in (HTTP ${String(response.status)})`);
2792
+ const started = await response.json().catch(() => ({}));
2793
+ const deviceAuthId = started.device_auth_id;
2794
+ const userCode = started.user_code;
2795
+ if (typeof deviceAuthId !== "string" || typeof userCode !== "string") throw new Error("could not start device code sign-in (unreadable answer)");
2796
+ const named = Number(started.interval);
2797
+ const interval = (Number.isFinite(named) && named >= 0 ? named : 5) * 1e3 + deps.pollMarginMs;
2798
+ const completion = (async () => {
2799
+ for (;;) {
2800
+ await Promise.race([setTimeout(interval, void 0, { signal }), aborted(signal)]).catch(() => {
2801
+ throw new LoginFailure("cancelled");
2802
+ });
2803
+ let poll;
2804
+ try {
2805
+ poll = await deps.fetch(`${deps.endpoints.issuer}/api/accounts/deviceauth/token`, {
2806
+ method: "POST",
2807
+ headers,
2808
+ body: JSON.stringify({
2809
+ device_auth_id: deviceAuthId,
2810
+ user_code: userCode
2811
+ }),
2812
+ signal
2813
+ });
2814
+ } catch {
2815
+ if (signal.aborted) throw new LoginFailure("cancelled");
2816
+ continue;
2817
+ }
2818
+ if (poll.status === 403 || poll.status === 404) continue;
2819
+ if (!poll.ok) throw new LoginFailure("device-failed", `HTTP ${String(poll.status)}`);
2820
+ const granted = await poll.json().catch(() => ({}));
2821
+ if (typeof granted.authorization_code !== "string" || typeof granted.code_verifier !== "string") throw new LoginFailure("device-failed", "unreadable answer");
2822
+ return resultOf(await exchangeCode(deps.fetch, deps.endpoints, {
2823
+ code: granted.authorization_code,
2824
+ verifier: granted.code_verifier,
2825
+ redirectUri: `${deps.endpoints.issuer}/deviceauth/callback`
2826
+ }, deps.now, signal));
2827
+ }
2828
+ })();
2829
+ completion.catch(() => {});
2830
+ return {
2831
+ url: `${deps.endpoints.issuer}/codex/device`,
2832
+ userCode,
2833
+ completion
2834
+ };
2835
+ }
2836
+ return {
2837
+ async refresh(credential, signal) {
2838
+ const before = parseStored(credential);
2839
+ if (before === void 0) throw new LoginFailure("expired");
2840
+ const outcome = await refreshTokens(deps.fetch, deps.endpoints, before, deps.now, signal);
2841
+ if (outcome.kind === "refused") throw new LoginFailure("expired");
2842
+ if (outcome.kind === "failed") throw new LoginFailure("exchange-failed", outcome.detail);
2843
+ return resultOf(outcome.tokens);
2844
+ },
2845
+ methods: [{
2846
+ id: "browser",
2847
+ label: "Sign in with a browser",
2848
+ kind: "browser"
2849
+ }, {
2850
+ id: "device",
2851
+ label: "Sign in with a device code",
2852
+ kind: "device"
2853
+ }],
2854
+ begin(method, signal) {
2855
+ if (method === "browser") return browser(signal);
2856
+ if (method === "device") return device(signal);
2857
+ return Promise.reject(/* @__PURE__ */ new Error(`no sign-in method "${method}"`));
2858
+ }
2859
+ };
2860
+ }
2861
+ //#endregion
2862
+ //#region src/type.ts
2863
+ /** Stable subscription supplier identity. */
2864
+ const TYPE = "chatgpt";
2865
+ /** Host-managed sealed login field. */
2866
+ const LOGIN_FIELD = "account";
2867
+ /** Declares login and subscription access while the protocol owns concrete model calls. */
2868
+ function chatgptProviderType(fetch, options = {}, support) {
2869
+ return {
2870
+ type: TYPE,
2871
+ label: "ChatGPT subscription",
2872
+ fields: [{
2873
+ key: LOGIN_FIELD,
2874
+ kind: "login",
2875
+ label: "ChatGPT account",
2876
+ required: true
2877
+ }],
2878
+ endpoints: [{
2879
+ id: "subscription",
2880
+ label: "ChatGPT subscription",
2881
+ capabilities: {
2882
+ chat: {
2883
+ protocolIds: ["chatgpt-subscription"],
2884
+ defaultProtocolId: "chatgpt-subscription"
2885
+ },
2886
+ image: {
2887
+ protocolIds: ["chatgpt-subscription"],
2888
+ defaultProtocolId: "chatgpt-subscription"
2889
+ }
2890
+ }
2891
+ }],
2892
+ login: createLogin({
2893
+ fetch,
2894
+ endpoints: options.endpoints ?? ENDPOINTS,
2895
+ now: options.now ?? Date.now,
2896
+ callbackPort: options.callbackPort ?? 1455,
2897
+ pollMarginMs: options.pollMarginMs ?? 3e3,
2898
+ userAgent: "yaoguang"
2899
+ }),
2900
+ catalog: support ? {
2901
+ ...catalog,
2902
+ source: support.catalogueSource("https://models.dev/api.json", "models")
2903
+ } : catalog
2904
+ };
2905
+ }
2906
+ //#endregion
2907
+ //#region src/index.ts
2908
+ const plugin = {
2909
+ name: "@eum-agent/llm-provider-chatgpt",
2910
+ requires: [
2911
+ "model-catalogue-sources",
2912
+ "network",
2913
+ "openai-responses-codec",
2914
+ "file-media"
2915
+ ],
2916
+ optionalServices: ["model-support"],
2917
+ apply(ctx) {
2918
+ const support = lazySupport(() => ctx.services.find("model-support"));
2919
+ const catalogue = ctx.services.get("model-catalogue-sources");
2920
+ const network = ctx.services.get("network");
2921
+ ctx.modelProtocols.register(chatgptProtocolType(support, network.fetch, ctx.services.get("openai-responses-codec"), {}, ctx.services.get("file-media").imageInfo));
2922
+ ctx.modelProviders.register(chatgptProviderType(network.fetch, {}, catalogue));
2923
+ }
2924
+ };
2925
+ //#endregion
2926
+ export { plugin as default };