Repository navigation
Expand file tree
/
Copy pathindex.mjs
More file actions
1521 lines (1438 loc) · 69.5 KB
/
Copy pathindex.mjs
File metadata and controls
1521 lines (1438 loc) · 69.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
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
892
893
894
895
896
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
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
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
// Command Code's plans (commandcode.ai) as an OpenCode provider plugin.
//
// An account is a Command Code API key. It is got the way Command Code's
// CLI gets one (Studio approves a key for this machine and posts it to a
// callback on 127.0.0.1), taken from the CLI's own sign-in
// (~/.commandcode/auth.json, read only), or pasted from Studio's keys page.
// A key doesn't expire: there is nothing to refresh.
//
// Every plan but Go is served on the Provider API: chat completions and
// Responses at /provider/v1, Anthropic's Messages at /provider/v1/messages,
// the key in Authorization and x-api-key. Go's key has no Provider API: it
// is only taken where the CLI itself asks, POST /alpha/generate, in the
// CLI's own format (command-code 1.72.2), which the fetch here writes from
// the chat completion OpenCode sends and turns back into one.
//
// Zero Data Retention (commandcode.ai/docs/resources/zdr) is asked for as
// the CLI asks it: x-cmd-zdr: 1 on every request for a reply, the Provider
// API's and /alpha/generate's alike (command-code's buildCommandAuthHeaders
// on CMD_ZDR=1: 1.84.0's bundle, and 1.74.1 as captured). It is on when the user turns on the provider's "Zero Data
// Retention" switch in magpie, or when CMD_ZDR is "1" in the environment,
// as for the CLI.
import { createServer, STATUS_CODES } from "node:http"
import { randomBytes, randomUUID } from "node:crypto"
import { readFile } from "node:fs/promises"
import { homedir } from "node:os"
import { basename, join } from "node:path"
const ID = "commandcode-plan"
const API = "https://api.commandcode.ai"
const STUDIO = "https://commandcode.ai"
const BASE = API + "/provider/v1"
const CLI_VERSION = "1.72.2"
const MAX_TOKENS = 64000 // the CLI's max_tokens when none is asked
// MAX_OUTPUT is the most max_tokens Command Code takes at /alpha/generate,
// for any model: more is refused, the whole request with it ("Too big:
// expected number to be <=200000 at params.max_tokens") — magpie's
// CommandCodeMaxOutput
const MAX_OUTPUT = 200_000
// TypeSafe's System One, as the CLI asks it (command-code 1.88.2:
// SYSTEM_ONE_ROUTE "/provider/v1/systemone", SYSTEM_ONE_MODELS
// ["typesafe/jev"]): a decision model, which answers typed questions
// ({state, questions}) with probabilities and holds no conversation. Its
// list (/provider/v1/models) doesn't name it. SYSTEM_ONE_CLI is the CLI
// whose request this is, sent as its x-command-code-version.
const JEV = "typesafe/jev"
const SYSTEM_ONE = BASE + "/systemone"
const SYSTEM_ONE_CLI = "1.88.2"
// the CLI's resolveSystemOneModel table
const JEV_NAMES = { jev: JEV, "jev-latest": JEV, [JEV]: JEV, "typesafe-ai/jev": JEV }
const JEV_MODEL = { id: JEV, name: "Jev", context: 0, decides: true }
const ORIGINS = ["https://commandcode.ai", "https://staging.commandcode.ai"]
const SIGN_IN_TIMEOUT = 10 * 60 * 1000
const CHAT = "@ai-sdk/openai-compatible"
const MESSAGES = "@ai-sdk/anthropic"
const RESPONSES = "@ai-sdk/openai" // the Responses API, which magpie speaks to it on
// ---- models -------------------------------------------------------------------
// THINK_OFF are the models the Provider API turns thinking off for at
// reasoning_effort "off" (CHENJIAMIAN, magpie-community/plugins#334, on a
// GOAT key: 0 reasoning tokens on each, where "none" is refused as no
// option and thinking {"type":"disabled"} is ignored). Others, DeepSeek V4
// Flash Fast and Kimi K3 among them, refuse "off" ("does not support
// reasoning_effort \"off\""). Each is offered magpie's off level, none,
// beside the levels magpie gave it before (DeepSeek's: low, high, max),
// and none or "off" asked of it is sent as "off" (providerBody). Go's
// /alpha/generate is not known to take "off": its levels stay the CLI's.
const THINK_OFF = new Set([
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-vision-exp",
"deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4.1-flash-fast",
])
const OFF_EFFORTS = ["none", "low", "high", "max"]
// The plan's models when its list can't be asked: Claude on Messages, the
// rest on chat completions. Pictures as models.dev has them (DeepSeek V4
// and GLM-5.3 read text alone, so say nothing).
const MODELS = [
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", context: 1_000_000, images: true },
{ id: "claude-opus-5-5", name: "Claude Opus 5.5", context: 1_000_000, images: true },
{ id: "gpt-6-sol", name: "GPT-6 Sol", context: 1_050_000, images: true },
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", context: 1_000_000, efforts: OFF_EFFORTS },
{ id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash", context: 1_000_000, efforts: OFF_EFFORTS },
{ id: "moonshotai/Kimi-K3", name: "Kimi K3", context: 1_000_000, images: true },
{ id: "zai-org/GLM-5.3", name: "GLM-5.3", context: 1_000_000 },
{ id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", context: 1_000_000, images: true },
]
// The models the Go plan is let use, as the CLI's own table has it
// (1.73.0): every model its picker shows (less the hidden) but the
// "premium" ones and those it blocks for Go (GO_REFUSED). They stand in
// until Command Code's list is fetched (goModels), or when it can't be,
// and give that list the reasoning levels and pictures it doesn't say —
// magpie's cmdGoModels.
const EFF5 = ["low", "medium", "high", "xhigh", "max"]
// FELL_BACK marks a list handed back for one that can't be had (magpie's
// plugin host reads it; OpenCode never sees a symbol's key)
const FELL_BACK = Symbol.for("magpie.fellBack")
const GO_MODELS = [
{ id: "gpt-6-luna", name: "GPT-6 Luna", context: 1_050_000, images: true, efforts: EFF5 },
{ id: "gpt-5.6-luna", name: "GPT-5.6 Luna", context: 1_050_000, images: true, efforts: EFF5 },
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", context: 1_000_000, efforts: ["high", "max"] },
{ id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash", context: 1_000_000, efforts: ["high", "max"] },
{ id: "deepseek/deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (exp)", context: 1_000_000, images: true, efforts: ["high", "max"] },
{ id: "deepseek/deepseek-v4-flash-fast", name: "DeepSeek V4 Flash Fast", context: 1_000_000, efforts: ["low", "high", "max"] },
{ id: "deepseek/deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", context: 1_000_000, images: true, efforts: ["low", "high", "max"] },
{ id: "deepseek/deepseek-v4.1-flash-fast", name: "DeepSeek V4.1 Flash Fast", context: 1_000_000, images: true, efforts: ["low", "high", "max"] },
{ id: "moonshotai/Kimi-K3", name: "Kimi K3", context: 1_000_000, images: true, efforts: ["low", "high", "max"] },
{ id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", context: 256_000, images: true },
{ id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", context: 262_000, images: true },
{ id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", context: 256_000, images: true },
{ id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", context: 256_000, images: true },
{ id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", context: 1_048_576, images: true, efforts: ["low", "high", "max"] },
{ id: "z-ai/glm-5.3-flashx", name: "GLM-5.3 FlashX", context: 1_000_000, images: true, efforts: ["low", "high", "max"] },
{ id: "zai-org/GLM-5.3", name: "GLM-5.3", context: 1_000_000, efforts: ["low", "high", "max"] },
{ id: "zai-org/GLM-5.2", name: "GLM-5.2", context: 1_000_000, efforts: ["high", "max"] },
{ id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", context: 1_000_000 },
{ id: "zai-org/GLM-5.1", name: "GLM-5.1", context: 200_000 },
{ id: "zai-org/GLM-5", name: "GLM-5", context: 200_000 },
{ id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", context: 1_000_000, images: true, efforts: ["low", "medium", "high"] },
{ id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", context: 200_000 },
{ id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", context: 200_000 },
{ id: "xiaomi/mimo-v2.6-pro", name: "MiMo V2.6 Pro", context: 1_048_576, images: true },
{ id: "xiaomi/mimo-v2.6-flash", name: "MiMo V2.6 Flash", context: 1_048_576, images: true },
{ id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", context: 1_000_000 },
{ id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", context: 1_000_000, images: true },
{ id: "Qwen/Qwen3.8-Omni-Flash", name: "Qwen 3.8 Omni Flash", context: 1_000_000, images: true, efforts: ["low", "medium", "xhigh"] },
{ id: "Qwen/Qwen3.8-Max-0902", name: "Qwen 3.8 Max 0902", context: 1_000_000, images: true, efforts: ["low", "medium", "xhigh"] },
{ id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", context: 1_000_000, images: true, efforts: ["low", "medium", "xhigh"] },
{ id: "Qwen/Qwen3.8-27B", name: "Qwen 3.8 27B", context: 262_144, images: true, efforts: ["low", "medium", "xhigh"] },
{ id: "Qwen/Qwen3.8-Flash", name: "Qwen 3.8 Flash", context: 1_000_000, images: true, efforts: ["low", "medium", "xhigh"] },
{ id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", context: 1_000_000 },
{ id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", context: 1_000_000, images: true },
{ id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", context: 1_000_000, images: true },
{ id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", context: 200_000 },
{ id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", context: 200_000, images: true },
{ id: "meituan/LongCat-2.0", name: "LongCat 2.0", context: 1_048_576 },
{ id: "stepfun/Step-5-Preview", name: "Step 5 Preview", context: 1_000_000, images: true, efforts: ["low", "medium", "high"] },
{ id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", context: 256_000, images: true },
{ id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", context: 262_144 },
{ id: "tencent/hy3-paid", name: "Tencent Hy3", context: 262_144 },
{ id: "tencent/hy4-preview", name: "Tencent Hy4 Preview", context: 1_048_576, efforts: ["low", "medium", "high"] },
{ id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", context: 1_000_000, images: true, efforts: ["low", "medium", "high"] },
{ id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", context: 1_000_000, images: true, efforts: ["low", "medium", "high"] },
{ id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", context: 1_000_000 },
{ id: "thinkingmachines/inkling", name: "Inkling", context: 256_000, images: true },
{ id: "thinkingmachines/inkling-small", name: "Inkling Small", context: 1_000_000, images: true },
{ id: "stealth/space-bunny-alpha", name: "Space Bunny Alpha", context: 1_000_000, images: true, efforts: ["low", "medium", "high"] },
{ id: "stealth/pixel-canary", name: "Pixel Canary", context: 262_144, images: true, efforts: ["low", "medium", "xhigh"] },
{ id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", context: 256_000 },
{ id: "inclusionai/ling-3.0-flash-free", name: "Ling 3.0 Flash", context: 256_000 },
{ id: "inclusionai/ling-3.0-flash-sante:free", name: "Ling 3.0 Flash Sante", context: 262_144 },
{ id: "inclusionai/ling-3.1-flash:free", name: "Ling 3.1 Flash", context: 262_144, efforts: ["low", "medium", "high"] },
{ id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", context: 1_048_576, images: true, efforts: ["low", "medium", "high", "xhigh"] },
{ id: "meta/muse-spark-1.3-contributor", name: "Muse Spark 1.3 Contributor", context: 1_048_576, images: true, efforts: ["low", "medium", "high", "xhigh"] },
{ id: "xai/grok-4.5", name: "Grok 4.5", context: 500_000, images: true, efforts: ["low", "medium", "high"] },
]
// The models each plan is let use, as the CLI's own table has it
// (command-code 1.79.2: evaluateModelAccess over its model categories and
// its plans' allowedCategories and blockedModels). Command Code's list is
// the same 87 models for every plan, asked with a key or without, and the
// Provider API turns a model the plan hasn't away only when it is asked
// (403 "MODEL_NOT_IN_PLAN: Claude Opus 5.5 available in Provider and above
// plans or extra on demand usage"). So a plan's list is Command Code's less
// what its plan refuses: a premium model when the plan has the open models
// alone (Go, GOAT), and the models it blocks. A model the table doesn't
// name the CLI lets any plan pick; one the plan hasn't after all is refused
// with MODEL_NOT_IN_PLAN, which the agent is told. Blocks are by the
// model's id: the CLI's own block of "vercel-ai-gateway:xiaomi/mimo-v2.6-
// pro-ultraspeed" misses its "cai" model, and Go is still kept off it.
const PREMIUM = new Set([
"claude-sonnet-5", "claude-sonnet-4-6", "claude-fable-5-1", "claude-fable-5",
"claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7",
"claude-haiku-4-5-20251001", "gpt-5.6-terra", "gpt-6-astra", "gpt-6.1-sol",
"gpt-6-sol", "gpt-5.5", "gpt-5.4", "gpt-5.3-codex", "gpt-5.4-mini",
"google/gemini-3.5-flash", "google/gemini-3.1-flash-lite", "sakana/fugu-ultra",
"meta/muse-spark-1.1",
])
const GO_BLOCKED = new Set([
"meta/muse-spark-1.2", "meta/muse-spark-1.3", "xai/grok-4.6", "xai/grok-4.7",
"xiaomi/mimo-v2.6-pro-ultraspeed", "google/gemini-3.7-flash", "google/gemini-3.8-flash",
"gpt-5.6-sol", "claude-sonnet-5-5", "claude-haiku-5-5",
])
const PRO_BLOCKED = new Set([
"claude-fable-5-1", "claude-fable-5", "claude-opus-5-5", "claude-opus-5",
"claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5-20251101",
"gpt-6-astra", "gpt-6.1-sol", "sakana/fugu-ultra",
])
// by the plan's name (PLAN_NAMES): the old Pro and the new are let the same
const PLAN_MODELS = {
Go: { premium: false, blocked: GO_BLOCKED },
GOAT: { premium: false },
Pro: { premium: true, blocked: PRO_BLOCKED },
Provider: { premium: true, blocked: new Set(["gpt-6.1-sol"]) },
Max: { premium: true },
Ultra: { premium: true },
"Teams Pro": { premium: true },
}
// refuses is whether the plan named refuses the model: never for a plan
// the table doesn't know, "No plan" and an unread one among them.
function refuses(plan, id) {
const p = PLAN_MODELS[plan]
if (!p) return false
return (!p.premium && PREMIUM.has(id)) || !!p.blocked?.has(id)
}
// The models of Command Code's list the Go plan is refused — magpie's
// cmdGoRefused.
const GO_REFUSED = new Set([...PREMIUM, ...GO_BLOCKED])
const GO_EFFORTS = Object.fromEntries(GO_MODELS.map((m) => [m.id, m.efforts ?? []]))
const isClaude = (id) => /^claude-/i.test(id) || id.startsWith("anthropic/")
// FREE are the models Command Code's CLI marks FREE in its picker (1.73.2:
// badge:"free", "{name} is free and uses shared capacity"); its API's list
// doesn't say so, and Space Bunny Alpha's and Pixel Canary's ids name
// nothing free — magpie's cmdFree. Neither says a model is served at a
// discount.
const FREE = new Set([
"stealth/space-bunny-alpha", "stealth/pixel-canary",
"poolside/laguna-s-2.1-free", "inclusionai/ling-3.0-flash-free",
"inclusionai/ling-3.0-flash-sante:free", "inclusionai/ling-3.1-flash:free",
"MiniMaxAI/MiniMax-M3-Free", "minimax/minimax-m3-free",
"minimax/minimax-m2.7-free", "meituan/LongCat-2.0:free", "tencent/Hy3",
])
// configModel is a model as opencode.json's provider.models has it.
function configModel(m) {
const npm = m.npm ?? (isClaude(m.id) ? MESSAGES : CHAT)
return {
name: m.name,
...(npm !== CHAT ? { provider: { npm, api: BASE } } : {}),
limit: { context: m.context ?? 0, output: m.output ?? 0 },
...(m.images ? { attachment: true, modalities: { input: ["text", "image"], output: ["text"] } } : {}),
...(m.efforts?.length
? { reasoning: true, variants: Object.fromEntries(m.efforts.map((e) => [e, { reasoningEffort: e }])) }
: {}),
tool_call: !m.decides,
// magpie's own field: a decision model, asked at /systemone
...(m.decides ? { decides: true } : {}),
}
}
// runtimeModel is a model as OpenCode's provider.models hook returns it.
// Whether it takes pictures is said only when known (m.images a boolean):
// Command Code's list doesn't say, and a false said for every model
// unknown is taken by magpie as the plan's answer, over models.dev's,
// which the built-in asks for a model its list says nothing of
// (yetone/magpie: every model of the plan without image input).
function runtimeModel(m) {
const npm = m.npm ?? (isClaude(m.id) ? MESSAGES : CHAT)
const input = { text: true, ...(typeof m.images === "boolean" ? { image: m.images } : {}), audio: false, video: false, pdf: false }
return {
id: m.id,
providerID: ID,
name: m.name ?? m.id,
api: { id: m.id, url: BASE, npm },
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: m.context ?? 0, output: m.output ?? 0 },
capabilities: {
temperature: true,
reasoning: !!m.efforts?.length,
attachment: !!m.images,
toolcall: true,
input,
output: { text: true, image: false, audio: false, video: false, pdf: false },
interleaved: false,
},
release_date: "",
variants: Object.fromEntries((m.efforts ?? []).map((e) => [e, { reasoningEffort: e }])),
// magpie's plugin host reads it; the CLI calls the model free
...(FREE.has(m.id) ? { free: true } : {}),
// magpie's plugin host reads it: a decision model, asked at /systemone
...(m.decides ? { decides: true } : {}),
}
}
// withJev is a plan's list with Jev, which Command Code's list doesn't
// name: the CLI offers it to every plan, and Command Code answers for the
// plan when it is asked.
function withJev(out) {
if (!out[JEV]) out[JEV] = runtimeModel(JEV_MODEL)
return out
}
// liveModels is the Provider API's list, with the APIs each model is
// served on (its Claude models on /messages alone, the open ones on
// /chat/completions and /responses, some on /responses alone); the default list's names and
// windows fill in what it leaves out. A model that turns thinking off
// (THINK_OFF) has its levels, off among them; the list names no levels,
// and magpie gives every other model its maker's. The list says nothing
// of pictures: a model takes them as the default list or the CLI's table (GO_MODELS)
// says, and every Claude model does; of any other nothing is said, and
// magpie answers from models.dev.
async function liveModels(key) {
const res = await fetch(BASE + "/models", { headers: { Authorization: `Bearer ${key}`, "x-api-key": key } })
if (!res.ok) throw new Error(`models: ${res.status}`)
const body = await res.json()
const known = Object.fromEntries(MODELS.map((m) => [m.id, m]))
const table = Object.fromEntries(GO_MODELS.map((m) => [m.id, m]))
const out = []
for (const m of body?.data ?? []) {
if (!m?.id) continue
const k = known[m.id]
let images = Array.isArray(m.modalities?.input) ? m.modalities.input.includes("image") : undefined
if (images === undefined) images = k?.images ?? table[m.id]?.images ?? (isClaude(m.id) || undefined)
const eps = (m.supported_endpoints ?? []).map((e) => String(e).replace(/\/$/, "").replace(/^\/v1/, ""))
let npm = isClaude(m.id) ? MESSAGES : CHAT
// the first of the APIs it is served on, in magpie's order (chat
// completions, Responses, Messages), as the built-in picks it
if (eps.length) npm = eps.includes("/chat/completions") ? CHAT : eps.includes("/responses") ? RESPONSES : eps.includes("/messages") ? MESSAGES : CHAT
const ctx = Number(m.context_length)
out.push({
id: m.id,
name: m.name && m.name !== m.id ? m.name : m.display_name || k?.name || m.id,
context: Number.isFinite(ctx) && ctx > 0 ? ctx : k?.context ?? 0,
images,
npm,
...(THINK_OFF.has(m.id) ? { efforts: OFF_EFFORTS } : {}),
})
}
if (!out.length) throw new Error("models: an empty list")
return out
}
// goModels is the Go plan's list, as magpie's cmdGoFetch has it: the
// Provider API's list, which answers without a key (Go's key has no
// Provider API) and is asked without it, less what Go is refused; each
// model takes its name when the list gives none, its reasoning levels and,
// when the list doesn't say, its pictures and window from GO_MODELS
// (catalog.Decorate). Every Go model is asked on chat completions, which
// the fetch sends on to /alpha/generate. An account with credits beyond
// the plan's (open) is let every model, as the CLI lets it.
async function goModels(open = false) {
const url = BASE + "/models"
// the CLI asks its catalog with accept alone, under Node's fetch, whose
// User-Agent is "node" (installModelCatalogManager)
const res = await fetch(url, { headers: { Accept: "application/json", "User-Agent": "node" }, signal: AbortSignal.timeout(8_000) })
const text = await res.text()
// catalog.FetchURL's errors
if (res.status !== 200) {
const status = `${res.status} ${statusText(res.status)}`.trimEnd()
const msg = listError(text)
throw new Error(msg ? `${url}: ${status} (${msg})` : `${url}: ${status}`)
}
let body
try {
body = JSON.parse(text)
if (body === null || typeof body !== "object" || Array.isArray(body)) throw 0
} catch {
throw new Error(`${url}: not a model list`)
}
let rows = Array.isArray(body.data) ? body.data : []
if (!rows.length) rows = Array.isArray(body.models) ? body.models : []
const known = Object.fromEntries(GO_MODELS.map((m) => [m.id, m]))
const listed = []
for (const r of rows) {
const id = (typeof r?.id === "string" && r.id) || (typeof r?.name === "string" && r.name) || ""
// a model that draws, or isn't for text (embeddings, speech), is
// left out, as catalog.Chat and fetchOne leave it
if (!id || drawsID(id) || !textID(id)) continue
const input = Array.isArray(r.modalities?.input) ? r.modalities.input.includes("image") : undefined
const ctx = typeof r.context_length === "number" && r.context_length > 0 ? Math.trunc(r.context_length) : 0
let m = { id, name: (typeof r.display_name === "string" && r.display_name) || id, context: ctx, images: input, npm: CHAT }
let k = known[id]
const i = id.lastIndexOf("/")
if (!k && i >= 0) k = known[id.slice(i + 1)]
if (k) {
m = {
...m,
name: m.name === id ? k.name : m.name,
efforts: k.efforts,
images: input === undefined ? k.images : input,
context: m.context || k.context,
}
}
listed.push(m)
}
if (!listed.length) throw new Error(`${url}: no models listed`)
const out = open ? listed : listed.filter((m) => !GO_REFUSED.has(m.id))
if (!out.length) throw new Error("Command Code listed no models for the Go plan")
return out
}
// drawsID and textID are catalog.DrawsID and catalog.textModel on an id.
const drawsID = (id) =>
["image", "imagen", "imagine", "dall-e", "flux", "seedream", "cogview", "stable-diffusion", "sdxl", "wanx", "kolors", "hidream"].some((w) => id.toLowerCase().includes(w))
const textID = (id) =>
!["embed", "-tts", "image", "audio", "-live", "robotics", "computer-use", "deep-research", "transcribe", "realtime", "moderation", "whisper", "dall-e", "sora"].some((w) => id.includes(w))
// listError is a model list's error reply's message ({"error":{"message"}},
// {"error":"…"}, {"message"}), cut at 160 characters, as catalog's
// errorMessage reads it.
function listError(text) {
let v
try {
v = JSON.parse(text)
} catch {
return ""
}
if (v === null || typeof v !== "object" || Array.isArray(v)) return ""
let msg = typeof v.message === "string" ? v.message : ""
if (typeof v.error?.message === "string" && v.error.message) msg = v.error.message
else if (typeof v.error === "string" && v.error) msg = v.error
msg = msg.trim()
const r = [...msg]
return r.length > 160 ? r.slice(0, 160).join("") + "…" : msg
}
// ---- the account --------------------------------------------------------------
// SIGNED_OUT is what a key Command Code turns away says, in the words
// magpie reads as a sign-in gone (signInGone): the account is marked
// signed out, the gateway passes over it, and its row offers Sign in again
// (blue12345655 on Discord: a signed-out account stayed "signed in", its
// card saying only "Unauthorized", with no way to sign in again).
const SIGNED_OUT = "Command Code's sign-in has expired; sign in again"
// keyRefused says Command Code turns the key away for good: /alpha/whoami,
// which answers every key it knows, refuses it (401 UNAUTHORIZED "Invalid
// 'Authorization' header or token.", as every path answers a key it
// doesn't know). Asked only after another path's 401 or 403, so a refusal
// that is about a model or a plan, or a whoami that fails some other way,
// leaves the account as it is.
async function keyRefused(key) {
try {
await accountJSON("/alpha/whoami", key, 10_000)
return false
} catch (e) {
return e.status === 401 || e.status === 403
}
}
async function accountJSON(path, key, timeout = 15_000) {
const res = await fetch(API + path, {
headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
signal: AbortSignal.timeout(timeout),
})
if (!res.ok) {
const e = new Error(`${path}: ${res.status}`)
e.status = res.status
throw e
}
return res.json()
}
// The plan names, by planId prefix, the longest first, with the dollars of
// credits each gives a month (the CLI's getPlanInfo; "individual-pro-v1"
// is the old Pro, $80 of them).
const PLAN_NAMES = [
["individual-provider", "Provider", 15],
["individual-pro-v1", "Pro", 80],
["individual-goat", "GOAT", 70],
["individual-ultra", "Ultra", 300],
["individual-max", "Max", 150],
["individual-pro", "Pro", 30],
["individual-go", "Go", 10],
["teams-pro", "Teams Pro", 40],
]
const NO_PLAN = "No plan"
function planOf(id) {
const s = String(id ?? "").toLowerCase().replaceAll("_", "-")
return PLAN_NAMES.find(([p]) => s.startsWith(p)) ?? ["", "", 0]
}
const planName = (id) => planOf(id)[1]
// subscriptionOf is the account's plan as billing/subscriptions says: its
// id and name, when its period ends and whether it renews then; NO_PLAN
// when it has none, undefined when it can't be read.
async function subscriptionOf(key, timeout) {
let d
try {
const r = await accountJSON("/alpha/billing/subscriptions", key, timeout)
// a 200 with success false when Command Code couldn't tell
// ("write CONNECTION_CLOSED …"): unread, not no plan
if (r?.success === false) return undefined
d = r?.data
} catch {
return undefined
}
if (d?.planId && d.status !== "canceled" && d.status !== "incomplete_expired") {
const renew = typeof d.cancelAtPeriodEnd === "boolean" ? (d.cancelAtPeriodEnd ? "off" : "auto") : ""
return { id: d.planId, plan: planName(d.planId) || d.planId, until: when(d.currentPeriodEnd), renew }
}
return { id: "", plan: NO_PLAN }
}
// planNow is the account's plan, for the models and the endpoint Go takes:
// the subscription read in the last ten minutes, else read now, with the
// plan the sign-in saved going meanwhile when it takes more than a moment.
async function planNow(key, saved) {
return (await subscriptionNow(key, saved ? 2_000 : waits.subWait))?.plan || saved || ""
}
// ---- the allowance ------------------------------------------------------------
// As magpie's built-in Command Code account shows it
// (internal/provider/commandcode_plan.go, cmdQuota).
// statusText is a refused request's status as magpie says it (Go's
// http.StatusText).
const statusText = (s) =>
({ 413: "Request Entity Too Large", 414: "Request URI Too Long", 416: "Requested Range Not Satisfiable", 418: "I'm a teapot", 509: "" })[s] ?? STATUS_CODES[s] ?? ""
// number reads an amount given as a JSON number or as a string of one.
function number(v) {
if (typeof v === "number") return v
if (typeof v !== "string" || !v.trim()) return undefined
const n = Number(v.trim())
return Number.isNaN(n) ? undefined : n
}
const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/
// when reads a reset time: unix seconds or milliseconds, or RFC 3339; an
// ISO time, undefined when there is none.
function when(v) {
if (typeof v === "string" && RFC3339.test(v) && !Number.isNaN(Date.parse(v))) return new Date(v).toISOString()
let n = number(v)
if (n === undefined || !(n > 0)) return undefined
if (n < 1e12) n *= 1000
const d = new Date(Math.trunc(n))
return Number.isNaN(d.getTime()) ? undefined : d.toISOString()
}
// money is dollars to the cent, a half cent to the even one, as magpie
// writes them.
function money(v) {
const exact = Math.abs(v).toFixed(20)
const tie = /\.\d\d50*$/.test(exact)
const s = tie && Number(exact[exact.indexOf(".") + 2]) % 2 === 0 ? exact.slice(0, exact.indexOf(".") + 3) : Math.abs(v).toFixed(2)
return "$" + (v < 0 ? "-" : "") + s
}
// limits is the plan's 5-hour and weekly windows in a credits reply,
// those it gives a cap.
function limits(c) {
const out = []
const w = c?.windowLimits
if (!w) return out
for (const [name, span, x] of [["5 hours", 5 * 3600, w.fiveHour], ["Weekly", 7 * 24 * 3600, w.weekly]]) {
if (!x) continue
const used = number(x.used)
const cap = number(x.cap)
if (used === undefined || cap === undefined || !(cap > 0)) continue
const at = when(x.resetAt)
out.push({ name, used: Math.min(100, (100 * Math.max(0, used)) / cap), span, ...(at ? { resetsAt: at } : {}) })
}
return out
}
// allowanceOf makes a credits reply into the plan's windows: the 5-hour
// and weekly limits, and the dollars as one more, used of the CLI's pool
// (the plan's month of credits, or what is left of it if more, and the
// bought and free ones) as its /usage bar is. The dollars, the plan's
// whole allowance, go second, after the 5-hour window: magpie's account
// line and menu bar show an account's first two windows, and with them
// last the Credits bar was never there (01huadalang on Discord). A reply with neither windows
// nor a plan tells a balance: a key's money, not an allowance. The month's
// credits come back at until, the end of the plan's period
// (billing/subscriptions' currentPeriodEnd), when it is known
// (yetone/magpie#2454).
function allowanceOf(c, until) {
const out = { windows: limits(c) }
const [, name, planMonthly] = planOf(c?.credits?.planId)
if (name) out.plan = name
let monthly = 0
let left = 0
let known = false
for (const [i, v] of [c?.credits?.monthlyCredits, c?.credits?.purchasedCredits, c?.credits?.freeCredits].entries()) {
let n = number(v)
if (n === undefined) continue
n = Math.max(0, n)
if (i === 0) monthly = n
left += n
known = true
}
if (known && planMonthly > 0) {
const pool = Math.max(planMonthly, monthly) + left - monthly
out.windows.splice(1, 0, { name: "Credits", used: Math.min(100, (100 * (pool - left)) / pool), display: money(pool - left) + " / " + money(pool), ...(until ? { resetsAt: until } : {}) })
} else if (known && !out.windows.length) out.balance = money(left)
return out
}
// Each key's subscription as last read, and the reading under way.
// billing/subscriptions takes 15–20 seconds at times, longer than magpie
// waits for a card (15): the allowance waits for it waits.subWait at most, then
// goes with the one read before while the reading goes on for next time.
const subsSeen = new Map()
// GO_NO_API is the Provider API refusing a Go plan's key: "Your Go plan
// doesn't include API access. Upgrade to Provider or higher …" (#969)
const GO_NO_API = /\bGo plan doesn.t include API access/i
const SUB_KEEP = 10 * 60 * 1000
const waits = { subWait: 9_000, creditWait: 5_000 }
// Each key's credits beyond its plan's, as last read: bought or free ones
// let the account every model, the plan's or not (the CLI's
// evaluateModelAccess; Command Code's refusal: "… or extra on demand
// usage").
const extraSeen = new Map()
const hasExtra = (c) => number(c?.credits?.purchasedCredits) > 0 || number(c?.credits?.freeCredits) > 0
// extraNow is whether the key has such credits: read now, else as last
// read, else none, so the plan's list stands when they can't be read and
// a model it leaves out is still refused by Command Code, as it was asked.
async function extraNow(key) {
try {
const v = hasExtra(await accountJSON("/alpha/billing/credits", key, waits.creditWait))
extraSeen.set(key, v)
return v
} catch {
return extraSeen.get(key) ?? false
}
}
function subscriptionNow(key, wait = waits.subWait) {
const seen = subsSeen.get(key) ?? {}
if (seen.sub && Date.now() - seen.at < SUB_KEEP) return Promise.resolve(seen.sub)
if (!seen.reading) {
seen.reading = subscriptionOf(key, 60_000).then((sub) => {
const now = subsSeen.get(key) ?? {}
subsSeen.set(key, sub ? { sub, at: Date.now() } : { sub: now.sub, at: now.at })
return sub
})
subsSeen.set(key, seen)
}
let timer
const late = new Promise((r) => (timer = setTimeout(() => r(seen.sub), wait)))
return Promise.race([seen.reading.then((sub) => sub ?? seen.sub), late]).finally(() => clearTimeout(timer))
}
// usage is the account's allowance, and the subscription read for it. The
// plan is said even while the credits can't be read (Command Code answers
// 503, "Couldn't verify your credit balance just now", at times), and the
// plan saved (saved: { id, plan }) stands for the subscription while it
// can't be read, or is slow to: then it is waited for a moment only.
async function usage(key, saved) {
const [read, credits] = await Promise.all([
subscriptionNow(key, saved ? 2_000 : waits.subWait),
accountJSON("/alpha/billing/credits", key, 12_000).then(
(c) => ({ c }),
(e) => ({ e }),
),
])
const sub = read ?? saved
let out = { windows: [] }
// a key whoami refuses too is signed out: said so, without the plan,
// which is no longer the key's to tell
if ((credits.e?.status === 401 || credits.e?.status === 403) && (await keyRefused(key))) return { out: { error: SIGNED_OUT, windows: [] }, gone: true }
if (credits.e) out.error = credits.e.status ? statusText(credits.e.status) : credits.e.message
else {
// billing/credits leaves the plan out: the month of credits the pool
// is made of is the subscription's plan's
const c = credits.c ?? {}
extraSeen.set(key, hasExtra(c))
out = allowanceOf({ ...c, credits: { ...(c.credits ?? {}), planId: c.credits?.planId || sub?.id || "" } }, sub?.until)
}
if (sub) {
out.plan = sub.plan
if (sub.until) out.until = sub.until
if (sub.renew) out.renew = sub.renew
}
return { out, read, clean: !credits.e }
}
// signedIn names a new key's account with whoami and reads its plan. Only
// whoami turning the key down (401, 403) fails: it has answered a key just
// made with a 500, so one that errs is asked again a few times, then
// passed over for the name Studio sent with the key.
async function signedIn(a) {
let me
for (let tries = 0; ; tries++) {
try {
me = (await accountJSON("/alpha/whoami", a.apiKey))?.user
break
} catch (e) {
if (e.status === 401 || e.status === 403) throw new Error("Command Code didn't take the new key")
if (tries >= 2) break
await new Promise((r) => setTimeout(r, 1000))
}
}
const who = me?.userName || a.userName || me?.email || me?.id || a.userId || "Command Code"
const sub = await subscriptionOf(a.apiKey, 30_000)
if (sub) subsSeen.set(a.apiKey, { sub, at: Date.now() })
const plan = sub?.plan ?? ""
return { who, plan, email: me?.email ?? "" }
}
// success is what a sign-in gives OpenCode: the key, and who it is.
async function success(a) {
const { who, plan } = await signedIn(a)
const metadata = { email: who }
if (a.userId) metadata.userId = a.userId
if (a.keyName) metadata.keyName = a.keyName
if (plan) metadata.plan = plan
return { type: "success", provider: ID, key: a.apiKey, metadata }
}
// ---- the browser sign-in --------------------------------------------------------
function page(ok, title, text) {
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c])
return `<!doctype html><meta charset="utf-8"><title>${esc(title)}</title>
<style>body{font:15px system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;margin:0;color:#222;background:#fafafa}
@media (prefers-color-scheme:dark){body{color:#eee;background:#161616}}main{text-align:center;max-width:28rem;padding:1rem}
.d{font-size:2rem;color:${ok ? "#2a9d5c" : "#c0392b"}}</style>
<main><div class="d">${ok ? "✓" : "✕"}</div><h1>${esc(title)}</h1><p>${esc(text)}</p></main>`
}
// browserSignIn is the CLI's: Studio asks the user to approve a key for
// this machine, then posts it (a form, or JSON from an older Studio) to
// the callback, which sends the browser on to a page saying how it went.
async function browserSignIn() {
const state = randomBytes(24).toString("base64url")
let status = { state: "waiting" }
let settle
const done = new Promise((r) => (settle = r))
const finish = (st, result) => {
if (status.state !== "waiting") return
status = st
settle(result)
}
const server = createServer(async (req, res) => {
const origin = ORIGINS.includes(req.headers.origin) ? req.headers.origin : ORIGINS[0]
res.setHeader("Access-Control-Allow-Origin", origin)
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
if (req.headers["access-control-request-private-network"] === "true") res.setHeader("Access-Control-Allow-Private-Network", "true")
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname
const html = (code, body) => {
res.writeHead(code, { "Content-Type": "text/html; charset=utf-8" })
res.end(body)
}
if (req.method === "OPTIONS") return res.writeHead(204).end()
if (path === "/callback/complete" && req.method === "GET") {
return status.state === "done"
? html(200, page(true, "You're signed in", `${status.who} is signed in. You can close this tab.`))
: html(200, page(false, "Sign-in didn't finish", status.error || "Start it again."))
}
if (path === "/cancel") {
finish({ state: "canceled" }, { type: "failed", error: "the sign-in was canceled" })
return res.writeHead(204).end()
}
if (path !== "/callback") return res.writeHead(404).end()
if (req.method !== "POST") return res.writeHead(405).end()
let raw = ""
for await (const chunk of req) {
raw += chunk
if (raw.length > 10_000) break
}
const isJSON = String(req.headers["content-type"] ?? "").trim().toLowerCase().startsWith("application/json")
let got = {}
if (isJSON) {
try {
got = JSON.parse(raw) ?? {}
} catch {}
} else got = Object.fromEntries(new URLSearchParams(raw))
const answer = (ok, msg) => {
if (isJSON) {
res.writeHead(ok ? 200 : 400, { "Content-Type": "application/json" })
return res.end(JSON.stringify({ success: ok, error: msg }))
}
res.writeHead(303, { Location: "/callback/complete?state=" + encodeURIComponent(got.state ?? ""), "Cache-Control": "no-store" })
res.end()
}
if (got.state !== state) return html(403, page(false, "This link isn't from this sign-in", "Start it again."))
if (status.state !== "waiting") return answer(false, "this sign-in is over")
if (got.error) {
const msg = got.error === "access_denied" ? "the sign-in was denied" : got.error_description || got.error
finish({ state: "failed", error: msg }, { type: "failed", error: msg })
return answer(false, msg)
}
if (!got.apiKey) return answer(false, "Command Code sent back no key")
try {
const r = await success({ apiKey: got.apiKey, userId: got.userId, userName: got.userName, keyName: got.keyName })
finish({ state: "done", who: r.metadata.email }, r)
answer(true, "")
} catch (e) {
finish({ state: "failed", error: e.message }, { type: "failed", error: e.message })
answer(false, e.message)
}
})
await new Promise((resolve, reject) => {
server.once("error", reject)
server.listen(0, "127.0.0.1", resolve)
})
const port = server.address().port
const timer = setTimeout(() => finish({ state: "failed", error: "the sign-in timed out" }, { type: "failed", error: "the sign-in timed out" }), SIGN_IN_TIMEOUT)
// the browser comes back for its page after the key: the server stays
// up a while for it
done.then(() => {
clearTimeout(timer)
setTimeout(() => server.close(), 10_000).unref?.()
})
const q = new URLSearchParams({ callback: `http://127.0.0.1:${port}/callback`, state, mode: "redirect" })
return {
url: `${STUDIO}/studio/auth/cli?${q}`,
instructions: "Approve the key for this machine in Command Code Studio.",
method: "auto",
callback: () => done,
}
}
// cliSignIn takes the account Command Code's CLI is signed in to, reading
// ~/.commandcode/auth.json and changing nothing.
async function cliSignIn() {
const path = join(homedir(), ".commandcode", "auth.json")
return {
url: "",
instructions: `Uses the account in ${path}.`,
method: "auto",
callback: async () => {
try {
const a = JSON.parse(await readFile(path, "utf8"))
if (!String(a?.apiKey ?? "").trim()) return { type: "failed", error: `Command Code's CLI isn't signed in: ${path} has no key` }
const r = await success(a)
r.metadata.cli = true
return r
} catch (e) {
return { type: "failed", error: e?.code === "ENOENT" ? "Command Code's CLI isn't signed in: run `commandcode login`" : e instanceof SyntaxError ? `${path} can't be read` : e.message }
}
},
}
}
// liveKey is the key an account has now: one taken from the CLI's sign-in
// reads the CLI's again, as a `commandcode login` since may have changed
// it, and keeps the one it was saved with while the CLI has none.
async function liveKey(auth) {
if (!auth?.metadata?.cli) return auth?.key
try {
const a = JSON.parse(await readFile(join(homedir(), ".commandcode", "auth.json"), "utf8"))
const k = String(a?.apiKey ?? "").trim()
if (k) return k
} catch {}
return auth.key
}
// ---- the Go plan: /alpha/generate ----------------------------------------------
const session = randomUUID() // one x-session-id for as long as this runs, as one CLI session has
const EFFORT_RANK = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]
// fitEffort is the level the model has nearest the one asked, the higher
// on a tie.
function fitEffort(want, levels) {
if (want === "ultra" && !levels.includes(want)) want = "max"
if (!levels.length || levels.includes(want)) return want
const at = EFFORT_RANK.indexOf(want)
if (at < 0) return want
let best = want
let dist = EFFORT_RANK.length
for (const l of levels) {
const i = EFFORT_RANK.indexOf(l)
if (i < 0 || l === "none") continue
const d = Math.abs(i - at)
if (d < dist || (d === dist && i > at)) [best, dist] = [l, d]
}
return best
}
const textOf = (c) =>
typeof c === "string" ? c : Array.isArray(c) ? c.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : ""
function imagePart(p) {
const u = typeof p.image_url === "string" ? p.image_url : p.image_url?.url
if (!u) return null
const m = /^data:([^;,]+)[;,]/.exec(u)
return m ? { type: "image", image: u, mimeType: m[1] } : { type: "image", image: u }
}
function parseArgs(s) {
if (s && typeof s === "object") return s
try {
const v = JSON.parse(s || "{}")
return v && typeof v === "object" ? v : {}
} catch {
return {}
}
}
// WORK_DIR and WORK_GIT are where magpie tells a plugin's fetch the agent
// that asked works: the folder, percent-encoded, and "yes" or "no" for a
// git repository, each left off when magpie doesn't know it (magpie's
// provider.WorkDirHeader and WorkGitHeader). They are for the plugin, not
// the vendor.
const WORK_DIR = "x-magpie-work-dir"
const WORK_GIT = "x-magpie-work-git"
// workPlace takes WORK_DIR and WORK_GIT off h: {dir?, git?}, what magpie
// said of where the agent works.
function workPlace(h) {
const place = {}
const dir = h.get(WORK_DIR)
if (dir) {
try {
place.dir = decodeURIComponent(dir)
} catch {}
}
const git = h.get(WORK_GIT)
if (place.dir && (git === "yes" || git === "no")) place.git = git
h.delete(WORK_DIR)
h.delete(WORK_GIT)
return place
}
// generateBody is a chat completion request as the CLI asks
// /alpha/generate, with no structure, branch or status behind it. The
// working directory and whether it is a git repository are the agent's as
// magpie tells them (place, yetone magpie-community/plugins#86); untold,
// the CLI's config outside a git repository, in the user's home.
function generateBody(chat, place = {}) {
const system = []
const messages = []
const names = {} // a tool call's id → its tool
let results = null
const flush = () => {
if (results?.length) messages.push({ role: "tool", content: results })
results = null
}
for (const m of chat.messages ?? []) {
if (m.role === "system" || m.role === "developer") {
system.push(textOf(m.content))
continue
}
if (m.role === "tool") {
results ??= []
results.push({
type: "tool-result",
toolCallId: m.tool_call_id,
toolName: names[m.tool_call_id] || "unknown",
output: { type: "text", value: textOf(m.content) },
})
continue
}
flush()
if (m.role === "assistant") {
const parts = []
const thought = m.reasoning_content ?? m.reasoning
if (typeof thought === "string" && thought) parts.push({ type: "reasoning", text: thought })
const t = textOf(m.content)
if (t) parts.push({ type: "text", text: t })
for (const c of m.tool_calls ?? []) {
names[c.id] = c.function?.name
parts.push({ type: "tool-call", toolCallId: c.id, toolName: c.function?.name, input: parseArgs(c.function?.arguments) })
}
if (parts.length) messages.push({ role: "assistant", content: parts })
continue
}
const parts = []
if (typeof m.content === "string") {
if (m.content) parts.push({ type: "text", text: m.content })
} else
for (const p of m.content ?? []) {
if (p?.type === "text" && p.text) parts.push({ type: "text", text: p.text })
else if (p?.type === "image_url") {
const im = imagePart(p)
if (im) parts.push(im)
}
}
if (parts.length) messages.push({ role: "user", content: parts })
}
flush()
const tools = (chat.tools ?? [])
.filter((t) => t?.type === "function" && t.function?.name)
.map((t) => ({
name: t.function.name,
description: t.function.description ?? "",
input_schema: t.function.parameters ?? { type: "object", properties: {} },
}))
const params = {
model: chat.model,
messages,
tools,
system: system.join("\n\n"),
// within what Command Code takes: more is refused, the whole request
// with it
max_tokens: Math.min(chat.max_completion_tokens || chat.max_tokens || MAX_TOKENS, MAX_OUTPUT),
stream: true,
}
if (typeof chat.temperature === "number") params.temperature = chat.temperature
// off ("none", or a client's "off") is not known to be taken here: it
// is sent as nothing, the model's own default
const effort = chat.reasoning_effort
if (effort && effort !== "none" && effort !== "off") params.reasoning_effort = fitEffort(effort, GO_EFFORTS[chat.model] ?? [])
const d = new Date()
const date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`
return {
config: {