package provider // A ZCode subscription is Z.ai's GLM Coding Plan, which ZCode (Zhipu's // desktop app) signs in to. The plan is served on an Anthropic-compatible // endpoint to a plain API key, `.`, that ZCode mints for the // account and names zcode-api-key; magpie uses that key as ZCode does. // // ZCode's own account is read, never changed, from its credential store, // ~/.zcode/v2/credentials.json: each value is "enc:v1:" + iv.tag.ciphertext // (base64url), AES-256-GCM under sha256 of $ZCODE_CREDENTIAL_SECRET, or // else of "zcode-credential-fallback:::". Further // accounts are signed in by magpie with ZCode's own polling sign-in // (zcode.z.ai/api/v1/oauth/cli/…), and their key is kept in logins.json. // // The plan has no model list to ask; the models are ZCode's, as its // built-in config lists them for the coding plan. import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "os/user" "path/filepath" "runtime" "strings" "time" "github.com/yetone/magpie/internal/catalog" ) // Where ZCode's coding plan and its sign-in are; vars so tests can point // them elsewhere. var ( ZCodeZaiBase = "https://api.z.ai/api/anthropic" ZCodeBigModelBase = "https://open.bigmodel.cn/api/anthropic" zcodeAPI = "https://zcode.z.ai" zcodeZaiAPI = "https://api.z.ai" ) // zcodeAppVersion is the ZCode whose sign-in magpie makes. const zcodeAppVersion = "3.14.3" var zcodeModels = []catalog.Model{ {ID: "GLM-5.3", Name: "GLM-5.3", Context: 1_000_000}, {ID: "GLM-5.3-Flash", Name: "GLM-5.3-Flash", Context: 1_000_000}, {ID: "GLM-5.2", Name: "GLM-5.2", Context: 1_000_000}, {ID: "GLM-5-Turbo", Name: "GLM-5-Turbo", Context: 200_000}, } // zcodeKey is a coding plan's key and where it is served. type zcodeKey struct { Key string `json:"apiKey"` Base string `json:"base"` } // ---- ZCode's own account ------------------------------------------------------ func zcodeCredentialsPath() string { home, _ := os.UserHomeDir() return filepath.Join(home, ".zcode", "v2", "credentials.json") } // zcodeSecret is the key ZCode encrypts its credentials with. func zcodeSecret() []byte { seed := os.Getenv("ZCODE_CREDENTIAL_SECRET") if seed == "" { home, _ := os.UserHomeDir() name := "" if u, err := user.Current(); err == nil { name = u.Username if i := strings.LastIndexByte(name, '\\'); i >= 0 { name = name[i+1:] // DOMAIN\user; node's userInfo() has the user alone } } platform := runtime.GOOS if platform == "windows" { platform = "win32" } seed = "zcode-credential-fallback:" + platform + ":" + home + ":" + name } sum := sha256.Sum256([]byte(seed)) return sum[:] } func zcodeDecrypt(secret []byte, v string) (string, bool) { rest, ok := strings.CutPrefix(v, "enc:v1:") if !ok { return "", false } parts := strings.Split(rest, ".") if len(parts) != 3 { return "", false } var raw [3][]byte for i, p := range parts { b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(p, "=")) if err != nil { return "", false } raw[i] = b } block, err := aes.NewCipher(secret) if err != nil { return "", false } gcm, err := cipher.NewGCMWithNonceSize(block, len(raw[0])) if err != nil { return "", false } out, err := gcm.Open(nil, raw[0], append(raw[2], raw[1]...), nil) return string(out), err == nil } // zcodeOwn is the account ZCode is signed in to and its coding plan's key; // ok is false when it has none. func zcodeOwn() (who string, k zcodeKey, ok bool) { var store map[string]string if !readJSON(zcodeCredentialsPath(), &store) { return "", zcodeKey{}, false } secret := zcodeSecret() for name, v := range store { // account-provider:coding-plan:account:zai-individual-coding-plan:account::api-key if !strings.Contains(name, ":coding-plan:") || !strings.HasSuffix(name, ":api-key") { continue } key, ok := zcodeDecrypt(secret, v) if !ok || !strings.Contains(key, ".") { continue } base := ZCodeZaiBase if strings.Contains(name, ":bigmodel-") { base = ZCodeBigModelBase } // Z.ai's first, as ZCode lists it if k.Key == "" || (base == ZCodeZaiBase && k.Base != ZCodeZaiBase) { k = zcodeKey{Key: key, Base: base} } } if k.Key == "" { return "", zcodeKey{}, false } var info struct { Email string `json:"email"` Name string `json:"name"` ID string `json:"user_id"` } if v, ok := store["oauth:zai:user_info"]; ok { if s, ok := zcodeDecrypt(secret, v); ok { _ = json.Unmarshal([]byte(s), &info) } } return zcodeWho(info.Email, info.Name, info.ID), k, true } // zcodeWho names a Z.ai account: its email, or the phone number of one // signed in by phone (its email is then @phone.local). func zcodeWho(email, name, id string) string { email = strings.TrimSuffix(email, "@phone.local") return firstNonEmpty(email, name, id, "ZCode") } func firstNonEmpty(ss ...string) string { for _, s := range ss { if s = strings.TrimSpace(s); s != "" { return s } } return "" } // ---- the accounts ------------------------------------------------------------- type zcodeLoginKey struct { Login key zcodeKey } func zcodeSaved(l savedLogin) (zcodeKey, bool) { var k zcodeKey if json.Unmarshal(l.Auth, &k) != nil || k.Key == "" { return zcodeKey{}, false } if k.Base == "" { k.Base = ZCodeZaiBase } return k, true } // zcodeLogins is every ZCode account signed in, the first in use first. func zcodeLogins() []zcodeLoginKey { ownUser, own, hasOwn := zcodeOwn() if !hasOwn { ownUser = "" } var out []zcodeLoginKey for _, l := range sideLogins("zcode", ownUser, func(l savedLogin) bool { _, ok := zcodeSaved(l) return ok }) { k := own if !l.saved.own() { k, _ = zcodeSaved(l.saved) } out = append(out, zcodeLoginKey{l.Login, k}) } return out } func zcodeSide() []sideLogin { var out []sideLogin for _, z := range zcodeLogins() { out = append(out, sideLogin{Login: z.Login}) } return out } func zcodeLoginList() []Login { return loginsOf(zcodeSide()) } func switchZCodeLogin(user string) error { return switchSideLogin("zcode", user, zcodeSide()) } func setZCodeLoginOn(user string, on bool) error { return setSideLoginOn("zcode", user, on, zcodeSide()) } func forgetZCodeLogin(user string) error { return forgetSideLogin("zcode", user, "ZCode's own sign-in; sign out in ZCode", zcodeSide(), nil) } func zcodeAccount() (Provider, bool) { ls := zcodeLogins() if len(ls) == 0 { return Provider{}, false } return zcodeProvider(ls[0].User, ls[0].Plan, ls[0].key), true } // zcodeAlsoOn is the ZCode accounts in use behind the first. func zcodeAlsoOn() []Provider { var out []Provider for _, l := range zcodeLogins() { if !l.Active && l.On { out = append(out, zcodeProvider(l.User, l.Plan, l.key)) } } return out } func zcodeProvider(who, plan string, k zcodeKey) Provider { acct := &Account{Agent: "zcode", User: who, Plan: plan} acct.sign = func(ctx context.Context, req *http.Request, body []byte) error { req.Header.Del("Authorization") req.Header.Set("x-api-key", k.Key) req.Header.Set("Authorization", "Bearer "+k.Key) return nil } acct.models = func() []catalog.Model { return zcodeModels } return Provider{ID: "zcode", Name: "ZCode", Icon: "zcode", Anthropic: k.Base, Website: "https://zcode.z.ai", Account: acct} } // ---- allowance ---------------------------------------------------------------- // zcodeQuota is a coding plan's allowance: credits per five hours and per // week, as ZCode shows them. func zcodeQuota(ctx context.Context, l Login, k zcodeKey) SubscriptionQuota { q := SubscriptionQuota{Provider: "zcode", Name: "ZCode", Icon: "zcode", Plan: l.Plan, User: l.User, Windows: []QuotaWindow{}} var data struct { Limits []struct { Type string `json:"type"` Unit int `json:"unit"` Number int `json:"number"` Usage float64 `json:"usage"` Current float64 `json:"currentValue"` Remaining float64 `json:"remaining"` Percent float64 `json:"percentage"` Reset int64 `json:"nextResetTime"` } `json:"limits"` Level string `json:"level"` } if err := zcodeGet(ctx, zcodeRoot(k.Base)+"/api/monitor/usage/quota/limit", k.Key, &data); err != nil { q.Error = err.Error() return q } if data.Level != "" { q.Plan = "GLM Coding " + strings.ToUpper(data.Level[:1]) + data.Level[1:] } for _, x := range data.Limits { span := zcodeSpan(x.Unit, x.Number) w := QuotaWindow{Name: zcodeWindowName(span), Used: x.Percent, Span: span} if x.Usage > 0 { used := x.Usage - x.Remaining w.Used = 100 * used / x.Usage w.Display = fmt.Sprintf("%s / %s", compactNumber(used), compactNumber(x.Usage)) } if x.Reset > 0 { t := time.UnixMilli(x.Reset) w.ResetsAt = &t } q.Windows = append(q.Windows, w) } return q } // zcodeSpan reads a limit's window: unit 3 counts hours, 6 weeks (and // 4, 5 days and months by the same count). func zcodeSpan(unit, n int) time.Duration { n = max(n, 1) switch unit { case 1: return time.Duration(n) * time.Minute case 3: return time.Duration(n) * time.Hour case 4: return time.Duration(n) * 24 * time.Hour case 5: return time.Duration(n) * 30 * 24 * time.Hour case 6: return time.Duration(n) * 7 * 24 * time.Hour } return 0 } func zcodeWindowName(span time.Duration) string { switch { case span == 0: return "Credits" case span < 24*time.Hour: return fmt.Sprintf("%d hours", int(span.Hours())) case span == 7*24*time.Hour: return "Weekly" case span >= 28*24*time.Hour: return "Monthly" } return fmt.Sprintf("%d days", int(span.Hours()/24)) } // zcodeRoot is the site a plan's endpoint is on: https://api.z.ai. func zcodeRoot(base string) string { if u, err := url.Parse(base); err == nil && u.Host != "" { return u.Scheme + "://" + u.Host } return zcodeZaiAPI } func zcodeLoginQuota(ctx context.Context, l Login) SubscriptionQuota { for _, z := range zcodeLogins() { if strings.EqualFold(z.User, l.User) { return zcodeQuota(ctx, l, z.key) } } return SubscriptionQuota{Provider: "zcode", Plan: l.Plan, Windows: []QuotaWindow{}, Error: "not signed in"} } // zcodePlan names the coding plan a key has, "" for none. func zcodePlan(ctx context.Context, k zcodeKey) (string, error) { var subs []struct { Product string `json:"productName"` Status string `json:"status"` } if err := zcodeGet(ctx, zcodeRoot(k.Base)+"/api/biz/subscription/list", k.Key, &subs); err != nil { return "", err } for _, s := range subs { if strings.EqualFold(s.Status, "VALID") { return s.Product, nil } } return "", nil } // ---- Z.ai's API --------------------------------------------------------------- // zcodeCall asks one of Z.ai's JSON endpoints, which wrap what they say in // {code, msg, data}: code 0 or 200 is a success. func zcodeCall(ctx context.Context, method, u, auth string, body, dst any) error { var rd io.Reader if body != nil { b, _ := json.Marshal(body) rd = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, u, rd) if err != nil { return err } if auth != "" { req.Header.Set("Authorization", auth) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "ZCode/"+zcodeAppVersion) res, err := http.DefaultClient.Do(req) if err != nil { return err } defer res.Body.Close() b, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20)) var env struct { Code json.RawMessage `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } if res.StatusCode < 200 || res.StatusCode >= 300 { if json.Unmarshal(b, &env) == nil && env.Msg != "" { return fmt.Errorf("%s (%d)", env.Msg, res.StatusCode) } return &accountStatusError{status: res.StatusCode} } if err := json.Unmarshal(b, &env); err != nil { return err } if c := strings.Trim(string(env.Code), `"`); c != "" && c != "0" && c != "200" && c != "null" { return errors.New(firstNonEmpty(env.Msg, "error "+c)) } if dst == nil || len(env.Data) == 0 || string(env.Data) == "null" { return nil } return json.Unmarshal(env.Data, dst) } // zcodeGet asks with a coding plan's key, which goes bare in Authorization. func zcodeGet(ctx context.Context, u, key string, dst any) error { return zcodeCall(ctx, http.MethodGet, u, key, nil, dst) } // ---- signing in --------------------------------------------------------------- // startZCodeSignIn is ZCode's own polling sign-in: zcode.z.ai opens a flow, // the browser signs in to Z.ai, and the flow is asked until it is ready. // The account's coding plan key is then found or made, as ZCode does it. func startZCodeSignIn(s *signInFlow) error { ctx, cancel := context.WithCancel(context.Background()) b := make([]byte, 32) _, _ = rand.Read(b) poll := "Bearer " + hex.EncodeToString(b) var flow struct { ID string `json:"flow_id"` URL string `json:"authorize_url"` Expires float64 `json:"expires_at"` Interval float64 `json:"poll_interval_sec"` } if err := zcodeCall(ctx, http.MethodPost, zcodeAPI+"/api/v1/oauth/cli/init", poll, map[string]string{"provider": "zai"}, &flow); err != nil { cancel() return fmt.Errorf("ZCode sign-in: %w", err) } u, err := url.Parse(flow.URL) if flow.ID == "" || err != nil || u.Scheme != "https" { cancel() return errors.New("ZCode gave no sign-in page") } // where Z.ai sends the browser back, as ZCode sets it back := zcodeAPI + "/app/oauth/login?" + url.Values{"redirect": {"zcode://oauth/callback"}, "app_version": {zcodeAppVersion}}.Encode() q := u.Query() q.Set("redirect_uri", back) u.RawQuery = q.Encode() s.mu.Lock() s.st.URL = u.String() s.stop = cancel s.mu.Unlock() interval := time.Duration(max(flow.Interval, 1)) * time.Second deadline := time.Unix(int64(flow.Expires), 0) if flow.Expires == 0 { deadline = time.Now().Add(5 * time.Minute) } go func() { defer cancel() fail := func(msg string) { s.finish(SignInState{State: "failed", Error: msg}) } for { select { case <-ctx.Done(): return case <-time.After(interval): } if time.Now().After(deadline) { fail("the sign-in expired; start again") return } var got struct { Status string `json:"status"` Zai struct { AccessToken string `json:"access_token"` } `json:"zai"` User struct { ID string `json:"user_id"` Email string `json:"email"` Name string `json:"name"` } `json:"user"` } err := zcodeCall(ctx, http.MethodGet, zcodeAPI+"/api/v1/oauth/cli/poll/"+url.PathEscape(flow.ID), poll, nil, &got) switch { case ctx.Err() != nil: return case err != nil: var st *accountStatusError if errors.As(err, &st) && st.status >= 400 && st.status < 500 && st.status != 408 && st.status != 429 { fail("ZCode sign-in: " + err.Error()) return } continue // a hiccup: ask again case got.Status == "pending" || got.Status == "": continue case got.Status == "failed": fail("the sign-in was declined on Z.ai") return case got.Status != "ready" || got.Zai.AccessToken == "": fail("ZCode sign-in: unexpected answer " + got.Status) return } who := zcodeWho(got.User.Email, got.User.Name, got.User.ID) k, plan, err := zcodeMintKey(ctx, got.Zai.AccessToken) if err != nil { fail(err.Error()) return } auth, _ := json.Marshal(k) if err := addSideLogin(savedLogin{Agent: "zcode", User: who, Plan: plan, Auth: auth}, func(savedLogin) {}); err != nil { fail(err.Error()) return } ownUser, _, ok := zcodeOwn() s.finish(SignInState{State: "done", User: who, Plan: plan, Using: ok && strings.EqualFold(ownUser, who)}) return } }() return nil } // zcodeMintKey turns a Z.ai sign-in into its coding plan key: Z.ai's // business token, then the key named zcode-api-key in the account's default // project, made if it isn't there, with its secret. func zcodeMintKey(ctx context.Context, zaiToken string) (zcodeKey, string, error) { var biz struct { Token string `json:"access_token"` } if err := zcodeCall(ctx, http.MethodPost, zcodeZaiAPI+"/api/auth/z/login", "", map[string]string{"token": zaiToken}, &biz); err != nil || biz.Token == "" { return zcodeKey{}, "", fmt.Errorf("Z.ai sign-in: %v", firstErr(err, "no token")) } bearer := "Bearer " + biz.Token var info struct { Orgs []struct { ID string `json:"organizationId"` Name string `json:"organizationName"` Projects []struct { ID string `json:"projectId"` Name string `json:"projectName"` Type any `json:"projectType"` } `json:"projects"` } `json:"organizations"` } if err := zcodeCall(ctx, http.MethodGet, zcodeZaiAPI+"/api/biz/customer/getCustomerInfo", bearer, nil, &info); err != nil { return zcodeKey{}, "", fmt.Errorf("Z.ai account: %w", err) } // the default organization and project, as ZCode picks them org, proj := "", "" for _, o := range info.Orgs { var ps []string def := "" for _, p := range o.Projects { if p.ID == "" || fmt.Sprint(p.Type) == "2" { continue } ps = append(ps, p.ID) if def == "" && strings.Contains(p.Name, "默认项目") { def = p.ID } } if o.ID == "" || len(ps) == 0 { continue } if def == "" { def = ps[0] } if org == "" || strings.Contains(o.Name, "默认机构") { org, proj = o.ID, def if strings.Contains(o.Name, "默认机构") { break } } } if org == "" { return zcodeKey{}, "", errors.New("this Z.ai account has no project for an API key") } keys := zcodeZaiAPI + "/api/biz/v1/organization/" + url.PathEscape(org) + "/projects/" + url.PathEscape(proj) + "/api_keys" type apiKey struct { Name string `json:"name"` APIKey string `json:"apiKey"` } var list []apiKey if err := zcodeCall(ctx, http.MethodGet, keys, bearer, nil, &list); err != nil { return zcodeKey{}, "", fmt.Errorf("Z.ai API keys: %w", err) } id := "" for _, k := range list { if k.Name == "zcode-api-key" { id = strings.TrimSpace(k.APIKey) } } if id == "" { var made apiKey if err := zcodeCall(ctx, http.MethodPost, keys, bearer, map[string]string{"name": "zcode-api-key"}, &made); err != nil { return zcodeKey{}, "", fmt.Errorf("Z.ai API key: %w", err) } id = strings.TrimSpace(made.APIKey) } var secret struct { Secret string `json:"secretKey"` } if id != "" { if err := zcodeCall(ctx, http.MethodGet, keys+"/copy/"+url.PathEscape(id), bearer, nil, &secret); err != nil { return zcodeKey{}, "", fmt.Errorf("Z.ai API key: %w", err) } } if id == "" || strings.TrimSpace(secret.Secret) == "" { return zcodeKey{}, "", errors.New("Z.ai gave no API key") } k := zcodeKey{Key: id + "." + strings.TrimSpace(secret.Secret), Base: ZCodeZaiBase} plan, err := zcodePlan(ctx, k) if err != nil { return zcodeKey{}, "", fmt.Errorf("GLM Coding Plan: %w", err) } if plan == "" { return zcodeKey{}, "", errors.New("this Z.ai account has no GLM Coding Plan — subscribe at z.ai/subscribe, then add it again") } return k, plan, nil } func firstErr(err error, otherwise string) string { if err != nil { return err.Error() } return otherwise }