@wuyaos/pi-model-config 0.1.2 → 0.1.4
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/README.md +8 -0
- package/model-config.schema.json +7 -1
- package/package.json +1 -1
- package/src/command.ts +1069 -1053
- package/src/config.ts +4 -0
- package/src/i18n.ts +10 -0
- package/src/image-apis/common.ts +219 -0
- package/src/image-apis/dashscope.ts +42 -0
- package/src/image-apis/gemini.ts +36 -0
- package/src/image-apis/index.ts +15 -0
- package/src/image-apis/openai.ts +32 -0
- package/src/image-apis/xai.ts +27 -0
- package/src/runtime.ts +20 -3
- package/src/selector.ts +433 -433
- package/src/transport.ts +3 -2
- package/src/types.ts +4 -2
package/src/command.ts
CHANGED
|
@@ -1,1053 +1,1069 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import type { AnyModel } from "@earendil-works/pi-ai";
|
|
3
|
-
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
4
|
-
import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
authDescription,
|
|
7
|
-
readConfigFiles,
|
|
8
|
-
redactUrl,
|
|
9
|
-
sanitizeError,
|
|
10
|
-
validateHttpUrl,
|
|
11
|
-
validateProviderId,
|
|
12
|
-
writeConfigTransaction,
|
|
13
|
-
} from "./config.ts";
|
|
14
|
-
import { buildDiscoveryCandidates, discoverModelsFromCandidates } from "./discovery.ts";
|
|
15
|
-
import { formatError, mcError, t, type UiLanguage } from "./i18n.ts";
|
|
16
|
-
import {
|
|
17
|
-
showConnectionPanel,
|
|
18
|
-
showPreviewPanel,
|
|
19
|
-
showProviderPanel,
|
|
20
|
-
showConfirmationPanel,
|
|
21
|
-
type MenuPanelState,
|
|
22
|
-
type OverviewPanelState,
|
|
23
|
-
type PanelItem,
|
|
24
|
-
} from "./panel.ts";
|
|
25
|
-
import { candidateFromExternal, candidateFromInstalled, candidateFromManaged, mergeCandidates, selectModels, type ModelPanelState } from "./selector.ts";
|
|
26
|
-
import { CHAT_APIS, CLASSIFIER_APIS, IMAGE_APIS, PROXY_CHAT_APIS } from "./transport.ts";
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
api,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
draft.
|
|
206
|
-
|
|
207
|
-
draft.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
draft.temporaryKey =
|
|
215
|
-
draft.authMode = "
|
|
216
|
-
} else {
|
|
217
|
-
draft.apiKeyRaw = undefined;
|
|
218
|
-
draft.temporaryKey = undefined;
|
|
219
|
-
draft.authMode = "
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{ id: "
|
|
237
|
-
{ id: "
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
case "
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
case "
|
|
271
|
-
case "
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
case "
|
|
289
|
-
const value = await ctx.ui.select("
|
|
290
|
-
if (value) draft.
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
): {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
].
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
`name=${
|
|
463
|
-
`owned=${
|
|
464
|
-
`enabled=${
|
|
465
|
-
`base=${redactUrl(
|
|
466
|
-
`discovery=${redactUrl(
|
|
467
|
-
`auth=${
|
|
468
|
-
`discoveryAuth=${
|
|
469
|
-
`proxy=${redactUrl(
|
|
470
|
-
`apis=${
|
|
471
|
-
`defaults=${
|
|
472
|
-
].join(" | ");
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
): Promise<{ id: string; isNew:
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
const
|
|
600
|
-
if (
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
t(language, "
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
ctx.ui.notify(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
return (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
if (
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
model.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
if (
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const
|
|
1034
|
-
const
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { AnyModel } from "@earendil-works/pi-ai";
|
|
3
|
+
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
4
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
authDescription,
|
|
7
|
+
readConfigFiles,
|
|
8
|
+
redactUrl,
|
|
9
|
+
sanitizeError,
|
|
10
|
+
validateHttpUrl,
|
|
11
|
+
validateProviderId,
|
|
12
|
+
writeConfigTransaction,
|
|
13
|
+
} from "./config.ts";
|
|
14
|
+
import { buildDiscoveryCandidates, discoverModelsFromCandidates } from "./discovery.ts";
|
|
15
|
+
import { formatError, mcError, t, type UiLanguage } from "./i18n.ts";
|
|
16
|
+
import {
|
|
17
|
+
showConnectionPanel,
|
|
18
|
+
showPreviewPanel,
|
|
19
|
+
showProviderPanel,
|
|
20
|
+
showConfirmationPanel,
|
|
21
|
+
type MenuPanelState,
|
|
22
|
+
type OverviewPanelState,
|
|
23
|
+
type PanelItem,
|
|
24
|
+
} from "./panel.ts";
|
|
25
|
+
import { candidateFromExternal, candidateFromInstalled, candidateFromManaged, mergeCandidates, selectModels, type ModelPanelState } from "./selector.ts";
|
|
26
|
+
import { CHAT_APIS, CLASSIFIER_APIS, IMAGE_APIS, PROXY_CHAT_APIS } from "./transport.ts";
|
|
27
|
+
import { resolveImageBaseUrl } from "./image-apis/common.ts";
|
|
28
|
+
import { SELF_IMAGE_APIS } from "./image-apis/index.ts";
|
|
29
|
+
import type {
|
|
30
|
+
CandidateModel,
|
|
31
|
+
ManagedModel,
|
|
32
|
+
ModelConfigFile,
|
|
33
|
+
ModelMetadataEntry,
|
|
34
|
+
ModelsJsonFile,
|
|
35
|
+
ModelsJsonProvider,
|
|
36
|
+
ProviderConnectionConfig,
|
|
37
|
+
ProviderDraft,
|
|
38
|
+
} from "./types.ts";
|
|
39
|
+
import { identityOf } from "./types.ts";
|
|
40
|
+
import { loadUiLanguage, saveUiLanguage } from "./ui-config.ts";
|
|
41
|
+
import { secretInput, withCancellableLoader } from "./ui.ts";
|
|
42
|
+
import { showModelTestFlow } from "./model-test.ts";
|
|
43
|
+
import { showContextProbeFlow } from "./context-probe.ts";
|
|
44
|
+
import { editModelParameters } from "./model-editor.ts";
|
|
45
|
+
import { acceptedThinkingMap, resolveThinkingMap, showThinkingProbeFlow } from "./thinking-probe.ts";
|
|
46
|
+
import { defaultChatThinking, protocolThinkingMap } from "./thinking-defaults.ts";
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export interface StartupIssue {
|
|
50
|
+
providerId?: string;
|
|
51
|
+
error: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function displayError(language: UiLanguage, error: unknown, secrets: Array<string | undefined> = []): string {
|
|
55
|
+
return formatError(language, error, sanitizeError(error, secrets));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function confirmAction(
|
|
59
|
+
ctx: ExtensionCommandContext,
|
|
60
|
+
language: UiLanguage,
|
|
61
|
+
title: string,
|
|
62
|
+
summary: string[],
|
|
63
|
+
details: string[],
|
|
64
|
+
confirmLabel: string,
|
|
65
|
+
severity: "warning" | "error" = "warning",
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
const result = await showConfirmationPanel(ctx, {
|
|
68
|
+
title,
|
|
69
|
+
summary,
|
|
70
|
+
details,
|
|
71
|
+
severity,
|
|
72
|
+
defaultActionIndex: 0,
|
|
73
|
+
cancelActionId: "cancel",
|
|
74
|
+
actions: [
|
|
75
|
+
{ id: "cancel", label: t(language, "confirmCancel") },
|
|
76
|
+
{ id: "confirm", label: confirmLabel, shortcut: "ctrl+s", severity },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
return result === "confirm";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function authModeLabel(language: UiLanguage, draft: ProviderDraft): string {
|
|
83
|
+
if (draft.authMode === "models-json") {
|
|
84
|
+
const envReference = /^\$(?:\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*)$/u.test(draft.apiKeyRaw ?? "");
|
|
85
|
+
return t(language, envReference ? "authEnvOption" : "authLiteralOption");
|
|
86
|
+
}
|
|
87
|
+
return draft.authMode === "reuse-pi" ? t(language, "authReuseOption")
|
|
88
|
+
: draft.authMode === "temporary" ? t(language, "authTemporaryOption") : t(language, "authAnonymousOption");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function discoveryAuthLabel(language: UiLanguage, value: ProviderDraft["authHeader"]): string {
|
|
92
|
+
const key = value === "auto" ? "authHeaderAuto"
|
|
93
|
+
: value === "bearer" ? "authHeaderBearer"
|
|
94
|
+
: value === "x-api-key" ? "authHeaderXApiKey"
|
|
95
|
+
: value === "api-key" ? "authHeaderApiKey"
|
|
96
|
+
: value === "x-goog-api-key" ? "authHeaderGoogle"
|
|
97
|
+
: value === "anthropic" ? "authHeaderAnthropic" : "authHeaderNone";
|
|
98
|
+
return t(language, key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function modelTypeLabel(language: UiLanguage, type: ManagedModel["type"]): string {
|
|
102
|
+
return type === "classifier" ? t(language, "selectorTypeClassifier")
|
|
103
|
+
: type === "image" ? t(language, "selectorTypeImage")
|
|
104
|
+
: t(language, "selectorTypeChat");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function imageApiLabel(language: UiLanguage, api: string): string {
|
|
108
|
+
const key = api === "openai-images" ? "imageApiOpenai"
|
|
109
|
+
: api === "gemini-images" ? "imageApiGemini"
|
|
110
|
+
: api === "xai-images" ? "imageApiXai"
|
|
111
|
+
: api === "dashscope-images" ? "imageApiDashscope" : "imageApiOpenrouter";
|
|
112
|
+
return `${t(language, key)} (${api})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseManagedChat(raw: Record<string, unknown>, provider: ModelsJsonProvider, config: ProviderConnectionConfig, applyDefaults = false): ManagedModel | undefined {
|
|
116
|
+
if (typeof raw.id !== "string") return undefined;
|
|
117
|
+
const api = typeof raw.api === "string" ? raw.api : (provider.api ?? config.chatApi ?? "openai-completions");
|
|
118
|
+
const observedMap = typeof raw.thinkingLevelMap === "object" && raw.thinkingLevelMap && !Array.isArray(raw.thinkingLevelMap)
|
|
119
|
+
? raw.thinkingLevelMap as any
|
|
120
|
+
: undefined;
|
|
121
|
+
const resolvedThinking = applyDefaults
|
|
122
|
+
? defaultChatThinking(api, { reasoning: typeof raw.reasoning === "boolean" ? raw.reasoning : undefined, thinkingLevelMap: observedMap }, "existing")
|
|
123
|
+
: { reasoning: raw.reasoning === true, ...(observedMap !== undefined ? { thinkingLevelMap: observedMap } : {}) };
|
|
124
|
+
const thinking = {
|
|
125
|
+
reasoning: resolvedThinking.reasoning,
|
|
126
|
+
...(resolvedThinking.thinkingLevelMap !== undefined ? { thinkingLevelMap: resolvedThinking.thinkingLevelMap } : {}),
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
type: "chat",
|
|
130
|
+
id: raw.id,
|
|
131
|
+
name: typeof raw.name === "string" ? raw.name : raw.id,
|
|
132
|
+
api,
|
|
133
|
+
...(typeof raw.baseUrl === "string" ? { baseUrl: raw.baseUrl } : provider.baseUrl ? { baseUrl: provider.baseUrl } : {}),
|
|
134
|
+
...thinking,
|
|
135
|
+
input: Array.isArray(raw.input) ? raw.input.filter((v): v is "text" | "image" => v === "text" || v === "image") : ["text"],
|
|
136
|
+
cost: typeof raw.cost === "object" && raw.cost ? raw.cost as ManagedModel["cost"] : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
137
|
+
contextWindow: typeof raw.contextWindow === "number" ? raw.contextWindow : 128_000,
|
|
138
|
+
maxTokens: typeof raw.maxTokens === "number" ? raw.maxTokens : 16_384,
|
|
139
|
+
...(typeof raw.headers === "object" && raw.headers ? { headers: raw.headers as Record<string, string> } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function draftFrom(providerId: string, provider: ModelsJsonProvider | undefined, config: ProviderConnectionConfig | undefined, isNew: boolean): ProviderDraft {
|
|
144
|
+
const apiKeyRaw = provider?.apiKey;
|
|
145
|
+
return {
|
|
146
|
+
id: providerId,
|
|
147
|
+
name: config?.name ?? provider?.name ?? providerId,
|
|
148
|
+
owned: config?.owned ?? isNew,
|
|
149
|
+
enabled: config?.enabled !== false,
|
|
150
|
+
baseUrl: provider?.baseUrl,
|
|
151
|
+
discoveryUrl: config?.discoveryUrl,
|
|
152
|
+
authMode: config?.authMode ?? (apiKeyRaw ? "models-json" : isNew ? "anonymous" : "reuse-pi"),
|
|
153
|
+
authHeader: config?.discoveryAuthHeader ?? "auto",
|
|
154
|
+
apiKeyRaw,
|
|
155
|
+
proxyUrl: config?.proxyUrl,
|
|
156
|
+
chatApi: config?.chatApi ?? provider?.api ?? "openai-completions",
|
|
157
|
+
classifierApi: config?.classifierApi ?? "typesafe-system-one",
|
|
158
|
+
imageApi: config?.imageApi ?? "openrouter-images",
|
|
159
|
+
defaultType: config?.defaultType ?? "chat",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function inputPositiveInteger(ctx: ExtensionCommandContext, language: UiLanguage, title: string, current: number): Promise<number> {
|
|
164
|
+
const value = await ctx.ui.input(title, String(current));
|
|
165
|
+
if (value === undefined || value.trim() === "") return current;
|
|
166
|
+
const number = Number(value);
|
|
167
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw mcError("MCFG_E_POSITIVE_INTEGER");
|
|
168
|
+
return number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function editUrl(
|
|
172
|
+
ctx: ExtensionCommandContext,
|
|
173
|
+
language: UiLanguage,
|
|
174
|
+
label: string,
|
|
175
|
+
current: string | undefined,
|
|
176
|
+
kind: "base" | "discovery" | "proxy",
|
|
177
|
+
): Promise<string | undefined> {
|
|
178
|
+
const set = t(language, kind === "discovery" ? "discoveryUrlSet" : kind === "proxy" ? "proxySet" : "urlSet");
|
|
179
|
+
const clear = t(language, kind === "discovery" ? "discoveryUrlClear" : kind === "proxy" ? "proxyDisable" : "urlRestore");
|
|
180
|
+
const back = t(language, "back");
|
|
181
|
+
const currentValue = current ? redactUrl(current, language) : kind === "base" ? t(language, "valueDefault") : kind === "proxy" ? t(language, "valueProxyDisabled") : t(language, "valueUnset");
|
|
182
|
+
const action = await ctx.ui.select(`${label}: ${currentValue}`, [set, clear, back]);
|
|
183
|
+
if (!action || action === back) return current;
|
|
184
|
+
if (action === clear) return undefined;
|
|
185
|
+
const value = await ctx.ui.input(label, current ?? "https://example.com/v1");
|
|
186
|
+
if (value === undefined || !value.trim()) return current;
|
|
187
|
+
return validateHttpUrl(value, label);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function configureAuth(ctx: ExtensionCommandContext, language: UiLanguage, draft: ProviderDraft, allowTemporary: boolean): Promise<void> {
|
|
191
|
+
const literal = t(language, "authLiteralOption");
|
|
192
|
+
const env = t(language, "authEnvOption");
|
|
193
|
+
const reuse = t(language, "authReuseOption");
|
|
194
|
+
const temporary = t(language, "authTemporaryOption");
|
|
195
|
+
const anonymous = t(language, "authAnonymousOption");
|
|
196
|
+
const cancel = t(language, "mainCancel");
|
|
197
|
+
const options = [literal, env, reuse, ...(allowTemporary ? [temporary] : []), anonymous, cancel];
|
|
198
|
+
const choice = await ctx.ui.select(t(language, "authTitle"), options);
|
|
199
|
+
if (!choice || choice === cancel) return;
|
|
200
|
+
if (choice === literal) {
|
|
201
|
+
const secret = await secretInput(ctx, language, t(language, "secretApiKey"));
|
|
202
|
+
if (secret === undefined) return;
|
|
203
|
+
if (!secret) throw mcError("MCFG_E_KEY_EMPTY");
|
|
204
|
+
if (/^\[(?:已掩码|masked)\]$/iu.test(secret)) throw mcError("MCFG_E_KEY_MASKED");
|
|
205
|
+
draft.apiKeyRaw = secret.startsWith("$") ? `$${secret}` : secret.startsWith("!") ? `$${secret}` : secret;
|
|
206
|
+
draft.temporaryKey = undefined;
|
|
207
|
+
draft.authMode = "models-json";
|
|
208
|
+
} else if (choice === env) {
|
|
209
|
+
const name = await ctx.ui.input(t(language, "envName"), "PROVIDER_API_KEY");
|
|
210
|
+
if (name === undefined) return;
|
|
211
|
+
const normalized = name.trim();
|
|
212
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(normalized)) throw mcError("MCFG_E_ENV_NAME");
|
|
213
|
+
draft.apiKeyRaw = `$${normalized}`;
|
|
214
|
+
draft.temporaryKey = undefined;
|
|
215
|
+
draft.authMode = "models-json";
|
|
216
|
+
} else if (choice === reuse) {
|
|
217
|
+
draft.apiKeyRaw = undefined;
|
|
218
|
+
draft.temporaryKey = undefined;
|
|
219
|
+
draft.authMode = "reuse-pi";
|
|
220
|
+
} else if (choice === temporary) {
|
|
221
|
+
const secret = await secretInput(ctx, language, t(language, "secretTemporary"));
|
|
222
|
+
if (secret === undefined) return;
|
|
223
|
+
if (!secret) throw mcError("MCFG_E_KEY_EMPTY");
|
|
224
|
+
draft.temporaryKey = secret;
|
|
225
|
+
draft.authMode = "temporary";
|
|
226
|
+
} else {
|
|
227
|
+
draft.apiKeyRaw = undefined;
|
|
228
|
+
draft.temporaryKey = undefined;
|
|
229
|
+
draft.authMode = "anonymous";
|
|
230
|
+
draft.authHeader = "none";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function connectionPanelFields(language: UiLanguage, draft: ProviderDraft): PanelItem[] {
|
|
235
|
+
return [
|
|
236
|
+
{ id: "name", label: t(language, "fieldName", { value: draft.name }), details: [t(language, "panelDescName")] },
|
|
237
|
+
{ id: "id", label: t(language, "fieldId", { value: draft.id }), details: [t(language, "panelDescId")] },
|
|
238
|
+
{ id: "ownership", label: t(language, "fieldOwnership", { value: t(language, draft.owned ? "owned" : "externalOwned") }), details: [t(language, "panelDescOwnership")] },
|
|
239
|
+
{ id: "baseUrl", label: t(language, "fieldBaseUrl", { value: draft.baseUrl ? redactUrl(draft.baseUrl, language) : t(language, "valueDefault") }), details: [t(language, "panelDescBaseUrl")] },
|
|
240
|
+
{ id: "discoveryUrl", label: t(language, "fieldDiscoveryUrl", { value: draft.discoveryUrl ? redactUrl(draft.discoveryUrl, language) : t(language, "valueUnset") }), details: [t(language, "panelDescDiscoveryUrl")] },
|
|
241
|
+
{ id: "auth", label: t(language, "fieldAuth", { value: authModeLabel(language, draft) }), details: [t(language, "panelDescAuth")] },
|
|
242
|
+
{ id: "discoveryAuth", label: t(language, "fieldDiscoveryAuth", { value: discoveryAuthLabel(language, draft.authHeader) }), details: [t(language, "panelDescDiscoveryAuth")] },
|
|
243
|
+
{ id: "proxy", label: t(language, "fieldProxy", { value: draft.proxyUrl ? redactUrl(draft.proxyUrl, language) : t(language, "valueProxyDisabled") }), details: [t(language, "panelDescProxy")] },
|
|
244
|
+
{ id: "defaultType", label: t(language, "fieldDefaultType", { value: modelTypeLabel(language, draft.defaultType) }), details: [t(language, "panelDescDefaultType")] },
|
|
245
|
+
{ id: "chatApi", label: t(language, "fieldChatApi", { value: draft.chatApi }), details: [t(language, "panelDescChatApi")] },
|
|
246
|
+
{ id: "classifierApi", label: t(language, "fieldClassifierApi", { value: draft.classifierApi }), details: [t(language, "panelDescClassifierApi")] },
|
|
247
|
+
{ id: "imageApi", label: t(language, "fieldImageApi", { value: draft.imageApi }), details: [t(language, "panelDescImageApi")] },
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function editConnectionPanel(ctx: ExtensionCommandContext, language: UiLanguage, draft: ProviderDraft, state: MenuPanelState, canTakeover: boolean): Promise<"models" | "takeover" | "testModels" | "preview" | "back"> {
|
|
252
|
+
while (true) {
|
|
253
|
+
const result = await showConnectionPanel(ctx, language, draft.id, connectionPanelFields(language, draft), state, canTakeover);
|
|
254
|
+
if (result.action !== "field") return result.action;
|
|
255
|
+
try {
|
|
256
|
+
switch (result.fieldId) {
|
|
257
|
+
case "name": {
|
|
258
|
+
const value = await ctx.ui.input(t(language, "providerDisplayName"), draft.name);
|
|
259
|
+
if (value?.trim()) draft.name = value.trim();
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "id": ctx.ui.notify(t(language, "fieldIdReadonly"), "info"); break;
|
|
263
|
+
case "ownership": {
|
|
264
|
+
const next = !draft.owned;
|
|
265
|
+
if (next && await confirmAction(ctx, language, t(language, "takeoverTitle"), [t(language, "takeoverBody")], [], t(language, "confirmTakeover"))) draft.owned = true;
|
|
266
|
+
if (!next) draft.owned = false;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "baseUrl": draft.baseUrl = await editUrl(ctx, language, t(language, "baseUrl"), draft.baseUrl, "base"); break;
|
|
270
|
+
case "discoveryUrl": draft.discoveryUrl = await editUrl(ctx, language, t(language, "discoveryUrl"), draft.discoveryUrl, "discovery"); break;
|
|
271
|
+
case "auth": await configureAuth(ctx, language, draft, true); break;
|
|
272
|
+
case "discoveryAuth": {
|
|
273
|
+
const choices = (["auto", "bearer", "x-api-key", "api-key", "x-goog-api-key", "anthropic", "none"] as const)
|
|
274
|
+
.map((mode) => ({ mode, label: discoveryAuthLabel(language, mode) }));
|
|
275
|
+
const value = await ctx.ui.select(t(language, "discoveryAuthTitle"), choices.map((choice) => choice.label));
|
|
276
|
+
const selected = choices.find((choice) => choice.label === value);
|
|
277
|
+
if (selected) draft.authHeader = selected.mode;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "proxy": draft.proxyUrl = await editUrl(ctx, language, t(language, "proxyUrl"), draft.proxyUrl, "proxy"); break;
|
|
281
|
+
case "defaultType": {
|
|
282
|
+
const choices = (["chat", "classifier", "image"] as const).map((type) => ({ type, label: modelTypeLabel(language, type) }));
|
|
283
|
+
const value = await ctx.ui.select(t(language, "defaultTypeTitle"), choices.map((item) => item.label));
|
|
284
|
+
const selected = choices.find((item) => item.label === value);
|
|
285
|
+
if (selected) draft.defaultType = selected.type;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case "chatApi": {
|
|
289
|
+
const value = await ctx.ui.select("Chat API", [...CHAT_APIS]);
|
|
290
|
+
if (value) draft.chatApi = value;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "classifierApi": {
|
|
294
|
+
const value = await ctx.ui.select("Classifier API", [...CLASSIFIER_APIS]);
|
|
295
|
+
if (value) draft.classifierApi = value;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "imageApi": {
|
|
299
|
+
const choices = [...IMAGE_APIS].map((api) => ({ api, label: imageApiLabel(language, api) }));
|
|
300
|
+
const value = await ctx.ui.select("Image API", choices.map((choice) => choice.label));
|
|
301
|
+
const selected = choices.find((choice) => choice.label === value);
|
|
302
|
+
if (selected) draft.imageApi = selected.api;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
ctx.ui.notify(displayError(language, error, [draft.apiKeyRaw, draft.temporaryKey]), "error");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function installedModels(ctx: ExtensionCommandContext, providerId: string): AnyModel[] {
|
|
313
|
+
return [
|
|
314
|
+
...ctx.modelRegistry.getModelsOfType("chat", providerId),
|
|
315
|
+
...ctx.modelRegistry.getModelsOfType("classifier", providerId),
|
|
316
|
+
...ctx.modelRegistry.getModelsOfType("image", providerId),
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function previousManaged(provider: ModelsJsonProvider | undefined, config: ProviderConnectionConfig | undefined): ManagedModel[] {
|
|
321
|
+
if (!provider || !config) return config?.models ? structuredClone(config.models) : [];
|
|
322
|
+
const ids = new Set(config.managedChatIds ?? []);
|
|
323
|
+
const chat = (provider.models ?? [])
|
|
324
|
+
.filter((raw) => typeof raw.id === "string" && ids.has(raw.id))
|
|
325
|
+
.map((raw) => parseManagedChat(raw, provider, config))
|
|
326
|
+
.filter((model): model is ManagedModel => model !== undefined);
|
|
327
|
+
return [...chat, ...structuredClone(config.models ?? [])];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function validateSelection(
|
|
331
|
+
draft: ProviderDraft,
|
|
332
|
+
selected: ManagedModel[],
|
|
333
|
+
installed: AnyModel[],
|
|
334
|
+
existingProvider: ModelsJsonProvider | undefined,
|
|
335
|
+
): void {
|
|
336
|
+
if (draft.baseUrl) validateHttpUrl(draft.baseUrl, "Base URL");
|
|
337
|
+
if (draft.discoveryUrl) validateHttpUrl(draft.discoveryUrl, "Discovery URL");
|
|
338
|
+
if (draft.proxyUrl) validateHttpUrl(draft.proxyUrl, "Provider proxy URL");
|
|
339
|
+
if (draft.owned) {
|
|
340
|
+
const missingExistingUrl = (existingProvider?.models ?? []).some((model) => typeof model.baseUrl !== "string" && !draft.baseUrl);
|
|
341
|
+
const effectiveModelUrl = (model: ManagedModel) => model.type === "image"
|
|
342
|
+
? resolveImageBaseUrl(model.api, model.baseUrl, draft.baseUrl)
|
|
343
|
+
: model.baseUrl ?? draft.baseUrl;
|
|
344
|
+
const missingSelectedUrl = selected.some((model) => !effectiveModelUrl(model));
|
|
345
|
+
if (missingExistingUrl || missingSelectedUrl) throw mcError("MCFG_E_BASE_URL_REQUIRED");
|
|
346
|
+
for (const model of existingProvider?.models ?? []) validateHttpUrl(String(model.baseUrl ?? draft.baseUrl), `${draft.id}/${String(model.id)} Base URL`);
|
|
347
|
+
for (const model of selected) validateHttpUrl(effectiveModelUrl(model)!, `${draft.id}/${model.id} Base URL`);
|
|
348
|
+
}
|
|
349
|
+
if (!draft.owned && selected.some((model) => model.type !== "chat")) throw mcError("MCFG_E_NOT_OWNED", { provider: draft.id });
|
|
350
|
+
if (!draft.owned && (draft.proxyUrl || draft.authMode === "anonymous") && !new Set<string>(getBuiltinProviders()).has(draft.id)) {
|
|
351
|
+
throw mcError("MCFG_E_EXTERNAL_TRANSPORT");
|
|
352
|
+
}
|
|
353
|
+
const existingChatApis = new Set([
|
|
354
|
+
...installed.filter((model) => model.type !== "classifier" && model.type !== "image").map((model) => model.api),
|
|
355
|
+
...(existingProvider?.models ?? []).map((model) => typeof model.api === "string" ? model.api : (existingProvider?.api ?? draft.chatApi)),
|
|
356
|
+
]);
|
|
357
|
+
if (draft.proxyUrl) {
|
|
358
|
+
const unsupported = [...existingChatApis].filter((api) => !PROXY_CHAT_APIS.has(api));
|
|
359
|
+
if (unsupported.length) throw mcError("MCFG_E_PROXY_UNSUPPORTED", { identity: `${draft.id}: ${unsupported.join(", ")}` });
|
|
360
|
+
}
|
|
361
|
+
if (draft.authMode === "anonymous") {
|
|
362
|
+
const unsupported = [...existingChatApis].filter((api) => ["pi-messages", "bedrock-converse-stream", "google-generative-ai", "google-vertex"].includes(api));
|
|
363
|
+
if (unsupported.length) throw mcError("MCFG_E_ANONYMOUS_UNSUPPORTED", { identity: `${draft.id}: ${unsupported.join(", ")}` });
|
|
364
|
+
}
|
|
365
|
+
for (const model of selected) {
|
|
366
|
+
const supported = model.type === "chat" ? CHAT_APIS : model.type === "classifier" ? CLASSIFIER_APIS : IMAGE_APIS;
|
|
367
|
+
if (!supported.has(model.api)) throw mcError("MCFG_E_UNSUPPORTED_API", { identity: `${draft.id}/${model.type}/${model.id}`, api: model.api });
|
|
368
|
+
if (draft.proxyUrl && model.type === "chat" && !PROXY_CHAT_APIS.has(model.api)) throw mcError("MCFG_E_PROXY_UNSUPPORTED", { identity: `${draft.id}/${model.type}/${model.id}` });
|
|
369
|
+
if (draft.authMode === "anonymous" && (["pi-messages", "bedrock-converse-stream", "google-generative-ai", "google-vertex"].includes(model.api)
|
|
370
|
+
|| model.type === "image" && SELF_IMAGE_APIS.has(model.api))) {
|
|
371
|
+
throw mcError("MCFG_E_ANONYMOUS_UNSUPPORTED", { identity: `${draft.id}/${model.type}/${model.id}` });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (draft.authMode === "models-json" && !draft.apiKeyRaw) throw mcError("MCFG_E_AUTH_REQUIRED");
|
|
375
|
+
if (draft.authMode === "models-json" && /^\[(?:已掩码|masked)\]$/iu.test(draft.apiKeyRaw ?? "")) throw mcError("MCFG_E_KEY_MASKED");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function chatToJson(model: ManagedModel): Record<string, unknown> {
|
|
379
|
+
if (model.type !== "chat") throw mcError("MCFG_E_INTERNAL_CHAT_ONLY");
|
|
380
|
+
const { type: _type, ...value } = model;
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function removeProvider(
|
|
385
|
+
files: { models: ModelsJsonFile; config: ModelConfigFile },
|
|
386
|
+
providerId: string,
|
|
387
|
+
): { models: ModelsJsonFile; config: ModelConfigFile } {
|
|
388
|
+
const models = structuredClone(files.models);
|
|
389
|
+
const config = structuredClone(files.config);
|
|
390
|
+
delete models.providers[providerId];
|
|
391
|
+
delete config.providers[providerId];
|
|
392
|
+
return { models, config };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function applyChanges(
|
|
396
|
+
files: { models: ModelsJsonFile; config: ModelConfigFile },
|
|
397
|
+
providerId: string,
|
|
398
|
+
draft: ProviderDraft,
|
|
399
|
+
selected: ManagedModel[],
|
|
400
|
+
adoptedChatIds: ReadonlySet<string> = new Set(),
|
|
401
|
+
modelMetadata: ModelMetadataEntry[] = [],
|
|
402
|
+
): { models: ModelsJsonFile; config: ModelConfigFile } {
|
|
403
|
+
const models = structuredClone(files.models);
|
|
404
|
+
const config = structuredClone(files.config);
|
|
405
|
+
const oldConfig = config.providers[providerId];
|
|
406
|
+
const oldProvider = models.providers[providerId] ?? {};
|
|
407
|
+
const provider = structuredClone(oldProvider);
|
|
408
|
+
const oldManagedIds = new Set(oldConfig?.managedChatIds ?? []);
|
|
409
|
+
const originalRows = new Map(
|
|
410
|
+
(oldProvider.models ?? []).filter((row) => typeof row.id === "string").map((row) => [row.id as string, row]),
|
|
411
|
+
);
|
|
412
|
+
const managedScopeIds = new Set([...oldManagedIds, ...adoptedChatIds]);
|
|
413
|
+
const unmanagedRows = (oldProvider.models ?? []).filter((row) => typeof row.id !== "string" || !managedScopeIds.has(row.id));
|
|
414
|
+
const selectedChat = selected.filter((model) => model.type === "chat");
|
|
415
|
+
provider.models = [...unmanagedRows, ...selectedChat.map((model) => ({
|
|
416
|
+
...structuredClone(originalRows.get(model.id) ?? {}),
|
|
417
|
+
...chatToJson(model),
|
|
418
|
+
}))];
|
|
419
|
+
if (draft.name) provider.name = draft.name;
|
|
420
|
+
if (draft.baseUrl) provider.baseUrl = draft.baseUrl; else delete provider.baseUrl;
|
|
421
|
+
if (draft.owned) provider.api = draft.chatApi;
|
|
422
|
+
if (draft.authMode === "models-json") provider.apiKey = draft.apiKeyRaw; else delete provider.apiKey;
|
|
423
|
+
models.providers[providerId] = provider;
|
|
424
|
+
|
|
425
|
+
config.providers[providerId] = {
|
|
426
|
+
...(oldConfig ?? {}),
|
|
427
|
+
name: draft.name,
|
|
428
|
+
owned: draft.owned,
|
|
429
|
+
enabled: draft.enabled,
|
|
430
|
+
authMode: draft.authMode as "models-json" | "reuse-pi" | "anonymous",
|
|
431
|
+
discoveryAuthHeader: draft.authHeader,
|
|
432
|
+
chatApi: draft.chatApi,
|
|
433
|
+
classifierApi: draft.classifierApi,
|
|
434
|
+
imageApi: draft.imageApi,
|
|
435
|
+
defaultType: draft.defaultType,
|
|
436
|
+
managedChatIds: selectedChat.map((model) => model.id),
|
|
437
|
+
models: selected.filter((model): model is Extract<ManagedModel, { type: "classifier" | "image" }> => model.type !== "chat"),
|
|
438
|
+
modelMetadata: modelMetadata.map((entry) => structuredClone(entry)),
|
|
439
|
+
};
|
|
440
|
+
if (draft.discoveryUrl) config.providers[providerId]!.discoveryUrl = draft.discoveryUrl; else delete config.providers[providerId]!.discoveryUrl;
|
|
441
|
+
if (draft.proxyUrl) config.providers[providerId]!.proxyUrl = draft.proxyUrl; else delete config.providers[providerId]!.proxyUrl;
|
|
442
|
+
return { models, config };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildPreviewDetails(
|
|
446
|
+
language: UiLanguage,
|
|
447
|
+
providerId: string,
|
|
448
|
+
before: ManagedModel[],
|
|
449
|
+
after: ManagedModel[],
|
|
450
|
+
oldConfig: ProviderConnectionConfig | undefined,
|
|
451
|
+
oldProvider: ModelsJsonProvider | undefined,
|
|
452
|
+
draft: ProviderDraft,
|
|
453
|
+
): { providerDetails: string[]; modelDetails: string[] } {
|
|
454
|
+
const oldMap = new Map(before.map((model) => [identityOf(model, providerId), model]));
|
|
455
|
+
const newMap = new Map(after.map((model) => [identityOf(model, providerId), model]));
|
|
456
|
+
const added = [...newMap.keys()].filter((key) => !oldMap.has(key));
|
|
457
|
+
const deleted = [...oldMap.keys()].filter((key) => !newMap.has(key));
|
|
458
|
+
const retained = [...newMap.keys()].filter((key) => oldMap.has(key));
|
|
459
|
+
const modified = retained.filter((key) => JSON.stringify(oldMap.get(key)) !== JSON.stringify(newMap.get(key)));
|
|
460
|
+
const unchanged = retained.length - modified.length;
|
|
461
|
+
const oldConnection = [
|
|
462
|
+
`name=${oldConfig?.name ?? oldProvider?.name ?? providerId}`,
|
|
463
|
+
`owned=${oldConfig?.owned ?? false}`,
|
|
464
|
+
`enabled=${oldConfig?.enabled !== false}`,
|
|
465
|
+
`base=${redactUrl(oldProvider?.baseUrl, language)}`,
|
|
466
|
+
`discovery=${redactUrl(oldConfig?.discoveryUrl, language)}`,
|
|
467
|
+
`auth=${authDescription(language, oldConfig, oldProvider)}`,
|
|
468
|
+
`discoveryAuth=${oldConfig?.discoveryAuthHeader ?? "auto"}`,
|
|
469
|
+
`proxy=${redactUrl(oldConfig?.proxyUrl, language)}`,
|
|
470
|
+
`apis=${oldConfig?.chatApi ?? oldProvider?.api ?? "openai-completions"}/${oldConfig?.classifierApi ?? "typesafe-system-one"}/${oldConfig?.imageApi ?? "openrouter-images"}`,
|
|
471
|
+
`defaults=${oldConfig?.defaultType ?? "chat"},${oldConfig?.defaultContextWindow ?? "legacy-unset"},${oldConfig?.defaultMaxTokens ?? "legacy-unset"}`,
|
|
472
|
+
].join(" | ");
|
|
473
|
+
const isEnvReference = /^\$(?:\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*)$/u.test(draft.apiKeyRaw ?? "");
|
|
474
|
+
const newAuth = draft.authMode === "models-json"
|
|
475
|
+
? isEnvReference ? t(language, "valueEnv", { value: draft.apiKeyRaw! }) : t(language, "valueLiteralMasked")
|
|
476
|
+
: draft.authMode === "reuse-pi" ? t(language, "valueReuseAuth") : t(language, "valueAnonymous");
|
|
477
|
+
const nextConnection = [
|
|
478
|
+
`name=${draft.name}`,
|
|
479
|
+
`owned=${draft.owned}`,
|
|
480
|
+
`enabled=${draft.enabled}`,
|
|
481
|
+
`base=${redactUrl(draft.baseUrl, language)}`,
|
|
482
|
+
`discovery=${redactUrl(draft.discoveryUrl, language)}`,
|
|
483
|
+
`auth=${newAuth}`,
|
|
484
|
+
`discoveryAuth=${draft.authHeader}`,
|
|
485
|
+
`proxy=${redactUrl(draft.proxyUrl, language)}`,
|
|
486
|
+
`apis=${draft.chatApi}/${draft.classifierApi}/${draft.imageApi}`,
|
|
487
|
+
`defaults=${draft.defaultType},legacy-fallback-not-applied`,
|
|
488
|
+
].join(" | ");
|
|
489
|
+
const describe = (key: ReturnType<typeof identityOf>) => {
|
|
490
|
+
const model = newMap.get(key) ?? oldMap.get(key)!;
|
|
491
|
+
return `${modelTypeLabel(language, model.type)}/${model.id}`;
|
|
492
|
+
};
|
|
493
|
+
const settingsChanged = oldConnection !== nextConnection;
|
|
494
|
+
const keyWillUpdate = draft.authMode === "models-json" && oldProvider?.apiKey !== draft.apiKeyRaw;
|
|
495
|
+
return {
|
|
496
|
+
providerDetails: [
|
|
497
|
+
settingsChanged ? t(language, "previewSettingsChanged") : t(language, "previewSettingsUnchanged"),
|
|
498
|
+
...(settingsChanged ? [t(language, "diffConnection", { before: oldConnection, after: nextConnection })] : []),
|
|
499
|
+
...(keyWillUpdate ? [t(language, "previewKeyUpdated")] : []),
|
|
500
|
+
],
|
|
501
|
+
modelDetails: [
|
|
502
|
+
...(added.length ? [t(language, "previewModelsAdded", { items: added.map(describe).join(", ") })] : []),
|
|
503
|
+
...(deleted.length ? [t(language, "previewModelsRemoved", { items: deleted.map(describe).join(", ") })] : []),
|
|
504
|
+
...(modified.length ? [t(language, "previewModelsModified", { items: modified.map(describe).join(", ") })] : []),
|
|
505
|
+
t(language, "previewModelsUnchanged", { count: unchanged }),
|
|
506
|
+
t(language, "diffSafety"),
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface ProviderChoice {
|
|
512
|
+
id: string;
|
|
513
|
+
label: string;
|
|
514
|
+
priority: number;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function buildConfiguredProviderChoices(
|
|
518
|
+
language: UiLanguage,
|
|
519
|
+
builtinProviderIds: ReadonlySet<string>,
|
|
520
|
+
models: ModelsJsonFile,
|
|
521
|
+
config: ModelConfigFile,
|
|
522
|
+
): ProviderChoice[] {
|
|
523
|
+
const ids = new Set([...Object.keys(models.providers), ...Object.keys(config.providers)]);
|
|
524
|
+
const choices: ProviderChoice[] = [];
|
|
525
|
+
for (const id of ids) {
|
|
526
|
+
const owned = config.providers[id]?.owned === true;
|
|
527
|
+
const builtin = builtinProviderIds.has(id);
|
|
528
|
+
const name = config.providers[id]?.name ?? models.providers[id]?.name;
|
|
529
|
+
const displayBase = name && name !== id ? `${name} (${id})` : id;
|
|
530
|
+
const display = config.providers[id]?.enabled === false ? `${displayBase} · ${t(language, "providerDisabled")}` : displayBase;
|
|
531
|
+
const status = t(language, builtin ? "providerTagBuiltin" : "providerTagCustom");
|
|
532
|
+
choices.push(owned
|
|
533
|
+
? { id, label: t(language, "providerLabelCustom", { display, status }), priority: 0 }
|
|
534
|
+
: builtin
|
|
535
|
+
? { id, label: t(language, "providerLabelBuiltin", { display, status }), priority: 1 }
|
|
536
|
+
: { id, label: t(language, "providerLabelCustom", { display, status }), priority: 2 });
|
|
537
|
+
}
|
|
538
|
+
return choices.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function buildProviderPanelItems(
|
|
542
|
+
ctx: ExtensionCommandContext,
|
|
543
|
+
language: UiLanguage,
|
|
544
|
+
models: ModelsJsonFile,
|
|
545
|
+
config: ModelConfigFile,
|
|
546
|
+
): PanelItem[] {
|
|
547
|
+
const builtinIds = new Set<string>(getBuiltinProviders());
|
|
548
|
+
const choices = buildConfiguredProviderChoices(language, builtinIds, models, config);
|
|
549
|
+
return choices.map((choice) => {
|
|
550
|
+
const provider = models.providers[choice.id];
|
|
551
|
+
const providerConfig = config.providers[choice.id];
|
|
552
|
+
const installed = installedModels(ctx, choice.id);
|
|
553
|
+
const chat = new Set(installed.filter((model) => model.type !== "classifier" && model.type !== "image").map((model) => model.id));
|
|
554
|
+
const classifier = new Set(installed.filter((model) => model.type === "classifier").map((model) => model.id));
|
|
555
|
+
const image = new Set(installed.filter((model) => model.type === "image").map((model) => model.id));
|
|
556
|
+
for (const model of provider?.models ?? []) if (typeof model.id === "string") chat.add(model.id);
|
|
557
|
+
for (const model of providerConfig?.models ?? []) (model.type === "classifier" ? classifier : image).add(model.id);
|
|
558
|
+
const managedChatIds = new Set(providerConfig?.managedChatIds ?? []);
|
|
559
|
+
const takeoverCount = (provider?.models ?? []).filter((model) => typeof model.id === "string" && !managedChatIds.has(model.id)).length;
|
|
560
|
+
const protocols = new Set<string>();
|
|
561
|
+
if (provider?.api) protocols.add(provider.api);
|
|
562
|
+
if (providerConfig?.chatApi) protocols.add(providerConfig.chatApi);
|
|
563
|
+
if (providerConfig?.classifierApi) protocols.add(providerConfig.classifierApi);
|
|
564
|
+
if (providerConfig?.imageApi) protocols.add(providerConfig.imageApi);
|
|
565
|
+
for (const model of provider?.models ?? []) if (typeof model.api === "string") protocols.add(model.api);
|
|
566
|
+
for (const model of providerConfig?.models ?? []) protocols.add(model.api);
|
|
567
|
+
return {
|
|
568
|
+
id: choice.id,
|
|
569
|
+
label: choice.label,
|
|
570
|
+
canTakeoverExisting: takeoverCount > 0,
|
|
571
|
+
enabled: providerConfig?.enabled !== false,
|
|
572
|
+
details: [
|
|
573
|
+
t(language, "panelSummaryName", { value: providerConfig?.name ?? provider?.name ?? choice.id }),
|
|
574
|
+
t(language, "panelSummaryId", { value: choice.id }),
|
|
575
|
+
t(language, "panelSummaryProtocol", { value: [...protocols].join(", ") || t(language, "valueUnset") }),
|
|
576
|
+
t(language, "panelSummaryBaseUrl", { value: provider?.baseUrl ? redactUrl(provider.baseUrl, language) : t(language, "valueDefault") }),
|
|
577
|
+
t(language, "panelSummaryDiscoveryUrl", { value: providerConfig?.discoveryUrl ? redactUrl(providerConfig.discoveryUrl, language) : t(language, "valueUnset") }),
|
|
578
|
+
t(language, "panelSummaryAuth", { value: authDescription(language, providerConfig, provider) }),
|
|
579
|
+
t(language, "panelSummaryProxy", { value: providerConfig?.proxyUrl ? redactUrl(providerConfig.proxyUrl, language) : t(language, "valueProxyDisabled") }),
|
|
580
|
+
],
|
|
581
|
+
modelDetails: [
|
|
582
|
+
t(language, "panelSummaryModels", { chat: chat.size, classifier: classifier.size, image: image.size }),
|
|
583
|
+
...(takeoverCount ? [t(language, "panelTakeoverCount", { count: takeoverCount })] : []),
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function buildBuiltinProviderChoices(language: UiLanguage, builtinProviderIds: Iterable<string>): ProviderChoice[] {
|
|
590
|
+
return [...new Set(builtinProviderIds)]
|
|
591
|
+
.sort((a, b) => a.localeCompare(b))
|
|
592
|
+
.map((id) => ({ id, label: t(language, "providerLabelBuiltin", { display: id, status: t(language, "providerTagBuiltin") }), priority: 0 }));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function chooseBuiltinProvider(ctx: ExtensionCommandContext, language: UiLanguage): Promise<{ id: string; isNew: false } | undefined> {
|
|
596
|
+
const choices = buildBuiltinProviderChoices(language, getBuiltinProviders());
|
|
597
|
+
const labelToId = new Map(choices.map((choice) => [choice.label, choice.id]));
|
|
598
|
+
const back = t(language, "back");
|
|
599
|
+
const selected = await ctx.ui.select(t(language, "newProviderTitle"), [...choices.map((choice) => choice.label), back]);
|
|
600
|
+
if (!selected || selected === back) return undefined;
|
|
601
|
+
const id = labelToId.get(selected);
|
|
602
|
+
return id ? { id, isNew: false } : undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function chooseCustomProvider(
|
|
606
|
+
ctx: ExtensionCommandContext,
|
|
607
|
+
language: UiLanguage,
|
|
608
|
+
runtimeIds: ReadonlySet<string>,
|
|
609
|
+
models: ModelsJsonFile,
|
|
610
|
+
config: ModelConfigFile,
|
|
611
|
+
): Promise<{ id: string; isNew: boolean } | undefined> {
|
|
612
|
+
const raw = await ctx.ui.input(t(language, "newProviderId"), "my-provider");
|
|
613
|
+
if (raw === undefined) return undefined;
|
|
614
|
+
const id = validateProviderId(raw);
|
|
615
|
+
const existingIds = new Set([...runtimeIds, ...getBuiltinProviders(), ...Object.keys(models.providers), ...Object.keys(config.providers)]);
|
|
616
|
+
if (existingIds.has(id)) {
|
|
617
|
+
ctx.ui.notify(t(language, "providerExists", { id }), "info");
|
|
618
|
+
return { id, isNew: false };
|
|
619
|
+
}
|
|
620
|
+
return { id, isNew: true };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function chooseLanguage(ctx: ExtensionCommandContext, agentDir: string, current: UiLanguage): Promise<UiLanguage> {
|
|
624
|
+
const zh = t(current, "languageZh");
|
|
625
|
+
const en = t(current, "languageEn");
|
|
626
|
+
const choice = await ctx.ui.select(t(current, "languageTitle"), [zh, en, t(current, "mainCancel")]);
|
|
627
|
+
if (!choice || choice === t(current, "mainCancel")) return current;
|
|
628
|
+
const next: UiLanguage = choice === en ? "en" : "zh-CN";
|
|
629
|
+
saveUiLanguage(agentDir, next);
|
|
630
|
+
ctx.ui.notify(t(next, "languageSaved", { language: t(next, next === "en" ? "languageEn" : "languageZh") }), "info");
|
|
631
|
+
return next;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function draftPanelItem(language: UiLanguage, draft: ProviderDraft, selected: ManagedModel[], adoptableCount: number): PanelItem {
|
|
635
|
+
const builtin = new Set<string>(getBuiltinProviders()).has(draft.id);
|
|
636
|
+
const displayBase = draft.name && draft.name !== draft.id ? `${draft.name} (${draft.id})` : draft.id;
|
|
637
|
+
const display = draft.enabled ? displayBase : `${displayBase} · ${t(language, "providerDisabled")}`;
|
|
638
|
+
const chat = selected.filter((model) => model.type === "chat").length;
|
|
639
|
+
const classifier = selected.filter((model) => model.type === "classifier").length;
|
|
640
|
+
const image = selected.filter((model) => model.type === "image").length;
|
|
641
|
+
return {
|
|
642
|
+
id: draft.id,
|
|
643
|
+
label: t(language, builtin ? "providerLabelBuiltin" : "providerLabelCustom", { display, status: t(language, builtin ? "providerTagBuiltin" : "providerTagCustom") }),
|
|
644
|
+
canTakeoverExisting: adoptableCount > 0,
|
|
645
|
+
enabled: draft.enabled,
|
|
646
|
+
details: [
|
|
647
|
+
t(language, "providerEnabledStatus", { value: t(language, draft.enabled ? "providerEnabled" : "providerDisabled") }),
|
|
648
|
+
t(language, "panelSummaryName", { value: draft.name }),
|
|
649
|
+
t(language, "panelSummaryId", { value: draft.id }),
|
|
650
|
+
t(language, "panelSummaryProtocol", { value: [draft.chatApi, draft.classifierApi, draft.imageApi].join(", ") }),
|
|
651
|
+
t(language, "panelSummaryBaseUrl", { value: draft.baseUrl ? redactUrl(draft.baseUrl, language) : t(language, "valueDefault") }),
|
|
652
|
+
t(language, "panelSummaryDiscoveryUrl", { value: draft.discoveryUrl ? redactUrl(draft.discoveryUrl, language) : t(language, "valueUnset") }),
|
|
653
|
+
t(language, "panelSummaryAuth", { value: authModeLabel(language, draft) }),
|
|
654
|
+
t(language, "panelSummaryProxy", { value: draft.proxyUrl ? redactUrl(draft.proxyUrl, language) : t(language, "valueProxyDisabled") }),
|
|
655
|
+
],
|
|
656
|
+
modelDetails: [
|
|
657
|
+
t(language, "panelSummaryModels", { chat, classifier, image }),
|
|
658
|
+
t(language, "detailFetchHint"),
|
|
659
|
+
...(adoptableCount ? [t(language, "panelTakeoverCount", { count: adoptableCount })] : []),
|
|
660
|
+
],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function chooseProviderToAdd(
|
|
665
|
+
ctx: ExtensionCommandContext,
|
|
666
|
+
language: UiLanguage,
|
|
667
|
+
runtimeIds: ReadonlySet<string>,
|
|
668
|
+
models: ModelsJsonFile,
|
|
669
|
+
config: ModelConfigFile,
|
|
670
|
+
): Promise<{ id: string; isNew: boolean } | undefined> {
|
|
671
|
+
const builtin = t(language, "addBuiltinChoice");
|
|
672
|
+
const custom = t(language, "addCustomChoice");
|
|
673
|
+
const back = t(language, "back");
|
|
674
|
+
const choice = await ctx.ui.select(t(language, "addProviderTitle"), [builtin, custom, back]);
|
|
675
|
+
if (!choice || choice === back) return undefined;
|
|
676
|
+
return choice === builtin
|
|
677
|
+
? chooseBuiltinProvider(ctx, language)
|
|
678
|
+
: chooseCustomProvider(ctx, language, runtimeIds, models, config);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: StartupIssue[] = []): void {
|
|
682
|
+
pi.registerCommand("model-config", {
|
|
683
|
+
description: t("zh-CN", "commandDescription"),
|
|
684
|
+
handler: async (args, ctx) => {
|
|
685
|
+
const agentDir = getAgentDir();
|
|
686
|
+
let language = loadUiLanguage(agentDir);
|
|
687
|
+
const activeSecrets: Array<string | undefined> = [];
|
|
688
|
+
if (ctx.mode !== "tui") {
|
|
689
|
+
ctx.ui.notify(t(language, "tuiRequired"), "error");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
if (args.trim().toLowerCase() === "language") {
|
|
694
|
+
await chooseLanguage(ctx, agentDir, language);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
let startupIssuesReported = false;
|
|
698
|
+
const overviewState: OverviewPanelState = { search: "", focus: "list", actionIndex: 0, detailScroll: 0 };
|
|
699
|
+
mainLoop: while (true) {
|
|
700
|
+
if (!startupIssuesReported && startupIssues.length) {
|
|
701
|
+
const localized = startupIssues.slice(0, 3).map((issue) => {
|
|
702
|
+
const message = displayError(language, issue.error);
|
|
703
|
+
return issue.providerId ? `${issue.providerId}: ${message}` : message;
|
|
704
|
+
}).join("; ");
|
|
705
|
+
const more = startupIssues.length > 3 ? t(language, "startupMore", { count: startupIssues.length - 3 }) : "";
|
|
706
|
+
ctx.ui.notify(t(language, "startupIssues", { issues: localized, more }), "warning");
|
|
707
|
+
startupIssuesReported = true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const modelsPath = join(agentDir, "models.json");
|
|
711
|
+
const configPath = join(agentDir, "model-config.json");
|
|
712
|
+
const files = readConfigFiles(modelsPath, configPath);
|
|
713
|
+
if (files.recoveredTxId) ctx.ui.notify(t(language, "transactionRecovered", { txId: files.recoveredTxId }), "warning");
|
|
714
|
+
const overviewItems = buildProviderPanelItems(ctx, language, files.models, files.config);
|
|
715
|
+
const overview = await showProviderPanel(ctx, language, overviewItems, overviewState);
|
|
716
|
+
if (overview.action === "close") return;
|
|
717
|
+
if (overview.action === "language") {
|
|
718
|
+
language = await chooseLanguage(ctx, agentDir, language);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const runtimeIds = new Set<string>();
|
|
723
|
+
for (const type of ["chat", "classifier", "image"] as const) {
|
|
724
|
+
for (const model of ctx.modelRegistry.getModelsOfType(type)) runtimeIds.add(model.provider);
|
|
725
|
+
}
|
|
726
|
+
if (overview.action === "delete") {
|
|
727
|
+
const providerId = overview.providerId;
|
|
728
|
+
const providerConfig = files.config.providers[providerId];
|
|
729
|
+
const modelsProvider = files.models.providers[providerId];
|
|
730
|
+
const builtin = new Set<string>(getBuiltinProviders()).has(providerId);
|
|
731
|
+
if (ctx.model?.provider === providerId) {
|
|
732
|
+
ctx.ui.notify(t(language, "deleteCurrentModelBlocked"), "warning");
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (!builtin && !providerConfig?.owned && !modelsProvider) {
|
|
736
|
+
ctx.ui.notify(t(language, "deleteThirdPartyBlocked"), "error");
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const confirmed = await confirmAction(ctx, language, t(language, "deleteProviderTitle", { provider: providerId }),
|
|
740
|
+
[t(language, "deleteProviderSummary", { provider: providerId })],
|
|
741
|
+
[
|
|
742
|
+
t(language, "deleteProviderModels"),
|
|
743
|
+
t(language, "deleteProviderOverlay"),
|
|
744
|
+
t(language, "deleteProviderAuthRetained"),
|
|
745
|
+
t(language, "deleteProviderOtherExtension"),
|
|
746
|
+
...(builtin ? [t(language, "deleteProviderBuiltinRestore")] : []),
|
|
747
|
+
],
|
|
748
|
+
t(language, "confirmDeleteProvider"), "error");
|
|
749
|
+
if (!confirmed) continue;
|
|
750
|
+
try {
|
|
751
|
+
const next = removeProvider(files, providerId);
|
|
752
|
+
const transaction = writeConfigTransaction(files.modelsSnapshot, files.configSnapshot, next.models, next.config);
|
|
753
|
+
ctx.ui.notify(t(language, "deleteProviderSuccess", { provider: providerId, count: transaction.backups.length }), "info");
|
|
754
|
+
} catch (error) {
|
|
755
|
+
ctx.ui.notify(displayError(language, error, activeSecrets), "error");
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (overview.action === "toggle") {
|
|
760
|
+
const providerId = overview.providerId;
|
|
761
|
+
const providerConfig = files.config.providers[providerId];
|
|
762
|
+
const modelsProvider = files.models.providers[providerId];
|
|
763
|
+
const builtin = new Set<string>(getBuiltinProviders()).has(providerId);
|
|
764
|
+
const currentlyEnabled = providerConfig?.enabled !== false;
|
|
765
|
+
if (currentlyEnabled && ctx.model?.provider === providerId) {
|
|
766
|
+
ctx.ui.notify(t(language, "disableCurrentModelBlocked"), "warning");
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (currentlyEnabled && !builtin && !providerConfig?.owned && !modelsProvider) {
|
|
770
|
+
ctx.ui.notify(t(language, "disableThirdPartyBlocked"), "error");
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const nextConfig = structuredClone(files.config);
|
|
775
|
+
nextConfig.providers[providerId] = {
|
|
776
|
+
...(providerConfig ?? { authMode: modelsProvider?.apiKey ? "models-json" : "reuse-pi" }),
|
|
777
|
+
enabled: !currentlyEnabled,
|
|
778
|
+
};
|
|
779
|
+
const transaction = writeConfigTransaction(files.modelsSnapshot, files.configSnapshot, files.models, nextConfig);
|
|
780
|
+
ctx.ui.notify(t(language, "toggleProviderSuccess", { provider: providerId, count: transaction.backups.length }), "info");
|
|
781
|
+
} catch (error) {
|
|
782
|
+
ctx.ui.notify(displayError(language, error, activeSecrets), "error");
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const target = overview.action === "add"
|
|
787
|
+
? await chooseProviderToAdd(ctx, language, runtimeIds, files.models, files.config)
|
|
788
|
+
: "providerId" in overview
|
|
789
|
+
? { id: overview.providerId, isNew: false }
|
|
790
|
+
: undefined;
|
|
791
|
+
if (!target) continue;
|
|
792
|
+
|
|
793
|
+
const oldProvider = files.models.providers[target.id];
|
|
794
|
+
const oldConfig = files.config.providers[target.id];
|
|
795
|
+
const draft = draftFrom(target.id, oldProvider, oldConfig, target.isNew);
|
|
796
|
+
const baselineDraft = structuredClone(draft);
|
|
797
|
+
const managedBefore = previousManaged(oldProvider, oldConfig);
|
|
798
|
+
const baselineSelected = structuredClone(managedBefore);
|
|
799
|
+
const persistedMetadata = new Map((oldConfig?.modelMetadata ?? []).map((entry) => [`${entry.type}\0${entry.id}`, entry]));
|
|
800
|
+
let selectedMetadata: ModelMetadataEntry[] = managedBefore.map((model) => {
|
|
801
|
+
const stored = persistedMetadata.get(`${model.type}\0${model.id}`);
|
|
802
|
+
if (stored) return structuredClone(stored);
|
|
803
|
+
const raw = model.type === "chat" ? oldProvider?.models?.find((entry) => entry.id === model.id) : oldConfig?.models?.find((entry) => entry.type === model.type && entry.id === model.id);
|
|
804
|
+
const explicitThinkingMap = model.type === "chat" && typeof (raw as any)?.thinkingLevelMap === "object" && (raw as any).thinkingLevelMap !== null && !Array.isArray((raw as any).thinkingLevelMap);
|
|
805
|
+
return { type: model.type, id: model.id, contextSource: model.type === "image" ? undefined : typeof (raw as any)?.contextWindow === "number" ? "existing" : "compat", maxTokensSource: model.type === "chat" ? typeof (raw as any)?.maxTokens === "number" ? "existing" : "compat" : undefined, thinkingSource: explicitThinkingMap ? "existing" : undefined };
|
|
806
|
+
});
|
|
807
|
+
const baselineMetadata = structuredClone(selectedMetadata);
|
|
808
|
+
|
|
809
|
+
let selectedDraft = structuredClone(managedBefore);
|
|
810
|
+
const adoptedChatIds = new Set<string>();
|
|
811
|
+
let cachedRemote: CandidateModel[] = [];
|
|
812
|
+
let page: "connection" | "model" | "preview" = "connection";
|
|
813
|
+
let previewOrigin: "connection" | "model" = "model";
|
|
814
|
+
const connectionState: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 };
|
|
815
|
+
const modelState: ModelPanelState = { search: "", focus: "list", actionIndex: 0, detailScroll: 0 };
|
|
816
|
+
const previewState: MenuPanelState = { selectedId: "provider", focus: "list", actionIndex: 0, detailScroll: 0 };
|
|
817
|
+
activeSecrets.push(draft.apiKeyRaw);
|
|
818
|
+
|
|
819
|
+
const adoptableIds = () => {
|
|
820
|
+
const alreadyManaged = new Set(oldConfig?.managedChatIds ?? []);
|
|
821
|
+
return (oldProvider?.models ?? [])
|
|
822
|
+
.filter((model) => typeof model.id === "string" && !alreadyManaged.has(model.id) && !adoptedChatIds.has(model.id))
|
|
823
|
+
.map((model) => model.id as string);
|
|
824
|
+
};
|
|
825
|
+
const selectionFingerprint = (models: ManagedModel[]) => JSON.stringify([...models].sort((a, b) => identityOf(a, target.id).localeCompare(identityOf(b, target.id))));
|
|
826
|
+
const metadataFingerprint = (entries: ModelMetadataEntry[]) => JSON.stringify([...entries].sort((a, b) => `${a.type}\0${a.id}`.localeCompare(`${b.type}\0${b.id}`)));
|
|
827
|
+
const isDirty = () => JSON.stringify(draft) !== JSON.stringify(baselineDraft)
|
|
828
|
+
|| selectionFingerprint(selectedDraft) !== selectionFingerprint(baselineSelected)
|
|
829
|
+
|| metadataFingerprint(selectedMetadata) !== metadataFingerprint(baselineMetadata)
|
|
830
|
+
|| adoptedChatIds.size > 0;
|
|
831
|
+
const preparePreview = async (): Promise<boolean> => {
|
|
832
|
+
if (draft.authMode === "temporary") {
|
|
833
|
+
ctx.ui.notify(t(language, "temporarySaveBlocked"), "warning");
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
try { validateSelection(draft, selectedDraft, installedModels(ctx, target.id), oldProvider); }
|
|
837
|
+
catch (error) { ctx.ui.notify(displayError(language, error, activeSecrets), "error"); return false; }
|
|
838
|
+
if ((managedBefore.length > 0 || adoptedChatIds.size > 0) && selectedDraft.length === 0) {
|
|
839
|
+
const empty = await showConfirmationPanel(ctx, {
|
|
840
|
+
title: t(language, "emptyTitle"), summary: [t(language, "emptyBody")], severity: "error",
|
|
841
|
+
defaultActionIndex: 0, cancelActionId: "cancel",
|
|
842
|
+
actions: [
|
|
843
|
+
{ id: "cancel", label: t(language, "confirmCancel") },
|
|
844
|
+
{ id: "confirm", label: t(language, "confirmEmpty"), shortcut: "ctrl+s", severity: "error" },
|
|
845
|
+
],
|
|
846
|
+
});
|
|
847
|
+
if (empty !== "confirm") return false;
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
850
|
+
};
|
|
851
|
+
const adoptExisting = async (): Promise<boolean> => {
|
|
852
|
+
const ids = adoptableIds();
|
|
853
|
+
if (!ids.length) { ctx.ui.notify(t(language, "takeoverModelsNone"), "info"); return false; }
|
|
854
|
+
const action = await showConfirmationPanel(ctx, {
|
|
855
|
+
title: t(language, "takeoverModelsTitle"), summary: [t(language, "takeoverModelsBody", { provider: target.id, count: ids.length })],
|
|
856
|
+
details: [t(language, "diffSafety")], severity: "warning", defaultActionIndex: 0, cancelActionId: "cancel",
|
|
857
|
+
actions: [
|
|
858
|
+
{ id: "cancel", label: t(language, "confirmCancel") },
|
|
859
|
+
{ id: "confirm", label: t(language, "confirmTakeover"), shortcut: "ctrl+t", severity: "warning" },
|
|
860
|
+
],
|
|
861
|
+
});
|
|
862
|
+
if (action !== "confirm") return false;
|
|
863
|
+
const parseConfig: ProviderConnectionConfig = oldConfig ?? {
|
|
864
|
+
authMode: draft.authMode === "temporary" ? "reuse-pi" : draft.authMode,
|
|
865
|
+
chatApi: draft.chatApi,
|
|
866
|
+
};
|
|
867
|
+
const selectedKeys = new Set(selectedDraft.map((model) => identityOf(model, target.id)));
|
|
868
|
+
for (const raw of oldProvider?.models ?? []) {
|
|
869
|
+
if (typeof raw.id !== "string" || !ids.includes(raw.id)) continue;
|
|
870
|
+
const model = parseManagedChat(raw, oldProvider!, parseConfig, true);
|
|
871
|
+
if (model && !selectedKeys.has(identityOf(model, target.id))) selectedDraft.push(model);
|
|
872
|
+
adoptedChatIds.add(raw.id);
|
|
873
|
+
}
|
|
874
|
+
return true;
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
providerSession: while (true) {
|
|
878
|
+
if (page === "connection") {
|
|
879
|
+
const connection = await editConnectionPanel(ctx, language, draft, connectionState, adoptableIds().length > 0);
|
|
880
|
+
activeSecrets.push(draft.apiKeyRaw, draft.temporaryKey);
|
|
881
|
+
if (connection === "models") { page = "model"; continue; }
|
|
882
|
+
if (connection === "takeover") { if (await adoptExisting()) page = "model"; continue; }
|
|
883
|
+
if (connection === "testModels") {
|
|
884
|
+
if (!draft.enabled) { ctx.ui.notify(t(language, "testProviderDisabled"), "warning"); continue; }
|
|
885
|
+
if (draft.authMode === "temporary" || isDirty()) { ctx.ui.notify(t(language, "testDraftDirty"), "warning"); continue; }
|
|
886
|
+
const models = installedModels(ctx, target.id);
|
|
887
|
+
if (!models.length) { ctx.ui.notify(t(language, "testNoModels"), "info"); continue; }
|
|
888
|
+
await showModelTestFlow(ctx, language, models, [draft.apiKeyRaw, draft.temporaryKey]);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (connection === "preview") {
|
|
892
|
+
if (await preparePreview()) { previewOrigin = "connection"; page = "preview"; }
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (!isDirty()) continue mainLoop;
|
|
896
|
+
const leave = await showConfirmationPanel(ctx, {
|
|
897
|
+
title: t(language, "dirtyLeaveTitle"), summary: [t(language, "dirtyLeaveBody")], severity: "warning",
|
|
898
|
+
defaultActionIndex: 0, cancelActionId: "edit",
|
|
899
|
+
actions: [
|
|
900
|
+
{ id: "edit", label: t(language, "dirtyReturnEdit") },
|
|
901
|
+
{ id: "preview", label: t(language, "dirtyPreviewSave"), shortcut: "ctrl+s" },
|
|
902
|
+
{ id: "discard", label: t(language, "dirtyDiscard"), shortcut: "ctrl+d", severity: "error" },
|
|
903
|
+
],
|
|
904
|
+
});
|
|
905
|
+
if (leave === "edit") continue;
|
|
906
|
+
if (leave === "discard") continue mainLoop;
|
|
907
|
+
if (await preparePreview()) { previewOrigin = "connection"; page = "preview"; }
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (page === "model") {
|
|
912
|
+
const installedRaw = installedModels(ctx, target.id);
|
|
913
|
+
const installed = installedRaw.map(candidateFromInstalled);
|
|
914
|
+
const managedChatIds = new Set(oldConfig?.managedChatIds ?? []);
|
|
915
|
+
const parseConfig: ProviderConnectionConfig = oldConfig ?? {
|
|
916
|
+
authMode: draft.authMode === "temporary" ? "reuse-pi" : draft.authMode,
|
|
917
|
+
chatApi: draft.chatApi,
|
|
918
|
+
};
|
|
919
|
+
const external = (oldProvider?.models ?? [])
|
|
920
|
+
.filter((model) => typeof model.id === "string" && !managedChatIds.has(model.id))
|
|
921
|
+
.map((model) => parseManagedChat(model, oldProvider!, parseConfig, adoptedChatIds.has(typeof model.id === "string" ? model.id : "")))
|
|
922
|
+
.filter((model): model is ManagedModel => model !== undefined)
|
|
923
|
+
.map((model) => {
|
|
924
|
+
const raw = oldProvider?.models?.find((entry) => entry.id === model.id);
|
|
925
|
+
const explicitMap = typeof raw?.thinkingLevelMap === "object" && raw.thinkingLevelMap !== null && !Array.isArray(raw.thinkingLevelMap);
|
|
926
|
+
const defaultThinkingSource = model.type === "chat"
|
|
927
|
+
? explicitMap ? "existing" as const : adoptedChatIds.has(model.id) && protocolThinkingMap(model.api) ? "protocol" as const : undefined
|
|
928
|
+
: undefined;
|
|
929
|
+
const metadata = persistedMetadata.get(`${model.type}\0${model.id}`) ?? { type: model.type, id: model.id, contextSource: model.type === "image" ? undefined : "compat", maxTokensSource: model.type === "chat" ? "compat" : undefined, thinkingSource: defaultThinkingSource };
|
|
930
|
+
return adoptedChatIds.has(model.id) ? candidateFromManaged(model, metadata) : candidateFromExternal(model, metadata);
|
|
931
|
+
});
|
|
932
|
+
const candidates = mergeCandidates(target.id, [...external, ...installed], managedBefore.map((model) => candidateFromManaged(model, persistedMetadata.get(`${model.type}\0${model.id}`))), cachedRemote);
|
|
933
|
+
const selection = await selectModels(ctx, language, target.id, draft, candidates, selectedDraft, modelState, selectedMetadata);
|
|
934
|
+
selectedDraft = selection.selected;
|
|
935
|
+
selectedMetadata = selection.metadata.map((entry) => ({ ...(selectedMetadata.find((old) => old.type === entry.type && old.id === entry.id) ?? {}), ...entry }));
|
|
936
|
+
if (selection.action === "back") { page = "connection"; continue; }
|
|
937
|
+
if (selection.action === "edit") {
|
|
938
|
+
const editTarget = selection.target;
|
|
939
|
+
if (editTarget?.readOnlyReason === "external-config") { ctx.ui.notify(t(language, "editorExternalTakeover"), "warning"); continue; }
|
|
940
|
+
const managedIndex = editTarget ? selectedDraft.findIndex((model) => model.id === editTarget.id && model.type === editTarget.type) : -1;
|
|
941
|
+
if (managedIndex < 0) { ctx.ui.notify(t(language, "editorManagedOnly"), "warning"); continue; }
|
|
942
|
+
const model = selectedDraft[managedIndex]!;
|
|
943
|
+
const metadataIndex = selectedMetadata.findIndex((entry) => entry.type === model.type && entry.id === model.id);
|
|
944
|
+
const metadata = metadataIndex >= 0 ? selectedMetadata[metadataIndex]! : { type: model.type, id: model.id };
|
|
945
|
+
const edited = await editModelParameters(ctx, language, model, metadata);
|
|
946
|
+
if (edited) { selectedDraft[managedIndex] = edited.model; if (metadataIndex >= 0) selectedMetadata[metadataIndex] = edited.metadata; else selectedMetadata.push(edited.metadata); }
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (selection.action === "probeThinking") {
|
|
950
|
+
const id = modelState.selectedId;
|
|
951
|
+
const managedIndex = selectedDraft.findIndex((model) => model.id === id);
|
|
952
|
+
const runtimeModel = installedRaw.find((model) => model.id === id);
|
|
953
|
+
const model = managedIndex >= 0 ? selectedDraft[managedIndex] : undefined;
|
|
954
|
+
if (!model || model.type !== "chat" || !runtimeModel || !draft.enabled || draft.authMode === "temporary" || isDirty()) { ctx.ui.notify(t(language, "thinkingProbeBlocked"), "warning"); continue; }
|
|
955
|
+
const currentCandidate = candidates.find((candidate) => candidate.id === id && candidate.type === "chat");
|
|
956
|
+
const resolved = resolveThinkingMap(currentCandidate?.source === "remote" ? (currentCandidate as any).thinkingLevelMap : undefined, (runtimeModel as any).thinkingLevelMap, model.thinkingLevelMap, model.api);
|
|
957
|
+
if (!resolved.map) { ctx.ui.notify(t(language, "thinkingProbeBlocked"), "warning"); continue; }
|
|
958
|
+
const probe = await showThinkingProbeFlow(ctx, language, runtimeModel, resolved.map, [draft.apiKeyRaw, draft.temporaryKey]);
|
|
959
|
+
if (!probe) continue;
|
|
960
|
+
model.reasoning = probe.accepted.length > 0;
|
|
961
|
+
model.thinkingLevelMap = acceptedThinkingMap(resolved.map, probe.accepted);
|
|
962
|
+
const metadataIndex = selectedMetadata.findIndex((entry) => entry.type === model.type && entry.id === model.id);
|
|
963
|
+
const metadata = { ...(metadataIndex >= 0 ? selectedMetadata[metadataIndex] : { type: model.type, id: model.id }), thinkingSource: "probed" as const, verifiedAt: new Date().toISOString() };
|
|
964
|
+
if (metadataIndex >= 0) selectedMetadata[metadataIndex] = metadata; else selectedMetadata.push(metadata);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (selection.action === "probe") {
|
|
968
|
+
const id = modelState.selectedId;
|
|
969
|
+
const managedIndex = selectedDraft.findIndex((model) => model.id === id);
|
|
970
|
+
const runtimeModel = installedRaw.find((model) => model.id === id);
|
|
971
|
+
if (!id || managedIndex < 0 || !runtimeModel || runtimeModel.type === "image" || !draft.enabled || draft.authMode === "temporary" || isDirty()) {
|
|
972
|
+
ctx.ui.notify(t(language, "probeBlocked"), "warning");
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const probe = await showContextProbeFlow(ctx, language, runtimeModel, [draft.apiKeyRaw, draft.temporaryKey]);
|
|
976
|
+
if (!probe?.lower) continue;
|
|
977
|
+
const model = selectedDraft[managedIndex]!;
|
|
978
|
+
if (model.type === "chat" && probe.lower < model.maxTokens) { ctx.ui.notify(t(language, "probeMaxConflict"), "warning"); continue; }
|
|
979
|
+
if (model.type !== "image") model.contextWindow = probe.lower;
|
|
980
|
+
const metadataIndex = selectedMetadata.findIndex((entry) => entry.type === model.type && entry.id === model.id);
|
|
981
|
+
const metadata = { ...(metadataIndex >= 0 ? selectedMetadata[metadataIndex] : { type: model.type, id: model.id }), contextSource: "probed" as const, verifiedAt: probe.verifiedAt, probeLower: probe.lower, probeUpper: probe.upper, probeEstimated: probe.estimated };
|
|
982
|
+
if (metadataIndex >= 0) selectedMetadata[metadataIndex] = metadata; else selectedMetadata.push(metadata);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (selection.action === "fetch") {
|
|
986
|
+
const fallbackBases = [
|
|
987
|
+
...(oldProvider?.models ?? []).map((model) => typeof model.baseUrl === "string" ? model.baseUrl : undefined),
|
|
988
|
+
...installedRaw.map((model) => model.baseUrl),
|
|
989
|
+
];
|
|
990
|
+
const discoveryCandidates = buildDiscoveryCandidates(draft, fallbackBases);
|
|
991
|
+
if (!draft.discoveryUrl) {
|
|
992
|
+
const edit = t(language, "discoveryRequiredEdit");
|
|
993
|
+
const cancel = t(language, "discoveryRequiredCancel");
|
|
994
|
+
if (discoveryCandidates.length) {
|
|
995
|
+
const use = t(language, "discoveryUseCandidates", { url: redactUrl(discoveryCandidates[0], language), count: Math.max(0, discoveryCandidates.length - 1) });
|
|
996
|
+
const choice = await showConfirmationPanel(ctx, {
|
|
997
|
+
title: t(language, "discoveryDerivedTitle"), summary: [use], details: [t(language, "discoveryCandidateSafety")],
|
|
998
|
+
defaultActionIndex: 2, cancelActionId: "cancel",
|
|
999
|
+
actions: [
|
|
1000
|
+
{ id: "use", label: t(language, "discoveryUseAction"), shortcut: "ctrl+r" },
|
|
1001
|
+
{ id: "edit", label: edit },
|
|
1002
|
+
{ id: "cancel", label: cancel },
|
|
1003
|
+
],
|
|
1004
|
+
});
|
|
1005
|
+
if (choice === "edit") page = "connection";
|
|
1006
|
+
else if (choice !== "use") continue;
|
|
1007
|
+
} else {
|
|
1008
|
+
const choice = await showConfirmationPanel(ctx, {
|
|
1009
|
+
title: t(language, "discoveryRequired"), summary: [t(language, "discoveryNoCandidate")],
|
|
1010
|
+
defaultActionIndex: 1, cancelActionId: "cancel",
|
|
1011
|
+
actions: [{ id: "edit", label: edit }, { id: "cancel", label: cancel }],
|
|
1012
|
+
});
|
|
1013
|
+
if (choice === "edit") page = "connection";
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (page === "connection") continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (!discoveryCandidates.length) { ctx.ui.notify(t(language, "discoveryRequired"), "warning"); continue; }
|
|
1019
|
+
const auth = draft.authMode === "reuse-pi" ? await ctx.modelRegistry.getProviderAuth(target.id) : undefined;
|
|
1020
|
+
const resolvedModelBase = draft.baseUrl ?? fallbackBases.find((value) => { if (!value) return false; try { validateHttpUrl(value, "Base URL"); return true; } catch { return false; } });
|
|
1021
|
+
const discoveryDraft = resolvedModelBase ? { ...draft, baseUrl: resolvedModelBase } : draft;
|
|
1022
|
+
const result = await withCancellableLoader(ctx, t(language, "fetching", { url: redactUrl(discoveryCandidates[0], language) }), (signal) => discoverModelsFromCandidates(discoveryDraft, discoveryCandidates, auth, signal));
|
|
1023
|
+
if (result.cancelled) { ctx.ui.notify(t(language, "fetchCancelled"), "info"); continue; }
|
|
1024
|
+
if (result.error) { ctx.ui.notify(displayError(language, result.error, [draft.apiKeyRaw, draft.temporaryKey]), "error"); continue; }
|
|
1025
|
+
draft.discoveryUrl = result.value!.url;
|
|
1026
|
+
cachedRemote = [...result.value!.models];
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (await preparePreview()) { previewOrigin = "model"; page = "preview"; }
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const details = buildPreviewDetails(language, target.id, managedBefore, selectedDraft, oldConfig, oldProvider, draft);
|
|
1034
|
+
const preview = await showPreviewPanel(ctx, language, target.id, details.providerDetails, details.modelDetails, previewState);
|
|
1035
|
+
if (preview.action === "back") { page = previewOrigin; continue; }
|
|
1036
|
+
if (preview.action === "discard") {
|
|
1037
|
+
const discard = await showConfirmationPanel(ctx, {
|
|
1038
|
+
title: t(language, "discardConfirmTitle"), summary: [t(language, "discardConfirmBody")], severity: "error",
|
|
1039
|
+
defaultActionIndex: 0, cancelActionId: "cancel",
|
|
1040
|
+
actions: [
|
|
1041
|
+
{ id: "cancel", label: t(language, "dirtyReturnEdit") },
|
|
1042
|
+
{ id: "discard", label: t(language, "dirtyDiscard"), shortcut: "ctrl+d", severity: "error" },
|
|
1043
|
+
],
|
|
1044
|
+
});
|
|
1045
|
+
if (discard === "discard") continue mainLoop;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
const compatItems = selectedMetadata.filter((entry) => entry.contextSource === "compat" || entry.maxTokensSource === "compat").map((entry) => entry.type + '/' + entry.id);
|
|
1049
|
+
const risk = compatItems.length ? [t(language, "compatSaveRisk", { items: compatItems.join(", ") })] : [];
|
|
1050
|
+
const confirmed = await confirmAction(ctx, language, t(language, "finalSaveTitle"), [t(language, "finalSaveBody"), ...risk], [...details.providerDetails, ...details.modelDetails], t(language, "confirmSave"));
|
|
1051
|
+
if (!confirmed) continue;
|
|
1052
|
+
try {
|
|
1053
|
+
const next = applyChanges(files, target.id, draft, selectedDraft, adoptedChatIds, selectedMetadata);
|
|
1054
|
+
const transaction = writeConfigTransaction(files.modelsSnapshot, files.configSnapshot, next.models, next.config);
|
|
1055
|
+
ctx.ui.notify(t(language, "saveSuccess", { count: transaction.backups.length }), "info");
|
|
1056
|
+
continue mainLoop;
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
ctx.ui.notify(displayError(language, error, activeSecrets), "error");
|
|
1059
|
+
page = previewOrigin;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
ctx.ui.notify(displayError(language, error, activeSecrets), "error");
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
}
|