migrate: a moved built-in keeps its models' reasoning levels, its own account's plan and one own row, and the move checks each account through its proxy (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — Command Code's, Grok's and WorkBuddy's models their vendor gives no levels for lost the ones the built-in borrowed from their maker; moved, they borrow them again. Devin's own account moved without the plan the Devin CLI says, which the built-in showed. The agent signing in to another account while moved wrote a new own row for the built-in, which came back beside the one set aside as a second own account; nothing is written while moved, and going back keeps the one. And the move's check of an account asked the vendor directly, not through the proxy its requests take. Found walking Command Code, Grok, Devin, ZCode and WorkBuddy through sandbox moves. TestMovedKeepsEfforts, TestMovedOwnRowNotWritten, TestDevinOwnMovesWithPlan and TestPluginProxy fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
This commit is contained in:
9 files changed
+128
-11
No files matched your search
@@ -875,11 +875,14 @@ const handlers = {
|
||||
usage,
|
||||
// check tries one account as a request would: its loader, then its
|
||||
// models as the plugin lists them for it
|
||||
async check(p) {
|
||||
const key = accountKey(p.provider, p.account)
|
||||
await load({ provider: p.provider, account: key })
|
||||
const pi = await info(p.provider, key, true)
|
||||
return { models: Object.keys(pi.models) }
|
||||
// (through the account's proxy, as its requests go)
|
||||
check(p) {
|
||||
return via.run(p.proxy ?? "", async () => {
|
||||
const key = accountKey(p.provider, p.account)
|
||||
await load({ provider: p.provider, account: key })
|
||||
const pi = await info(p.provider, key, true)
|
||||
return { models: Object.keys(pi.models) }
|
||||
})
|
||||
},
|
||||
// import keeps a sign-in made elsewhere (a built-in subscription's, moved
|
||||
// onto its plugin) as one more account, or as the account it already is
|
||||
|
||||
@@ -337,6 +337,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
t.Setenv("http_proxy", globalSrv.URL)
|
||||
t.Setenv("FAKE_BASE", "http://vendor.invalid/v1")
|
||||
t.Setenv("FAKE_USAGE", "http://vendor.invalid/usage")
|
||||
t.Setenv("FAKE_MODELS", "http://vendor.invalid/models")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
abs, _ := filepath.Abs("testdata/fake/index.js")
|
||||
@@ -380,6 +381,10 @@ func TestPluginProxy(t *testing.T) {
|
||||
if u, err := AccountUsage(mine, "fakeco", ""); err != nil || u.Plan != "own" || last(&own) != "http://vendor.invalid/usage" {
|
||||
t.Fatalf("usage with its own proxy: %+v, %v", u, err)
|
||||
}
|
||||
// a move's check of the account asks the vendor as its requests do
|
||||
if ms, err := Check(mine, "fakeco", ""); err != nil || !slices.Contains(ms, "fake-own") || last(&own) != "http://vendor.invalid/models" {
|
||||
t.Fatalf("a check with its own proxy: %v, %v", ms, err)
|
||||
}
|
||||
// direct: vendor.invalid can't be reached but through a proxy
|
||||
mu.Lock()
|
||||
n := len(global) + len(own)
|
||||
|
||||
@@ -494,7 +494,7 @@ func Check(ctx context.Context, provider, account string) ([]string, error) {
|
||||
var r struct {
|
||||
Models []string `json:"models"`
|
||||
}
|
||||
if err := Call(ctx, "check", map[string]any{"provider": provider, "account": account}, &r); err != nil {
|
||||
if err := Call(ctx, "check", map[string]any{"provider": provider, "account": account, "proxy": proxyOf(ctx)}, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Models, nil
|
||||
|
||||
+5
@@ -99,6 +99,11 @@ export const FakePlugin = async ({ client }) => ({
|
||||
// fake-1 costs the plan nothing, as a WorkBuddy model of x0.00 credits
|
||||
p.models["fake-1"].free = true
|
||||
if (auth?.key === "few") return { "fake-1": p.models["fake-1"] }
|
||||
// $FAKE_MODELS: the vendor's list, whose answer names one more model
|
||||
if (process.env.FAKE_MODELS && auth) {
|
||||
const r = await fetch(process.env.FAKE_MODELS).then((r) => r.text()).catch(() => "")
|
||||
if (r) p.models["fake-" + r] = { ...p.models["fake-1"], id: "fake-" + r, name: r }
|
||||
}
|
||||
return p.models
|
||||
},
|
||||
},
|
||||
|
||||
@@ -195,6 +195,17 @@ func Moved(id string) bool {
|
||||
return ok && m.State == MovePlugin
|
||||
}
|
||||
|
||||
// movedAgent is whether the logins.json agent's accounts are a moved
|
||||
// built-in's, its plugin's now.
|
||||
func movedAgent(agent string) bool {
|
||||
for id, mv := range movers {
|
||||
if slices.Contains(mv.agents, agent) && Moved(id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OnPlugins are the built-ins moved onto their plugins.
|
||||
func OnPlugins() []string {
|
||||
var out []string
|
||||
@@ -391,7 +402,8 @@ func move(ctx context.Context, id string, mv *mover) (err error) {
|
||||
if a.Lapsed {
|
||||
continue
|
||||
}
|
||||
models, err := plugin.Check(ctx, id, moved[i].Key)
|
||||
// through the account's proxy, as its requests go
|
||||
models, err := plugin.Check(ViaLogin(ctx, id, a.User), id, moved[i].Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s through the plugin: %w", a.User, err)
|
||||
}
|
||||
@@ -665,6 +677,12 @@ func MoveBack(ctx context.Context, id string) error {
|
||||
// the backup first, for back to write into the accounts it came from
|
||||
prep := func(ls []savedLogin, _ map[string]string) []savedLogin {
|
||||
for _, b := range m.Backup {
|
||||
// the agent's own sign-in is one row: one written while moved
|
||||
// (the agent signed in to another account meanwhile) gives
|
||||
// way to the one set aside, which takes the agent's user anew
|
||||
if b.own() {
|
||||
ls = slices.DeleteFunc(ls, func(l savedLogin) bool { return l.Agent == b.Agent && l.own() && !sameMoved(l, b) })
|
||||
}
|
||||
if !slices.ContainsFunc(ls, func(l savedLogin) bool { return sameMoved(l, b) }) {
|
||||
ls = append(ls, b)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/plugin"
|
||||
)
|
||||
|
||||
// A built-in moved onto its plugin offers the reasoning levels it did for
|
||||
// a model its vendor lists none for (Command Code's, Grok's, WorkBuddy's):
|
||||
// its maker's, as the built-in borrowed them; a plugin of its own gives
|
||||
// what it says.
|
||||
func TestMovedKeepsEfforts(t *testing.T) {
|
||||
movedPlugin(t, "grok", map[string]map[string]any{})
|
||||
os.MkdirAll(filepath.Dir(catalog.CachePath()), 0o755)
|
||||
os.WriteFile(catalog.CachePath(), []byte(`{"xai":{"id":"xai","models":{"grok-4.7":{"id":"grok-4.7",
|
||||
"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}]}}}}`), 0o644)
|
||||
catalog.Reset()
|
||||
t.Cleanup(catalog.Reset)
|
||||
id := "grok-4.7"
|
||||
if len(borrowedEfforts(id)) == 0 {
|
||||
t.Fatal("the catalog written gives grok-4.7 no efforts")
|
||||
}
|
||||
pp := plugin.Provider{ID: "grok", Spec: "opencode-grok-auth", Models: []plugin.Model{{ID: id, Reasoning: true}}}
|
||||
if got := pluginCatalog(pp)[0].Efforts; !slices.Equal(got, borrowedEfforts(id)) {
|
||||
t.Fatalf("moved: %s's efforts %v, want %v", id, got, borrowedEfforts(id))
|
||||
}
|
||||
pp.ID = "someplugin"
|
||||
if got := pluginCatalog(pp)[0].Efforts; len(got) != 0 {
|
||||
t.Fatalf("a plugin's own provider borrowed %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Moved, the agent's own sign-in is the plugin's: the agent signing in to
|
||||
// another account meanwhile writes no row of the built-in's, which came
|
||||
// back beside the one set aside as a second own account.
|
||||
func TestMovedOwnRowNotWritten(t *testing.T) {
|
||||
movedPlugin(t, "grok", map[string]map[string]any{})
|
||||
sideLogins("grok", "new@x", func(savedLogin) bool { return true })
|
||||
for _, l := range readLogins() {
|
||||
if l.Agent == "grok" {
|
||||
t.Fatalf("a grok row written while moved: %+v", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Devin's own account moves with the plan the Devin CLI says, as the
|
||||
// built-in showed it, though its saved row has none.
|
||||
func TestDevinOwnMovesWithPlan(t *testing.T) {
|
||||
home := claudeHome(t)
|
||||
data := filepath.Join(home, "data")
|
||||
t.Setenv("XDG_DATA_HOME", data)
|
||||
os.MkdirAll(filepath.Join(data, "devin"), 0o700)
|
||||
if err := os.WriteFile(filepath.Join(data, "devin", "credentials.toml"), devinCredentials("cog_own_key_1234", "", "", ""), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
devinStatus.Lock()
|
||||
devinStatus.user, devinStatus.plan, devinStatus.ok, devinStatus.at = "dev@example.com", "Devin Pro", true, time.Now()
|
||||
devinStatus.Unlock()
|
||||
t.Cleanup(forgetDevinStatus)
|
||||
own := movingOf(t, "devin")["dev@example.com"]
|
||||
md, _ := own.Auth["metadata"].(map[string]any)
|
||||
if !own.Own || own.Plan != "Devin Pro" || md["plan"] != "Devin Pro" {
|
||||
t.Fatalf("Devin's own: %+v", own)
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,17 @@ func init() {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// the CLI's own account has its plan from the CLI, as the
|
||||
// built-in shows it, not saved on its row
|
||||
plan := l.Plan
|
||||
if l.Home == "" {
|
||||
if _, p, ok := devinIdentity(); ok && p != "" {
|
||||
plan = p
|
||||
}
|
||||
}
|
||||
md := map[string]any{"email": l.User}
|
||||
if l.Plan != "" {
|
||||
md["plan"] = l.Plan
|
||||
if plan != "" {
|
||||
md["plan"] = plan
|
||||
}
|
||||
if s := strings.TrimRight(server, "/"); s != "" && s != devinServer {
|
||||
md["server"] = s
|
||||
@@ -36,7 +44,7 @@ func init() {
|
||||
if l.Home == "" {
|
||||
md["cli"] = true // the plugin reads the CLI's key again, as the built-in does
|
||||
}
|
||||
out = append(out, Moving{User: l.User, First: l.Active, On: l.On, Lapsed: l.Lapsed != "", Plan: l.Plan, Own: l.Home == "",
|
||||
out = append(out, Moving{User: l.User, First: l.Active, On: l.On, Lapsed: l.Lapsed != "", Plan: plan, Own: l.Home == "",
|
||||
Auth: map[string]any{"type": "api", "key": key, "metadata": md}})
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -152,6 +152,11 @@ func pluginCatalog(pp plugin.Provider) []catalog.Model {
|
||||
if m.Reasoning {
|
||||
c.Efforts = m.Variants
|
||||
}
|
||||
// a built-in moved onto its plugin keeps the levels it had for a
|
||||
// model its vendor gives none: its maker's, as effortsOf borrows
|
||||
if len(c.Efforts) == 0 && Moved(pp.ID) {
|
||||
c.Efforts = borrowedEfforts(m.ID)
|
||||
}
|
||||
// OpenCode's price of a model it has none for is 0, as the
|
||||
// plugins give a plan's models: a price is only one above it
|
||||
if m.Cost != nil && (m.Cost.Input > 0 || m.Cost.Output > 0) {
|
||||
|
||||
@@ -35,7 +35,9 @@ func sideLogins(agent, ownUser string, usable func(savedLogin) bool) []sideLogin
|
||||
loginsMu.Lock()
|
||||
defer loginsMu.Unlock()
|
||||
ls := readLogins()
|
||||
if ownUser != "" {
|
||||
// moved onto its plugin, the agent's own sign-in is the plugin's: its
|
||||
// row, set aside, is kept for going back
|
||||
if ownUser != "" && !movedAgent(agent) {
|
||||
found := false
|
||||
for i := range ls {
|
||||
if ls[i].Agent == agent && ls[i].own() {
|
||||
|
||||
Reference in new issue
Block a user