plugins: a plugin's models show as the built-in's did (插件版…保持一模一样) — a model a plugin says its plan serves at no cost (WorkBuddy's x0.00 credits) is marked free, as the built-in marked it; Cursor's plugin models not named 1M hold what the catalog knows their base to (GPT-4o 128K, Kimi K2 131K), not Cursor's 200K, as the built-in's list; ZCode's own account moves marked as ZCode's, for the plugin (0.1.2) to read ZCode's own sign-in for each request and follow its switches, and comes back as ZCode's own with nothing saved. TestPluginUsage (free), TestCursorPluginContext and TestZCodeMover 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:
yetone committed 2026-10-01 01:18:04 +08:00
1 parent 1ec5495b26
commit 1ce05f0270
8 files changed
+83 -11

No files matched your search

+1
View File
@@ -487,6 +487,7 @@ async function providers() {
released: m.release_date ?? "",
cost: m.cost,
variants: Object.keys(m.variants ?? {}),
free: m.free === true,
})),
})
}
+4 -1
View File
@@ -35,7 +35,10 @@ type Model struct {
Image bool `json:"image"`
Released string `json:"released"`
Variants []string `json:"variants"`
Cost *struct {
// Free is set by the plugin on a model the plan serves at no cost to
// its allowance (WorkBuddy's "credits": "x0.00")
Free bool `json:"free"`
Cost *struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
} `json:"cost"`
+2
View File
@@ -82,6 +82,8 @@ export const FakePlugin = async ({ client }) => ({
if (auth?.type === "oauth" && auth.refresh?.startsWith("rot-")) {
await client.auth.set({ path: { id: "fakeco" }, body: { ...auth, refresh: auth.refresh + "+" } })
}
// 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"] }
return p.models
},
+27
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/plugin"
)
func TestParseCursorModels(t *testing.T) {
@@ -197,3 +198,29 @@ func TestTokenExpiry(t *testing.T) {
}
}
}
// Cursor's plugin gives each model Cursor's 200K unless the name says 1M;
// a model known to hold less keeps its own there, as the built-in's list.
func TestCursorPluginContext(t *testing.T) {
pp := plugin.Provider{ID: "cursor", Models: []plugin.Model{
{ID: "gpt-4o", Name: "GPT-4o", Context: 200_000},
{ID: "kimi-k2-high", Name: "Kimi K2", Context: 200_000},
{ID: "claude-opus-5-5-high", Name: "Claude Opus 5.5", Context: 1_000_000},
{ID: "composer-2.5", Name: "Composer 2.5", Context: 200_000},
}}
got := map[string]int{}
for _, m := range pluginCatalog(pp) {
got[m.ID] = m.Context
}
want := map[string]int{"gpt-4o": 128_000, "kimi-k2-high": 131_072, "claude-opus-5-5-high": 1_000_000, "composer-2.5": 200_000}
for id, n := range want {
if got[id] != n {
t.Errorf("%s: %d, want %d", id, got[id], n)
}
}
// another plugin's models are its own
pp.ID = "fakeco"
if c := pluginCatalog(pp)[0].Context; c != 200_000 {
t.Fatalf("fakeco gpt-4o: %d", c)
}
}
+10 -2
View File
@@ -182,11 +182,19 @@ func TestZCodeMover(t *testing.T) {
own := ms["own@example.com"]
var st map[string]any
if !own.Own || own.Auth["access"] != "own.secret" || json.Unmarshal([]byte(str(own.Auth["refresh"])), &st) != nil ||
st["site"] != "zai" || st["key"] != "own.secret" || st["device"] != zcodeDeviceMid() || st["base"] != ZCodeZaiBase {
st["site"] != "zai" || st["key"] != "own.secret" || st["device"] != zcodeDeviceMid() || st["base"] != ZCodeZaiBase || st["source"] != "zcode" {
t.Fatalf("ZCode's own: %+v %+v", own, st)
}
// marked as ZCode's, it comes back as ZCode's own, with nothing saved
var ownBack map[string]any
_ = json.Unmarshal([]byte(jsonText(own.Auth)), &ownBack)
before := len(readLogins())
if ls, user, err := movers["zcode"].back(readLogins(), "own@example.com", ownBack); err != nil || len(ls) != before || user != "own@example.com" {
t.Fatalf("ZCode's own back: %v %q %+v", err, user, ls)
}
tm := ms["team@x"]
if json.Unmarshal([]byte(str(tm.Auth["refresh"])), &st) != nil || st["site"] != "bigmodel" || st["org"] != "o1" ||
st = nil
if json.Unmarshal([]byte(str(tm.Auth["refresh"])), &st) != nil || st["source"] != nil || st["site"] != "bigmodel" || st["org"] != "o1" ||
st["project"] != "p1" || st["token"] != "biz" || st["plan"] != "Team" || tm.Auth["expires"] != int64(0) {
t.Fatalf("team@x: %+v %+v", tm.Auth, st)
}
+17 -7
View File
@@ -12,9 +12,11 @@ import (
// the requests need as its refresh, as the plugin's own sign-in keeps
// them. Nothing here rotates: a key lasts until it is deleted.
//
// ZCode's own sign-in is kept encrypted, where the plugin can't read it,
// so it goes as a copy of what magpie reads now. A key or a team seat
// lasts, and the copy with it; ZCode's session token alone (the Start
// ZCode's own sign-in goes as a copy of what magpie reads now, marked as
// ZCode's (source "zcode"): the plugin from 0.1.2 reads ZCode's own
// credentials in its place for each request, as the built-in does, so it
// follows ZCode's switches; an older one uses the copy. A key or a team
// seat lasts, and the copy with it; ZCode's session token alone (the Start
// Plan) is renewed by ZCode, and a copy would lapse behind it, so such an
// account stops the move and the built-in carries on.
func init() {
@@ -29,13 +31,13 @@ func init() {
return nil, errors.New("ZCode's own sign-in has no coding plan key, only ZCode's session, which the plugin can't follow")
}
// the own account's copy goes back to nothing: ZCode keeps it
out = append(out, Moving{User: l.User, First: l.Active, On: l.On, Lapsed: l.Lapsed != "", Own: l.Own, Auth: zcodeOut(l.User, l.Plan, k)})
out = append(out, Moving{User: l.User, First: l.Active, On: l.On, Lapsed: l.Lapsed != "", Own: l.Own, Auth: zcodeOut(l.User, l.Plan, k, l.Own)})
}
return out, nil
},
back: func(ls []savedLogin, user string, auth map[string]any) ([]savedLogin, string, error) {
var s struct {
Site, Device, Key, Base, JWT, Token, Org, Project, Plan string
Site, Device, Key, Base, JWT, Token, Org, Project, Plan, Source string
}
if json.Unmarshal([]byte(str(auth["refresh"])), &s) != nil {
// a key signed in to in the plugin
@@ -49,6 +51,10 @@ func init() {
} else if s.Key == "" && s.JWT == "" {
s.Key = str(auth["access"])
}
// ZCode's own goes back to nothing: ZCode keeps it
if _, _, ok := zcodeOwn(); ok && s.Source == "zcode" {
return ls, ownUser(ls, "zcode", firstNonEmpty(user, str(auth["accountId"]))), nil
}
k := zcodeKey{Key: s.Key, Base: s.Base, JWT: s.JWT, Token: s.Token, Org: s.Org, Project: s.Project}
if k.Base == "" {
k.Base = ZCodeZaiBase
@@ -76,13 +82,17 @@ func init() {
}
}
// zcodeOut is a ZCode account as the plugin's sign-in keeps one.
func zcodeOut(user, plan string, k zcodeKey) map[string]any {
// zcodeOut is a ZCode account as the plugin's sign-in keeps one;
// ZCode's own is marked as ZCode's, for the plugin to follow.
func zcodeOut(user, plan string, k zcodeKey, own bool) map[string]any {
site := "zai"
if strings.Contains(k.Base, "bigmodel.cn") {
site = "bigmodel"
}
state := map[string]any{"site": site, "device": zcodeDeviceMid(), "base": k.Base}
if own {
state["source"] = "zcode"
}
for name, v := range map[string]string{"key": k.Key, "jwt": k.JWT, "token": k.Token, "org": k.Org, "project": k.Project, "plan": plan} {
if v != "" {
state[name] = v
+14
View File
@@ -84,6 +84,20 @@ func TestPluginUsage(t *testing.T) {
t.Fatalf("%d usage cards for the plugin's accounts, want 3", cards)
}
// a model the plugin says the plan serves at no cost shows as free,
// as WorkBuddy's built-in marked its x0.00 models
free := map[string]bool{}
for _, pp := range plugin.Cached() {
if pp.ID == "fakeco" {
for _, m := range pluginCatalog(pp) {
free[m.ID] = m.Free
}
}
}
if !free["fake-1"] || free["fake-claude"] {
t.Fatalf("free models %v, want fake-1 alone", free)
}
// the gateway asks the allowance by the account's UsageAgent
var agent string
for _, p := range All() {
+8 -1
View File
@@ -133,11 +133,18 @@ func pluginCatalog(pp plugin.Provider) []catalog.Model {
c := catalog.Model{
ID: m.ID, Name: m.Name, Provider: pp.ID, Released: m.Released,
APIs: []string{string(pluginProtocol(pp.ID, m))}, Images: m.Image,
Context: m.Input, Output: m.Output,
Context: m.Input, Output: m.Output, Free: m.Free,
}
if c.Context == 0 {
c.Context = m.Context
}
// Cursor's own ids no catalog knows: one not named a 1M model
// holds what the catalog knows its base to, as the built-in's did
if pp.ID == "cursor" && c.Context <= cursorDefaultContext {
if n := cursorContext(m.ID, m.Name); c.Context == 0 || n < c.Context {
c.Context = n
}
}
if c.Name == "" {
c.Name = m.ID
}