Files
magpie/internal/provider/modelprefs.go
T

1160 lines
43 KiB
Go

package provider
import (
"errors"
"fmt"
"maps"
"slices"
"strings"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/settings"
)
// A model's name is the vendor's, or models.dev's, and agents are shown it
// with the provider after it (Claude Opus 5.5 · Claude Code). The user can
// give one of a provider's models a name of their own instead (Opus): it is
// kept in settings' ModelNames by "<provider id>/<model id>", apart from the
// vendor's list, so a Refresh, a restart or an upgrade leaves it, and the
// same model another provider serves keeps its own. Only the name changes:
// the model's id, and where requests for it go, stay as they were. The
// provider still goes after it (Opus · Claude Code), same as the vendor's
// own name, so a picker full of renamed models can still be told apart by
// vendor; it's left off only when the name given already says it.
// The user can also keep only some of the reasoning levels a model has
// (low, medium and high of its six): settings' ModelEfforts, kept the same
// way. The lists magpie hands out — the gateway's, and those it writes into
// the agents' files — offer only those, in the vendor's order; a request
// for another still reaches the vendor as it did. A model whose levels
// aren't known (one models.dev doesn't list, a custom provider's) can be
// given some of Levels the same way, which it is then taken to have.
// ModelName is the name the user gave a provider's model, if any.
func ModelName(pid, model string) (string, bool) {
return modelNameIn(settings.Load().ModelNames, pid, model)
}
func modelNameIn(names map[string]string, pid, model string) (string, bool) {
n, ok := names[pid+"/"+model]
return n, ok && n != ""
}
// splitRef is "provider/model" as the provider it names, by its id now,
// and the model.
func splitRef(ref string) (*Provider, string, error) {
r := strings.TrimPrefix(strings.TrimSpace(ref), "magpie/")
if strings.HasPrefix(r, GroupPrefix) {
return nil, "", errors.New("that is a routing group, not a provider's model")
}
pid, model, ok := strings.Cut(r, "/")
if !ok || pid == "" || model == "" {
return nil, "", fmt.Errorf("name a model as provider/model, not %q", ref)
}
p, err := Find(pid)
if err != nil {
return nil, "", err
}
return p, model, nil
}
// SetModelName names a provider's model, spelled "provider/model"; an empty
// name gives it back its own. The agents that keep the models in files of
// their own are told (catalog.Touched), as for any other change of the
// catalog.
func SetModelName(ref, name string) error {
return touchedIf(setModelName(ref, name))
}
// touchedIf tells the agents of a change to the catalog that was made.
func touchedIf(changed bool, err error) error {
if changed {
catalog.Touched()
}
return err
}
func setModelName(ref, name string) (bool, error) {
p, model, err := splitRef(ref)
if err != nil {
return false, err
}
name = strings.Join(strings.Fields(name), " ")
if len([]rune(name)) > 80 {
return false, errors.New("a model's name is at most 80 characters")
}
if name != "" && !p.serves(model) {
return false, fmt.Errorf("%s has no model %s (magpie provider %s lists them)", p.ID, model, p.ID)
}
s := settings.Load()
key := p.ID + "/" + model
if s.ModelNames[key] == name {
return false, nil
}
if name == "" {
delete(s.ModelNames, key)
} else {
if s.ModelNames == nil {
s.ModelNames = map[string]string{}
}
s.ModelNames[key] = name
}
if err := settings.Save(s); err != nil {
return false, err
}
return true, nil
}
// SetModelPrice is what a provider's model costs the user, in USD per million
// tokens, kept in settings' ModelPrices the way a name is. A nil price takes
// the user's away, leaving the model at what its provider lists and only then
// at its maker's on models.dev; a price of zero is not that, but a model
// served at no cost. A price no vendor could charge is refused, naming the
// part that is wrong.
//
// Unlike the other model preferences this does not tell the agents. What a
// call costs is not what an agent picks a model by, and the model lists
// magpie keeps in the agents' own files are not its to rewrite over a number
// in a cost report.
func SetModelPrice(id string, p *catalog.Price) error {
if p == nil {
_, err := DropModelPrice(id)
return err
}
var key string
if model, every := strings.CutPrefix(strings.TrimSpace(id), settings.AnyProvider); every {
// a model from any provider, or from none still there: no provider
// is asked whether it serves it, since what it prices is mostly
// usage whose provider is gone, or that never went through magpie
key = AnyPriceKey(model)
} else {
pr, model, err := splitRef(id)
if err != nil {
return err
}
// a price for a model the provider does not serve is a price that
// never applies and nothing later says so; the same refusal `magpie
// model name` makes for the same id.
if model != "*" && !pr.serves(model) {
return fmt.Errorf("%s has no model %s (magpie provider %s lists them, and magpie model price '*/%s' prices it from any provider)", pr.ID, model, pr.ID, model)
}
// the entry is written under the id the provider has now, the way a
// name is: a key under a display name is a price the provider is
// never asked for, and nothing later would say so.
key = pr.ID + "/" + model
}
s := settings.Load()
m := settings.StatedPrice(*p)
if err := settings.CheckModelPrice(key, m); err != nil {
return err
}
if s.ModelPrices == nil {
s.ModelPrices = map[string]settings.ModelPrice{}
}
s.ModelPrices[key] = m
return settings.Save(s)
}
// AnyPriceKey is the key a price for a model from any provider is stored at:
// its id lower-cased, the way a session's bare model is looked up.
func AnyPriceKey(model string) string {
return settings.AnyProvider + strings.ToLower(strings.TrimSpace(model))
}
// PriceKey is the key a price for pid's model is stored at, and whether the
// provider naming that key is still there at all.
//
// The spelling that was given comes first, because a price outlives the
// provider it was set for and nothing rewrites the key when that provider
// goes: "b/vendor/m" can still be where a price is held while a provider of
// some other id answers to "b" as its display name. Resolving that name
// first would take the second provider's price away instead — none is stored
// under it, so nothing would change, the caller would be told it had, and
// the price the user meant would stay in the file to be counted at. Only
// where nothing is stored under the spelling is the provider looked for, by
// the id it has or the one it was renamed from: that is where SetModelPrice
// keeps a price set through a display name, and the same resolution
// EffectivePrice reads it back under.
func PriceKey(pid, model string) (string, bool) {
if pid+"/" == settings.AnyProvider {
return AnyPriceKey(model), false
}
key := pid + "/" + model
if _, under := settings.Load().ModelPrices[key]; under {
_, there := byIDOrWas(pid)
return key, there
}
if p, ok := byIDOrWas(pid); ok {
return p.ID + "/" + model, true
}
// and last a display name, which is what a provider of the user's is
// often asked by. Nothing writes a price under one — SetModelPrice
// re-keys the way SetModelName does — but the spelling above is the only
// thing that could have pointed at another provider's key, and a key
// nothing is stored under reaches no price at all without this.
if p, err := Find(pid); err == nil {
return p.ID + "/" + model, true
}
return key, false
}
// DropModelPrice takes a user's price away under the key it is stored at,
// without asking whether the provider is still there, and reports whether
// there was one there to take away. A price outlives the provider it was set
// for: that provider can be deleted, and `magpie model prices` still lists
// the price and usage is still counted at it, so a removal that resolved the
// provider first would leave the user no way to take it away. Which of the
// keys a price is ever kept under this one is, is PriceKey's to say.
//
// A key holding no price is still no error — there is nothing there to take
// away, which is the state it is left in either way — but saying so is what
// tells a mistyped id from a price that was cleared: `magpie model price --
// reset` over a price still in the file would have the user believe it gone.
func DropModelPrice(key string) (bool, error) {
key = strings.TrimPrefix(strings.TrimSpace(key), "magpie/")
if strings.HasPrefix(key, GroupPrefix) {
return false, errors.New("that is a routing group, not a provider's model")
}
pid, model, ok := strings.Cut(key, "/")
if !ok || pid == "" || model == "" {
return false, fmt.Errorf("name a model as provider/model, not %q", key)
}
key, _ = PriceKey(pid, model)
s := settings.Load()
if _, ok := s.ModelPrices[key]; !ok {
return false, nil
}
delete(s.ModelPrices, key)
return true, settings.Save(s)
}
// Levels are the reasoning levels a model whose own aren't known can be
// given.
var Levels = []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"}
// SetModelEfforts keeps only these of a provider's model's reasoning
// levels in the lists magpie hands out; none, or all it has, offers them
// all again. They must be levels the model has — or, for a model whose
// levels aren't known, any of Levels, which it is given; none takes them
// away.
func SetModelEfforts(ref string, efforts []string) error {
return touchedIf(setModelEfforts(ref, efforts))
}
func setModelEfforts(ref string, efforts []string) (bool, error) {
p, model, err := splitRef(ref)
if err != nil {
return false, err
}
all := p.Known(model)
var keep []string
for _, e := range efforts {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
continue
}
if len(all) == 0 && !slices.Contains(Levels, e) {
return false, fmt.Errorf("%q is not a reasoning level (they are %s)", e, strings.Join(Levels, ", "))
}
if len(all) > 0 && !slices.Contains(all, e) {
return false, fmt.Errorf("%s/%s has no reasoning level %q (it has %s)", p.ID, model, e, strings.Join(all, ", "))
}
if !slices.Contains(keep, e) {
keep = append(keep, e)
}
}
keep = effortsKept(all, keep) // in the vendor's order
if len(keep) == len(all) {
keep = nil
}
s := settings.Load()
key := p.ID + "/" + model
if slices.Equal(s.ModelEfforts[key], keep) {
return false, nil
}
if len(keep) == 0 {
delete(s.ModelEfforts, key)
} else {
if s.ModelEfforts == nil {
s.ModelEfforts = map[string][]string{}
}
s.ModelEfforts[key] = keep
}
if err := settings.Save(s); err != nil {
return false, err
}
return true, nil
}
// ModelEfforts are the reasoning levels the user kept of the provider's
// models, by model id.
func (p Provider) ModelEfforts() map[string][]string {
out := map[string][]string{}
for k, es := range settings.Load().ModelEfforts {
if m, ok := strings.CutPrefix(k, p.ID+"/"); ok && len(es) > 0 {
out[m] = es
}
}
return out
}
// effortsKept is all without the levels the user didn't keep, in its own
// order; all of them when none of those kept is among them any more (the
// vendor's list changed). A model with none known has those the user gave
// it.
func effortsKept(all, kept []string) []string {
if len(kept) == 0 {
return all
}
if len(all) == 0 {
return slices.DeleteFunc(slices.Clone(Levels), func(e string) bool { return !slices.Contains(kept, e) })
}
out := slices.DeleteFunc(slices.Clone(all), func(e string) bool { return !slices.Contains(kept, e) })
if len(out) == 0 {
return all
}
return out
}
// SetModelImage says whether a provider's model takes images, in place
// of what its vendor's list says. nil gives that answer back.
func SetModelImage(ref string, images *bool) error {
return touchedIf(setModelImage(ref, images))
}
func setModelImage(ref string, images *bool) (bool, error) {
p, model, err := splitRef(ref)
if err != nil {
return false, err
}
if images != nil && !p.serves(model) {
return false, fmt.Errorf("%s has no model %s (magpie provider %s lists them)", p.ID, model, p.ID)
}
if images != nil {
if vendor, known := vendorSees(p, model); known && *images == vendor {
images = nil
}
}
s := settings.Load()
key := p.ID + "/" + model
if images == nil {
if _, ok := s.ModelImages[key]; !ok {
return false, nil
}
delete(s.ModelImages, key)
} else {
if cur, ok := s.ModelImages[key]; ok && cur == *images {
return false, nil
}
if s.ModelImages == nil {
s.ModelImages = map[string]bool{}
}
s.ModelImages[key] = *images
}
if err := settings.Save(s); err != nil {
return false, err
}
return true, nil
}
// SetModelAPI says which one of a provider's APIs a model is asked on —
// chat, responses or anthropic — for a relay whose one key serves some of
// its models on one and others on another (01huadalang on Discord: 有的供应商
// 一个 api 里有很多模型但是不同协议); "" leaves it to the vendor's list and
// the URLs the provider has, as before. It must be an API the provider has
// a URL for: a preset's or a subscription's as well as a custom
// provider's, when it has more than one (01huadalang: OpenCode Go's
// deepseek-v4.1-flash answers on Responses too, and magpie asked it on
// chat alone).
func SetModelAPI(ref, api string) error {
return touchedIf(setModelAPI(ref, api))
}
func setModelAPI(ref, api string) (bool, error) {
p, model, err := splitRef(ref)
if err != nil {
return false, err
}
proto := Protocol(strings.TrimSpace(api))
if proto != "" {
if !slices.Contains(Protocols, proto) {
return false, fmt.Errorf("a model's API is chat, responses or anthropic, not %q", api)
}
if p.Base(proto) == "" {
return false, fmt.Errorf("%s has no %s URL to ask %s on: add it under More endpoints first", p.ID, proto, model)
}
if !p.serves(model) {
return false, fmt.Errorf("%s has no model %s (magpie provider %s lists them)", p.ID, model, p.ID)
}
}
s := settings.Load()
key := p.ID + "/" + model
if s.ModelAPIs[key] == string(proto) {
return false, nil
}
if proto == "" {
delete(s.ModelAPIs, key)
} else {
if s.ModelAPIs == nil {
s.ModelAPIs = map[string]string{}
}
s.ModelAPIs[key] = string(proto)
}
if err := settings.Save(s); err != nil {
return false, err
}
return true, nil
}
// ModelAPI is the API the user said p's model is asked on, when p has a
// URL for it still.
func (p Provider) ModelAPI(model string) (Protocol, bool) {
if len(p.Speaks()) < 2 {
return "", false
}
proto := Protocol(heldSettings().ModelAPIs[p.ID+"/"+model])
if proto == "" || !slices.Contains(Protocols, proto) || p.Base(proto) == "" {
return "", false
}
return proto, true
}
// ModelPref is what the provider editor's Names & levels changed of one
// model, sent with its Save: each part left nil is as it was. Name "" gives
// the model its own name back, Efforts [] all its levels, OwnImages the
// vendor's answer for whether it sees images, API "" every API the
// provider has for it, Same "" its own id to merge it with other
// vendors' by (see setModelSame), and OwnPrice its list price again in
// place of Price, what the user said it costs (SetModelPrice, #819).
type ModelPref struct {
Name *string `json:"name,omitempty"`
Efforts *[]string `json:"efforts,omitempty"`
Images *bool `json:"images,omitempty"`
OwnImages bool `json:"ownImages,omitempty"`
API *string `json:"api,omitempty"`
Same *string `json:"same,omitempty"`
Price *catalog.Price `json:"price,omitempty"`
OwnPrice bool `json:"ownPrice,omitempty"`
}
// SetModelPrefs makes the changes to a provider's models, by model id, as
// SetModelName, SetModelEfforts, SetModelImage, SetModelAPI and
// setModelSame do, and tells the agents
// once, after them all, rather than once a change. It stops at the first
// that fails, telling the agents of those made before it.
func SetModelPrefs(pid string, prefs map[string]ModelPref) error {
changed := false
set := func(c bool, err error) error {
changed = changed || c
return err
}
err := func() error {
for _, model := range slices.Sorted(maps.Keys(prefs)) {
m, ref := prefs[model], pid+"/"+model
if m.Name != nil {
if err := set(setModelName(ref, *m.Name)); err != nil {
return err
}
}
if m.Efforts != nil {
if err := set(setModelEfforts(ref, *m.Efforts)); err != nil {
return err
}
}
if m.Images != nil || m.OwnImages {
images := m.Images
if m.OwnImages {
images = nil
}
if err := set(setModelImage(ref, images)); err != nil {
return err
}
}
if m.API != nil {
if err := set(setModelAPI(ref, *m.API)); err != nil {
return err
}
}
if m.Same != nil {
if err := set(setModelSame(ref, *m.Same)); err != nil {
return err
}
}
// a price isn't what an agent picks a model by: the agents
// aren't told of it
if m.Price != nil || m.OwnPrice {
price := m.Price
if m.OwnPrice {
price = nil
}
if err := SetModelPrice(ref, price); err != nil {
return err
}
}
}
return nil
}()
return touchedIf(changed, err)
}
// setModelSame says which model a provider's model, spelt "provider/model",
// is the same as, for one a vendor names its own way (Volcengine Ark's
// dated ids, kyzhouxu on #583): the routing groups magpie finds merge it
// with that model from every other provider (autoGroups). The name is a
// model's id as any vendor spells it ("deepseek-v4.1-flash", or with a
// vendor's prefix); "" — or a name that is the model's own however spelt —
// merges it by its own id again.
func setModelSame(ref, same string) (bool, error) {
p, model, err := splitRef(ref)
if err != nil {
return false, err
}
same = strings.TrimSpace(same)
if len([]rune(same)) > 80 {
return false, errors.New("the model a model is the same as is at most 80 characters")
}
if same != "" && Slug(sameModel(same)) == "" {
return false, fmt.Errorf("%q names no model: give a model's id, as deepseek-v4.1-flash", same)
}
if sameModel(same) == sameModel(model) {
same = "" // its own id: merged by it already
}
if same != "" && !p.serves(model) {
return false, fmt.Errorf("%s has no model %s (magpie provider %s lists them)", p.ID, model, p.ID)
}
s := settings.Load()
key := p.ID + "/" + model
if s.ModelSameAs[key] == same {
return false, nil
}
if same == "" {
delete(s.ModelSameAs, key)
} else {
if s.ModelSameAs == nil {
s.ModelSameAs = map[string]string{}
}
s.ModelSameAs[key] = same
}
if err := settings.Save(s); err != nil {
return false, err
}
return true, nil
}
// ImageOverride is the user's answer for whether pid's model takes images.
func ImageOverride(pid, model string) (bool, bool) {
v, ok := settings.Load().ModelImages[pid+"/"+model]
return v, ok
}
// ApplyImage is what magpie tells of model: the user's answer when they
// gave one, else images as the vendor's list has it (known is that list's
// explicit answer, nil when it didn't say).
func ApplyImage(pid, model string, images bool, known *bool) (bool, *bool) {
if v, ok := ImageOverride(pid, model); ok {
return v, &v
}
return images, known
}
// vendorSees is whether p's list says model takes images, and whether it
// said so at all.
func vendorSees(p *Provider, model string) (bool, bool) {
for _, m := range p.Available() {
if m.ID != model {
continue
}
if m.ImageInput != nil {
return *m.ImageInput, true
}
return m.Images || catalog.SeesImages(m.ID), false
}
return false, false
}
// renameModelPrefs moves the names, levels, image answers and everything
// else the user said of a provider's models to the id it has now. The
// settings walk their per-model maps themselves — settings.RenamePerModel,
// by the convention a Model* field of type map[string]X — and move each of
// them whether or not the ones before it moved anything, so a map added to
// them later is moved as well and there is nothing here to write for it.
func renameModelPrefs(s *settings.Settings, from, to string) bool {
moved := s.RenamePerModel(from, to)
// the models a user has hidden from a picker are keyed by provider as
// well, and are not one of the per-model preference maps: they say
// which models are shown, not what a model is called or costs
// (and so is the order they are listed in)
hidden := false
for _, m := range []map[string][]string{s.HiddenModels, s.OrderedModels} {
for _, ids := range m {
for i, id := range ids {
if rest, ok := strings.CutPrefix(id, from+"/"); ok {
ids[i], hidden = to+"/"+rest, true
}
}
}
}
return moved || hidden
}
// Label is how an agent's list names the entry: its name (the user's own,
// when they gave one, else the vendor's) with its provider's after it, or
// "routing group" for a group's — dropped only when a name of the user's
// own already carries the provider's, so it isn't said twice.
func (e Entry) Label() string {
by := e.Provider.Name
if e.Group != "" {
by = "routing group"
}
if e.Default != "" && strings.Contains(strings.ToLower(e.Name), strings.ToLower(by)) {
return e.Name
}
return e.Name + " · " + by
}
// Labels are how an agent's list names each of es, in order: as Label, or,
// when the user wants names plain (settings' PlainNames, #335), by the
// name alone — but for two or more the list would call the same, as a
// routing group found for a model is called with that model left in the
// list, which keep their provider's after it to tell them apart. With
// their own names plain (PlainOwnNames, #92), a name the user gave — a
// model's, or a routing group's they made (#868: "· routing group" was
// what an agent's narrow menu cut off) — is that name just as they wrote
// it, but for two the list would call the same, and the rest keep Label's.
func Labels(es []Entry) []string {
out := make([]string, len(es))
s := heldSettings()
plain := s.PlainNames
own := !plain && s.PlainOwnNames
same := map[string]int{}
if plain || own {
for _, e := range es {
same[strings.ToLower(e.Name)]++
}
}
for i, e := range es {
out[i] = e.Label()
mine := own && (e.Default != "" || e.Named)
if (plain || mine) && e.Name != "" && same[strings.ToLower(e.Name)] == 1 {
out[i] = e.Name
}
}
return out
}
// The ways the agents' lists name models (SuffixMode): every name with its
// provider's after it, as by default; all but the names the user gave
// models (#92); or none (#335).
const (
SuffixOn = "on"
SuffixOwn = "own"
SuffixOff = "off"
)
// SuffixMode is how the agents' lists name models: SuffixOn, SuffixOwn or
// SuffixOff.
func SuffixMode() string {
s := settings.Load()
switch {
case s.PlainNames:
return SuffixOff
case s.PlainOwnNames:
return SuffixOwn
}
return SuffixOn
}
// SetSuffixMode has the agents' model lists name models as mode says (see
// Labels), and the agents told when it changes.
func SetSuffixMode(mode string) error {
plain, own := false, false
switch mode {
case SuffixOn:
case SuffixOwn:
own = true
case SuffixOff:
plain = true
default:
return fmt.Errorf("provider in model names: on, own or off, not %q", mode)
}
s := settings.Load()
if s.PlainNames == plain && s.PlainOwnNames == own {
return nil
}
s.PlainNames, s.PlainOwnNames = plain, own
if err := settings.Save(s); err != nil {
return err
}
catalog.Touched()
return nil
}
// SetCodexAgentsV1 turns settings.CodexAgentsV1 on or off, and has Codex's
// lists written and asked for again.
func SetCodexAgentsV1(on bool) error {
s := settings.Load()
if s.CodexAgentsV1 == on {
return nil
}
s.CodexAgentsV1 = on
if err := settings.Save(s); err != nil {
return err
}
catalog.Touched()
return nil
}
// SetCodexAutoReview sets settings.CodexAutoReview, the model Codex's
// auto-review runs on ("" for Codex's own pick), and has Codex's lists
// written and asked for again.
func SetCodexAutoReview(id string) error {
s := settings.Load()
if s.CodexAutoReview == id {
return nil
}
s.CodexAutoReview = id
if err := settings.Save(s); err != nil {
return err
}
catalog.Touched()
return nil
}
// SetFullContext turns settings.FullContext on or off: Codex and Claude
// Code are told a model's whole window, or the working one, again.
func SetFullContext(on bool) error {
s := settings.Load()
if s.FullContext == on {
return nil
}
s.FullContext = on
if err := settings.Save(s); err != nil {
return err
}
catalog.Touched()
return nil
}
// SetPlainNames has the agents' model lists name models by their names
// alone (see Labels), or with their providers' again, and the agents told.
func SetPlainNames(on bool) error {
if on {
return SetSuffixMode(SuffixOff)
}
return SetSuffixMode(SuffixOn)
}
// ModelNames are the names the user gave the provider's models, by model id.
func (p Provider) ModelNames() map[string]string {
out := map[string]string{}
for k, n := range settings.Load().ModelNames {
if m, ok := strings.CutPrefix(k, p.ID+"/"); ok && n != "" {
out[m] = n
}
}
return out
}
// EffortsOf is the reasoning levels one of a provider's listed models has,
// before any the user left out.
func EffortsOf(m catalog.Model) []string { return effortsOf(m) }
// serves reports whether model is one of the provider's, listed or exposed:
// a name given to a model it doesn't have would never be shown.
func (p Provider) serves(model string) bool {
has := func(m catalog.Model) bool { return m.ID == model }
return slices.ContainsFunc(p.Available(), has) || slices.ContainsFunc(p.Exposed(), has)
}
// ServesModel is whether the provider really serves model, listed or
// exposed: the question SetUpstreamName asks before it keeps a name for
// one, and the same one, since a name given for a model the provider does
// not serve is a request magpie would never send it. It is the answer a
// caller told to take a name off needs as well: a model the provider has
// nothing like is not asked for by any name, so reporting which one is in
// force for it would say magpie asks for a model it does not know.
func ServesModel(p Provider, model string) bool { return p.serves(model) }
// UpstreamName is the name to send a provider for one of its models: the one
// the user gave, for the model or for every model of that provider, else the
// name magpie knows the model by. Every "*" in the name the user gave is that
// model, wherever it stands: "vendor-c/*" for a provider's every model sends
// vendor-c/model-3 for model-3, so one name covers a relay that namespaces
// its models and still asks for each of them by its own, and "vendor/*/pro"
// sends vendor/model-3/pro. A name with no "*" in it is that one name for
// every model of the key, which is the right answer only for a relay that
// does serve them all alike.
//
// The two are deliberately different things. Everything a user or an agent
// sees — the catalog, the routing groups' membership, what a call is recorded
// and priced as — is in magpie's name for the model, so a rename upstream
// never renames a model here. Only the request that goes out carries the
// vendor's name, and what a reply says answered is compared with it, so a
// model asked for by the name the vendor serves it under is not read as
// another one.
func UpstreamName(p Provider, model string) string {
return UpstreamNameIn(settings.Load().ModelWires, p.ID, model)
}
// UpstreamNameIn is UpstreamName over a settings' ModelWires already read,
// for a caller that walks a whole ledger of models in a row. A name that is
// only whitespace — one an older magpie stored, or one hand-edited into the
// file — is no name, as it is where one is given.
func UpstreamNameIn(wires map[string]string, pid, model string) string {
n := strings.TrimSpace(wires[pid+"/"+model])
if n == "" {
n = strings.TrimSpace(wires[pid+"/*"])
}
if n == "" {
return model
}
return strings.ReplaceAll(n, "*", model)
}
// UpstreamNameAs is UpstreamName for a request that goes out asking for sent,
// which is not always the model magpie knows: an Antigravity account is asked
// for the variant of it the effort picks, and that is the id the body carries.
// The two levels of the lookup are kept apart, since they are about different
// things. A name given for the model itself is the model's, whichever variant
// answers for it, so a "*" in it is that model and not the variant: the vendor
// is asked for the model by the name its owner gave, as it is everywhere else
// in magpie, and not for a "*" it never heard of. A "*" in the provider's own
// name is whatever the request is going out under, so one name for a provider
// asks for gemini-3.7-flash-high and gemini-3.7-flash-low each by its own,
// rather than for the one id whatever level the effort picked.
func UpstreamNameAs(p Provider, model, sent string) string {
return UpstreamNameAsIn(settings.Load().ModelWires, p.ID, model, sent)
}
// UpstreamNameAsIn is UpstreamNameAs over a settings' ModelWires already
// read. A name that is only whitespace is no name, as it is in
// UpstreamNameIn: the lookup falls through to the level below it, and a
// request with no name at all goes out under the id it was built with.
func UpstreamNameAsIn(wires map[string]string, pid, model, sent string) string {
if sent == model {
return UpstreamNameIn(wires, pid, model)
}
if n := strings.TrimSpace(wires[pid+"/"+model]); n != "" {
return strings.ReplaceAll(n, "*", model)
}
if n := strings.TrimSpace(wires[pid+"/*"]); n != "" {
return strings.ReplaceAll(n, "*", sent)
}
return sent
}
// SentNameIn is the name a call on the provider's model goes out under at
// that effort: the name the vendor would be asked for now, which is the one
// magpie knows the model by unless the user gave one. It is UpstreamNameAs for
// the id that call asks for, which is not the model magpie knows everywhere:
// on an Antigravity account it is the variant of it that effort picks, so a
// name given for the provider asks for each level by its own id, and a ledger
// reading a record back reads the vendor's reply against the name that
// really went out — the same one the gateway sent — rather than against the
// family it is one level of.
//
// The effort is the one the gateway sent, already fitted to the model's own
// levels, which is what a record keeps of it, so a level of the model's own
// is one of its variants and goes out as that one. An effort that is no
// level of the family names the same id the gateway asked for all the same:
// the level of the family's own nearest (AntigravitySentID), which is where
// the envelope went out.
//
// It is SentNameOnIn for the caller that has nothing but the id — a record in
// the ledger, which keeps the id the call went out on and nothing else. A
// provider that is an account is built with that account's own id as its own
// (googleProvider and the rest of the account providers), so the id says
// which account it was, and a provider of the user's own is no account's
// whatever its id is. A record's provider need not even be signed in now,
// which is why nothing here goes looking for the account behind the id: the
// id a call went out on is all a record has and all it can be read by.
func SentNameIn(wires map[string]string, pid, model, effort string) string {
return SentNameOnIn(wires, pid, pid, model, effort)
}
// SentNameOnIn is SentNameIn for a caller holding the provider itself, and so
// the account behind it: the call goes out on that account, and it is the
// account the id the effort picks is a question about. A provider's id is
// that account's own id today (googleProvider and the rest of the account
// providers build each one with it), so passing the id for the account reads
// the same — but the two are one id by construction rather than by rule, and
// a provider of the user's own has no account at all whatever its id is. So
// the route, which is holding the provider, asks about the account and lets
// the gateway's dispatch (codeAssistID, on the same agent) be the one rule.
func SentNameOnIn(wires map[string]string, pid, account, model, effort string) string {
return UpstreamNameAsIn(wires, pid, model, antigravitySentID(account, model, effort))
}
// antigravitySentID is the id a call on the model goes out under at that
// effort on an Antigravity account. Every other account is asked for the
// model magpie knows, and so is one of a model this account does not serve
// as a family of levels.
func antigravitySentID(account, model, effort string) string {
if account != "antigravity" {
return model
}
return AntigravitySentID(model, effort)
}
// AntigravitySentID is the id a request for model goes out under at that
// effort on an Antigravity account: the variant of the family the effort
// picks, at the level of that family nearest the effort asked for; with no
// effort asked for, the family's own default. One of the family's own
// variant ids — an id a pick or an agent's model list kept from before the
// families were one model — goes as it is when no effort is asked for, and
// at the level the effort picks otherwise, since the effort the client
// asks for wins, as it does for the family. Any other id goes as it is.
//
// The gateway sends the envelope under this and the ledger judges what the
// vendor answered against it, so there is one of it rather than one each:
// a reply naming the id a call went out under is that model answering, and
// read against the family instead every level of it reads as another
// model having swapped in, in the routing view and in the ledger alike.
func AntigravitySentID(model, effort string) string {
if base, _, ok := AntigravityBase(model); ok {
if effort == "" {
return model
}
model = base
}
vs, ok := AntigravityVariants(model)
if !ok {
return model
}
if effort == "" {
return vs[""]
}
var levels []string
for _, l := range Levels {
if vs[l] != "" {
levels = append(levels, l)
}
}
if id := vs[nearestEffort(effort, levels)]; id != "" {
return id
}
return vs[""]
}
// nearestEffort is the level of levels nearest want in Levels' order, a tie
// going up, or want itself where the levels are none or hold it: an effort a
// family does not take is asked at the level of its own nearest, the rule
// the gateway fits an effort to a model's levels by on the way out
// (fitEffort), kept here where a ledger reading a record back can reach it
// too.
func nearestEffort(want string, levels []string) string {
at := slices.Index(Levels, want)
if len(levels) == 0 || at < 0 || slices.Contains(levels, want) {
return want
}
best, dist := want, len(Levels)
for _, l := range levels {
i := slices.Index(Levels, l)
if i < 0 || l == "none" {
continue
}
d := i - at
if d < 0 {
d = -d
}
if d < dist || d == dist && i > at {
best, dist = l, d
}
}
return best
}
// SetUpstreamName is the name to send a provider for one of its models; an
// empty name takes the user's away and sends the one magpie knows. A name is
// only given for a model the provider really serves: a relay lists the model
// under the id magpie knows it by and asks for it under the vendor's own, and
// one it does not list is a request magpie would never send, so naming it is
// refused here, as a price and a limit are for the same id. A "*" stands
// for every model of the provider rather than for one, and a removal is
// exempt, so an entry left for a model that has since gone can still be
// taken away.
//
// A provider magpie asks through an agent's own backend has no model field
// to put a name in — the request goes out carrying the id the agent knows —
// so a name given for one of its models would never be sent (asksOwnBackend).
//
// The ref is taken as the provider it names, by its id now, and the name is
// kept under that id: a ref is as likely to be spelled with an id the
// provider had before a rename (Was) or with the name it is shown by, and
// every lookup of a wire name goes by the id the provider has (UpstreamName).
// A name under any other spelling of the provider is one no lookup will ever
// match — a name the provider is never asked for, and a removal that takes
// away nothing.
//
// The key is checked here, where it is given, and not as the whole settings
// are written: a hand-edited key no lookup will ever match is a no-op of the
// user's own making, and must not keep every other setting from being saved.
//
// Kept in settings, and saved without telling the agents, like a price: which
// model an agent picks, and what it is called, have not changed — only the
// name the request goes out under.
//
// A removal goes to DropUpstreamName, which works off the key rather than
// off this provider, so that taking a name away is the same operation
// whether or not the provider it was given for is still there.
//
// A blank name is a removal, and whether there was a name there to take away
// is DropUpstreamName's to say, not this one's: a caller with something of
// its own to say about a name that was not there — `magpie model wire --
// reset`, which tells a mistyped id from a model already asked for by the
// name magpie knows it by — asks DropUpstreamName for the removal itself,
// the way `magpie model price --reset` asks DropModelPrice. A caller that
// only wants the model left as it is has nothing to tell the user either
// way, which is why a blank name is an error and not a second answer.
func SetUpstreamName(ref, name string) error {
p, model, err := splitRef(ref)
if err != nil {
return err
}
key := p.ID + "/" + model
if err := settings.CheckModelKey("wire name", key); err != nil {
return err
}
if name = strings.TrimSpace(name); name == "" {
_, err := DropUpstreamName(key)
return err
}
if model != "*" && !p.serves(model) {
return fmt.Errorf("%s has no model %s (magpie provider %s lists them)", p.ID, model, p.ID)
}
if p.asksOwnBackend() {
return fmt.Errorf("%s is %s's own account: magpie asks it for the model by the id it knows, so there is no upstream name to give it", p.Name, p.Account.Agent)
}
s := settings.Load()
if s.ModelWires == nil {
s.ModelWires = map[string]string{}
}
s.ModelWires[key] = name
return settings.Save(s)
}
// DropUpstreamName takes a name off under the key it is stored at, and says
// whether there was one to take. A name outlives the provider it was given
// for: that provider can be deleted, and the name is then in force for
// whichever provider takes that id next — its requests would go out renamed
// to a vendor that never heard of it, and nothing would say so — so a
// removal that resolved the provider first would leave the user no way to
// take away the one name doing that. A key holding no name is not an error:
// there is nothing there to take away, which is the state it is left in
// either way, and false says so to a caller that has something else to say.
//
// Which key a ref is taken at is wireKey's: the name comes off from where it
// is really kept, rather than from under whichever provider the ref names
// now.
func DropUpstreamName(key string) (bool, error) {
key = strings.TrimPrefix(strings.TrimSpace(key), "magpie/")
if strings.HasPrefix(key, GroupPrefix) {
return false, errors.New("that is a routing group, not a provider's model")
}
pid, model, ok := strings.Cut(key, "/")
if !ok || pid == "" || model == "" {
return false, fmt.Errorf("name a model as provider/model, not %q", key)
}
s := settings.Load()
key = wireKey(s.ModelWires, pid, model)
if err := settings.CheckModelKey("wire name", key); err != nil {
return false, err
}
if _, ok := s.ModelWires[key]; !ok {
return false, nil
}
delete(s.ModelWires, key)
return true, settings.Save(s)
}
// HasUpstreamName reports whether a name is kept under exactly the key it is
// given, the provider's part spelled as it is. No other spelling of the
// provider is tried: a caller deciding whether to remove a name by this key
// has to know that this key is the one a name is kept under, which is the
// one question wireKey's order is about.
//
// A key spelled as an agent spells one — with magpie/ in front of it, and
// the spaces a ref is typed with — is that same key, as it is for a removal
// (DropUpstreamName): a name kept under it is there whichever way it is
// spelled, and a check that answered for one spelling only would say a name
// was not kept where it is. The one caller has had that prefix taken off
// already (splitModelRef), so nothing depends on it yet.
func HasUpstreamName(key string) bool {
key = strings.TrimPrefix(strings.TrimSpace(key), "magpie/")
_, ok := settings.Load().ModelWires[key]
return ok
}
// wireKey is the key a name for "pid/model", as the user spelled it, is kept
// under and taken off by, and the three spellings are tried in one order:
// the ref as it is given, the id the provider has or had, the name it is
// shown by. The first is where a name really is once the provider it was
// given for is gone — nothing rewrites the key, so it is still there under
// an id no provider has now, and it is the one a removal has to reach.
// Resolving that id through the name another provider is shown by instead
// would take away that provider's own name instead, and leave the one that
// is in force for whoever takes the id next in the file.
//
// The id a provider has, or had before a rename (Was), is where a name is
// written and read (SetUpstreamName, UpstreamName), and the name a provider
// is shown by is the last of the three, for a ref spelled that way and with
// nothing kept under either of the other two.
func wireKey(wires map[string]string, pid, model string) string {
key := pid + "/" + model
if _, ok := wires[key]; ok {
return key
}
if p, ok := byIDOrWas(pid); ok {
return p.ID + "/" + model
}
if p, err := Find(pid); err == nil {
return p.ID + "/" + model
}
return key
}
// UpstreamNames are the names the user gave the provider's models, by model
// id, with the one given for all of them under "*" — each as it will be sent,
// trimmed, "*" and all, which is what a list of them shows. A name that is
// only whitespace is none, and is not listed.
func (p Provider) UpstreamNames() map[string]string {
out := map[string]string{}
for k, n := range settings.Load().ModelWires {
if m, ok := strings.CutPrefix(k, p.ID+"/"); ok {
if n = strings.TrimSpace(n); n != "" {
out[m] = n
}
}
}
return out
}
// asksOwnBackend reports whether p is a signed-in account magpie reaches
// through that agent's own backend rather than through a vendor API: the
// request goes out carrying the model id the agent already knows, with no
// model field for magpie to name differently, so a wire name given for one
// of its models would never be sent. It is the account dispatch in
// internal/gateway that decides which agents those are, so this and that
// have to keep saying the same thing — a name accepted for an account the
// gateway asks its own way is a name never sent — and
// TestAsksOwnBackendAgreesWithTheGatewayDispatch reads the dispatch itself
// rather than a list written out a second time to keep them so.
//
// Codex is deliberately not among them: it is served by the ChatGPT backend
// (codex_backend.go), which has no provider id to key a name by.
func (p Provider) asksOwnBackend() bool {
if p.Account == nil {
return false
}
switch p.Account.Agent {
case "claude", "cursor", "devin", "kiro", "zed", CommandCodePlanID:
return true
}
// Qoder is one backend however many sites it is signed in to, and the
// gateway's dispatch covers the whole family rather than the site the
// account was made on, so the family is asked of rather than one id of
// it: a name refused for the one subscription has to be refused for
// the other too, which is as much magpie's own there.
return slices.Contains(qoderAgents, p.Account.Agent)
}