provider: moving Devin to its plugin no longer fails over the effort variants picked before Devin's families were one model — the move's check named every pick the plugin doesn't list as one it doesn't serve, and a Devin account picked from before the fold (swe-2-high, claude-opus-5-5-medium, claude-opus-5-5-low-fast…, which the editor shows folded into SWE-2 and Claude Opus 5.5) failed with nineteen of them, "Adaptive" twice. The plugin serves those as the built-in does: the plugin host sends a model it doesn't list under its own id, and the Devin plugin passes a variant's id to Devin as it is (variantFor), as it keeps a picked variant in its list. A mover may now say the plugin serves a pick it doesn't list (served): Devin's is a variant of a model the plugin lists (devinBase strips the tier and effort words: claude-opus-5-5-low-fast → claude-opus-5-5), and Adaptive and Fusion, which the built-in never served either (Devin's API doesn't take them). A variant of a model the plugin doesn't list still stops the move. The names an unserved move gives are each named once. TestDevinMoveKeepsVariants and TestMoveToPlugin's served-variant case fail without the change. No real move was run (it would move the live Devin account). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ ./internal/plugin/ pass (provider flaked once under load, passes again).
This commit is contained in:
1 parent
901fd87ec0
commit
64922e1810
5 files changed
+88
-2
No files matched your search
@@ -901,6 +901,35 @@ func devinTierOf(id string) (tier, level string) {
|
||||
return "", devinLevel(id)
|
||||
}
|
||||
|
||||
// devinBase is the model a Devin variant id is a variant of: the id less
|
||||
// its tier and effort words (claude-opus-5-5-low-fast is claude-opus-5-5,
|
||||
// swe-2-high swe-2); an id at no effort is itself.
|
||||
func devinBase(id string) string {
|
||||
tier, level := devinTierOf(id)
|
||||
if level == "" {
|
||||
return id
|
||||
}
|
||||
low := strings.ToLower(id)
|
||||
cut := func(word string) bool {
|
||||
for _, sep := range []string{"-", "_"} {
|
||||
if strings.HasSuffix(low, sep+word) {
|
||||
low, id = low[:len(low)-len(sep+word)], id[:len(low)-len(sep+word)]
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if tier != "" {
|
||||
cut(tier)
|
||||
}
|
||||
for _, l := range devinLevels {
|
||||
if l.level == level && cut(l.word) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// devinEffortOf is the effort a Devin id runs at, in a tier or not.
|
||||
func devinEffortOf(id string) string {
|
||||
_, l := devinTierOf(id)
|
||||
|
||||
@@ -204,3 +204,32 @@ func TestDevinTiers(t *testing.T) {
|
||||
t.Errorf("offered to the agent: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The built-in's Devin picks from before the families were one model
|
||||
// (swe-2-high, claude-opus-5-5-low-fast) are variants of a model the plugin
|
||||
// lists, which it passes to Devin as they are: moving loses none of them,
|
||||
// nor Adaptive, which the built-in never served. A variant of a model the
|
||||
// plugin doesn't list, and a model of its own, are lost (moving Devin named every
|
||||
// level the user had picked was named as one the plugin doesn't serve).
|
||||
func TestDevinMoveKeepsVariants(t *testing.T) {
|
||||
for id, want := range map[string]string{
|
||||
"swe-2-high": "swe-2", "claude-opus-5-5-low-fast": "claude-opus-5-5", "swe-1-7-lightning-medium": "swe-1-7-lightning",
|
||||
"GPT-6-Sol_HIGH": "GPT-6-Sol", "claude-fable-5-1-xhigh": "claude-fable-5-1", "swe-1-6-fast": "swe-1-6-fast", "glm-5-2-1m": "glm-5-2-1m", "swe-2": "swe-2",
|
||||
} {
|
||||
if got := devinBase(id); got != want {
|
||||
t.Errorf("devinBase(%q) = %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
served := movers["devin"].served
|
||||
listed := []string{"swe-2", "claude-opus-5-5", "claude-opus-5-5-fast", "swe-1-7-lightning"}
|
||||
for _, m := range []string{"swe-2-high", "swe-2-max", "claude-opus-5-5-medium-fast", "swe-1-7-lightning-medium", "Adaptive", "adaptive"} {
|
||||
if !served(m, listed) {
|
||||
t.Errorf("moving loses %s", m)
|
||||
}
|
||||
}
|
||||
for _, m := range []string{"claude-fable-5-1-high", "gpt-6-luna", "swe-2"} {
|
||||
if served(m, listed) {
|
||||
t.Errorf("%s counts as served", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,6 +219,9 @@ type mover struct {
|
||||
// what else of the built-in's they carry (a key onto its provider),
|
||||
// whose saving syncs the agents, which read the accounts.
|
||||
settle func(auths map[string]map[string]any) error
|
||||
// served is whether the plugin, listing listed, still serves model, one
|
||||
// of the built-in's picks it doesn't list; nil for none.
|
||||
served func(model string, listed []string) bool
|
||||
}
|
||||
|
||||
var movers = map[string]*mover{}
|
||||
@@ -548,7 +551,9 @@ func move(ctx context.Context, id string, mv *mover) (err error) {
|
||||
return &moveError{MoveWhy{Code: "account", Args: map[string]string{"user": a.User, "error": err.Error()}}, fmt.Sprintf("%s doesn't work through the plugin: %s", a.User, err), err}
|
||||
}
|
||||
if !tried {
|
||||
if missing := slices.DeleteFunc(slices.Clone(inUse), func(m string) bool { return slices.Contains(c.Models, m) }); len(missing) > 0 {
|
||||
if missing := slices.DeleteFunc(slices.Clone(inUse), func(m string) bool {
|
||||
return slices.Contains(c.Models, m) || mv.served != nil && mv.served(m, c.Models)
|
||||
}); len(missing) > 0 {
|
||||
names := strings.Join(modelNames(id, missing), ", ")
|
||||
return &moveError{MoveWhy{Code: "unserved", Args: map[string]string{"models": names}}, fmt.Sprintf("the plugin doesn't serve %s. Untick them under Models, or keep the built-in.", names), nil}
|
||||
}
|
||||
@@ -588,7 +593,9 @@ func modelNames(id string, ids []string) []string {
|
||||
if names[m] != "" {
|
||||
m = names[m]
|
||||
}
|
||||
out = append(out, m)
|
||||
if !slices.Contains(out, m) {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -19,6 +20,16 @@ func init() {
|
||||
pkg: "@magpie-community/opencode-devin-auth",
|
||||
min: "0.1.5", // a failure's status and its sign-in mark as the built-in's
|
||||
agents: []string{"devin"},
|
||||
// a variant picked before the families were one model (swe-2-high)
|
||||
// goes to Devin as it is, through the plugin too, which keeps the
|
||||
// picks it is given; Adaptive and Fusion the built-in never served
|
||||
served: func(model string, listed []string) bool {
|
||||
if id := strings.ToLower(model); id == "adaptive" || id == "fusion" {
|
||||
return true
|
||||
}
|
||||
base := devinBase(model)
|
||||
return base != model && slices.Contains(listed, base)
|
||||
},
|
||||
out: func() ([]Moving, error) {
|
||||
var out []Moving
|
||||
for _, l := range devinLogins() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -150,6 +151,15 @@ func TestMoveToPlugin(t *testing.T) {
|
||||
if len(fakeSaved(t)) != 1 || plugin.SignedIn("fakeco") {
|
||||
t.Fatal("a move short of a model moved")
|
||||
}
|
||||
// one the mover says the plugin serves though it doesn't list it (a
|
||||
// Devin variant) isn't named, nor is a name twice
|
||||
movers["fakeco"].served = func(m string, listed []string) bool { return m == "fake-1-high" && slices.Contains(listed, "fake-1") }
|
||||
reset(fakeLogin("a@fake", "r-a", false, true))
|
||||
inUse = []string{"fake-1", "fake-1-high", "gone-model", "gone-model"}
|
||||
if err := Move(ctx, "fakeco"); err == nil || strings.Contains(err.Error(), "fake-1-high") || strings.Count(err.Error(), "gone-model") != 1 {
|
||||
t.Fatalf("Move with a served variant and a model the plugin lacks = %v", err)
|
||||
}
|
||||
movers["fakeco"].served = nil
|
||||
inUse = []string{"fake-1", "fake-claude"}
|
||||
|
||||
// an account signed in through the plugin already gets back what it had
|
||||
|
||||
Reference in new issue
Block a user