zcode: a Start Plan account neither lists nor offers GLM-5.3, and the move to the plugin doesn't count it lost (悠悠哥 on Discord: "仍使用内置:插件不提供 GLM-5.3" moving ZCode onto its plugin) — GLM-5.3 is the Coding Plan's alone (ZCode's builtin config lists GLM-5.3-Flash, GLM-5.2 and GLM-5-Turbo for account:zai-start-plan), and the plugin serves a Start Plan account what ZCode does. But a Z.ai account with a key made and no Coding Plan bought, whose requests go to the Start Plan, was taken for a Coding Plan account until a request found its route (zcodeOnStart with no context said the Coding Plan), and afterwards was listed whatever list was fetched last (the Coding Plan's, another account's or its own before), so GLM-5.3 was listed, could be ticked, and the move found the plugin "short" of it. Now: before a route is found, the plan the account showed at sign-in says it (zcodeOnStartAs, zcodeStartNamed: "Start Plan"); on the Start Plan, a model the plan doesn't serve (zcodeStartServes: ZCode's config on the machine, else zcodeStartModels) is unusable, taken out of a fetched list and out of the picks, with a list fetched or not (Exposed); and a mover may say which models the built-in served on an account (mover.builtin), ZCode's saying a Start Plan account was never served GLM-5.3, so one the built-in didn't serve either is no loss. A model really lost is named with the account and its plan: "the plugin doesn't serve GLM-5.3 for a@x (GLM Coding Pro), though the built-in does" (插件没有为 … 提供 …(内置提供)), the why's args carrying user. TestZCodeStartPlanHasNoGLM53 (a Start Plan account before and after the Coding Plan's list was fetched, GLM-5.3 ticked or not; a Coding Plan account still has it; the mover's builtin) fails without the change (it listed GLM-5.3 GLM-5.3-Flash GLM-5.2 GLM-5-Turbo); TestMoveToPlugin's new case (a model the built-in didn't serve moves; one it did is named with "a@fake (Fake Start)"); plugin-move-unserved.test.cjs (WebKit, en and zh, with an account and without) fails without the app.js change and passes with it, plugin-move.test.cjs passes. No ZCode request or sign-in was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
This commit is contained in:
1 parent
06584e15fc
commit
96446806bd
11 files changed
+289
-9
No files matched your search
@@ -7986,7 +7986,9 @@ function moveWhy(m, p) {
|
||||
case "offline": return t("magpie couldn't reach npm to install the plugin. Check the network or proxy, then try again.");
|
||||
case "install": return t("npm couldn't install the plugin: {line}", { line: a.line });
|
||||
case "lapsed": return t("every {name} account needs signing in again. Sign one in above, then move.", { name: p.name });
|
||||
case "unserved": return t("the plugin doesn't serve {models}. Untick them under Models, or keep the built-in.", { models: a.models });
|
||||
case "unserved": return a.user
|
||||
? t("the plugin doesn't serve {models} for {user}, though the built-in does. Untick them under Models, or keep the built-in.", { models: a.models, user: a.user })
|
||||
: t("the plugin doesn't serve {models}. Untick them under Models, or keep the built-in.", { models: a.models });
|
||||
case "account": return t("{user} doesn't work through the plugin: {error}", { user: a.user, error: a.error });
|
||||
}
|
||||
return m.error || "";
|
||||
|
||||
@@ -479,6 +479,7 @@ const I18N = {
|
||||
"every {name} account needs signing in again. Sign one in above, then move.": "所有 {name} 账号都需要重新登录。先在上方登录一个,再迁移。",
|
||||
"every {name} account needs signing in again. Sign one in under Providers, then move.": "所有 {name} 账号都需要重新登录。先在「服务商」里登录一个,再迁移。",
|
||||
"the plugin doesn't serve {models}. Untick them under Models, or keep the built-in.": "插件不提供 {models}。可在「模型」里取消勾选,或继续使用内置。",
|
||||
"the plugin doesn't serve {models} for {user}, though the built-in does. Untick them under Models, or keep the built-in.": "插件没有为 {user} 提供 {models}(内置提供)。可在「模型」里取消勾选,或继续使用内置。",
|
||||
"{user} doesn't work through the plugin: {error}": "{user} 无法通过插件使用:{error}",
|
||||
"{name} now runs on its plugin — {accounts}, {models}. Use the built-in again from here any time.": "{name} 现在由插件运行——{accounts},{models}。随时可以在这里改回内置。",
|
||||
"{name} now runs on its plugin — {accounts}, {models}. Its editor in Providers moves it back.": "{name} 现在由插件运行——{accounts},{models}。在「服务商」里编辑它即可改回。",
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// A move held back by a model the plugin doesn't offer says for which
|
||||
// account (and its plan) the built-in serves it, so a reader can tell a
|
||||
// model really lost from one their plan never had; a why with no account
|
||||
// (an older magpie's) still reads as before. In English and Chinese; the
|
||||
// API is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
const pkg = "@magpie-community/opencode-zed-auth";
|
||||
|
||||
function serve(lang, posts, why) {
|
||||
let move = { package: pkg, state: "" };
|
||||
const payload = () => {
|
||||
const onPlugin = move.state === "plugin";
|
||||
const zed = {
|
||||
id: "zed", name: "Zed", icon: "zed", chat: onPlugin ? "plugin://zed/v1" : "", responses: "", anthropic: onPlugin ? "plugin://zed" : "", catalog: "",
|
||||
// enough models that the editor scrolls to reach Runs on
|
||||
models: Array.from({ length: 30 }, (_, i) => ({ id: "m" + i, name: "Model " + i, on: i === 0 })),
|
||||
agents: [], fallback: [], headers: {}, keyList: [], key: {},
|
||||
account: { agent: "zed", agentName: "Zed", user: "ada", logins: [{ user: "ada", active: true, on: true, own: true }, { user: "bob", on: true }] },
|
||||
move,
|
||||
};
|
||||
return {
|
||||
providers: [zed], presets: [], excluded: [], gateway: { running: true, window: true },
|
||||
plugins: onPlugin ? [{ id: "zed", pid: "zed", name: "Zed", icon: "zed", spec: pkg, signedIn: true, models: 1, methods: [] }] : [],
|
||||
onPlugins: onPlugin ? ["zed"] : [],
|
||||
};
|
||||
};
|
||||
return async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
|
||||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: { lang, theme: "light" } });
|
||||
if (url.pathname === "/api/providers") return json(payload());
|
||||
if (url.pathname === "/api/groups") return json({ groups: [] });
|
||||
// the page asks for the market in parts (#488)
|
||||
if (url.pathname === "/api/plugins/market" || url.pathname === "/api/plugins" || url.pathname === "/api/plugins/listings") {
|
||||
const m = ({ listings: [{ package: pkg, name: "Zed", icon: "zed", providers: ["zed"], community: true, replaces: "zed", summary: { en: "Zed", zh: "Zed" }, npm: { version: "0.1.0" } }],
|
||||
state: { bun: true, bunVersion: "1.3.0", plugins: [], movable: move.state === "plugin" ? [] : [{ id: "zed", name: "Zed", package: pkg, accounts: 2 }] } });
|
||||
return json(url.pathname === "/api/plugins" ? m.state : url.pathname === "/api/plugins/listings" ? { listings: m.listings } : m);
|
||||
}
|
||||
if (url.pathname === "/api/provider/move" || url.pathname === "/api/provider/moveback") {
|
||||
posts.push({ path: url.pathname, body: route.request().postDataJSON() });
|
||||
await new Promise((r) => setTimeout(r, 400)); // long enough to read the button
|
||||
// the plugin doesn't offer a model the built-in serves on ada
|
||||
if (url.pathname.endsWith("/move")) {
|
||||
move = { package: pkg, state: "failed", error: "unserved", why };
|
||||
return route.fulfill({ status: 400, json: { error: move.error, why } });
|
||||
}
|
||||
move = { package: pkg, state: url.pathname.endsWith("moveback") ? "back" : "plugin" };
|
||||
return json(payload());
|
||||
}
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
try { await route.fulfill({ body: await fs.readFile(file), contentType }); } catch { await route.fulfill({ status: 404, body: "" }); }
|
||||
};
|
||||
}
|
||||
|
||||
const L = {
|
||||
en: { look: "Review the move",
|
||||
withUser: "It stays built-in: the plugin doesn't serve Model 0 for ada (Zed Pro), though the built-in does. Untick them under Models, or keep the built-in.",
|
||||
bare: "It stays built-in: the plugin doesn't serve Model 0. Untick them under Models, or keep the built-in." },
|
||||
zh: { look: "查看迁移",
|
||||
withUser: "仍使用内置:插件没有为 ada (Zed Pro) 提供 Model 0(内置提供)。可在「模型」里取消勾选,或继续使用内置。",
|
||||
bare: "仍使用内置:插件不提供 Model 0。可在「模型」里取消勾选,或继续使用内置。" },
|
||||
};
|
||||
|
||||
const launch = (engine) => engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" });
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh"]) {
|
||||
for (const [kind, why] of [["withUser", { code: "unserved", args: { models: "Model 0", user: "ada (Zed Pro)" } }], ["bare", { code: "unserved", args: { models: "Model 0" } }]]) {
|
||||
test(`${engine} ${lang}: a move short of a model says it (${kind})`, async (t) => {
|
||||
const w = L[lang];
|
||||
const browser = await launch(engine);
|
||||
t.after(() => browser.close());
|
||||
const page = await (await browser.newContext({ viewport: { width: 900, height: 700 }, reducedMotion: "reduce" })).newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [], posts = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang, posts, why));
|
||||
await page.goto("http://magpie.test/?view=providers");
|
||||
const line = page.locator("#movable");
|
||||
await line.locator("button", { hasText: w.look }).click();
|
||||
const ed = page.locator("#modal .editor");
|
||||
await ed.locator(".move button").click();
|
||||
const said = ed.locator(".move-why");
|
||||
await said.waitFor({ state: "visible" });
|
||||
assert.equal(await said.innerText(), w[kind]);
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,8 @@ type Account struct {
|
||||
// reach, factory.go; another model for Copilot's Auto, copilot_refused.go).
|
||||
retry func(ctx context.Context, model string, status int, body []byte) bool
|
||||
// unusable is set on a Copilot account: whether a model its list offers
|
||||
// is one the account was refused (copilot_refused.go).
|
||||
// is one the account was refused (copilot_refused.go); and on a ZCode
|
||||
// account on the Start Plan: whether it is one only the Coding Plan has.
|
||||
unusable func(model string) bool
|
||||
// explain adds what the user can do about a refusal the account's
|
||||
// backend answered, "" when there is nothing to add (factory.go).
|
||||
|
||||
@@ -87,7 +87,8 @@ type Migration struct {
|
||||
// offline magpie couldn't reach npm (or Bun's download) to install the plugin
|
||||
// install npm couldn't install it: line
|
||||
// lapsed every account of name needs signing in again
|
||||
// unserved the plugin doesn't serve models (their names)
|
||||
// unserved the plugin doesn't serve models (their names) for user (with
|
||||
// its plan), though the built-in does
|
||||
// account user doesn't work through the plugin: error
|
||||
type MoveWhy struct {
|
||||
Code string `json:"code"`
|
||||
@@ -222,6 +223,11 @@ type mover struct {
|
||||
// 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
|
||||
// builtin is whether the built-in served model on account a, by the
|
||||
// plan a is on; nil for every model on every account. One it didn't
|
||||
// (a ZCode Start Plan account's GLM-5.3) is no loss when the plugin
|
||||
// doesn't list it either.
|
||||
builtin func(ctx context.Context, a Moving, model string) bool
|
||||
}
|
||||
|
||||
// movers are the deprecated built-ins and their plugins, by id. Once one
|
||||
@@ -598,10 +604,15 @@ func move(ctx context.Context, id string, mv *mover) (err error) {
|
||||
}
|
||||
if !tried {
|
||||
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)
|
||||
return slices.Contains(c.Models, m) || mv.served != nil && mv.served(m, c.Models) ||
|
||||
mv.builtin != nil && !mv.builtin(ctx, a, m)
|
||||
}); 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}
|
||||
who := a.User
|
||||
if a.Plan != "" {
|
||||
who += " (" + a.Plan + ")"
|
||||
}
|
||||
return &moveError{MoveWhy{Code: "unserved", Args: map[string]string{"models": names, "user": who}}, fmt.Sprintf("the plugin doesn't serve %s for %s, though the built-in does. Untick them under Models, or keep the built-in.", names, who), nil}
|
||||
}
|
||||
tried = true
|
||||
}
|
||||
|
||||
@@ -160,6 +160,28 @@ func TestMoveToPlugin(t *testing.T) {
|
||||
t.Fatalf("Move with a served variant and a model the plugin lacks = %v", err)
|
||||
}
|
||||
movers["fakeco"].served = nil
|
||||
// one the built-in never served on the account either (a ZCode Start
|
||||
// Plan account's GLM-5.3) is no loss and isn't named; one it did is,
|
||||
// with the account and its plan
|
||||
movers["fakeco"].builtin = func(_ context.Context, a Moving, m string) bool { return a.User != "a@fake" || m != "never-model" }
|
||||
reset(func() savedLogin { l := fakeLogin("a@fake", "r-a", false, true); l.Plan = "Fake Start"; return l }())
|
||||
inUse = []string{"fake-1", "never-model"}
|
||||
if err := Move(ctx, "fakeco"); err != nil {
|
||||
t.Fatalf("Move with a model the built-in didn't serve either = %v", err)
|
||||
}
|
||||
if err := MoveBack(ctx, "fakeco"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reset(func() savedLogin { l := fakeLogin("a@fake", "r-a", false, true); l.Plan = "Fake Start"; return l }())
|
||||
inUse = []string{"fake-1", "never-model", "gone-model"}
|
||||
err = Move(ctx, "fakeco")
|
||||
if err == nil || strings.Contains(err.Error(), "never-model") || !strings.Contains(err.Error(), "gone-model for a@fake (Fake Start)") {
|
||||
t.Fatalf("Move with a model the built-in served on the account = %v", err)
|
||||
}
|
||||
if w := WhyOf(err); w == nil || w.Code != "unserved" || w.Args["user"] != "a@fake (Fake Start)" || w.Args["models"] != "gone-model" {
|
||||
t.Fatalf("its why: %+v", w)
|
||||
}
|
||||
movers["fakeco"].builtin = nil
|
||||
inUse = []string{"fake-1", "fake-claude"}
|
||||
|
||||
// an account signed in through the plugin already gets back what it had
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
@@ -22,6 +23,18 @@ func init() {
|
||||
pkg: "@magpie-community/opencode-zcode-auth",
|
||||
min: "0.1.5", // a failure's status and its sign-in mark as the built-in's; MCP quota set aside as the built-in's
|
||||
agents: []string{"zcode"},
|
||||
// a Start Plan account was never served GLM-5.3, by the built-in
|
||||
// or by ZCode, and the plugin lists it no more than they do; its
|
||||
// plan as its requests found it, else as it showed it, nothing
|
||||
// asked of Z.ai
|
||||
builtin: func(_ context.Context, a Moving, model string) bool {
|
||||
for _, l := range zcodeLogins() {
|
||||
if strings.EqualFold(l.User, a.User) {
|
||||
return !zcodeOnStartAs(nil, l.key, l.Plan) || zcodeStartServes()(model)
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
out: func() ([]Moving, error) {
|
||||
var out []Moving
|
||||
for _, l := range zcodeLogins() {
|
||||
|
||||
@@ -536,6 +536,10 @@ func (p Provider) Exposed() []catalog.Model {
|
||||
// not in a Student plan's) or that it was refused is left out;
|
||||
// with none left, as if none were picked.
|
||||
picks = slices.DeleteFunc(slices.Clone(picks), func(id string) bool { _, ok := byID[id]; return !ok })
|
||||
} else if p.Account != nil && p.Account.unusable != nil {
|
||||
// with no list fetched, a pick the account can't be served (a
|
||||
// ZCode Start Plan account's GLM-5.3) is left out all the same
|
||||
picks = slices.DeleteFunc(slices.Clone(picks), p.Account.unusable)
|
||||
}
|
||||
if len(picks) > 0 {
|
||||
return pick(picks)
|
||||
|
||||
@@ -50,6 +50,7 @@ import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
@@ -340,17 +341,24 @@ func zcodeProvider(who, plan string, k zcodeKey) Provider {
|
||||
}
|
||||
acct.clientFor = zcodeStartClientFor
|
||||
acct.explain = func(status int, body []byte) string {
|
||||
if zcodeOnStart(nil, k) {
|
||||
if zcodeOnStartAs(nil, k, plan) {
|
||||
return zcodeStartExplain(status, body)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
acct.models = func() []catalog.Model {
|
||||
if zcodeOnStart(nil, k) {
|
||||
if zcodeOnStartAs(nil, k, plan) {
|
||||
return zcodeStartModels
|
||||
}
|
||||
return zcodeModels
|
||||
}
|
||||
// on the Start Plan, a model only the Coding Plan has (GLM-5.3) is
|
||||
// neither listed nor picked, though the list fetched last (another
|
||||
// account's, or this one's before its plan was found) has it
|
||||
startServes := sync.OnceValue(zcodeStartServes)
|
||||
acct.unusable = func(model string) bool {
|
||||
return zcodeOnStartAs(nil, k, plan) && !startServes()(model)
|
||||
}
|
||||
acct.fetch = func(ctx context.Context) ([]catalog.Model, error) {
|
||||
b := base(zcodeOnStart(ctx, k))
|
||||
ms, err := zcodeFetchModels(ctx, b)
|
||||
@@ -359,7 +367,7 @@ func zcodeProvider(who, plan string, k zcodeKey) Provider {
|
||||
}
|
||||
return ms, catalog.SaveLive("zcode", b, ms)
|
||||
}
|
||||
return Provider{ID: "zcode", Name: "ZCode", Icon: "zcode", Anthropic: base(zcodeOnStart(nil, k)), Website: "https://zcode.z.ai", Account: acct}
|
||||
return Provider{ID: "zcode", Name: "ZCode", Icon: "zcode", Anthropic: base(zcodeOnStartAs(nil, k, plan)), Website: "https://zcode.z.ai", Account: acct}
|
||||
}
|
||||
|
||||
// ---- allowance ----------------------------------------------------------------
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -60,6 +61,28 @@ var zcodeStartModels = func() []catalog.Model {
|
||||
return out
|
||||
}()
|
||||
|
||||
// zcodeStartNamed is whether a plan's name, as an account showed it, is
|
||||
// ZCode's Start Plan (zcodeBalance.active's name for it).
|
||||
func zcodeStartNamed(plan string) bool {
|
||||
p := strings.ToLower(plan)
|
||||
return strings.Contains(p, "start plan") || strings.Contains(p, "start-plan") || strings.Contains(plan, "体验")
|
||||
}
|
||||
|
||||
// zcodeStartServes is the Start Plan's models, as ZCode's config on this
|
||||
// machine lists them for account:zai-start-plan (zcodeStartModels without
|
||||
// one): whether it serves model. GLM-5.3 is the Coding Plan's alone.
|
||||
func zcodeStartServes() func(model string) bool {
|
||||
ms := zcodeStartModels
|
||||
if b, ok := zcodeLocalBuiltin(); ok {
|
||||
if l := b.models("account:zai-start-plan"); len(l) > 0 {
|
||||
ms = l
|
||||
}
|
||||
}
|
||||
return func(model string) bool {
|
||||
return slices.ContainsFunc(ms, func(m catalog.Model) bool { return strings.EqualFold(m.ID, model) })
|
||||
}
|
||||
}
|
||||
|
||||
// ZCodeStartBlockedHint is what the Start Plan's "request has been blocked
|
||||
// due to unusual activity" (HTTP 405, code 3012 "method not allowed")
|
||||
// means (#425): zcode.z.ai looks at what a request carries and turns away
|
||||
@@ -96,7 +119,14 @@ type zcodeRoute struct {
|
||||
// zcodeOnStart says whether k's requests go to the Start Plan, asking
|
||||
// when the last answer is old; with ctx nil it only says what was found
|
||||
// last (the Coding Plan when nothing was).
|
||||
func zcodeOnStart(ctx context.Context, k zcodeKey) bool {
|
||||
func zcodeOnStart(ctx context.Context, k zcodeKey) bool { return zcodeOnStartAs(ctx, k, "") }
|
||||
|
||||
// zcodeOnStartAs is zcodeOnStart for an account that showed plan: with
|
||||
// ctx nil and nothing found yet, the plan's name says it. An account with
|
||||
// a key and no Coding Plan (the key made, the plan not bought) is on the
|
||||
// Start Plan, and was shown the Coding Plan's models, GLM-5.3 among them,
|
||||
// until a request found where it goes.
|
||||
func zcodeOnStartAs(ctx context.Context, k zcodeKey, plan string) bool {
|
||||
if k.JWT == "" || k.team() { // a team's seat is on the team's plan (zcode_team.go)
|
||||
return false
|
||||
}
|
||||
@@ -107,6 +137,9 @@ func zcodeOnStart(ctx context.Context, k zcodeKey) bool {
|
||||
zcodeRoutes.Lock()
|
||||
r, ok := zcodeRoutes.m[id]
|
||||
zcodeRoutes.Unlock()
|
||||
if ctx == nil && !ok {
|
||||
return zcodeStartNamed(plan)
|
||||
}
|
||||
if ctx == nil || ok && time.Since(r.at) < r.ttl {
|
||||
return r.start
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
func zcodeIDs(ms []catalog.Model) []string {
|
||||
var out []string
|
||||
for _, m := range ms {
|
||||
out = append(out, m.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 悠悠哥's move to the plugin was held back by GLM-5.3 on a Start Plan
|
||||
// (体验套餐) account: a Z.ai account with a key made and no Coding Plan
|
||||
// bought, its requests going to the Start Plan, was shown the Coding
|
||||
// Plan's models until a request found where it goes, and the list fetched
|
||||
// last (the Coding Plan's) after that, so GLM-5.3 was listed and could be
|
||||
// ticked, though neither ZCode nor the built-in serves it there. Now such an
|
||||
// account neither lists nor exposes it, picked or not; a Coding Plan one
|
||||
// still does. And the move doesn't count it as one the plugin drops.
|
||||
func TestZCodeStartPlanHasNoGLM53(t *testing.T) {
|
||||
signIn(t)
|
||||
old := zcodeBuiltinFiles
|
||||
zcodeBuiltinFiles = func() []string { return nil } // not this machine's ZCode
|
||||
t.Cleanup(func() { zcodeBuiltinFiles = old })
|
||||
zcodeRoutes.Lock()
|
||||
zcodeRoutes.m = map[string]zcodeRoute{}
|
||||
zcodeRoutes.Unlock()
|
||||
jwt := zcodeTestJWT(time.Now().Add(24 * time.Hour))
|
||||
start := zcodeKey{Key: "made.key", Base: ZCodeZaiBase, JWT: jwt}
|
||||
saveLogins(t, savedLogin{Agent: "zcode", User: "trial@x", Plan: "Start Plan", First: true, On: true, Auth: json.RawMessage(jsonText(start))})
|
||||
|
||||
has53 := func(ms []catalog.Model) bool { return slices.Contains(zcodeIDs(ms), "GLM-5.3") }
|
||||
p, ok := zcodeAccount()
|
||||
if !ok {
|
||||
t.Fatal("no ZCode account")
|
||||
}
|
||||
// before any request: its plan's name says the Start Plan
|
||||
if ms := p.Available(); has53(ms) || len(ms) == 0 {
|
||||
t.Fatalf("a Start Plan account before its route is found lists %v", zcodeIDs(ms))
|
||||
}
|
||||
p.Models = []string{"GLM-5.3", "GLM-5.2"}
|
||||
if ms := p.Exposed(); has53(ms) || !slices.Contains(zcodeIDs(ms), "GLM-5.2") {
|
||||
t.Fatalf("a Start Plan account with GLM-5.3 ticked exposes %v", zcodeIDs(ms))
|
||||
}
|
||||
// with the Coding Plan's list fetched last
|
||||
if err := catalog.SaveLive("zcode", ZCodeZaiBase, zcodeModels); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.Models = nil
|
||||
if ms := p.Available(); has53(ms) || len(ms) == 0 {
|
||||
t.Fatalf("a Start Plan account under the Coding Plan's list lists %v", zcodeIDs(ms))
|
||||
}
|
||||
p.Models = []string{"GLM-5.3", "GLM-5.2"}
|
||||
if ms := p.Exposed(); has53(ms) || !slices.Contains(zcodeIDs(ms), "GLM-5.2") {
|
||||
t.Fatalf("a Start Plan account under the Coding Plan's list exposes %v", zcodeIDs(ms))
|
||||
}
|
||||
|
||||
// the move: GLM-5.3 the built-in didn't serve on it, GLM-5.2 it did
|
||||
a := Moving{User: "trial@x", Plan: "Start Plan"}
|
||||
if b := movers["zcode"].builtin; b(context.Background(), a, "GLM-5.3") || !b(context.Background(), a, "GLM-5.2") {
|
||||
t.Fatal("the move counts GLM-5.3 as served on a Start Plan account")
|
||||
}
|
||||
|
||||
// a Coding Plan account still has it
|
||||
coding := zcodeKey{Key: "plan.key", Base: ZCodeZaiBase, JWT: jwt}
|
||||
saveLogins(t, savedLogin{Agent: "zcode", User: "pro@x", Plan: "GLM Coding Pro", First: true, On: true, Auth: json.RawMessage(jsonText(coding))})
|
||||
p, _ = zcodeAccount()
|
||||
p.Models = []string{"GLM-5.3"}
|
||||
if !has53(p.Available()) || !has53(p.Exposed()) {
|
||||
t.Fatalf("a Coding Plan account lists %v", zcodeIDs(p.Available()))
|
||||
}
|
||||
if !movers["zcode"].builtin(context.Background(), Moving{User: "pro@x", Plan: "GLM Coding Pro"}, "GLM-5.3") {
|
||||
t.Fatal("the move counts GLM-5.3 as not served on a Coding Plan account")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user