provider: Antigravity's models at several levels are one model with those levels (#150: 统一 Antigravity 模型变体与客户端推理档位的映射) — Antigravity lists gemini-3.7-flash-low, -medium and -high as three models, "Gemini 3.7 Flash (Low)" …, so a client picked a level twice: once in the model, once as its reasoning effort, with no telling which won. Now ids that are one id with an effort word after it (-low, -medium, -high, -extra-low, -minimal, -xhigh) are one model, gemini-3.7-flash "Gemini 3.7 Flash", declaring only the levels there are (low,medium,high; Gemini 3.1 Pro low,high), so Codex, Pi and the rest get reasoning levels for it. The level a variant is at is what Antigravity's name says where each name says a different one (gemini-3.5-flash-extra-low is "(Low)", gemini-3.5-flash-low "(Medium)"), else its id's word; a model only named like another (gemini-3-flash-agent, "Gemini 3.5 Flash (High)"; gemini-pro-agent) or with no sibling (gpt-oss-120b-medium) stays as listed. A request asks for the variant the effort picks, an effort it lacks at the nearest level (xhigh is high; medium on low/high is high); with none asked, the id without a level if Antigravity has one, else high — what Gemini 3 does unasked — else the highest. A variant's request carries no thinkingLevel, so the id and the level can't disagree. An old variant id still works: as named with no effort, and moved to the effort asked for. Saved picks and agents set to a variant (antigravity/gemini-3.7-flash-high) are moved to the family, at that level where the agent's own is unset, as Cursor's are. For Gemini 3 models without variants, medium now goes as medium to Flash (which takes minimal/low/medium/high) and still as high to Pro (low/high). Tested against the list an account's fetchAvailableModels gave (sandbox cache)
This commit is contained in:
8 files changed
+498
-6
No files matched your search
@@ -70,6 +70,11 @@ func RenameLegacy() {
|
||||
// Cursor's id. It runs at start-up; errors are swallowed as RenameLegacy's.
|
||||
func MoveCursorEfforts() { moveEfforts("cursor", provider.CursorBase) }
|
||||
|
||||
// MoveAntigravityEfforts does the same for Antigravity's ids at a level
|
||||
// (antigravity/gemini-3.7-flash-high → antigravity/gemini-3.7-flash, at
|
||||
// high when the agent's effort is unset and it has that one).
|
||||
func MoveAntigravityEfforts() { moveEfforts("antigravity", provider.AntigravityBase) }
|
||||
|
||||
func moveEfforts(pid string, base func(string) (string, string, bool)) {
|
||||
offers := func(opts []Option, v string) bool {
|
||||
return slices.ContainsFunc(opts, func(o Option) bool { return o.Value == v })
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
// ---- Google Code Assist (provider side) --------------------------------------
|
||||
@@ -21,10 +23,58 @@ const skipSignature = "skip_thought_signature_validator"
|
||||
|
||||
var unsafeToolID = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
|
||||
// antigravityVariants are the ids of Antigravity's a model magpie offers
|
||||
// stands for, by level, and antigravityBaseOf the model and level one of
|
||||
// those ids is; vars so tests can stand in for them.
|
||||
var (
|
||||
antigravityVariants = provider.AntigravityVariants
|
||||
antigravityBaseOf = provider.AntigravityBase
|
||||
)
|
||||
|
||||
// antigravityModelID is Antigravity's id for a model magpie offers, at
|
||||
// the effort asked for: the family's variant at it, or at the nearest
|
||||
// level it has; with none asked, the family's default. One of
|
||||
// Antigravity's own ids at a level (gemini-3.7-flash-low, one an agent
|
||||
// was set to before) goes as it is when no effort is asked, and else is
|
||||
// the family's variant at the effort asked for: the effort the client
|
||||
// picks wins, as it does for the family. Any other id goes as it is.
|
||||
func antigravityModelID(model, effort string) string {
|
||||
if base, _, ok := antigravityBaseOf(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 effortRank {
|
||||
if vs[l] != "" {
|
||||
levels = append(levels, l)
|
||||
}
|
||||
}
|
||||
if id := vs[fitEffort(effort, levels)]; id != "" {
|
||||
return id
|
||||
}
|
||||
return vs[""]
|
||||
}
|
||||
|
||||
// buildCodeAssist builds the Code Assist envelope for a request, for the
|
||||
// app the account belongs to: "gemini" or "antigravity".
|
||||
// app the account belongs to: "gemini" or "antigravity". On Antigravity a
|
||||
// model that is a family of levels is asked for as the variant the effort
|
||||
// picks (antigravityModelID).
|
||||
func buildCodeAssist(r *Request, model, agent string) []byte {
|
||||
ag := agent == "antigravity"
|
||||
fixed := false // the id says the level it thinks at
|
||||
if ag {
|
||||
model = antigravityModelID(model, r.Effort)
|
||||
_, _, fixed = antigravityBaseOf(model)
|
||||
}
|
||||
claude := strings.Contains(strings.ToLower(model), "claude")
|
||||
toolID := func(id string) string {
|
||||
if !ag || id == "" {
|
||||
@@ -142,7 +192,7 @@ func buildCodeAssist(r *Request, model, agent string) []byte {
|
||||
if len(r.Stop) > 0 {
|
||||
gen["stopSequences"] = r.Stop
|
||||
}
|
||||
if tc := thinkingConfig(r, model, claude); tc != nil {
|
||||
if tc := thinkingConfig(r, model, claude, fixed); tc != nil {
|
||||
gen["thinkingConfig"] = tc
|
||||
// Claude's answer has to have room past its thinking
|
||||
if b, ok := tc["thinkingBudget"].(int); ok && claude && gen["maxOutputTokens"] == nil {
|
||||
@@ -157,8 +207,14 @@ func buildCodeAssist(r *Request, model, agent string) []byte {
|
||||
}
|
||||
|
||||
// thinkingConfig says how hard the model should think: a level for
|
||||
// Gemini 3, a budget for the rest. A model that doesn't think gets none.
|
||||
func thinkingConfig(r *Request, model string, claude bool) map[string]any {
|
||||
// Gemini 3, a budget for the rest. A model that doesn't think gets none,
|
||||
// and one whose id already says its level (fixed: Antigravity's
|
||||
// gemini-3.7-flash-low) gets no level that could say otherwise — the
|
||||
// effort asked for picked that id.
|
||||
//
|
||||
// Gemini 3's levels: Flash takes minimal, low, medium and high, so medium
|
||||
// goes as medium; Pro takes low and high only, so medium goes up to high.
|
||||
func thinkingConfig(r *Request, model string, claude, fixed bool) map[string]any {
|
||||
m := strings.ToLower(model)
|
||||
if strings.HasPrefix(m, "gpt-oss") || claude && !strings.Contains(m, "thinking") {
|
||||
return nil
|
||||
@@ -171,10 +227,13 @@ func thinkingConfig(r *Request, model string, claude bool) map[string]any {
|
||||
}
|
||||
tc := map[string]any{"includeThoughts": true}
|
||||
if strings.HasPrefix(m, "gemini-3") || strings.HasPrefix(m, "gemini-pro-agent") {
|
||||
if r.Effort != "" {
|
||||
if r.Effort != "" && !fixed {
|
||||
level := "high"
|
||||
if r.Effort == "low" {
|
||||
switch {
|
||||
case r.Effort == "low":
|
||||
level = "low"
|
||||
case r.Effort == "medium" && strings.Contains(m, "flash"):
|
||||
level = "medium"
|
||||
}
|
||||
tc["thinkingLevel"] = level
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
func codeAssistRequest(t *testing.T) *Request {
|
||||
@@ -141,3 +143,52 @@ func TestCodeAssistDecoder(t *testing.T) {
|
||||
t.Errorf("error events = %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
// On Antigravity a model that is one of its families of levels is asked
|
||||
// for as the variant the effort picks, with no thinking level that could
|
||||
// say otherwise; an old variant id moves with an effort asked; Gemini 3
|
||||
// Flash takes medium as medium, Pro as high.
|
||||
func TestCodeAssistAntigravityLevels(t *testing.T) {
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
var raw []catalog.Model
|
||||
for _, l := range []string{"gemini-3-flash - Gemini 3 Flash", "gemini-3.1-pro-high - Gemini 3.1 Pro (High)", "gemini-3.1-pro-low - Gemini 3.1 Pro (Low)",
|
||||
"gemini-3.7-flash-high - Gemini 3.7 Flash (High)", "gemini-3.7-flash-low - Gemini 3.7 Flash (Low)",
|
||||
"gemini-3.7-flash-medium - Gemini 3.7 Flash (Medium)", "gemini-pro-agent - Gemini 3.1 Pro (High)"} {
|
||||
id, name, _ := strings.Cut(l, " - ")
|
||||
raw = append(raw, catalog.Model{ID: id, Name: name})
|
||||
}
|
||||
if err := catalog.SaveLive("antigravity", "", raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// level: "" no thinkingConfig, "-" thinking at the level the id says
|
||||
for _, c := range []struct{ model, effort, agent, want, level string }{
|
||||
{"gemini-3.7-flash", "low", "antigravity", "gemini-3.7-flash-low", "-"},
|
||||
{"gemini-3.7-flash", "medium", "antigravity", "gemini-3.7-flash-medium", "-"},
|
||||
{"gemini-3.7-flash", "xhigh", "antigravity", "gemini-3.7-flash-high", "-"},
|
||||
{"gemini-3.7-flash", "", "antigravity", "gemini-3.7-flash-high", ""},
|
||||
{"gemini-3.7-flash-high", "", "antigravity", "gemini-3.7-flash-high", ""},
|
||||
{"gemini-3.7-flash-high", "low", "antigravity", "gemini-3.7-flash-low", "-"},
|
||||
{"gemini-3.1-pro", "medium", "antigravity", "gemini-3.1-pro-high", "-"},
|
||||
{"gemini-3-flash", "medium", "antigravity", "gemini-3-flash", "medium"},
|
||||
{"gemini-pro-agent", "medium", "antigravity", "gemini-pro-agent", "high"},
|
||||
{"gemini-pro-agent", "low", "antigravity", "gemini-pro-agent", "low"},
|
||||
{"gemini-3.7-flash", "low", "gemini", "gemini-3.7-flash", "low"},
|
||||
} {
|
||||
r := codeAssistRequest(t)
|
||||
r.Thinking, r.Effort = false, c.effort
|
||||
var env struct {
|
||||
Model string `json:"model"`
|
||||
Request map[string]any `json:"request"`
|
||||
}
|
||||
json.Unmarshal(buildCodeAssist(r, c.model, c.agent), &env)
|
||||
gen, _ := env.Request["generationConfig"].(map[string]any)
|
||||
tc, _ := gen["thinkingConfig"].(map[string]any)
|
||||
level, _ := tc["thinkingLevel"].(string)
|
||||
if tc != nil && level == "" {
|
||||
level = "-"
|
||||
}
|
||||
if env.Model != c.want || level != c.level {
|
||||
t.Errorf("%s at %q on %s: %s at %q, want %s at %q", c.model, c.effort, c.agent, env.Model, level, c.want, c.level)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package provider
|
||||
|
||||
// Antigravity lists some models once for each thinking level it serves
|
||||
// them at ("gemini-3.7-flash-low", "-medium", "-high", named "Gemini 3.7
|
||||
// Flash (Low)" …), so a client that also picks an effort had two to pick.
|
||||
// magpie offers such a family as one model ("gemini-3.7-flash", "Gemini
|
||||
// 3.7 Flash") with the levels there are, and the gateway asks for the id
|
||||
// the effort picks (AntigravityVariants).
|
||||
//
|
||||
// A family is only ids that are one id with an effort word after it: a
|
||||
// model named like another but with an id of its own (gemini-3-flash-agent
|
||||
// is "Gemini 3.5 Flash (High)") stays a model of its own, as does an id
|
||||
// with no sibling. Which level a variant is at is what Antigravity's name
|
||||
// for it says, where the names say one each: gemini-3.5-flash-extra-low is
|
||||
// "(Low)" and gemini-3.5-flash-low "(Medium)"; else its id's word.
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
// antigravityEffortWords are the effort words of Antigravity's ids and the
|
||||
// level each is, longest first.
|
||||
var antigravityEffortWords = []struct{ word, level string }{
|
||||
{"extra-high", "xhigh"}, {"extra-low", "minimal"}, {"xhigh", "xhigh"}, {"minimal", "minimal"},
|
||||
{"low", "low"}, {"medium", "medium"}, {"high", "high"},
|
||||
}
|
||||
|
||||
// splitAntigravityID is an id's family and the level its effort word
|
||||
// says, "" when it has none.
|
||||
func splitAntigravityID(id string) (family, effort string) {
|
||||
for _, e := range antigravityEffortWords {
|
||||
if b, ok := strings.CutSuffix(id, "-"+e.word); ok && b != "" {
|
||||
return b, e.level
|
||||
}
|
||||
}
|
||||
return id, ""
|
||||
}
|
||||
|
||||
// antigravityNameEffort is the level a name's last words in brackets say
|
||||
// ("Gemini 3.7 Flash (High)" is high), and the name without them.
|
||||
func antigravityNameEffort(name string) (string, string) {
|
||||
open := strings.LastIndex(name, " (")
|
||||
if open < 0 || !strings.HasSuffix(name, ")") {
|
||||
return name, ""
|
||||
}
|
||||
var level string
|
||||
switch strings.ToLower(name[open+2 : len(name)-1]) {
|
||||
case "minimal", "extra low":
|
||||
level = "minimal"
|
||||
case "low":
|
||||
level = "low"
|
||||
case "medium":
|
||||
level = "medium"
|
||||
case "high":
|
||||
level = "high"
|
||||
case "extra high", "xhigh":
|
||||
level = "xhigh"
|
||||
default:
|
||||
return name, ""
|
||||
}
|
||||
return strings.TrimSpace(name[:open]), level
|
||||
}
|
||||
|
||||
// antigravityFamily is one family of Antigravity's ids.
|
||||
type antigravityFamily struct {
|
||||
id string
|
||||
variants []catalog.Model
|
||||
efforts []string // each variant's level, "" for the id without one
|
||||
}
|
||||
|
||||
// byEffort is the id for each level, "" the one asked for when no level
|
||||
// is: the family's id without an effort word, if Antigravity has it; else
|
||||
// high — what Gemini 3 thinks at when it isn't told a level, so a client
|
||||
// that asks for none gets what the plain model would give; else the
|
||||
// highest there is.
|
||||
func (f *antigravityFamily) byEffort() map[string]string {
|
||||
out := map[string]string{}
|
||||
for i, v := range f.variants {
|
||||
if _, ok := out[f.efforts[i]]; !ok {
|
||||
out[f.efforts[i]] = v.ID
|
||||
}
|
||||
}
|
||||
if _, ok := out[""]; !ok {
|
||||
out[""] = out["high"]
|
||||
for i := len(cursorLevelRank) - 1; i >= 0 && out[""] == ""; i-- {
|
||||
out[""] = out[cursorLevelRank[i]]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// model is the family as one model: named as Antigravity names its
|
||||
// variants, without the level; with the levels there are, lowest first;
|
||||
// holding what the least of its variants does.
|
||||
func (f *antigravityFamily) model() catalog.Model {
|
||||
def := f.byEffort()[""]
|
||||
var m catalog.Model
|
||||
for _, v := range f.variants {
|
||||
if v.ID == def {
|
||||
m = v
|
||||
}
|
||||
}
|
||||
m.ID, m.Efforts = f.id, nil
|
||||
m.Name, _ = antigravityNameEffort(m.Name)
|
||||
if m.Name == "" || m.Name == def {
|
||||
m.Name = f.id
|
||||
}
|
||||
for _, v := range f.variants {
|
||||
if v.Context > 0 && (m.Context == 0 || v.Context < m.Context) {
|
||||
m.Context = v.Context
|
||||
}
|
||||
if v.Output > 0 && (m.Output == 0 || v.Output < m.Output) {
|
||||
m.Output = v.Output
|
||||
}
|
||||
m.Images = m.Images && v.Images
|
||||
}
|
||||
for _, l := range cursorLevelRank {
|
||||
if slices.Contains(f.efforts, l) {
|
||||
m.Efforts = append(m.Efforts, l)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// antigravityFamilies are the families of a list of Antigravity's ids, in
|
||||
// the order the list first has them.
|
||||
func antigravityFamilies(raw []catalog.Model) []*antigravityFamily {
|
||||
var out []*antigravityFamily
|
||||
by := map[string]*antigravityFamily{}
|
||||
for _, m := range raw {
|
||||
id, _ := splitAntigravityID(m.ID)
|
||||
f := by[id]
|
||||
if f == nil {
|
||||
f = &antigravityFamily{id: id}
|
||||
by[id] = f
|
||||
out = append(out, f)
|
||||
}
|
||||
f.variants = append(f.variants, m)
|
||||
}
|
||||
for _, f := range out {
|
||||
// the level each name says, if each says one and no two the same
|
||||
var named []string
|
||||
for _, v := range f.variants {
|
||||
if _, l := antigravityNameEffort(v.Name); l != "" && !slices.Contains(named, l) {
|
||||
named = append(named, l)
|
||||
}
|
||||
}
|
||||
for _, v := range f.variants {
|
||||
_, l := splitAntigravityID(v.ID)
|
||||
if len(named) == len(f.variants) {
|
||||
_, l = antigravityNameEffort(v.Name)
|
||||
}
|
||||
f.efforts = append(f.efforts, l)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// collapseAntigravityModels is Antigravity's list with each family one
|
||||
// model; a family of one keeps Antigravity's id and name.
|
||||
func collapseAntigravityModels(raw []catalog.Model) []catalog.Model {
|
||||
var out []catalog.Model
|
||||
for _, f := range antigravityFamilies(raw) {
|
||||
if len(f.variants) == 1 {
|
||||
out = append(out, f.variants[0])
|
||||
continue
|
||||
}
|
||||
out = append(out, f.model())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AntigravityVariants are Antigravity's ids a model magpie offers stands
|
||||
// for, by level, "" the default; ok is false for a model that stands for
|
||||
// none, which goes to Antigravity as it is.
|
||||
func AntigravityVariants(model string) (map[string]string, bool) {
|
||||
raw, _, ok := catalog.Live("antigravity")
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return antigravityVariantsIn(raw, model)
|
||||
}
|
||||
|
||||
func antigravityVariantsIn(raw []catalog.Model, model string) (map[string]string, bool) {
|
||||
for _, f := range antigravityFamilies(raw) {
|
||||
if f.id == model && len(f.variants) > 1 {
|
||||
return f.byEffort(), true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// AntigravityBase is the model magpie offers for one of Antigravity's ids
|
||||
// at a level ("gemini-3.7-flash-low" is gemini-3.7-flash at low), and that
|
||||
// level; ok is false for an id that is itself a model magpie offers, or
|
||||
// that no family has. Picks and agents' models saved before the families
|
||||
// were one model still name these ids.
|
||||
func AntigravityBase(id string) (base, effort string, ok bool) {
|
||||
raw, _, live := catalog.Live("antigravity")
|
||||
if !live {
|
||||
return "", "", false
|
||||
}
|
||||
return antigravityBaseIn(raw, id)
|
||||
}
|
||||
|
||||
func antigravityBaseIn(raw []catalog.Model, id string) (string, string, bool) {
|
||||
for _, f := range antigravityFamilies(raw) {
|
||||
if len(f.variants) < 2 {
|
||||
continue
|
||||
}
|
||||
for i, v := range f.variants {
|
||||
if v.ID == id && id != f.id {
|
||||
return f.id, f.efforts[i], true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// antigravityPicks are the user's picks of Antigravity's models with each
|
||||
// of its ids at a level the model magpie offers for it, in the order
|
||||
// picked, each once.
|
||||
func antigravityPicks(ids []string) []string {
|
||||
raw, _, live := catalog.Live("antigravity")
|
||||
if !live {
|
||||
return ids
|
||||
}
|
||||
var out []string
|
||||
for _, id := range ids {
|
||||
if base, _, ok := antigravityBaseIn(raw, id); ok {
|
||||
id = base
|
||||
}
|
||||
if !slices.Contains(out, id) {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
// antigravityListed is what Antigravity's fetchAvailableModels said for
|
||||
// an account (2026-09), as magpie keeps it.
|
||||
var antigravityListed = []catalog.Model{
|
||||
{ID: "claude-opus-4-6-thinking", Name: "Claude Opus 4.6 (Thinking)", Images: true},
|
||||
{ID: "gemini-3-flash", Name: "Gemini 3 Flash", Images: true},
|
||||
{ID: "gemini-3-flash-agent", Name: "Gemini 3.5 Flash (High)", Images: true},
|
||||
{ID: "gemini-3.1-pro-high", Name: "Gemini 3.1 Pro (High)", Images: true},
|
||||
{ID: "gemini-3.1-pro-low", Name: "Gemini 3.1 Pro (Low)", Images: true},
|
||||
{ID: "gemini-3.5-flash-extra-low", Name: "Gemini 3.5 Flash (Low)", Images: true},
|
||||
{ID: "gemini-3.5-flash-lite", Name: "Gemini 3.5 Flash Lite", Images: true},
|
||||
{ID: "gemini-3.5-flash-low", Name: "Gemini 3.5 Flash (Medium)", Images: true},
|
||||
{ID: "gemini-3.7-flash-high", Name: "Gemini 3.7 Flash (High)", Images: true},
|
||||
{ID: "gemini-3.7-flash-low", Name: "Gemini 3.7 Flash (Low)", Images: true},
|
||||
{ID: "gemini-3.7-flash-medium", Name: "Gemini 3.7 Flash (Medium)", Images: true},
|
||||
{ID: "gemini-3.7-flash-tiered", Images: true},
|
||||
{ID: "gemini-pro-agent", Name: "Gemini 3.1 Pro (High)", Images: true},
|
||||
{ID: "gpt-oss-120b-medium", Name: "GPT-OSS 120B (Medium)"},
|
||||
}
|
||||
|
||||
// Antigravity's ids of one model at several levels are one model with
|
||||
// those levels; a model with one id, or only a name like another's,
|
||||
// stays as Antigravity lists it.
|
||||
func TestCollapseAntigravityModels(t *testing.T) {
|
||||
var got []string
|
||||
for _, m := range collapseAntigravityModels(antigravityListed) {
|
||||
got = append(got, m.ID+"|"+m.Name+"|"+strings.Join(m.Efforts, ","))
|
||||
}
|
||||
want := []string{
|
||||
"claude-opus-4-6-thinking|Claude Opus 4.6 (Thinking)|",
|
||||
"gemini-3-flash|Gemini 3 Flash|",
|
||||
"gemini-3-flash-agent|Gemini 3.5 Flash (High)|",
|
||||
"gemini-3.1-pro|Gemini 3.1 Pro|low,high",
|
||||
// the names say the levels: extra-low is Low, low is Medium
|
||||
"gemini-3.5-flash|Gemini 3.5 Flash|low,medium",
|
||||
"gemini-3.5-flash-lite|Gemini 3.5 Flash Lite|",
|
||||
"gemini-3.7-flash|Gemini 3.7 Flash|low,medium,high",
|
||||
"gemini-3.7-flash-tiered||",
|
||||
"gemini-pro-agent|Gemini 3.1 Pro (High)|",
|
||||
"gpt-oss-120b-medium|GPT-OSS 120B (Medium)|",
|
||||
}
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Fatalf("got\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
|
||||
for model, want := range map[string]string{
|
||||
"gemini-3.7-flash": "=gemini-3.7-flash-high high=gemini-3.7-flash-high low=gemini-3.7-flash-low medium=gemini-3.7-flash-medium",
|
||||
"gemini-3.5-flash": "=gemini-3.5-flash-low low=gemini-3.5-flash-extra-low medium=gemini-3.5-flash-low",
|
||||
"gemini-3.1-pro": "=gemini-3.1-pro-high high=gemini-3.1-pro-high low=gemini-3.1-pro-low",
|
||||
} {
|
||||
vs, ok := antigravityVariantsIn(antigravityListed, model)
|
||||
var got []string
|
||||
for _, l := range []string{"", "high", "low", "medium"} {
|
||||
if vs[l] != "" {
|
||||
got = append(got, l+"="+vs[l])
|
||||
}
|
||||
}
|
||||
if !ok || strings.Join(got, " ") != want {
|
||||
t.Errorf("%s: %v %v, want %s", model, got, ok, want)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"gemini-3-flash", "gpt-oss-120b-medium", "gemini-3.7-flash-high", "gemini-pro-agent"} {
|
||||
if _, ok := antigravityVariantsIn(antigravityListed, id); ok {
|
||||
t.Errorf("%s has variants", id)
|
||||
}
|
||||
}
|
||||
|
||||
// with an id of no level, it is the default
|
||||
vs, _ := antigravityVariantsIn([]catalog.Model{{ID: "m-low"}, {ID: "m"}, {ID: "m-high"}}, "m")
|
||||
if vs[""] != "m" || vs["low"] != "m-low" {
|
||||
t.Errorf("variants %v", vs)
|
||||
}
|
||||
// without high, the highest there is
|
||||
vs, _ = antigravityVariantsIn([]catalog.Model{{ID: "m-low"}, {ID: "m-medium"}}, "m")
|
||||
if vs[""] != "m-medium" {
|
||||
t.Errorf("variants %v", vs)
|
||||
}
|
||||
}
|
||||
|
||||
// Picks saved before the levels were one model name Antigravity's own
|
||||
// ids: each is the model magpie offers for it now, once, where it was;
|
||||
// the rest stay.
|
||||
func TestAntigravityLegacyPicks(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
||||
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
||||
if err := catalog.SaveLive("antigravity", "", antigravityListed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for id, want := range map[string]string{"gemini-3.7-flash-high": "gemini-3.7-flash|high", "gemini-3.5-flash-extra-low": "gemini-3.5-flash|low",
|
||||
"gemini-3.5-flash-low": "gemini-3.5-flash|medium", "gemini-3.1-pro-low": "gemini-3.1-pro|low"} {
|
||||
if b, e, ok := AntigravityBase(id); !ok || b+"|"+e != want {
|
||||
t.Errorf("%s: %s|%s %v, want %s", id, b, e, ok, want)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"gemini-3.7-flash", "gpt-oss-120b-medium", "gemini-pro-agent", "gemini-3-flash-agent", "nope-low"} {
|
||||
if b, _, ok := AntigravityBase(id); ok {
|
||||
t.Errorf("%s taken for %s", id, b)
|
||||
}
|
||||
}
|
||||
legacy := []string{"gemini-3.7-flash-high", "gemini-3-flash", "gemini-3.7-flash-low", "gemini-pro-agent", "gemini-3.5-flash-low", "gone-model"}
|
||||
want := "gemini-3.7-flash,gemini-3-flash,gemini-pro-agent,gemini-3.5-flash,gone-model"
|
||||
if got := strings.Join(antigravityPicks(legacy), ","); got != want {
|
||||
t.Fatalf("picks %s, want %s", got, want)
|
||||
}
|
||||
|
||||
// what the account lists is the families, with their levels
|
||||
p := Provider{ID: "antigravity"}
|
||||
var got []string
|
||||
for _, m := range p.Available() {
|
||||
if strings.HasPrefix(m.ID, "gemini-3.7") {
|
||||
got = append(got, m.ID+"|"+strings.Join(p.Efforts(m.ID), ","))
|
||||
}
|
||||
}
|
||||
if strings.Join(got, " ") != "gemini-3.7-flash|low,medium,high gemini-3.7-flash-tiered|" {
|
||||
t.Fatalf("available %v", got)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,10 @@ func (p Provider) Available() []catalog.Model {
|
||||
live = collapseCursorModels(withCursorContexts(live))
|
||||
case "devin":
|
||||
live = withDevinContexts(live)
|
||||
case "antigravity":
|
||||
// after its names are filled in, and so that a family's
|
||||
// levels aren't taken off by a known model of its id
|
||||
return collapseAntigravityModels(catalog.Decorate(live, known))
|
||||
}
|
||||
return catalog.Decorate(live, known)
|
||||
}
|
||||
|
||||
@@ -213,6 +213,9 @@ func All() []Provider {
|
||||
if a.ID == "cursor" { // picked before its efforts were one model
|
||||
a.Models = cursorPicks(a.Models)
|
||||
}
|
||||
if a.ID == "antigravity" { // picked before its levels were one model
|
||||
a.Models = antigravityPicks(a.Models)
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -105,6 +105,7 @@ func run(args []string) error {
|
||||
settings.Migrate()
|
||||
agent.RenameLegacy()
|
||||
agent.MoveCursorEfforts()
|
||||
agent.MoveAntigravityEfforts()
|
||||
// a provider added, edited or removed, or a list fetched anew, reaches
|
||||
// the model lists agents keep in files of their own
|
||||
catalog.Changed = agent.SyncCatalog
|
||||
|
||||
Reference in new issue
Block a user