@magpie-community/opencode-zcode-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/index.mjs +711 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @magpie-community/opencode-zcode-auth
|
|
2
|
+
|
|
3
|
+
Signs in to Z.ai's GLM Coding Plan, or BigModel's (智谱). It does this the
|
|
4
|
+
way [ZCode](https://zcode.z.ai) does, and serves the plan's GLM models
|
|
5
|
+
over Anthropic's Messages API. Provider id: `zcode`.
|
|
6
|
+
|
|
7
|
+
## Signing in
|
|
8
|
+
|
|
9
|
+
- **ZCode: Z.ai GLM Coding Plan** and **ZCode: BigModel (智谱) GLM Coding Plan**
|
|
10
|
+
- zcode.z.ai opens a sign-in flow, and the browser signs in to Z.ai or to
|
|
11
|
+
BigModel. The plugin polls the flow until it is done, so there is
|
|
12
|
+
nothing to paste.
|
|
13
|
+
- With that sign-in it takes the key named `zcode-api-key` from the
|
|
14
|
+
account's default project, or makes it if it isn't there, as ZCode does.
|
|
15
|
+
Requests go to `api.z.ai/api/anthropic` or
|
|
16
|
+
`open.bigmodel.cn/api/anthropic`.
|
|
17
|
+
- If the account has no plan of its own, it looks for a seat on a team's
|
|
18
|
+
plan and uses that team project's `zcode-team-api-key`.
|
|
19
|
+
- Failing both, it uses ZCode's free **Start Plan**, served by zcode.z.ai
|
|
20
|
+
with ZCode's session token and ZCode's own headers.
|
|
21
|
+
- The Start Plan's token can't be refreshed. When it runs out, sign in
|
|
22
|
+
again.
|
|
23
|
+
- **GLM Coding Plan API key**
|
|
24
|
+
- Paste a key (`<id>.<secret>`) and say which site it is from.
|
|
25
|
+
|
|
26
|
+
## Where the sign-in is kept
|
|
27
|
+
|
|
28
|
+
In OpenCode's `auth.json` (magpie: `plugin-auth.json`), under `zcode`:
|
|
29
|
+
|
|
30
|
+
- **The browser sign-ins** are kept as an `oauth` entry:
|
|
31
|
+
- `access` is the key.
|
|
32
|
+
- `refresh` is JSON holding the site, the endpoint, the key, ZCode's
|
|
33
|
+
token, the team project, the plan and a device id of this plugin's own.
|
|
34
|
+
- **A pasted key** is kept as an `api` entry, with the site in `metadata`.
|
|
35
|
+
|
|
36
|
+
The plugin never reads or writes ZCode's own credential store.
|
|
37
|
+
|
|
38
|
+
## How requests are routed
|
|
39
|
+
|
|
40
|
+
Before each request, the plugin checks which plan the account is on, and
|
|
41
|
+
asks again every 10 minutes:
|
|
42
|
+
|
|
43
|
+
- If the key's account has a GLM Coding Plan
|
|
44
|
+
(`/api/biz/subscription/list`), requests go to that plan.
|
|
45
|
+
- Otherwise they go to the Start Plan.
|
|
46
|
+
|
|
47
|
+
Every request carries the key as both `x-api-key` and
|
|
48
|
+
`Authorization: Bearer`.
|
|
49
|
+
|
|
50
|
+
## Models
|
|
51
|
+
|
|
52
|
+
The models are the ones ZCode offers for the account's plan. They come
|
|
53
|
+
from ZCode's provider config: the release zcode.z.ai names, or an
|
|
54
|
+
installed ZCode's copy if that one is newer. The list is kept for 10
|
|
55
|
+
minutes. When the config can't be read, the plugin falls back to this list:
|
|
56
|
+
|
|
57
|
+
| Model | Context | Output | Efforts |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| GLM-5.3 | 1M | 128K | low, high, max |
|
|
60
|
+
| GLM-5.3-Flash | 1M | 128K | low, high, max |
|
|
61
|
+
| GLM-5.2 | 1M | 128K | none, high, max |
|
|
62
|
+
| GLM-5-Turbo | 200K | 64K | none, high |
|
|
63
|
+
|
|
64
|
+
The Start Plan has all of these except GLM-5.3.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
// ZCode's GLM Coding Plan, as an OpenCode provider plugin.
|
|
2
|
+
//
|
|
3
|
+
// ZCode (zcode.z.ai) signs in to a Z.ai account, or a BigModel (智谱,
|
|
4
|
+
// bigmodel.cn) one, through a flow zcode.z.ai opens and is polled for. With
|
|
5
|
+
// the account's sign-in it finds or makes the coding plan key it uses (the
|
|
6
|
+
// key named zcode-api-key in the account's default project) and sends the
|
|
7
|
+
// models' requests, Anthropic's Messages, to the plan's endpoint:
|
|
8
|
+
// api.z.ai/api/anthropic or open.bigmodel.cn/api/anthropic.
|
|
9
|
+
//
|
|
10
|
+
// An account with no plan of its own may have a seat on a team's plan (a
|
|
11
|
+
// project of type 2, and its zcode-team-api-key), or ZCode's free Start
|
|
12
|
+
// Plan, served by zcode.z.ai itself to ZCode's own session token (a JWT
|
|
13
|
+
// that can't be refreshed: when it runs out, sign in again).
|
|
14
|
+
//
|
|
15
|
+
// The sign-in is kept as OpenCode keeps an OAuth one: `access` is the key,
|
|
16
|
+
// `refresh` the rest of it as JSON (site, base, key, jwt, team project,
|
|
17
|
+
// plan, device id), `accountId` the account's email.
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
20
|
+
import { homedir } from "node:os"
|
|
21
|
+
import { join } from "node:path"
|
|
22
|
+
|
|
23
|
+
const PROVIDER = "zcode"
|
|
24
|
+
const ZCODE = "https://zcode.z.ai"
|
|
25
|
+
const ZAI_API = "https://api.z.ai"
|
|
26
|
+
const BIGMODEL_API = "https://bigmodel.cn" // BigModel's business API, where ZCode asks it
|
|
27
|
+
const ZAI_BASE = "https://api.z.ai/api/anthropic"
|
|
28
|
+
const BIGMODEL_BASE = "https://open.bigmodel.cn/api/anthropic"
|
|
29
|
+
const START_BASE = ZCODE + "/api/v1/zcode-plan/anthropic"
|
|
30
|
+
const APP_VERSION = "3.14.3" // the ZCode whose sign-in this makes
|
|
31
|
+
const UA = "ZCode/" + APP_VERSION
|
|
32
|
+
|
|
33
|
+
// ZCode's models before its config is read; the Start Plan has all but GLM-5.3.
|
|
34
|
+
const MODELS = [
|
|
35
|
+
{ id: "GLM-5.3", context: 1_000_000, output: 128_000, efforts: ["low", "high", "max"] },
|
|
36
|
+
{ id: "GLM-5.3-Flash", context: 1_000_000, output: 128_000, efforts: ["low", "high", "max"] },
|
|
37
|
+
{ id: "GLM-5.2", context: 1_000_000, output: 128_000, efforts: ["none", "high", "max"] },
|
|
38
|
+
{ id: "GLM-5-Turbo", context: 200_000, output: 64_000, efforts: ["none", "high"] },
|
|
39
|
+
]
|
|
40
|
+
const START_MODELS = MODELS.filter((m) => m.id !== "GLM-5.3")
|
|
41
|
+
const OUTPUT = 131_072 // when ZCode's config names none
|
|
42
|
+
|
|
43
|
+
const SITES = {
|
|
44
|
+
zai: { name: "Z.ai", api: ZAI_API, base: ZAI_BASE, subscribe: "z.ai/subscribe" },
|
|
45
|
+
bigmodel: { name: "BigModel", api: BIGMODEL_API, base: BIGMODEL_BASE, subscribe: "bigmodel.cn/glm-coding" },
|
|
46
|
+
}
|
|
47
|
+
const siteOf = (s) => (s === "bigmodel" ? "bigmodel" : "zai")
|
|
48
|
+
|
|
49
|
+
const platform = () => `${process.platform}-${process.arch}` // darwin-arm64, win32-x64, linux-x64, as ZCode names them
|
|
50
|
+
const uuid = () => crypto.randomUUID()
|
|
51
|
+
const anyDevice = uuid() // X-Device-Mid for calls made signed out (the model list)
|
|
52
|
+
const first = (...ss) => ss.map((s) => (typeof s === "string" ? s.trim() : "")).find((s) => s) ?? ""
|
|
53
|
+
|
|
54
|
+
class ZError extends Error {
|
|
55
|
+
constructor(message, status) {
|
|
56
|
+
super(message)
|
|
57
|
+
this.status = status
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// call asks one of Z.ai's JSON endpoints, which wrap what they say in
|
|
62
|
+
// {code, msg, data}: a code of 0 or 200 is a success. zcode.z.ai is told
|
|
63
|
+
// the machine's id (X-Device-Mid), as ZCode tells it.
|
|
64
|
+
async function call(method, url, { auth, body, headers, device, signal } = {}) {
|
|
65
|
+
const h = { "Content-Type": "application/json", Accept: "application/json", "User-Agent": UA }
|
|
66
|
+
if (auth) h.Authorization = auth
|
|
67
|
+
if (url.startsWith(ZCODE + "/")) h["X-Device-Mid"] = device || anyDevice
|
|
68
|
+
Object.assign(h, headers ?? {})
|
|
69
|
+
const res = await fetch(url, { method, headers: h, body: body === undefined ? undefined : JSON.stringify(body), signal })
|
|
70
|
+
const text = await res.text()
|
|
71
|
+
let env
|
|
72
|
+
try {
|
|
73
|
+
env = JSON.parse(text)
|
|
74
|
+
} catch {}
|
|
75
|
+
const code = env?.code == null ? "" : String(env.code)
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
if (env?.msg) throw new ZError(code && code !== "0" ? `${env.msg} (${res.status}, code ${code})` : `${env.msg} (${res.status})`, res.status)
|
|
78
|
+
throw new ZError(`${res.status} ${res.statusText}`.trim(), res.status)
|
|
79
|
+
}
|
|
80
|
+
if (!env || typeof env !== "object") throw new ZError("not JSON", res.status)
|
|
81
|
+
if (code && code !== "0" && code !== "200") throw new ZError(env.msg || `error ${code}`, res.status)
|
|
82
|
+
return env.data ?? null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- the account's plan ----------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
const rootOf = (base) => {
|
|
88
|
+
try {
|
|
89
|
+
const u = new URL(base)
|
|
90
|
+
return `${u.protocol}//${u.host}`
|
|
91
|
+
} catch {
|
|
92
|
+
return ZAI_API
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const isTeam = (s) => !!(s.org && s.project)
|
|
96
|
+
|
|
97
|
+
// plan names the coding plan a key has, "" for none.
|
|
98
|
+
async function plan(s) {
|
|
99
|
+
const subs = (await call("GET", rootOf(s.base) + "/api/biz/subscription/list", { auth: s.key })) ?? []
|
|
100
|
+
return (Array.isArray(subs) ? subs : []).find((x) => String(x.status).toUpperCase() === "VALID")?.productName ?? ""
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function jwtExpiry(jwt) {
|
|
104
|
+
const p = String(jwt ?? "").split(".")
|
|
105
|
+
if (p.length !== 3) return 0
|
|
106
|
+
try {
|
|
107
|
+
const claims = JSON.parse(Buffer.from(p[1], "base64url").toString("utf8"))
|
|
108
|
+
return Number(claims.exp) > 0 ? Number(claims.exp) * 1000 : 0
|
|
109
|
+
} catch {
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const jwtExpired = (jwt) => {
|
|
114
|
+
const t = jwtExpiry(jwt)
|
|
115
|
+
return t > 0 && Date.now() > t
|
|
116
|
+
}
|
|
117
|
+
const EXPIRED = "ZCode's sign-in has expired; sign in again"
|
|
118
|
+
|
|
119
|
+
const num = (v) => {
|
|
120
|
+
const n = typeof v === "string" ? parseFloat(v) : v
|
|
121
|
+
return typeof n === "number" && Number.isFinite(n) ? n : undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// startPlan is the Start Plan the account has now ({name, until}), as
|
|
125
|
+
// ZCode reads its balance: an "active" plan past its end is over.
|
|
126
|
+
async function startPlan(jwt, device) {
|
|
127
|
+
if (!jwt) throw new Error("not signed in to ZCode")
|
|
128
|
+
if (jwtExpired(jwt)) throw new Error(EXPIRED)
|
|
129
|
+
const b = (await call("GET", `${ZCODE}/api/v1/zcode-plan/billing/balance?app_version=${APP_VERSION}`, { auth: "Bearer " + jwt, device })) ?? {}
|
|
130
|
+
const now = num(b.server_time) > 0 ? num(b.server_time) : Date.now() / 1000
|
|
131
|
+
const isStart = (x) => x.includes("start-plan") || x.includes("start plan")
|
|
132
|
+
for (const p of b.plans ?? []) {
|
|
133
|
+
if (String(p.status ?? "").trim().toLowerCase() !== "active") continue
|
|
134
|
+
const end = num(p.ends_at)
|
|
135
|
+
if (end > 0 && end <= now) continue
|
|
136
|
+
const id = String(p.plan_id ?? "").trim().toLowerCase()
|
|
137
|
+
const n = String(p.name ?? "").trim().toLowerCase()
|
|
138
|
+
if ((id || n) && !isStart(id) && !isStart(n)) continue
|
|
139
|
+
return { name: first(p.name, "Start Plan"), until: end > 0 ? end * 1000 : 0 }
|
|
140
|
+
}
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// onStart says whether an account's requests go to the Start Plan: when it
|
|
145
|
+
// has ZCode's token and no coding plan key, or a key whose account has no
|
|
146
|
+
// coding plan. Asked again after 10 minutes (a minute when unsure).
|
|
147
|
+
const routes = new Map()
|
|
148
|
+
async function onStart(s) {
|
|
149
|
+
if (!s.jwt || isTeam(s)) return false // a team's seat is on the team's plan
|
|
150
|
+
if (!s.key) return true
|
|
151
|
+
const id = s.key + "\0" + s.jwt
|
|
152
|
+
const r = routes.get(id)
|
|
153
|
+
if (r && Date.now() - r.at < r.ttl) return r.start
|
|
154
|
+
let start = false
|
|
155
|
+
let sure = false
|
|
156
|
+
try {
|
|
157
|
+
start = !(await plan(s))
|
|
158
|
+
sure = true
|
|
159
|
+
} catch {
|
|
160
|
+
try {
|
|
161
|
+
if (await startPlan(s.jwt, s.device)) start = sure = true
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
routes.set(id, { start, at: Date.now(), ttl: sure ? 600_000 : 60_000 })
|
|
165
|
+
return start
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- keys ------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
function teamHeaders(base, org, project) {
|
|
171
|
+
return {
|
|
172
|
+
"Bigmodel-Organization": org,
|
|
173
|
+
"Bigmodel-Project": project,
|
|
174
|
+
"Set-Language": base.includes("bigmodel.cn") ? "zh" : "en",
|
|
175
|
+
"Accept-Language": "en-US,en",
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const keysURL = (root, org, project) =>
|
|
180
|
+
`${root}/api/biz/v1/organization/${encodeURIComponent(org)}/projects/${encodeURIComponent(project)}/api_keys`
|
|
181
|
+
|
|
182
|
+
// projectKey finds the project's key named want.name (of want.keyType, when
|
|
183
|
+
// given), makes it when it isn't there and reads its secret: `<id>.<secret>`.
|
|
184
|
+
async function projectKey(site, keys, auth, headers, want) {
|
|
185
|
+
const name = SITES[site].name
|
|
186
|
+
let list
|
|
187
|
+
try {
|
|
188
|
+
list = (await call("GET", keys, { auth, headers })) ?? []
|
|
189
|
+
} catch (e) {
|
|
190
|
+
throw new Error(`${name} API keys: ${e.message}`)
|
|
191
|
+
}
|
|
192
|
+
const typed = "keyType" in want
|
|
193
|
+
let id = ""
|
|
194
|
+
for (const k of Array.isArray(list) ? list : []) {
|
|
195
|
+
if (k.name === want.name && (!typed || String(k.keyType) === String(want.keyType)) && first(k.apiKey)) id = first(k.apiKey)
|
|
196
|
+
}
|
|
197
|
+
if (!id) {
|
|
198
|
+
try {
|
|
199
|
+
id = first((await call("POST", keys, { auth, headers, body: want }))?.apiKey)
|
|
200
|
+
} catch (e) {
|
|
201
|
+
throw new Error(`${name} API key: ${e.message}`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
let secret = ""
|
|
205
|
+
if (id) {
|
|
206
|
+
try {
|
|
207
|
+
secret = first((await call("GET", `${keys}/copy/${encodeURIComponent(id)}`, { auth, headers }))?.secretKey)
|
|
208
|
+
} catch (e) {
|
|
209
|
+
throw new Error(`${name} API key: ${e.message}`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (id && !secret && typed) return id // a team's key with no secret goes as it is
|
|
213
|
+
if (!id || !secret) throw new Error(`${name} gave no API key`)
|
|
214
|
+
return `${id}.${secret}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// teamKey is a team seat's key when the sign-in kept none.
|
|
218
|
+
const teamKeys = new Map()
|
|
219
|
+
async function teamKey(s) {
|
|
220
|
+
if (s.key) return s.key
|
|
221
|
+
const id = [s.token, s.org, s.project].join("\0")
|
|
222
|
+
const got = teamKeys.get(id)
|
|
223
|
+
if (got && (!got.err || Date.now() - got.at < 60_000)) {
|
|
224
|
+
if (got.err) throw got.err
|
|
225
|
+
return got.key
|
|
226
|
+
}
|
|
227
|
+
const site = s.base.includes("bigmodel.cn") ? "bigmodel" : "zai"
|
|
228
|
+
try {
|
|
229
|
+
const key = await projectKey(site, keysURL(SITES[site].api, s.org, s.project), s.token, teamHeaders(s.base, s.org, s.project), { name: "zcode-team-api-key", keyType: 2 })
|
|
230
|
+
teamKeys.set(id, { key, at: Date.now() })
|
|
231
|
+
return key
|
|
232
|
+
} catch (e) {
|
|
233
|
+
const err = new Error(`ZCode's team plan: ${e.message} — sign in again`)
|
|
234
|
+
teamKeys.set(id, { err, at: Date.now() })
|
|
235
|
+
throw err
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- signing in ------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
// bizAuth is the Authorization a sign-in gives the business API: Z.ai's
|
|
242
|
+
// token exchanged for a business one, as a Bearer; BigModel's goes bare.
|
|
243
|
+
async function bizAuth(site, token) {
|
|
244
|
+
if (site === "bigmodel") return token
|
|
245
|
+
let biz
|
|
246
|
+
try {
|
|
247
|
+
biz = await call("POST", ZAI_API + "/api/auth/z/login", { body: { token } })
|
|
248
|
+
} catch (e) {
|
|
249
|
+
throw new Error(`Z.ai sign-in: ${e.message}`)
|
|
250
|
+
}
|
|
251
|
+
if (!biz?.access_token) throw new Error("Z.ai sign-in: no token")
|
|
252
|
+
return "Bearer " + biz.access_token
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// mintKey is the account's own coding plan key, in its default project
|
|
256
|
+
// ("默认机构" / "默认项目", else the first), and the plan it has.
|
|
257
|
+
async function mintKey(site, root, auth, info) {
|
|
258
|
+
let org = ""
|
|
259
|
+
let proj = ""
|
|
260
|
+
for (const o of info?.organizations ?? []) {
|
|
261
|
+
const ps = (o.projects ?? []).filter((p) => p.projectId && String(p.projectType) !== "2")
|
|
262
|
+
if (!o.organizationId || !ps.length) continue
|
|
263
|
+
const def = (ps.find((p) => String(p.projectName ?? "").includes("默认项目")) ?? ps[0]).projectId
|
|
264
|
+
const named = String(o.organizationName ?? "").includes("默认机构")
|
|
265
|
+
if (!org || named) {
|
|
266
|
+
org = o.organizationId
|
|
267
|
+
proj = def
|
|
268
|
+
if (named) break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!org) throw new Error(`this ${SITES[site].name} account has no project for an API key`)
|
|
272
|
+
const key = await projectKey(site, keysURL(root, org, proj), auth, undefined, { name: "zcode-api-key" })
|
|
273
|
+
const s = { key, base: SITES[site].base }
|
|
274
|
+
try {
|
|
275
|
+
return { s, plan: await plan(s) }
|
|
276
|
+
} catch (e) {
|
|
277
|
+
throw new Error(`GLM Coding Plan: ${e.message}`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// teamSignIn is the account's seat on a team's plan: the first team project
|
|
282
|
+
// whose plan is in force and gives it a seat, and that project's key.
|
|
283
|
+
async function teamSignIn(site, root, auth, info) {
|
|
284
|
+
const base = SITES[site].base
|
|
285
|
+
const ps = []
|
|
286
|
+
for (const o of info?.organizations ?? [])
|
|
287
|
+
for (const p of o.projects ?? []) if (o.organizationId && p.projectId && String(p.projectType) === "2") ps.push({ org: o.organizationId, project: p.projectId })
|
|
288
|
+
if (!ps.length) return { err: "no team plan", none: true }
|
|
289
|
+
let why = ""
|
|
290
|
+
let last = ""
|
|
291
|
+
for (const p of ps) {
|
|
292
|
+
let d
|
|
293
|
+
try {
|
|
294
|
+
d = (await call("GET", root + "/api/biz/team/subscribe/product/querySubscribeDetail", { auth, headers: teamHeaders(base, p.org, p.project) })) ?? {}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
last = e.message
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
const status = String(d.status ?? "").toUpperCase()
|
|
300
|
+
const grant = String(d.memberGrantStatus ?? "").toUpperCase()
|
|
301
|
+
if (!(d.hasSubscription !== false && status === "EFFECTIVE" && grant === "VALID")) {
|
|
302
|
+
if (!why && status === "EFFECTIVE" && grant === "UNASSIGNED")
|
|
303
|
+
why = `this ${SITES[site].name} account is in a team with a GLM Coding Plan but has no seat on it yet — ask the team's admin to give it one, then sign in again`
|
|
304
|
+
if (!why && status === "EXPIRED") why = `the ${SITES[site].name} team's GLM Coding Plan this account is in has expired`
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
let key
|
|
308
|
+
try {
|
|
309
|
+
key = await projectKey(site, keysURL(root, p.org, p.project), auth, teamHeaders(base, p.org, p.project), { name: "zcode-team-api-key", keyType: 2 })
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return { err: `the team's GLM Coding Plan: ${e.message}`, why: `the team's GLM Coding Plan: ${e.message}` }
|
|
312
|
+
}
|
|
313
|
+
return { s: { key, base, token: auth, org: p.org, project: p.project }, plan: first(d.productName, "GLM Coding Team") }
|
|
314
|
+
}
|
|
315
|
+
if (why) return { err: why, why }
|
|
316
|
+
const err = `the team's GLM Coding Plan: ${last || "no seat on it"}`
|
|
317
|
+
return { err, why: err }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// signedIn is what a sign-in gives: the account's coding plan key and plan;
|
|
321
|
+
// or, with none of its own, a seat on a team's plan; or else ZCode's token
|
|
322
|
+
// for its Start Plan while the account has one.
|
|
323
|
+
async function signedIn(site, token, jwt, device) {
|
|
324
|
+
const name = SITES[site].name
|
|
325
|
+
const root = SITES[site].api
|
|
326
|
+
let s = null
|
|
327
|
+
let err = null
|
|
328
|
+
let team = ""
|
|
329
|
+
try {
|
|
330
|
+
const auth = await bizAuth(site, token)
|
|
331
|
+
let info
|
|
332
|
+
try {
|
|
333
|
+
info = await call("GET", root + "/api/biz/customer/getCustomerInfo", { auth })
|
|
334
|
+
} catch (e) {
|
|
335
|
+
throw new Error(`${name} account: ${e.message}`)
|
|
336
|
+
}
|
|
337
|
+
const own = await mintKey(site, root, auth, info).catch((e) => ({ e }))
|
|
338
|
+
if (own.e) err = own.e
|
|
339
|
+
else if (own.plan) return { ...own.s, jwt, plan: own.plan }
|
|
340
|
+
else s = own.s
|
|
341
|
+
const t = await teamSignIn(site, root, auth, info)
|
|
342
|
+
if (t.s) return { ...t.s, jwt, plan: t.plan }
|
|
343
|
+
team = t.why ?? ""
|
|
344
|
+
} catch (e) {
|
|
345
|
+
err = e
|
|
346
|
+
}
|
|
347
|
+
let berr = null
|
|
348
|
+
if (jwt) {
|
|
349
|
+
try {
|
|
350
|
+
const sp = await startPlan(jwt, device)
|
|
351
|
+
if (sp) return { ...(err || !s?.key ? { base: SITES[site].base } : s), jwt, plan: sp.name }
|
|
352
|
+
} catch (e) {
|
|
353
|
+
berr = e
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (team && berr) throw new Error(`${team}; ZCode's Start Plan: ${berr.message}`)
|
|
357
|
+
if (team) throw new Error(team)
|
|
358
|
+
if (err && berr) throw new Error(`${err.message}; ZCode's Start Plan: ${berr.message}`)
|
|
359
|
+
if (err) throw err
|
|
360
|
+
if (berr) throw new Error(`this ${name} account has no GLM Coding Plan, of its own or a team's, and ZCode's Start Plan could not be read: ${berr.message}`)
|
|
361
|
+
throw new Error(
|
|
362
|
+
`this ${name} account has no GLM Coding Plan, of its own or a team's, and ZCode's Start Plan has ended or was never started — subscribe at ${SITES[site].subscribe}, then sign in again`,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const who = (u) => first(String(u?.email ?? "").replace(/@phone\.local$/, ""), u?.name, u?.user_id, "ZCode")
|
|
367
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
368
|
+
|
|
369
|
+
// authorize opens ZCode's sign-in flow on site; its callback polls it.
|
|
370
|
+
async function authorize(site, log) {
|
|
371
|
+
const poll = "Bearer " + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")
|
|
372
|
+
const device = uuid()
|
|
373
|
+
let flow
|
|
374
|
+
try {
|
|
375
|
+
flow = await call("POST", ZCODE + "/api/v1/oauth/cli/init", { auth: poll, body: { provider: site }, device })
|
|
376
|
+
} catch (e) {
|
|
377
|
+
throw new Error(`ZCode sign-in: ${e.message}`)
|
|
378
|
+
}
|
|
379
|
+
let u
|
|
380
|
+
try {
|
|
381
|
+
u = new URL(flow?.authorize_url)
|
|
382
|
+
} catch {}
|
|
383
|
+
if (!flow?.flow_id || u?.protocol !== "https:") throw new Error("ZCode gave no sign-in page")
|
|
384
|
+
// where Z.ai or BigModel sends the browser back, as ZCode sets it (BigModel's page names it redirect)
|
|
385
|
+
const back = `${ZCODE}/app/oauth/login?` + new URLSearchParams({ redirect: "zcode://oauth/callback", app_version: APP_VERSION })
|
|
386
|
+
u.searchParams.set(site === "bigmodel" ? "redirect" : "redirect_uri", back)
|
|
387
|
+
const interval = Math.max(Number(flow.poll_interval_sec) || 0, 1) * 1000
|
|
388
|
+
const deadline = Number(flow.expires_at) > 0 ? Number(flow.expires_at) * 1000 : Date.now() + 5 * 60_000
|
|
389
|
+
return {
|
|
390
|
+
url: u.toString(),
|
|
391
|
+
instructions: `Sign in to ${SITES[site].name} in the browser; this finishes on its own.`,
|
|
392
|
+
method: "auto",
|
|
393
|
+
async callback() {
|
|
394
|
+
const fail = (msg) => (log(msg), { type: "failed" })
|
|
395
|
+
for (;;) {
|
|
396
|
+
await sleep(interval)
|
|
397
|
+
if (Date.now() > deadline) return fail("the sign-in expired; start again")
|
|
398
|
+
let got
|
|
399
|
+
try {
|
|
400
|
+
got = (await call("GET", `${ZCODE}/api/v1/oauth/cli/poll/${encodeURIComponent(flow.flow_id)}`, { auth: poll, device })) ?? {}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
if (e.status >= 400 && e.status < 500 && e.status !== 408 && e.status !== 429) return fail("ZCode sign-in: " + e.message)
|
|
403
|
+
continue // a hiccup: ask again
|
|
404
|
+
}
|
|
405
|
+
const token = site === "bigmodel" ? first(got.bigmodel?.access_token, got.bigmodel?.accessToken) : first(got.zai?.access_token)
|
|
406
|
+
if (got.status === "pending" || !got.status) continue
|
|
407
|
+
if (got.status === "failed") return fail(`the sign-in was declined on ${SITES[site].name}`)
|
|
408
|
+
if (got.status !== "ready" || !token) return fail("ZCode sign-in: unexpected answer " + got.status)
|
|
409
|
+
let s
|
|
410
|
+
try {
|
|
411
|
+
s = await signedIn(site, token, first(got.token), device)
|
|
412
|
+
} catch (e) {
|
|
413
|
+
return fail(e.message)
|
|
414
|
+
}
|
|
415
|
+
const state = { site, device, ...s }
|
|
416
|
+
return {
|
|
417
|
+
type: "success",
|
|
418
|
+
refresh: JSON.stringify(state),
|
|
419
|
+
access: s.key || s.jwt || "",
|
|
420
|
+
// the key doesn't run out; ZCode's token, when it is all there is, does
|
|
421
|
+
expires: s.key || isTeam(s) ? 0 : jwtExpiry(s.jwt),
|
|
422
|
+
accountId: who(got.user),
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// stateOf is a stored sign-in as the requests need it.
|
|
430
|
+
function stateOf(auth) {
|
|
431
|
+
if (auth?.type === "oauth") {
|
|
432
|
+
let s = {}
|
|
433
|
+
try {
|
|
434
|
+
s = JSON.parse(auth.refresh)
|
|
435
|
+
} catch {}
|
|
436
|
+
const site = siteOf(s.site)
|
|
437
|
+
return { ...s, site, base: s.base || SITES[site].base, key: s.key ?? (s.jwt ? "" : auth.access), device: s.device || anyDevice }
|
|
438
|
+
}
|
|
439
|
+
if (auth?.type === "api") {
|
|
440
|
+
const site = siteOf(auth.metadata?.site)
|
|
441
|
+
return { site, base: SITES[site].base, key: String(auth.key ?? "").trim(), device: anyDevice }
|
|
442
|
+
}
|
|
443
|
+
return null
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- models ----------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
// planID is ZCode's providerId for the plan served at base; a team's plan
|
|
449
|
+
// has the individual one's models.
|
|
450
|
+
const planID = (base) =>
|
|
451
|
+
base.includes("/zcode-plan/")
|
|
452
|
+
? "account:zai-start-plan"
|
|
453
|
+
: base.includes("bigmodel.cn")
|
|
454
|
+
? "account:bigmodel-individual-coding-plan"
|
|
455
|
+
: "account:zai-individual-coding-plan"
|
|
456
|
+
|
|
457
|
+
const efforts = (vs) => [...new Set(vs.map((v) => (v === "disabled" ? "none" : v === "enabled" ? "high" : v)))]
|
|
458
|
+
|
|
459
|
+
// modelsOf is a plan's models in ZCode's provider config.
|
|
460
|
+
function modelsOf(cfg, plan) {
|
|
461
|
+
const c = cfg?.config ?? {}
|
|
462
|
+
const ids = []
|
|
463
|
+
const on = new Set()
|
|
464
|
+
const add = (id) => {
|
|
465
|
+
if (!on.has(id.toLowerCase())) on.add(id.toLowerCase()), ids.push(id)
|
|
466
|
+
}
|
|
467
|
+
for (const r of c.providerConfigRules?.providerRules ?? []) if (r.providerId === plan) (r.builtinModelIds ?? []).forEach(add)
|
|
468
|
+
const off = new Set()
|
|
469
|
+
for (const r of c.modelConfigRules?.builtinProviderModelRules ?? []) {
|
|
470
|
+
if (r.providerId !== plan) continue
|
|
471
|
+
if (r.config?.enabled === false) off.add(String(r.modelId).toLowerCase())
|
|
472
|
+
else add(r.modelId)
|
|
473
|
+
}
|
|
474
|
+
const out = []
|
|
475
|
+
for (const id of ids) {
|
|
476
|
+
if (off.has(id.toLowerCase())) continue
|
|
477
|
+
const m = { id, context: 0, output: 0, efforts: [] }
|
|
478
|
+
for (const r of c.modelConfigRules?.modelRules ?? []) {
|
|
479
|
+
let re
|
|
480
|
+
try {
|
|
481
|
+
re = new RegExp(`^(?:${r.modelMatch})$`, "i")
|
|
482
|
+
} catch {
|
|
483
|
+
continue
|
|
484
|
+
}
|
|
485
|
+
if (!re.test(id)) continue
|
|
486
|
+
const x = r.config ?? {}
|
|
487
|
+
if (x.properties?.contextWindow > 0) m.context = x.properties.contextWindow
|
|
488
|
+
if (x.optionSpecs?.maxOutputTokens?.max > 0) m.output = x.optionSpecs.maxOutputTokens.max
|
|
489
|
+
if (typeof x.properties?.inputFormat?.supportsImage === "boolean") m.image = x.properties.inputFormat.supportsImage
|
|
490
|
+
if (x.optionSpecs?.reasoningLevel?.values?.length) m.efforts = efforts(x.optionSpecs.reasoningLevel.values)
|
|
491
|
+
}
|
|
492
|
+
out.push(m)
|
|
493
|
+
}
|
|
494
|
+
return out
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// localConfig is the latest provider config an installed ZCode has.
|
|
498
|
+
function localConfig() {
|
|
499
|
+
const files = []
|
|
500
|
+
const rt = join(homedir(), ".zcode", "v2", "runtime", "provider")
|
|
501
|
+
const walk = (dir, depth) => {
|
|
502
|
+
let es = []
|
|
503
|
+
try {
|
|
504
|
+
es = readdirSync(dir, { withFileTypes: true })
|
|
505
|
+
} catch {
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
for (const e of es) {
|
|
509
|
+
if (depth < 3 && e.isDirectory()) walk(join(dir, e.name), depth + 1)
|
|
510
|
+
else if (depth === 3 && e.name === "zcode-builtin.json") files.push(join(dir, e.name))
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
walk(rt, 0)
|
|
514
|
+
if (process.platform === "darwin") files.push("/Applications/ZCode.app/Contents/Resources/config/provider/zcode-builtin.json")
|
|
515
|
+
let best = null
|
|
516
|
+
for (const f of files) {
|
|
517
|
+
try {
|
|
518
|
+
if (!existsSync(f)) continue
|
|
519
|
+
const b = JSON.parse(readFileSync(f, "utf8"))
|
|
520
|
+
if (!best || (b.revision ?? 0) > (best.revision ?? 0)) best = b
|
|
521
|
+
} catch {}
|
|
522
|
+
}
|
|
523
|
+
return best
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// remoteConfig is the release of ZCode's provider config zcode.z.ai names now.
|
|
527
|
+
async function remoteConfig() {
|
|
528
|
+
const signal = AbortSignal.timeout(8000)
|
|
529
|
+
const c = await call("GET", `${ZCODE}/api/v1/client/configs?app_version=${APP_VERSION}&platform=${platform()}`, { signal })
|
|
530
|
+
const url = c?.configs?.builtin_provider_config_json
|
|
531
|
+
if (!String(url ?? "").startsWith("https://")) throw new Error("ZCode's configs name no provider config")
|
|
532
|
+
const res = await fetch(url, { headers: { "User-Agent": UA }, signal })
|
|
533
|
+
if (!res.ok) throw new Error(`ZCode's provider config: ${res.status}`)
|
|
534
|
+
return res.json()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// config is ZCode's provider config, the later of zcode.z.ai's and an
|
|
538
|
+
// installed ZCode's, kept for 10 minutes.
|
|
539
|
+
let cfgCache = null
|
|
540
|
+
async function zcodeConfig() {
|
|
541
|
+
if (cfgCache && Date.now() - cfgCache.at < 600_000) return cfgCache.cfg
|
|
542
|
+
let cfg = null
|
|
543
|
+
try {
|
|
544
|
+
cfg = await remoteConfig()
|
|
545
|
+
} catch {}
|
|
546
|
+
const local = localConfig()
|
|
547
|
+
if (local && (!cfg || (local.revision ?? 0) > (cfg.revision ?? 0))) cfg = local
|
|
548
|
+
cfgCache = { cfg, at: Date.now() }
|
|
549
|
+
return cfg
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// variants are a model's reasoning efforts as @ai-sdk/anthropic options.
|
|
553
|
+
const BUDGET = { minimal: 1024, low: 4096, medium: 8192, high: 16_000, max: 31_999 }
|
|
554
|
+
function variants(es) {
|
|
555
|
+
const out = {}
|
|
556
|
+
for (const e of es) out[e] = e === "none" ? { thinking: { type: "disabled" } } : { thinking: { type: "enabled", budgetTokens: BUDGET[e] ?? 16_000 } }
|
|
557
|
+
return out
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// entry is a model as a config hook declares it.
|
|
561
|
+
const entry = (m) => ({
|
|
562
|
+
id: m.id,
|
|
563
|
+
name: m.id,
|
|
564
|
+
reasoning: m.efforts.some((e) => e !== "none"),
|
|
565
|
+
tool_call: true,
|
|
566
|
+
attachment: !!m.image,
|
|
567
|
+
modalities: { input: m.image ? ["text", "image"] : ["text"], output: ["text"] },
|
|
568
|
+
limit: { context: m.context || 200_000, output: m.output || OUTPUT },
|
|
569
|
+
cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
570
|
+
variants: variants(m.efforts),
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// model is a model as OpenCode's provider hands it to provider.models.
|
|
574
|
+
function model(m, url) {
|
|
575
|
+
const e = entry(m)
|
|
576
|
+
return {
|
|
577
|
+
id: m.id,
|
|
578
|
+
providerID: PROVIDER,
|
|
579
|
+
name: m.id,
|
|
580
|
+
api: { id: m.id, url, npm: "@ai-sdk/anthropic" },
|
|
581
|
+
status: "active",
|
|
582
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
583
|
+
limit: e.limit,
|
|
584
|
+
options: {},
|
|
585
|
+
headers: {},
|
|
586
|
+
capabilities: {
|
|
587
|
+
temperature: true,
|
|
588
|
+
reasoning: e.reasoning,
|
|
589
|
+
attachment: e.attachment,
|
|
590
|
+
toolcall: true,
|
|
591
|
+
input: { text: true, image: !!m.image, audio: false, video: false, pdf: false },
|
|
592
|
+
output: { text: true, image: false, audio: false, video: false, pdf: false },
|
|
593
|
+
interleaved: false,
|
|
594
|
+
},
|
|
595
|
+
release_date: "",
|
|
596
|
+
variants: e.variants,
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---- the plugin ------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
export async function ZCodeAuthPlugin({ client }) {
|
|
603
|
+
const log = (message) => {
|
|
604
|
+
try {
|
|
605
|
+
client?.app?.log?.({ body: { service: "zcode-auth", level: "error", message } })
|
|
606
|
+
} catch {}
|
|
607
|
+
}
|
|
608
|
+
const oauth = (site) => ({
|
|
609
|
+
type: "oauth",
|
|
610
|
+
label: `ZCode: ${SITES[site].name}${site === "bigmodel" ? " (智谱)" : ""} GLM Coding Plan`,
|
|
611
|
+
authorize: () => authorize(site, log),
|
|
612
|
+
})
|
|
613
|
+
return {
|
|
614
|
+
config: async (cfg) => {
|
|
615
|
+
cfg.provider ??= {}
|
|
616
|
+
const was = cfg.provider[PROVIDER] ?? {}
|
|
617
|
+
cfg.provider[PROVIDER] = {
|
|
618
|
+
name: "ZCode",
|
|
619
|
+
npm: "@ai-sdk/anthropic",
|
|
620
|
+
api: ZAI_BASE + "/v1",
|
|
621
|
+
...was,
|
|
622
|
+
models: { ...Object.fromEntries(MODELS.map((m) => [m.id, entry(m)])), ...(was.models ?? {}) },
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
provider: {
|
|
626
|
+
id: PROVIDER,
|
|
627
|
+
// the account's plan's models, as ZCode's config lists them now
|
|
628
|
+
async models(provider, { auth } = {}) {
|
|
629
|
+
const s = stateOf(auth)
|
|
630
|
+
if (!s) return provider.models
|
|
631
|
+
const start = await onStart(s).catch(() => false)
|
|
632
|
+
const base = start ? START_BASE : s.base
|
|
633
|
+
let ms = modelsOf(await zcodeConfig().catch(() => null), planID(base))
|
|
634
|
+
if (!ms.length) ms = start ? START_MODELS : MODELS
|
|
635
|
+
const url = s.base + "/v1"
|
|
636
|
+
return Object.fromEntries(ms.map((m) => [m.id, model(m, url)]))
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
auth: {
|
|
640
|
+
provider: PROVIDER,
|
|
641
|
+
async loader(getAuth, provider) {
|
|
642
|
+
const s0 = stateOf(await getAuth())
|
|
643
|
+
if (!s0) return {}
|
|
644
|
+
const id = provider?.id ?? PROVIDER
|
|
645
|
+
return {
|
|
646
|
+
baseURL: s0.base + "/v1",
|
|
647
|
+
apiKey: s0.key || "zcode", // the fetch below puts the real one on
|
|
648
|
+
async fetch(input, init) {
|
|
649
|
+
const auth = await getAuth()
|
|
650
|
+
const s = stateOf(auth)
|
|
651
|
+
if (!s) throw new Error("not signed in to ZCode")
|
|
652
|
+
let url = input instanceof Request ? input.url : String(input)
|
|
653
|
+
const opts = input instanceof Request ? { method: input.method, headers: input.headers, body: input.body, signal: input.signal, duplex: "half", ...init } : { ...init }
|
|
654
|
+
const start = await onStart(s)
|
|
655
|
+
// the request goes to the plan the account is on, wherever it was made for
|
|
656
|
+
for (const b of [START_BASE, ZAI_BASE, BIGMODEL_BASE]) {
|
|
657
|
+
if (url.startsWith(b)) {
|
|
658
|
+
url = (start ? START_BASE : s.base) + url.slice(b.length)
|
|
659
|
+
break
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
let key = s.key
|
|
663
|
+
if (isTeam(s) && !key) {
|
|
664
|
+
key = await teamKey(s)
|
|
665
|
+
if (auth?.type === "oauth") {
|
|
666
|
+
const next = { ...auth, refresh: JSON.stringify({ ...s, key }), access: key }
|
|
667
|
+
await client?.auth?.set?.({ path: { id }, body: next }).catch?.(() => {})
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const h = new Headers(opts.headers)
|
|
671
|
+
if (start) {
|
|
672
|
+
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
673
|
+
key = s.jwt
|
|
674
|
+
// ZCode names itself on a Start Plan request
|
|
675
|
+
h.set("User-Agent", UA)
|
|
676
|
+
h.set("X-ZCode-App-Version", APP_VERSION)
|
|
677
|
+
h.set("X-Title", "Z Code@electron")
|
|
678
|
+
h.set("HTTP-Referer", ZCODE)
|
|
679
|
+
h.set("X-Platform", platform())
|
|
680
|
+
h.set("X-Device-Mid", s.device)
|
|
681
|
+
}
|
|
682
|
+
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
683
|
+
h.delete("authorization")
|
|
684
|
+
h.set("x-api-key", key)
|
|
685
|
+
h.set("Authorization", "Bearer " + key)
|
|
686
|
+
return fetch(url, { ...opts, headers: h })
|
|
687
|
+
},
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
methods: [
|
|
691
|
+
oauth("zai"),
|
|
692
|
+
oauth("bigmodel"),
|
|
693
|
+
{
|
|
694
|
+
type: "api",
|
|
695
|
+
label: "GLM Coding Plan API key",
|
|
696
|
+
prompts: [
|
|
697
|
+
{
|
|
698
|
+
type: "select",
|
|
699
|
+
key: "site",
|
|
700
|
+
message: "Where is the key from?",
|
|
701
|
+
options: [
|
|
702
|
+
{ label: "Z.ai", value: "zai", hint: "api.z.ai" },
|
|
703
|
+
{ label: "BigModel (智谱)", value: "bigmodel", hint: "open.bigmodel.cn" },
|
|
704
|
+
],
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
}
|
|
711
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magpie-community/opencode-zcode-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode provider plugin: signs in to Z.ai's or BigModel's (智谱) GLM Coding Plan as ZCode does, including team seats and ZCode's Start Plan",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"exports": "./index.mjs",
|
|
8
|
+
"files": ["index.mjs", "README.md"],
|
|
9
|
+
"keywords": ["opencode", "opencode-plugin", "magpie", "zcode", "z.ai", "bigmodel", "glm", "coding-plan"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/magpie-community/plugins.git",
|
|
14
|
+
"directory": "packages/zcode"
|
|
15
|
+
}
|
|
16
|
+
}
|