Files
magpie/internal/provider/cursor_models.go
T
yetone 0984edaa03 cursor: the Run goes to the agent API Cursor's server config names for the account — ServerConfigService/GetServerConfig on api2.cursor.sh, whose agentUrlConfig cursor-agent reads to pick agentUrl in privacy mode (which the Run always asks for) and agentnUrl otherwise — asked once a token, kept by the token's hash so another account asks again, asked afresh and the Run tried once more after a region error, and the global agentn.global.api5.cursor.sh while the config can't be had (retried after a minute), as a team served in one region only was turned away by the global API with 'This region is not yet available for your team'; Cursor's errors have words of their own, no longer Devin's: an unauthenticated or expired sign-in says to sign in to Cursor again in magpie, a region or team restriction says so plainly with a 403 and doesn't ask for a sign-in; and Cursor's expanded ids (grok-4.7-low … grok-4.7-xhigh-fast, Claude with and without thinking, gpt-5.5-extra-high) are offered as one model a family, with the efforts it really has, named as Cursor names its default without the effort and holding what the least of its variants holds, fast and thinking families of their own (GPT-5.5 is 1M, GPT-5.5 Fast isn't said to be) and a family of one kept as Cursor's id — the list saved stays Cursor's own, and the gateway turns model and effort (and Codex's Fast) back into the variant at that effort, Cursor's default for one between those it has, else the nearest, while any of Cursor's own ids still goes as it is
2026-09-28 17:14:07 +08:00

191 lines
5.4 KiB
Go

package provider
// Cursor lists a model once for each effort and speed it serves it at
// ("grok-4.7-low", "grok-4.7-low-fast", … "grok-4.7-xhigh-fast"), and a
// Claude with thinking apart from one without. magpie offers one model a
// family, with the efforts there are, and the gateway asks for the id the
// effort picks (CursorVariants). Fast stays a model of its own: its context
// isn't always the same ("GPT-5.5 1M", "GPT-5.5 Fast"), and only Codex has a
// way to ask for it.
import (
"slices"
"strings"
"github.com/yetone/magpie/internal/catalog"
)
// cursorEfforts are the effort words of Cursor's ids, the levels magpie
// names them and the words Cursor's names say them in; extra-high before
// high.
var cursorEfforts = []struct{ word, level, label string }{
{"extra-high", "xhigh", "Extra High"}, {"xhigh", "xhigh", "Extra High"}, {"minimal", "minimal", "Minimal"},
{"none", "none", "None"}, {"low", "low", "Low"}, {"medium", "medium", "Medium"}, {"high", "high", "High"}, {"max", "max", "Max"},
}
var cursorLevelRank = []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"}
// splitCursorID is a Cursor id taken apart: its family, and the effort
// ("" for none named) it is at. Thinking comes before the effort or after
// it ("claude-opus-5-thinking-high", "claude-4.6-opus-high-thinking"); in
// the family it goes after, as does fast.
func splitCursorID(id string) (family, effort string) {
s := id
fast, thinking := false, false
if b, ok := strings.CutSuffix(s, "-fast"); ok && b != "" {
s, fast = b, true
}
if b, ok := strings.CutSuffix(s, "-thinking"); ok && b != "" {
s, thinking = b, true
}
for _, e := range cursorEfforts {
if b, ok := strings.CutSuffix(s, "-"+e.word); ok && b != "" {
s, effort = b, e.level
break
}
}
if !thinking {
if b, ok := strings.CutSuffix(s, "-thinking"); ok && b != "" {
s, thinking = b, true
}
}
if thinking {
s += "-thinking"
}
if fast {
s += "-fast"
}
return s, effort
}
func cursorLabel(level string) string {
for _, e := range cursorEfforts {
if e.level == level {
return e.label
}
}
return ""
}
// withoutWords is name with the first run of these words taken out, and
// whether it had them.
func withoutWords(name, words string) (string, bool) {
ns, ws := strings.Fields(name), strings.Fields(words)
if len(ws) == 0 {
return name, false
}
for i := 0; i+len(ws) <= len(ns); i++ {
if slices.Equal(ns[i:i+len(ws)], ws) {
return strings.Join(append(slices.Clone(ns[:i]), ns[i+len(ws):]...), " "), true
}
}
return name, false
}
// cursorFamily is one family of Cursor's ids.
type cursorFamily struct {
id string
variants []catalog.Model
efforts []string // each variant's, "" for none named
}
// byEffort is the id for each effort, "" the one Cursor picks by default:
// the one without an effort in its id, else the one whose name doesn't
// say its effort ("Claude Opus 5.5 1M" is claude-opus-5-5-medium), else
// medium, else the first.
func (f *cursorFamily) byEffort() map[string]string {
out := map[string]string{}
def := slices.Index(f.efforts, "")
for i, v := range f.variants {
e := f.efforts[i]
if _, ok := out[e]; !ok {
out[e] = v.ID
}
if def < 0 && e != "" {
if _, said := withoutWords(v.Name, cursorLabel(e)); !said {
def = i
}
}
}
if def < 0 {
def = max(0, slices.Index(f.efforts, "medium"))
}
out[""] = f.variants[def].ID
return out
}
// model is the family as one model: named as its default is, without
// the effort; with the efforts there are, lowest first; holding what the
// least of its variants does.
func (f *cursorFamily) model() catalog.Model {
def := f.byEffort()[""]
m := catalog.Model{ID: f.id}
for i, v := range f.variants {
if v.ID == def {
m.Name, _ = withoutWords(v.Name, cursorLabel(f.efforts[i]))
}
if v.Context > 0 && (m.Context == 0 || v.Context < m.Context) {
m.Context = v.Context
}
}
for _, l := range cursorLevelRank {
if slices.Contains(f.efforts, l) {
m.Efforts = append(m.Efforts, l)
}
}
return m
}
// cursorFamilies are the families of a list of Cursor's ids, in the order
// the list first has them.
func cursorFamilies(raw []catalog.Model) []*cursorFamily {
var out []*cursorFamily
by := map[string]*cursorFamily{}
for _, m := range raw {
id, effort := splitCursorID(m.ID)
f := by[id]
if f == nil {
f = &cursorFamily{id: id}
by[id] = f
out = append(out, f)
}
f.variants = append(f.variants, m)
f.efforts = append(f.efforts, effort)
}
return out
}
// collapseCursorModels is Cursor's list with each family one model; a
// family of one keeps Cursor's id and name.
func collapseCursorModels(raw []catalog.Model) []catalog.Model {
var out []catalog.Model
for _, f := range cursorFamilies(raw) {
if len(f.variants) == 1 {
out = append(out, f.variants[0])
continue
}
out = append(out, f.model())
}
return out
}
// CursorVariants are Cursor's ids a model magpie offers stands for, by
// effort, "" the default; ok is false for a model that stands for none,
// which goes to Cursor as it is.
func CursorVariants(model string) (map[string]string, bool) {
raw, _, ok := catalog.Live("cursor")
if !ok {
return nil, false
}
return cursorVariantsIn(raw, model)
}
func cursorVariantsIn(raw []catalog.Model, model string) (map[string]string, bool) {
for _, f := range cursorFamilies(raw) {
if f.id == model && len(f.variants) > 1 {
return f.byEffort(), true
}
}
return nil, false
}