-
Notifications
You must be signed in to change notification settings - Fork 363
Expand file tree
/
Copy pathmodel_cli.go
More file actions
1228 lines (1188 loc) · 50.7 KB
/
Copy pathmodel_cli.go
File metadata and controls
1228 lines (1188 loc) · 50.7 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
package main
import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/settings"
)
// A model's name and reasoning levels, from the terminal: magpie model.
const modelUsage = `usage:
magpie model name <provider/model> the name the model goes by
magpie model name <provider/model> <name> name it so everywhere: in magpie, in the gateway's
model list, and in the lists magpie writes into the agents
magpie model name <provider/model> --reset give it back its own name
magpie model efforts <provider/model> the reasoning levels it offers, and those it has
magpie model efforts <provider/model> <l>,<l> offer only these of them, e.g. low,medium,high
magpie model efforts <provider/model> --reset offer every level it has again
magpie model price <provider/model> what the model costs you, and what you said it costs
magpie model price <provider/model> <in>,<out>,<cache read>,<cache write>
say what it costs, in USD per million tokens, all four parts as
0.12,1.20,0.01,0.15; 0 is a model served for nothing, which is
a price, not the absence of one; a fifth part is a 1-hour cache
write's (not given: a Claude model's is 2× input, any other's the 4th)
magpie model price <provider/model> <in>,<out>,<cache read>,<cache write> --tier 272k <in>,<out>,<cr>,<cw>
and what a request whose input, cached tokens included, is over
272K costs, the whole request; --tier may be given again
magpie model price <provider/model> --reset take your price off this model
magpie model price <model> <in>,<out>,<cache read>,<cache write>
what the model costs from any provider you have not priced it
for, kept as '*/<model>': for usage whose provider is gone, or
a model models.dev doesn't price, e.g. gemini-3-pro-preview
magpie model prices the models you priced
magpie model context <provider/model> how long a request it takes, and what you said it takes
magpie model context <provider/model> <n> say how long, as 200000 or 1m; '<provider>/*' is every model
magpie model context <provider/model> --reset take your limit off this model
magpie model output <provider/model> the most a reply of it may hold, and what you said
magpie model output <provider/model> <n> say the most, as 128000 or 128k; '<provider>/*' is every model
magpie model output <provider/model> --reset take your limit off this model
magpie model wire <provider/model> the name the vendor is asked for, and the one you gave
magpie model wire <provider/model> <name> ask for the model by this name, for a relay that serves it
magpie model wire '<provider>/*' <name> ask for every model of that provider by this name; a * in
it is the model, so vendor-c/* asks for model-3 as
vendor-c/model-3. Quote it: a shell reads a bare * as a glob
magpie model wire <provider/model> --reset ask for it by the name magpie knows it by again, under an
id of its own for a provider that has since been deleted,
and says when there was no name of its own to take away
magpie model wires the names your vendors are asked for models by
magpie model names the models you named or narrowed
magpie model suffix [on|own|off] whether the agents' lists name each model with its provider
(or "routing group") after it: on, as by default, "Sol · OpenAI";
own, a name you gave a model just as you wrote it, "Opus 5.5",
the others as on; off, "Sol" alone — but two a list would name
the same keep it
magpie model compact [on|off|<size>] whether Codex and Claude Code compact a long conversation at
272K: on, as by default, for a model of a longer window (in
Claude Code, a Claude model runs to its own); off, at the
model's whole window, 1M for a [1m] one; a size such as 500k
compacts there instead. The app's Settings → Long
conversations is the same switch; a provider's own Compact
at (its editor) comes before it
Each is looked for in this order: this model, then <provider id>/*, then the provider's own
list, then models.dev. --reset removes only the first, and says so when a <provider id>/* value
still applies. A price comes from '*/<model>' after <provider id>/* and before any list price.
Only what agents are shown changes with a name or levels: they still pick the model, and
requests still reach it, as <provider/model>. The same model from another provider keeps
its own name, levels and limits.
A price is one provider's tariff for one model, not the model's own: it changes what the
usage and session totals report, and nothing an agent can see, and it is saved without
rewriting the model lists in the agents' own files.
e.g. magpie model name claude/claude-opus-5-5 "Opus 5.5"
magpie model efforts openai/gpt-6 low,medium,high
magpie model price relay-a/gpt-5.5 0.12,0.60,0.01,0.15
magpie model context relay-a/claude-opus-5-5 1m`
func modelCmd(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
switch args[0] {
case "names", "ls", "list":
return modelNames()
case "name", "rename":
return modelName(args[1:])
case "efforts", "effort", "levels":
return modelEfforts(args[1:])
case "price", "cost":
return modelPrice(args[1:])
case "prices":
return modelPrices()
case "wires":
return modelWires()
case "suffix", "suffixes":
return modelSuffix(args[1:])
case "compact", "full-context":
return modelCompact(args[1:])
case "context", "ctx":
return modelContext(args[1:])
case "output", "max-output":
return modelOutput(args[1:])
case "wire", "upstream":
return modelWire(args[1:])
case "help", "-h", "--help":
fmt.Println(modelUsage)
return nil
}
return fmt.Errorf("unknown command %q\n\n%s", args[0], modelUsage)
}
// modelRef is the provider and model a typed provider/model names.
func modelRef(s string) (*provider.Provider, string, error) {
pid, model, err := splitModelRef(s)
if err != nil {
return nil, "", err
}
p, err := provider.Find(pid)
if err != nil {
return nil, "", err
}
return p, model, nil
}
// splitModelRef is "provider/model" as the id before the slash and the model
// after it, without asking for the provider. A ref the user must still be
// able to name is one whose provider may be gone: the key a price is
// stored at outlives the provider it was set for.
func splitModelRef(s string) (string, string, error) {
ref := strings.TrimPrefix(strings.TrimSpace(s), "magpie/")
pid, model, ok := strings.Cut(ref, "/")
if !ok || model == "" {
return "", "", fmt.Errorf("name a model as provider/model, not %q (magpie models lists them)", s)
}
return pid, model, nil
}
func isReset(args []string) bool {
return len(args) == 1 && slices.Contains([]string{"--reset", "-r", "--default", "default", "reset"}, args[0])
}
func modelName(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
p, model, err := modelRef(args[0])
if err != nil {
return err
}
own := model
for _, m := range p.Available() {
if m.ID == model && m.Name != "" {
own = m.Name
break
}
}
id := p.ID + "/" + model
rest := args[1:]
if len(rest) == 0 {
if n, ok := provider.ModelName(p.ID, model); ok {
fmt.Println(bold.Render(n), muted.Render("· "+id+" · its own name is "+own))
} else {
fmt.Println(bold.Render(own), muted.Render("· "+id+" · its own name"))
}
return nil
}
name := strings.Join(rest, " ")
if isReset(rest) {
name = ""
}
if err := provider.SetModelName(id, name); err != nil {
return err
}
if name == "" {
fmt.Println(green.Render("✓"), id, muted.Render("is called"), bold.Render(own), muted.Render("again"))
} else {
fmt.Println(green.Render("✓"), id, muted.Render("is called"), bold.Render(strings.Join(strings.Fields(name), " ")))
}
return nil
}
func modelEfforts(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
p, model, err := modelRef(args[0])
if err != nil {
return err
}
id := p.ID + "/" + model
all := p.Efforts(model)
rest := args[1:]
given := len(p.Known(model)) == 0 // levels aren't known: those it has are the user's
if len(rest) == 0 {
if len(all) == 0 {
fmt.Println(muted.Render(id+" has no reasoning levels known"), faint.Render("· give it some of "+strings.Join(provider.Levels, ", ")))
return nil
}
if given {
fmt.Println(bold.Render(strings.Join(all, " ")), muted.Render("· given it · --reset takes them away"))
return nil
}
kept, narrowed := p.ModelEfforts()[model]
for _, e := range all {
if !narrowed || slices.Contains(kept, e) {
fmt.Print(bold.Render(e), " ")
} else {
fmt.Print(faint.Render(e), " ")
}
}
if narrowed {
fmt.Println(muted.Render("· only the bold ones are offered · --reset offers them all"))
} else {
fmt.Println(muted.Render("· all offered"))
}
return nil
}
var levels []string
if !isReset(rest) {
for _, a := range rest {
for _, l := range strings.FieldsFunc(a, func(r rune) bool { return r == ',' || r == ' ' || r == '/' }) {
levels = append(levels, l)
}
}
}
if err := provider.SetModelEfforts(id, levels); err != nil {
return err
}
if given {
if all = p.Efforts(model); len(all) > 0 {
fmt.Println(green.Render("✓"), id, muted.Render("offers"), bold.Render(strings.Join(all, ", ")))
} else {
fmt.Println(green.Render("✓"), id, muted.Render("has no reasoning levels again"))
}
return nil
}
if kept, ok := p.ModelEfforts()[model]; ok {
fmt.Println(green.Render("✓"), id, muted.Render("offers"), bold.Render(strings.Join(kept, ", ")))
} else {
fmt.Println(green.Render("✓"), id, muted.Render("offers every level it has"), faint.Render(strings.Join(all, ", ")))
}
return nil
}
// modelPrice shows what a provider's model is counted at, or sets what the
// user says it costs in USD per million tokens. All four parts are given: a
// price is a cost report, and one missing a part would understate it.
func modelPrice(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
rest := args[1:]
// --reset comes before the provider is looked for, and works off the
// key the price is stored at: a price outlives the provider it was set
// for, and one whose provider is gone is still listed by `magpie model
// prices` and still counted, so a reset that resolved the provider
// first could not take it away — and would take another provider's
// price away instead where the gone one's name has been given to one.
ref := args[0]
// a model named with no provider, or as */model, is that model from any
// provider: what prices a session's model whose provider is gone, or one
// that never went through magpie
every := false
if r := strings.TrimPrefix(strings.TrimSpace(ref), "magpie/"); r != "" && !strings.Contains(r, "/") {
ref, every = settings.AnyProvider+r, true
} else {
every = strings.HasPrefix(r, settings.AnyProvider)
}
if isReset(rest) {
return resetModelPrice(ref)
}
if every {
return anyModelPrice(ref, rest)
}
p, model, err := modelRef(ref)
if err != nil {
return err
}
id := p.ID + "/" + model
if len(rest) == 0 {
pr, ok := provider.EffectivePrice(p.ID, model)
if !ok {
fmt.Println(muted.Render(id), faint.Render("· no price is known for it"))
return nil
}
fmt.Println(bold.Render(perMillion(pr)), muted.Render("· "+id))
for _, l := range priceDetail(pr) {
fmt.Println(faint.Render(l))
}
switch priceFrom(settings.Load(), p.ID, model) {
case "model":
fmt.Println(faint.Render(" · what you said this model costs · --reset takes that away"))
case "provider":
fmt.Println(faint.Render(" · what you said every model of this provider costs · magpie model price " +
typedRef(p.ID+"/*") + " --reset takes that away"))
case "any":
fmt.Println(faint.Render(" · what you said this model costs from any provider · magpie model price " +
typedRef(provider.AnyPriceKey(model)) + " --reset takes that away"))
case "ignored":
fmt.Println(faint.Render(" · a price you gave is not usable and is ignored"))
default:
fmt.Println(faint.Render(" · what its provider lists, else its maker's on models.dev · magpie model price " +
typedRef(id) + " <in>,<out>,<cache read>,<cache write> to change it"))
}
return nil
}
pr, err := parsePrice(rest)
if err != nil {
return err
}
if err := provider.SetModelPrice(id, &pr); err != nil {
return err
}
fmt.Println(green.Render("✓"), id, muted.Render("costs"), bold.Render(perMillion(pr)))
for _, t := range pr.Tiers {
fmt.Println(faint.Render(fmt.Sprintf(" · and $%.4g/$%.4g in/out the whole request over %s input tokens", t.Input, t.Output, tokenSize(t.Above))))
}
return nil
}
// parsePrice is a price as typed, in USD per million tokens: its four
// parts, a 1-hour cache write's after them if given, and then each
// "--tier <size>" with the parts of what a request whose input is over that
// size costs.
func parsePrice(rest []string) (catalog.Price, error) {
var groups [][]string
var sizes []string
cur := []string{}
for i := 0; i < len(rest); i++ {
a := rest[i]
size, isTier := strings.CutPrefix(a, "--tier=")
if !isTier && a == "--tier" {
if i+1 >= len(rest) {
return catalog.Price{}, fmt.Errorf("--tier takes the input size it starts over, like --tier 272k 20,75,2,25")
}
isTier, size = true, rest[i+1]
i++
}
if isTier {
groups, sizes, cur = append(groups, cur), append(sizes, size), []string{}
continue
}
cur = append(cur, a)
}
groups = append(groups, cur)
var out catalog.Price
for g, args := range groups {
var nums []float64
for _, a := range args {
for _, f := range strings.FieldsFunc(a, func(r rune) bool { return r == ',' || r == ' ' || r == '/' }) {
v, err := strconv.ParseFloat(f, 64)
if err != nil {
return catalog.Price{}, fmt.Errorf("a price is numbers in USD per million tokens, like 0.12,1.20,0.01,0.15, not %q", f)
}
nums = append(nums, v)
}
}
if len(nums) != 4 && len(nums) != 5 {
what := "the price"
if g > 0 {
what = "the tier over " + sizes[g-1]
}
return catalog.Price{}, fmt.Errorf("give all four parts of %s, input,output,cache read,cache write (and a 1-hour cache write after them if you like) — %d given", what, len(nums))
}
var oneHour float64
if len(nums) == 5 {
oneHour = nums[4]
}
if g == 0 {
out = catalog.Price{Input: nums[0], Output: nums[1], CacheRead: nums[2], CacheWrite: nums[3], CacheWrite1h: oneHour}
continue
}
above, err := provider.ParseTokens(sizes[g-1])
if err != nil || above <= 0 {
return catalog.Price{}, fmt.Errorf("--tier takes the input size it starts over, like 272k or 200000, not %q", sizes[g-1])
}
out.Tiers = append(out.Tiers, catalog.Tier{Above: above, Input: nums[0], Output: nums[1], CacheRead: nums[2], CacheWrite: nums[3], CacheWrite1h: oneHour})
}
slices.SortFunc(out.Tiers, func(a, b catalog.Tier) int { return a.Above - b.Above })
return out, nil
}
// priceDetail is the parts of a price after the two perMillion gives: its
// cache's, and what a request over each of its sizes costs.
// A 1-hour cache write is shown only where the price has one — a Claude
// model's, or one given — not one no vendor bills (PAMI on Discord).
func priceDetail(p catalog.Price) []string {
cache := " · cache read " + money(p.CacheRead) + ", cache write " + money(p.CacheWrite)
if p.CacheWrite1h > 0 {
cache += " (5 min), " + money(p.CacheWrite1h) + " (1 hour)"
}
lines := []string{cache}
for _, t := range p.Tiers {
lines = append(lines, " · over "+tokenSize(t.Above)+" input tokens, the whole request: "+
fmt.Sprintf("$%.4g/$%.4g in/out", t.Input, t.Output)+", cache "+money(t.CacheRead)+"/"+money(t.CacheWrite))
}
return lines
}
// tokenSize is a number of tokens as --tier takes it: 272K, 1M.
func tokenSize(n int) string {
switch {
case n >= 1e6 && n%1e6 == 0:
return strconv.Itoa(n/1e6) + "M"
case n >= 1e3 && n%1e3 == 0:
return strconv.Itoa(n/1e3) + "K"
}
return strconv.Itoa(n)
}
// anyModelPrice shows or sets what a model costs from any provider (*/model):
// the price a session's model is counted at when no price of a provider's
// own applies — its provider gone, the model no longer listed by it, or a
// model models.dev doesn't price.
func anyModelPrice(ref string, rest []string) error {
key := provider.AnyPriceKey(strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(ref), "magpie/"), settings.AnyProvider))
if key == settings.AnyProvider || key == settings.AnyProvider+"*" {
return fmt.Errorf("name one model, such as %s", typedRef("*/claude-opus-4.6"))
}
if len(rest) == 0 {
if pr, ok := statedPrice(settings.Load().ModelPrices, key); ok {
catalog.OneHourFor(strings.TrimPrefix(key, settings.AnyProvider), &pr)
fmt.Println(bold.Render(perMillion(pr)), muted.Render("· "+key))
for _, l := range priceDetail(pr) {
fmt.Println(faint.Render(l))
}
fmt.Println(faint.Render(" · what you said this model costs from any provider · --reset takes that away"))
return nil
}
fmt.Println(muted.Render(key), faint.Render("· no price of yours · magpie model price "+typedRef(key)+" <in>,<out>,<cache read>,<cache write>"))
return nil
}
pr, err := parsePrice(rest)
if err != nil {
return err
}
if err := provider.SetModelPrice(key, &pr); err != nil {
return err
}
fmt.Println(green.Render("✓"), key, muted.Render("costs"), bold.Render(perMillion(pr)),
faint.Render("from any provider you have not priced it for"))
return nil
}
// resetModelPrice takes a price off and says what the model is counted at
// now. The key it works off is the one the price is stored at, which is not
// always the one that was typed: where nothing is stored under the spelling
// it is the id the provider has now, which is where a price set through a
// display name is kept. provider.PriceKey is what decides that, and it
// decides it the same way for both callers, so this cannot resolve one way
// and the removal another.
//
// A key with no price under it is an error rather than a tick: there was
// nothing to take away, and a user who mistyped an id, or who is priced only
// through a provider-wide price, has to be told so rather than left with a
// ✓ over a price that is still counted.
func resetModelPrice(ref string) error {
pid, model, err := splitModelRef(ref)
if err != nil {
return err
}
key, there := provider.PriceKey(pid, model)
dropped, err := provider.DropModelPrice(key)
if err != nil {
return err
}
s := settings.Load()
wid, _, _ := strings.Cut(key, "/")
if strings.HasPrefix(key, settings.AnyProvider) {
if !dropped {
return fmt.Errorf("%s has no price of yours to reset — magpie model prices lists the ones you set", key)
}
fmt.Println(green.Render("✓"), key,
muted.Render("no longer has a price of yours; it is costed at what its provider lists, or its maker's on models.dev"))
return nil
}
if !dropped {
if _, wide := statedPrice(s.ModelPrices, wid+"/*"); wide {
return fmt.Errorf("%s has no price of its own; what it costs is what you set for every model of this provider, %s/*, which magpie model price %s --reset takes away",
key, wid, typedRef(wid+"/*"))
}
return fmt.Errorf("%s has no price of its own to reset — magpie model prices lists the ones you set", key)
}
if !there {
fmt.Println(green.Render("✓"), key,
muted.Render("no longer has a price of its own; its provider is gone, so it is costed at its maker's, if models.dev lists one"))
return nil
}
if _, wide := statedPrice(s.ModelPrices, wid+"/*"); wide {
fmt.Println(green.Render("✓"), key,
muted.Render("no longer has a price of its own; it still costs what you set for every model of this provider"))
return nil
}
fmt.Println(green.Render("✓"), key,
muted.Render("no longer has a price of its own; it is costed at what its provider lists, or its maker's"))
return nil
}
// perMillion is a price as the two numbers a reader wants first.
func perMillion(p catalog.Price) string {
return fmt.Sprintf("$%.4g/$%.4g per 1M in/out", p.Input, p.Output)
}
// money is one part of a per-million price as a price tuple writes it.
func money(v float64) string { return fmt.Sprintf("$%.4g", v) }
// statedPrice is the usable price a "<provider id>/<model id>" key holds, if
// it holds one at all. A key that is there with a part missing is not one:
// it would bill the rest of a call at zero.
func statedPrice(prices map[string]settings.ModelPrice, key string) (catalog.Price, bool) {
m, ok := prices[key]
if !ok {
return catalog.Price{}, false
}
p, bad := m.Price()
return p, bad == ""
}
// priceFrom says where a model's effective price came from: the price given
// for it, the one given for every model of its provider, a price that is
// there but unusable, or neither — which leaves the provider's own list price
// and then its maker's. The order is the one EffectivePrice looks in.
func priceFrom(s settings.Settings, providerID, model string) string {
if _, ok := statedPrice(s.ModelPrices, providerID+"/"+model); ok {
return "model"
}
if _, ok := statedPrice(s.ModelPrices, providerID+"/*"); ok {
return "provider"
}
if _, ok := statedPrice(s.ModelPrices, provider.AnyPriceKey(model)); ok {
return "any"
}
if _, given := s.ModelPrices[providerID+"/"+model]; given {
return "ignored"
}
if _, given := s.ModelPrices[providerID+"/*"]; given {
return "ignored"
}
return ""
}
// modelPrices lists what the user has said models cost, and which provider
// and model each is for.
func modelPrices() error {
prices := settings.Load().ModelPrices
if len(prices) == 0 {
fmt.Println(muted.Render("no model is priced by you yet"))
fmt.Println(faint.Render("magpie model price <provider/model> <in>,<out>,<cache read>,<cache write>"))
return nil
}
for _, id := range slices.Sorted(maps.Keys(prices)) {
p, bad := prices[id].Price()
if bad != "" {
fmt.Println(bold.Render(id), muted.Render("· no "+bad+" price given, and ignored"))
continue
}
fmt.Println(bold.Render(id), muted.Render("· "+perMillion(p)+
" · cache "+money(p.CacheRead)+"/"+money(p.CacheWrite)))
for _, t := range p.Tiers {
fmt.Println(faint.Render(fmt.Sprintf(" · over %s input tokens $%.4g/$%.4g in/out · cache %s/%s", tokenSize(t.Above), t.Input, t.Output, money(t.CacheRead), money(t.CacheWrite))))
}
}
return nil
}
// modelSuffix says whether the agents' lists name models with their
// providers' after them, all but the names the user gave (#92), or none
// (#335), or sets it.
func modelSuffix(args []string) error {
if len(args) == 0 {
switch provider.SuffixMode() {
case provider.SuffixOff:
fmt.Println("off", muted.Render("· agents' lists name a model alone, \"Sol\" · magpie model suffix on|own"))
case provider.SuffixOwn:
fmt.Println("own", muted.Render("· a name you gave a model just as you wrote it, \"Opus 5.5\"; the others \"Sol · OpenAI\" · magpie model suffix on|off"))
default:
fmt.Println("on", muted.Render("· agents' lists name a model with its provider, \"Sol · OpenAI\" · magpie model suffix own|off"))
}
return nil
}
mode := provider.SuffixOn
switch strings.ToLower(args[0]) {
case "on", "yes", "true":
case "own", "mine":
mode = provider.SuffixOwn
case "off", "no", "false":
mode = provider.SuffixOff
default:
return fmt.Errorf("magpie model suffix on|own|off, not %q", args[0])
}
if err := provider.SetSuffixMode(mode); err != nil {
return err
}
switch mode {
case provider.SuffixOff:
fmt.Println(green.Render("✓"), "agents' lists name each model alone", muted.Render("· two that would read the same keep their provider's"))
case provider.SuffixOwn:
fmt.Println(green.Render("✓"), "agents' lists name a model you named just as you wrote it", muted.Render("· the others with their provider"))
default:
fmt.Println(green.Render("✓"), "agents' lists name each model with its provider again")
}
return nil
}
// modelCompact says whether long conversations are compacted at the
// working window (settings.WorkingWindow) or the size the user gave, or
// sets it: off is the app's Full window, every model run to its whole
// window; a size is compacting there (#876).
func modelCompact(args []string) error {
if len(args) == 0 {
if s := settings.Load(); s.FullContext {
fmt.Println("off", muted.Render("· Codex and Claude Code run a conversation to the model's whole window · magpie model compact on"))
} else {
fmt.Println("on", muted.Render(fmt.Sprintf("· Codex and Claude Code compact at %dK on a longer window; in Claude Code a Claude model runs to its own · magpie model compact off", s.Compact()/1000)))
}
return nil
}
var full bool
switch strings.ToLower(args[0]) {
case "on", "yes", "true":
case "off", "no", "false", "full":
full = true
default:
n, err := provider.ParseTokens(args[0])
if err != nil || n <= 0 {
return fmt.Errorf("magpie model compact on|off|<size such as 500k>, not %q", args[0])
}
if err := provider.SetCompactAt(n); err != nil {
return err
}
fmt.Println(green.Render("✓"), fmt.Sprintf("Codex and Claude Code compact at %dK on a longer window", settings.Load().Compact()/1000))
return nil
}
if err := provider.SetFullContext(full); err != nil {
return err
}
if full {
fmt.Println(green.Render("✓"), "Codex and Claude Code run a conversation to the model's whole window", muted.Render("· every turn sends all of it"))
} else {
fmt.Println(green.Render("✓"), fmt.Sprintf("Codex and Claude Code compact at %dK again", settings.Load().Compact()/1000))
}
return nil
}
func modelNames() error {
s := settings.Load()
keys := slices.Sorted(maps.Keys(s.ModelNames))
for k := range s.ModelEfforts {
if !slices.Contains(keys, k) {
keys = append(keys, k)
}
}
slices.Sort(keys)
if len(keys) == 0 {
fmt.Println(muted.Render("no model is named or narrowed yet · magpie model name <provider/model> <name>"))
return nil
}
w := 0
for _, k := range keys {
w = max(w, len(k))
}
for _, k := range keys {
line := " " + pad(k, w)
if n := s.ModelNames[k]; n != "" {
line += " " + n
}
if es := s.ModelEfforts[k]; len(es) > 0 {
line += " " + muted.Render(strings.Join(es, "/"))
}
fmt.Println(line)
}
return nil
}
// modelWires lists the names the user gave the vendors their models are asked
// for, and which provider and model each is for. It is `magpie model prices`
// for the wire names: those are what a provider is asked for, and no other
// command says what is kept for a provider as a whole, so a name given for
// every model of one was written with no way to read it back.
//
// A name kept for a provider that has since been deleted is listed like any
// other: the name outlives the provider it was given for, and is in force for
// whichever provider takes that id next, so it is read off the file rather
// than off a provider that may not be there.
//
// A name that is only whitespace is no name and is not listed, as it is
// nowhere else a name is read: UpstreamNames leaves it out and a request
// under it goes out by the name magpie knows the model by. Listing it would
// print a line with nothing on it for a name no vendor is asked for, which
// is the one thing a listing read back off the file must not say.
func modelWires() error {
wires := settings.Load().ModelWires
rows := make([][2]string, 0, len(wires))
w := 0
for _, k := range slices.Sorted(maps.Keys(wires)) {
if n := strings.TrimSpace(wires[k]); n != "" {
rows = append(rows, [2]string{k, n})
w = max(w, len(k))
}
}
if len(rows) == 0 {
fmt.Println(muted.Render("no vendor is asked for a model by another name yet · magpie model wire <provider/model> <name>"))
return nil
}
for _, r := range rows {
fmt.Println(" " + pad(r[0], w) + " " + r[1])
}
return nil
}
func modelContext(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
p, model, err := modelRef(args[0])
if err != nil {
return err
}
id := p.ID + "/" + model
rest := args[1:]
if len(rest) == 0 {
if model == "*" {
// the wildcard is not a model of its own: it is the one value
// given to every model of the provider
n := p.Contexts["*"]
fmt.Println(bold.Render(tokenCount(n)), muted.Render("· "+id+" · every model of "+p.ID))
if n > 0 {
fmt.Println(faint.Render(" · what you said every model of this provider takes, unless that model has a window of its own"))
} else {
fmt.Println(faint.Render(" · what the vendor's list and models.dev say · magpie model context " + typedRef(id) + " <tokens> to say one for all of them"))
}
return nil
}
e, ok := provider.ServedEntryOf(id)
if !ok {
fmt.Println(muted.Render(id), faint.Render("· is not a model this provider serves"))
return nil
}
fmt.Println(bold.Render(tokenCount(e.Context)), muted.Render("· "+id))
switch {
case p.Contexts[model] > 0 && p.Contexts["*"] > 0:
// the model's own window is what it takes, and --reset takes
// it to the one its provider gives every model, not to the
// vendor's list
fmt.Println(faint.Render(" · what you said it takes · --reset goes back to the one you set for every model of this provider"))
case p.Contexts[model] > 0:
fmt.Println(faint.Render(" · what you said it takes · --reset goes back to the vendor's list"))
case p.Contexts["*"] > 0:
fmt.Println(faint.Render(" · what you set for every model of this provider"))
default:
fmt.Println(faint.Render(" · what the vendor's list and models.dev say · magpie model context " + typedRef(id) + " <tokens> to change it"))
}
return nil
}
if isReset(rest) {
dropped, err := provider.DropContext(*p, model)
if err != nil {
return err
}
// a window that was never set is one a --reset did not take off,
// and a ✓ over it says the model had one; where what the model
// takes now is the provider's own every-model window, that is the
// command the user is after
if !dropped {
if p.Contexts["*"] > 0 {
return fmt.Errorf("%s has no window of its own; what every model of this provider takes is what you set for %s, which %s takes away",
id, p.ID+"/*", resetCommand("context", p.ID+"/*"))
}
if _, served := provider.ServedEntryOf(id); served {
return fmt.Errorf("%s has no window of its own to reset — magpie model context %s says what it takes now", id, typedRef(id))
}
return fmt.Errorf("%s has no window of its own to reset", id)
}
fmt.Println(green.Render("✓"), id, muted.Render("has no window of your own · magpie model context "+typedRef(id)+" says what it takes now"))
return nil
}
n, err := parseTokens(rest[0])
if err != nil {
return fmt.Errorf("a window is a number of tokens, like 200000 or 1m, not %q", rest[0])
}
if n == 0 {
return fmt.Errorf("0 tokens is no window at all — %s takes the one you set off %s",
resetCommand("context", id), resetTakesOff(p, model))
}
if err := provider.SetContext(*p, model, n); err != nil {
return err
}
fmt.Println(green.Render("✓"), id, muted.Render("takes"), bold.Render(tokenCount(n)))
return nil
}
func modelOutput(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
rest := args[1:]
// A --reset is the one thing here that does not go looking for the
// provider first. A reply limit outlives the provider it was set for,
// and a provider can be deleted: the entry is then left in the settings
// answering for a model nothing serves, and refusing the ref would
// leave the user no way to take it off but the file itself.
if isReset(rest) {
key, dropped, err := provider.DropModelOutput(args[0])
if err != nil {
return err
}
// a limit that was never set is one a --reset did not take off,
// and a ✓ over it says the model had one; where what the model
// answers with now is the provider's own every-model limit, that is
// the command the user is after
if !dropped {
wid, _, _ := strings.Cut(key, "/")
if settings.Load().ModelOutputs[wid+"/*"] > 0 {
return fmt.Errorf("%s has no reply limit of its own; what every model of this provider answers with is what you set for %s, which %s takes away",
key, wid+"/*", resetCommand("output", wid+"/*"))
}
if _, served := provider.ServedEntryOf(key); served {
return fmt.Errorf("%s has no reply limit of its own to reset — magpie model output %s says what it answers with now", key, typedRef(key))
}
return fmt.Errorf("%s has no reply limit of its own to reset", key)
}
// the pointer to the query is only worth giving where the query
// reads the entry that was just cleared: with nothing serving that
// id there is no limit left in force to report, and where another
// provider answers to the spelling the query would read that
// provider's model instead of the one taken off here
line := "has no reply limit of your own"
if clearedEntryIs(key) {
line += " · magpie model output " + typedRef(key) + " says what it answers with now"
}
fmt.Println(green.Render("✓"), key, muted.Render(line))
return nil
}
p, model, err := modelRef(args[0])
if err != nil {
return err
}
id := p.ID + "/" + model
if len(rest) == 0 {
s := settings.Load()
if model == "*" {
// the wildcard is not a model of its own: it is the one limit
// given to every model of the provider
n := s.ModelOutputs[id]
fmt.Println(bold.Render(tokenCount(n)), muted.Render("· "+id+" · every model of "+p.ID))
if n > 0 {
fmt.Println(faint.Render(" · what you said every model of this provider answers with, unless that model has a limit of its own"))
} else {
fmt.Println(faint.Render(" · what the vendor's list and models.dev say · magpie model output " + typedRef(id) + " <tokens> to say one for all of them"))
}
return nil
}
e, ok := provider.ServedEntryOf(id)
if !ok {
fmt.Println(muted.Render(id), faint.Render("· is not a model this provider serves"))
return nil
}
fmt.Println(bold.Render(tokenCount(e.Output)), muted.Render("· "+id))
switch {
case s.ModelOutputs[id] > 0 && s.ModelOutputs[p.ID+"/*"] > 0:
// the model's own limit is what it answers with, and --reset
// takes it to the one its provider gives every model, not to
// the vendor's list
fmt.Println(faint.Render(" · what you said it answers with · --reset goes back to the one you set for every model of this provider"))
case s.ModelOutputs[id] > 0:
fmt.Println(faint.Render(" · what you said it answers with · --reset goes back to the vendor's list"))
case s.ModelOutputs[p.ID+"/*"] > 0:
fmt.Println(faint.Render(" · what you set for every model of this provider"))
default:
fmt.Println(faint.Render(" · what the vendor's list and models.dev say · magpie model output " + typedRef(id) + " <tokens> to change it"))
}
return nil
}
n, err := parseTokens(rest[0])
if err != nil {
return fmt.Errorf("a reply limit is a number of tokens, like 128000 or 128k, not %q", rest[0])
}
if n == 0 {
return fmt.Errorf("0 tokens is no reply limit at all — %s takes the one you set off %s",
resetCommand("output", id), resetTakesOff(p, model))
}
if err := provider.SetModelOutput(id, n); err != nil {
return err
}
fmt.Println(green.Render("✓"), id, muted.Render("answers with at most"), bold.Render(tokenCount(n)))
return nil
}
// typedRef is a provider/model spelled the way a command carrying it has to be
// typed: a wildcard in it is quoted, as a shell reads a bare * as a glob of
// the files in the directory the command runs in and never hands magpie the
// name. Every command magpie prints is one the user is meant to paste, so
// every one of them goes through here.
func typedRef(ref string) string {
if strings.Contains(ref, "*") {
return "'" + ref + "'"
}
return ref
}
// resetCommand is the command that takes a limit off, over the ref as typed.
func resetCommand(verb, ref string) string {
return fmt.Sprintf("magpie model %s %s --reset", verb, typedRef(ref))
}
// resetTakesOff is what the command taking a limit off would take off. The
// wildcard is not a model of its own but the one value given to every model
// of the provider, and a message calling that "this model" sends the user
// after a limit that was never set.
func resetTakesOff(p *provider.Provider, model string) string {
if model == "*" {
return "every model of " + p.ID
}
return "this model"
}
// clearedEntryIs reports whether a query about key reads the entry a removal
// just cleared, as opposed to another provider's model of the same name:
// ServedEntryOf is asked by the id as given and answers with the entry a
// query finds, so they are one entry only where the id still names the
// provider the entry is stored under. A reply limit outlives the provider it
// was set for and nothing rewrites its key when that provider is deleted, so
// "b/vendor/x" is still where a limit is held while a provider of another id
// has since been given the display name "b" — and a query spelled that way
// reads the second provider's model, which this command never touched.
func clearedEntryIs(key string) bool {
e, served := provider.ServedEntryOf(key)
return served && e.ID == key
}
func tokenCount(n int) string {
switch {
case n <= 0:
return "unknown"
case n%1000000 == 0:
return fmt.Sprintf("%dM", n/1000000)
case n%1000 == 0:
return fmt.Sprintf("%dk", n/1000)
default:
return fmt.Sprintf("%d", n)
}
}
func modelWire(args []string) error {
if len(args) == 0 {
return fmt.Errorf("%s", modelUsage)
}
// --reset comes before the provider is looked for, and works off the key
// the name is stored at: a name outlives the provider it was given for,
// and one whose provider has since been deleted is in force for
// whichever provider takes that id next, its requests going out renamed
// to a vendor that never heard of it. So it is the one name --reset
// most has to reach, and the only one a reset that resolved the
// provider first could not.
//
// What decides this is the key, not the lookup: a name under an id no
// provider has now is in force for whoever takes that id, and so is one
// under an id another provider is shown by — the provider such a ref
// resolves to holding a name of its own, which is the one a removal
// taken through it would take away instead. A ref that names its
// provider by that provider's own id keeps to the path below, which
// reports the removal as one.
if len(args) > 1 && isReset(args[1:]) {
pid, model, err := splitModelRef(args[0])
if err != nil {
return err
}
p, ferr := provider.Find(pid)
if ferr != nil || (p.ID != pid && provider.HasUpstreamName(pid+"/"+model)) {
dropped, err := provider.DropUpstreamName(pid + "/" + model)
if err != nil {
return err
}
if dropped {
fmt.Println(green.Render("✓"), pid+"/"+model,
muted.Render("no longer has a name of its own; a provider taking that id later is asked for it by the name magpie knows it by"))
return nil
}
// no name is kept under that key, so the id is a mistyped one
// and not a deleted provider's; the lookup below says which
// provider is not there, as it does for every other command
}
}
p, model, err := modelRef(args[0])
if err != nil {
return err
}
id := p.ID + "/" + model
every, shown := "'"+p.ID+"/*'", p.ID+"/"+model // quoted: a shell reads a bare * as a glob
if model == "*" {
shown = every
}
names := p.UpstreamNames()