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:
8 files changed
+83
-11
No files matched your search
@@ -487,6 +487,7 @@ async function providers() {
|
||||
released: m.release_date ?? "",
|
||||
cost: m.cost,
|
||||
variants: Object.keys(m.variants ?? {}),
|
||||
free: m.free === true,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user