@magpie-community/opencode-zcode-auth 0.1.7 → 0.1.9
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 +39 -2
- package/index.mjs +295 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,12 +54,44 @@ Before each request, the plugin checks which plan the account is on, and
|
|
|
54
54
|
asks again every 10 minutes:
|
|
55
55
|
|
|
56
56
|
- If the key's account has a GLM Coding Plan
|
|
57
|
-
(`/api/biz/subscription/list`), requests go to that plan.
|
|
58
|
-
|
|
57
|
+
(`/api/biz/subscription/list`), its requests go to that plan. A model
|
|
58
|
+
the account's gift plans also serve is listed again as
|
|
59
|
+
`<model>-Trial`; asking that entry spends the gift (see below).
|
|
60
|
+
- Otherwise they go to the gift plans.
|
|
59
61
|
|
|
60
62
|
Every request carries the key as both `x-api-key` and
|
|
61
63
|
`Authorization: Bearer`.
|
|
62
64
|
|
|
65
|
+
**The gift's trial entries.** An account with a GLM Coding Plan can
|
|
66
|
+
also hold gift plans from ZCode activities, ones that expire on a fixed
|
|
67
|
+
date. Their allowance would lapse unused if every request went to the
|
|
68
|
+
coding plan, so the plugin reads the gift's balance (per the sign-in's
|
|
69
|
+
session, kept for 10 minutes) and lists every model a live bucket
|
|
70
|
+
serves again as `<model>-Trial`, named in the catalog's spelling
|
|
71
|
+
(GLM-5.3-Flash-Trial). Asking a trial entry sends the model to the
|
|
72
|
+
gift, dressed as ZCode's own; asking the plain model spends the coding
|
|
73
|
+
plan — the choice is the user's. A bucket is judged spent only by its
|
|
74
|
+
`remaining_units` — a missing one is unknown, not spent. A trial entry
|
|
75
|
+
whose bucket is spent or blocked answers 429 without touching the
|
|
76
|
+
coding plan; when the gift answers a trial entry with a quota error
|
|
77
|
+
(code 1113 "Insufficient balance or no resource package", code 1005
|
|
78
|
+
"exceed quota limit", which it can answer inside an HTTP 200) that too
|
|
79
|
+
becomes a 429 and keeps the entry off for a minute. Any other gift
|
|
80
|
+
answer (its 405, its 401, its 5xx) goes on as it came: the user picked
|
|
81
|
+
the gift, so the coding plan is not spent behind their back. Every 429
|
|
82
|
+
counts as spent, Z.ai's 1302/1303 rate limits among them. A stream's
|
|
83
|
+
body is never read or buffered on the way.
|
|
84
|
+
|
|
85
|
+
## The usage card
|
|
86
|
+
|
|
87
|
+
An account with both plans shows both allowances: the coding plan's
|
|
88
|
+
windows first, then the gift's buckets, each with its own name, models
|
|
89
|
+
and end. The card's plan and term stay the coding plan's. A gift read
|
|
90
|
+
that fails leaves just the coding card — magpie hides a card's windows
|
|
91
|
+
behind an error, so a gift hiccup must not black out a working Coding
|
|
92
|
+
Plan. A gift that goes out of date while requests use it simply stops
|
|
93
|
+
showing.
|
|
94
|
+
|
|
63
95
|
## Models
|
|
64
96
|
|
|
65
97
|
The models are the ones ZCode offers for the account's plan. They come
|
|
@@ -75,3 +107,8 @@ minutes. When the config can't be read, the plugin falls back to this list:
|
|
|
75
107
|
| GLM-5-Turbo | 200K | 64K | none, high |
|
|
76
108
|
|
|
77
109
|
The Start Plan has all of these except GLM-5.3.
|
|
110
|
+
|
|
111
|
+
A gift plan's models appear again as `<model>-Trial` entries, so a
|
|
112
|
+
request can spend the gift's quota on purpose. A trial entry is listed
|
|
113
|
+
while any live bucket serves its model, spent buckets included (a spent
|
|
114
|
+
entry answers 429 rather than vanishing mid-session).
|
package/index.mjs
CHANGED
|
@@ -130,48 +130,128 @@ const num = (v) => {
|
|
|
130
130
|
return typeof n === "number" && Number.isFinite(n) ? n : undefined
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
133
|
+
// the alias is the catalog spelling of the model plus -trial, so the picker
|
|
134
|
+
// reads GLM-5.3-Trial while the loader strips it back to the raw name
|
|
135
|
+
const giftID = (m) => (MODELS.find((x) => x.id.toLowerCase() === String(m).toLowerCase())?.id ?? m) + "-Trial"
|
|
136
|
+
const uniqueModels = (names) => {
|
|
137
|
+
const byName = new Map()
|
|
138
|
+
for (const m of names) if (!byName.has(m.toLowerCase())) byName.set(m.toLowerCase(), m)
|
|
139
|
+
return [...byName.values()]
|
|
140
|
+
}
|
|
141
|
+
const giftNames = (g) => g?.allModels ?? g?.models ?? []
|
|
142
|
+
const giftModel = (g, id) => giftNames(g).find((m) => giftID(m).toLowerCase() === id.toLowerCase())
|
|
143
|
+
const bucketModels = (x) => (Array.isArray(x.capabilities) ? x.capabilities : [])
|
|
144
|
+
.map((c) => String(c ?? "").trim()).filter((c) => c.startsWith("model:") || !c.includes(":"))
|
|
145
|
+
.map((c) => c.replace(/^model:/, "").trim()).filter(Boolean)
|
|
146
|
+
|
|
147
|
+
// Both the card and the request route use the same test for a spent bucket.
|
|
148
|
+
// Older balances may omit remaining_units but still tell used and total.
|
|
149
|
+
const giftSpent = (x) => {
|
|
150
|
+
const left = num(x.remaining_units)
|
|
151
|
+
if (left !== undefined) return left <= 0
|
|
152
|
+
const total = num(x.total_units), used = num(x.used_units)
|
|
153
|
+
return total !== undefined && used !== undefined && used >= total
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// startPlan is the gift an account can spend now ({models}), as ZCode reads
|
|
157
|
+
// its balance: several plans can be active at once, and a model routes to
|
|
158
|
+
// the gift when any of them has a live bucket serving it — a bucket past
|
|
159
|
+
// its plan's own end, or one whose remaining_units says none, is skipped
|
|
160
|
+
// rather than asked and refused. models are those live buckets' names.
|
|
135
161
|
async function startPlan(jwt, device) {
|
|
136
162
|
if (!jwt) throw new Error("not signed in to ZCode")
|
|
137
163
|
if (jwtExpired(jwt)) throw new Error(EXPIRED)
|
|
138
164
|
const b = (await call("GET", `${ZCODE}/api/v1/zcode-plan/billing/balance?app_version=${APP_VERSION}`, { auth: "Bearer " + jwt, device })) ?? {}
|
|
139
165
|
const now = num(b.server_time) > 0 ? num(b.server_time) : Date.now() / 1000
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
const same = (x, p) => (x.user_plan_id && p.user_plan_id ? x.user_plan_id === p.user_plan_id : String(x.plan_id ?? "") === String(p.plan_id ?? ""))
|
|
167
|
+
const plans = Array.isArray(b.plans) ? b.plans : []
|
|
168
|
+
const balances = Array.isArray(b.balances) ? b.balances : []
|
|
169
|
+
const ms = new Set(), all = new Map()
|
|
170
|
+
let ttl = 600_000
|
|
171
|
+
let any = false
|
|
172
|
+
let name = ""
|
|
173
|
+
for (const p of plans) {
|
|
174
|
+
const st = String(p?.status ?? "").trim().toLowerCase()
|
|
175
|
+
const end = num(p?.ends_at)
|
|
176
|
+
if (st !== "active" || (end > 0 && end <= now)) continue
|
|
177
|
+
any = true
|
|
178
|
+
if (end > 0) ttl = Math.min(ttl, (end - now) * 1000)
|
|
179
|
+
if (!name) name = String(p?.name ?? "").trim()
|
|
180
|
+
for (const x of balances) {
|
|
181
|
+
if (!same(x, p)) continue
|
|
182
|
+
const exp = num(x.expires_at)
|
|
183
|
+
if (exp > 0 && exp <= now) continue
|
|
184
|
+
if (exp > 0) ttl = Math.min(ttl, (exp - now) * 1000)
|
|
185
|
+
// Spent buckets still declare models; only live ones can spend now.
|
|
186
|
+
for (const m of bucketModels(x)) {
|
|
187
|
+
if (!all.has(m.toLowerCase())) all.set(m.toLowerCase(), m)
|
|
188
|
+
if (!giftSpent(x)) ms.add(m)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
}
|
|
150
|
-
return null
|
|
192
|
+
return any ? { name: name || "Start Plan", models: [...ms], allModels: [...all.values()], ttl } : null
|
|
151
193
|
}
|
|
152
194
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
195
|
+
// plansOf says where an account's requests go and what more it can spend:
|
|
196
|
+
// {start, gift}. start is what onStart said: an account with no coding plan
|
|
197
|
+
// sends every request to the gift and reads no balance to do it; a team's
|
|
198
|
+
// seat, a sign-in with no ZCode token and a key whose plan-list read found
|
|
199
|
+
// no gift stay on the coding plan. gift is what a coding account also holds:
|
|
200
|
+
// the gift plans ({models}), and its requests spend the gift for a model it
|
|
201
|
+
// serves, until it says it is spent (see blocked). Asked again after 10
|
|
202
|
+
// minutes (a minute when unsure — a plan-list read that failed is retried
|
|
203
|
+
// sooner; a gift read that failed simply leaves gift null until the next
|
|
204
|
+
// ask).
|
|
156
205
|
const routes = new Map()
|
|
157
|
-
async function
|
|
158
|
-
if (
|
|
159
|
-
if (!s.key) return true
|
|
206
|
+
async function plansOf(s) {
|
|
207
|
+
if (isTeam(s) || !s.jwt) return { start: false, gift: null }
|
|
208
|
+
if (!s.key) return { start: true, gift: null }
|
|
160
209
|
const id = s.key + "\0" + s.jwt
|
|
161
210
|
const r = routes.get(id)
|
|
162
|
-
if (r && Date.now() - r.at < r.ttl) return r
|
|
211
|
+
if (r && Date.now() - r.at < r.ttl) return r
|
|
163
212
|
let start = false
|
|
213
|
+
let gift = null
|
|
164
214
|
let sure = false
|
|
165
215
|
try {
|
|
166
|
-
|
|
216
|
+
if (!(await plan(s))) start = true
|
|
217
|
+
else gift = await startPlan(s.jwt, s.device).catch(() => null)
|
|
167
218
|
sure = true
|
|
168
219
|
} catch {
|
|
220
|
+
// the plan list could not be read: a key account takes the dual route
|
|
221
|
+
// (start stays false, the gift is kept, a minute's ttl), so a gift
|
|
222
|
+
// refusal still replays to the coding plan rather than blacking out
|
|
169
223
|
try {
|
|
170
|
-
|
|
224
|
+
gift = await startPlan(s.jwt, s.device)
|
|
225
|
+
sure = false
|
|
171
226
|
} catch {}
|
|
172
227
|
}
|
|
173
|
-
|
|
174
|
-
|
|
228
|
+
const out = { start, gift, at: Date.now(), ttl: Math.min(sure ? 600_000 : 60_000, gift?.ttl ?? 600_000) }
|
|
229
|
+
routes.set(id, out)
|
|
230
|
+
return out
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// blocked remembers the models a gift said it has none left for: a key+jwt+
|
|
234
|
+
// model for a minute, so one turn that is refused does not ask again.
|
|
235
|
+
const blocked = new Map()
|
|
236
|
+
|
|
237
|
+
// giftServes: the gift's buckets name their models lowercased ("glm-5.3-
|
|
238
|
+
// flash"); what is asked for arrives as the plan lists it ("GLM-5.3-Flash").
|
|
239
|
+
const giftServes = (gift, m) => (gift.models ?? []).some((x) => x.toLowerCase() === m.toLowerCase())
|
|
240
|
+
const giftUsable = (s, gift, m) => !jwtExpired(s.jwt) && giftServes(gift, m) &&
|
|
241
|
+
Date.now() >= (blocked.get(s.key + "\0" + s.jwt + "\0" + m.toLowerCase()) ?? 0)
|
|
242
|
+
|
|
243
|
+
// spentUp is the gift saying it has nothing left to spend: its own no-
|
|
244
|
+
// resource-package code 1113, its "exceed quota limit" 1005 — a spent gift
|
|
245
|
+
// can answer a 200 with a JSON body carrying one of those codes. Every 429
|
|
246
|
+
// counts as spent, Z.ai's 1302/1303 rate limits among them.
|
|
247
|
+
function spentUp(text, status) {
|
|
248
|
+
if (status === 429) return true
|
|
249
|
+
let b
|
|
250
|
+
try { b = JSON.parse(text) } catch { return false }
|
|
251
|
+
if (!b || typeof b !== "object") return false
|
|
252
|
+
const code = (v) => String(v ?? "")
|
|
253
|
+
const c1 = code(b.code), c2 = code(b.error?.code)
|
|
254
|
+
return c1 === "1113" || c1 === "1005" || c2 === "1113" || c2 === "1005"
|
|
175
255
|
}
|
|
176
256
|
|
|
177
257
|
// ---- keys ------------------------------------------------------------------------
|
|
@@ -366,10 +446,12 @@ function startNum(v) {
|
|
|
366
446
|
return undefined
|
|
367
447
|
}
|
|
368
448
|
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
449
|
+
// giftOf reads the balance and returns the gift's card fields: its plans and
|
|
450
|
+
// windows. Several plans can be active at once, so every live plan's buckets
|
|
451
|
+
// show, each keeping its own models and end; the plan and end put on the
|
|
452
|
+
// card (for a gift-only account) are the soonest-ending live plan's. null
|
|
453
|
+
// when there is no live plan to show.
|
|
454
|
+
async function giftOf(s) {
|
|
373
455
|
if (!s.jwt) throw new Error("not signed in to ZCode")
|
|
374
456
|
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
375
457
|
const b = (await call("GET", `${ZCODE}/api/v1/zcode-plan/billing/balance?app_version=${APP_VERSION}`, { auth: "Bearer " + s.jwt, device: s.device })) ?? {}
|
|
@@ -382,7 +464,7 @@ async function startUsage(s) {
|
|
|
382
464
|
if (end > 0 && end <= now && p.status.trim().toLowerCase() === "active") p.status = "expired"
|
|
383
465
|
if (p.status.trim().toLowerCase() === "expired") over.add(p.user_plan_id + "\0" + p.plan_id)
|
|
384
466
|
}
|
|
385
|
-
const same = (x, p) => (x.user_plan_id && p.user_plan_id ? x.user_plan_id === p.user_plan_id : x.plan_id === p.plan_id)
|
|
467
|
+
const same = (x, p) => (x.user_plan_id && p.user_plan_id ? x.user_plan_id === p.user_plan_id : String(x.plan_id ?? "") === String(p.plan_id ?? ""))
|
|
386
468
|
const balances = (Array.isArray(b.balances) ? b.balances : [])
|
|
387
469
|
.map((x) => ({ ...x, user_plan_id: str(x?.user_plan_id), plan_id: str(x?.plan_id) }))
|
|
388
470
|
.filter((x) => {
|
|
@@ -395,32 +477,41 @@ async function startUsage(s) {
|
|
|
395
477
|
return keep
|
|
396
478
|
})
|
|
397
479
|
|
|
480
|
+
// every live plan counts as the gift: no plan-name filter decides it,
|
|
481
|
+
// the balance says what the session can spend
|
|
398
482
|
let active = null
|
|
399
483
|
for (const p of plans) {
|
|
400
484
|
if (p.status.trim().toLowerCase() !== "active") continue
|
|
401
|
-
const id = p.plan_id.trim().toLowerCase(), n = str(p.name).trim().toLowerCase()
|
|
402
|
-
const isStart = (x) => x.includes("start-plan") || x.includes("start plan")
|
|
403
|
-
if ((id || n) && !isStart(id) && !isStart(n)) continue
|
|
404
485
|
const end = startNum(p.ends_at)
|
|
405
|
-
|
|
406
|
-
|
|
486
|
+
if (end > 0 && end <= now) continue
|
|
487
|
+
if (active && !(end > 0 && (!startNum(active.end) || end < startNum(active.end)))) continue
|
|
488
|
+
active = { name: first(str(p.name), "Start Plan"), end, until: end > 0 ? new Date(Math.trunc(end) * 1000).toISOString() : "" }
|
|
407
489
|
}
|
|
408
|
-
if (!active) return
|
|
490
|
+
if (!active) return null
|
|
409
491
|
const out = { plan: active.name, windows: [] }
|
|
410
492
|
if (active.until) Object.assign(out, { until: active.until, renew: "off" })
|
|
493
|
+
const live = plans.filter((p) => {
|
|
494
|
+
if (p.status.trim().toLowerCase() !== "active") return false
|
|
495
|
+
const end = startNum(p.ends_at)
|
|
496
|
+
return !(end > 0 && end <= now)
|
|
497
|
+
})
|
|
411
498
|
for (const x of balances) {
|
|
499
|
+
const owner = live.find((p) => same(x, p))
|
|
500
|
+
if (!owner) continue
|
|
501
|
+
const exp = startNum(x.expires_at)
|
|
502
|
+
if (exp > 0 && exp <= now) continue // a bucket past its own end is over
|
|
412
503
|
const total = startNum(x.total_units), left = startNum(x.remaining_units)
|
|
413
504
|
let used = startNum(x.used_units)
|
|
414
505
|
if (total === undefined && used === undefined && left === undefined) continue
|
|
415
|
-
const models = (
|
|
416
|
-
|
|
417
|
-
const w = { name:
|
|
506
|
+
const models = bucketModels(x)
|
|
507
|
+
const base = first(str(x.show_name), models.join(", "), "Credits")
|
|
508
|
+
const w = { name: base, used: 0, _plan: str(owner.name), _spent: giftSpent(x) }
|
|
418
509
|
if (used === undefined) used = total !== undefined && left !== undefined ? total - left : 0
|
|
419
510
|
if (total > 0) {
|
|
420
511
|
w.used = (100 * used) / total
|
|
421
512
|
w.display = `${compact(used)} / ${compact(total)}`
|
|
422
513
|
}
|
|
423
|
-
|
|
514
|
+
if (w._spent) w.used = Math.max(100, w.used)
|
|
424
515
|
if (exp > 0) w.resetsAt = new Date(Math.trunc(exp) * 1000).toISOString()
|
|
425
516
|
let span = 0
|
|
426
517
|
for (const p of plans) {
|
|
@@ -440,6 +531,77 @@ async function startUsage(s) {
|
|
|
440
531
|
return out
|
|
441
532
|
}
|
|
442
533
|
|
|
534
|
+
// startUsage is a gift-only account's card: its buckets, or the words
|
|
535
|
+
// for having none to show. The card's header already names the plan, so
|
|
536
|
+
// the windows keep their own names.
|
|
537
|
+
const NO_START = "this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started"
|
|
538
|
+
async function startUsage(s) {
|
|
539
|
+
const g = await giftOf(s)
|
|
540
|
+
if (!g) return { error: NO_START }
|
|
541
|
+
for (const w of g.windows) {
|
|
542
|
+
if (w.models?.length) w.models = [...w.models, ...w.models.map(giftID)]
|
|
543
|
+
delete w._plan
|
|
544
|
+
delete w._spent
|
|
545
|
+
}
|
|
546
|
+
return g
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// dualUsage shows both pools but only the pool a request can use counts
|
|
550
|
+
// for its model. Live gift models don't spend the Coding windows; spent
|
|
551
|
+
// or blocked gift buckets stay visible without holding up the Coding Plan.
|
|
552
|
+
// Fresh usage also updates the request cache, so a replenished or spent
|
|
553
|
+
// gift takes effect before its ten-minute route cache expires. A failed
|
|
554
|
+
// gift read leaves the working Coding Plan's card and route alone.
|
|
555
|
+
async function dualUsage(s) {
|
|
556
|
+
const out = await codingUsage(s)
|
|
557
|
+
const cached = routes.get(s.key + "\0" + s.jwt)
|
|
558
|
+
try {
|
|
559
|
+
const g = await giftOf(s)
|
|
560
|
+
const gift = g ? { name: g.plan, models: [...new Set(g.windows.filter((w) => !w._spent)
|
|
561
|
+
.flatMap((w) => w.models ?? []).map((m) => m.toLowerCase()))],
|
|
562
|
+
allModels: uniqueModels(g.windows.flatMap((w) => w.models ?? [])) } : null
|
|
563
|
+
if (cached) {
|
|
564
|
+
cached.gift = gift
|
|
565
|
+
const ends = g ? [g.until, ...g.windows.map((w) => w.resetsAt)].map((t) => Date.parse(t)).filter(Number.isFinite) : []
|
|
566
|
+
if (ends.length) cached.ttl = Math.min(cached.ttl, Math.max(0, Math.min(...ends) - cached.at))
|
|
567
|
+
}
|
|
568
|
+
if (g) {
|
|
569
|
+
// the gift's models are its trial entries alone: a plain model stays
|
|
570
|
+
// on the coding plan, and the user picks the gift by its entry
|
|
571
|
+
const notModels = gift.allModels.map((m) => giftID(m).toLowerCase())
|
|
572
|
+
if (notModels.length) out.windows = out.windows.map((w) => w.aside ? w : { ...w, notModels })
|
|
573
|
+
// Several buckets may serve one model: one spent sibling cannot block it.
|
|
574
|
+
const best = new Map()
|
|
575
|
+
g.windows.forEach((w, i) => {
|
|
576
|
+
for (const m of w.models ?? []) {
|
|
577
|
+
const key = m.toLowerCase(), old = best.get(key), prev = g.windows[old]
|
|
578
|
+
if (old === undefined || (prev._spent && !w._spent) || (prev._spent === w._spent && w.used < prev.used)) best.set(key, i)
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
out.windows.push(...g.windows.map(({ _plan: p, _spent, ...w }, i) => {
|
|
582
|
+
const selected = gift.allModels.filter((m) => best.get(m.toLowerCase()) === i)
|
|
583
|
+
const models = selected.map(giftID)
|
|
584
|
+
const scoped = models.length ? { ...w, models } : { ...w, aside: true }
|
|
585
|
+
// the card names a bucket's model as the picker does (GLM-5.3-Flash-Trial);
|
|
586
|
+
// no plan prefix — the card's section header already says the account
|
|
587
|
+
let name = w.name
|
|
588
|
+
for (const m of selected) {
|
|
589
|
+
const at = name.toLowerCase().indexOf(m.toLowerCase())
|
|
590
|
+
if (at >= 0) name = name.slice(0, at) + giftID(m) + name.slice(at + m.length)
|
|
591
|
+
}
|
|
592
|
+
return { ...scoped, name }
|
|
593
|
+
}))
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
if (cached) {
|
|
597
|
+
cached.gift = null
|
|
598
|
+
cached.at = Date.now()
|
|
599
|
+
cached.ttl = 60_000 // an uncertain gift is read again sooner
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return out
|
|
603
|
+
}
|
|
604
|
+
|
|
443
605
|
// periodOf reads an entitlement's period: "daily", "weekly", "monthly".
|
|
444
606
|
function periodOf(p) {
|
|
445
607
|
p = p.toLowerCase()
|
|
@@ -989,6 +1151,26 @@ const entry = (m) => ({
|
|
|
989
1151
|
})
|
|
990
1152
|
|
|
991
1153
|
// model is a model as OpenCode's provider hands it to provider.models.
|
|
1154
|
+
// modelOf reads the model a Messages body asks for.
|
|
1155
|
+
function modelOf(text) {
|
|
1156
|
+
try {
|
|
1157
|
+
const m = JSON.parse(text)?.model
|
|
1158
|
+
return typeof m === "string" ? m : ""
|
|
1159
|
+
} catch {
|
|
1160
|
+
return ""
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// kept passes a response on as it came, the account kept: the built-in
|
|
1165
|
+
// never marked a ZCode account lapsed nor cleared one.
|
|
1166
|
+
function kept(res, text) {
|
|
1167
|
+
const h = new Headers(res.headers)
|
|
1168
|
+
h.delete("content-length")
|
|
1169
|
+
h.delete("content-encoding")
|
|
1170
|
+
h.set("X-Magpie-Sign-In", "kept")
|
|
1171
|
+
return new Response(text !== undefined ? text : res.body, { status: res.status, statusText: res.statusText, headers: h })
|
|
1172
|
+
}
|
|
1173
|
+
|
|
992
1174
|
function model(m, url) {
|
|
993
1175
|
const e = entry(m)
|
|
994
1176
|
return {
|
|
@@ -1168,15 +1350,24 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1168
1350
|
async models(provider, { auth } = {}) {
|
|
1169
1351
|
const s = stateOf(auth)
|
|
1170
1352
|
if (!s) return provider.models
|
|
1171
|
-
const start = await
|
|
1353
|
+
const { start, gift: known } = await plansOf(s).catch(() => ({ start: false, gift: null }))
|
|
1354
|
+
const gift = start ? await startPlan(s.jwt, s.device).catch(() => null) : known
|
|
1172
1355
|
const base = start ? START_BASE : s.base
|
|
1173
|
-
|
|
1356
|
+
const cfg = await zcodeConfig().catch(() => null)
|
|
1357
|
+
let ms = modelsOf(cfg, planID(base))
|
|
1174
1358
|
// ZCode's config can't be had: its table stands in, and magpie keeps
|
|
1175
1359
|
// the list it was told last, as the built-in keeps the one it fetched last
|
|
1176
1360
|
const fell = !ms.length
|
|
1177
1361
|
if (fell) ms = start ? START_MODELS : MODELS
|
|
1178
1362
|
const url = s.base + "/v1"
|
|
1179
1363
|
const out = Object.fromEntries(ms.map((m) => [m.id, model(m, url)]))
|
|
1364
|
+
const details = [...ms, ...modelsOf(cfg, planID(START_BASE)), ...START_MODELS]
|
|
1365
|
+
for (const raw of giftNames(gift)) {
|
|
1366
|
+
const id = giftID(raw)
|
|
1367
|
+
if (out[id]) continue
|
|
1368
|
+
const m = details.find((m) => m.id.toLowerCase() === raw.toLowerCase()) ?? { efforts: [] }
|
|
1369
|
+
out[id] = model({ ...m, id }, url)
|
|
1370
|
+
}
|
|
1180
1371
|
if (fell) out[Symbol.for("magpie.fellBack")] = true
|
|
1181
1372
|
return out
|
|
1182
1373
|
},
|
|
@@ -1196,14 +1387,8 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1196
1387
|
if (!s) throw new Error("not signed in to ZCode")
|
|
1197
1388
|
let url = input instanceof Request ? input.url : String(input)
|
|
1198
1389
|
const opts = input instanceof Request ? { method: input.method, headers: input.headers, body: input.body, signal: input.signal, duplex: "half", ...init } : { ...init }
|
|
1199
|
-
const start = await
|
|
1200
|
-
|
|
1201
|
-
for (const b of [START_BASE, ZAI_BASE, BIGMODEL_BASE]) {
|
|
1202
|
-
if (url.startsWith(b)) {
|
|
1203
|
-
url = (start ? START_BASE : s.base) + url.slice(b.length)
|
|
1204
|
-
break
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1390
|
+
const { start: alone, gift: known } = await plansOf(s)
|
|
1391
|
+
let gift = known
|
|
1207
1392
|
let key = s.key
|
|
1208
1393
|
if (isTeam(s) && !key) {
|
|
1209
1394
|
key = await teamKey(s)
|
|
@@ -1212,32 +1397,79 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1212
1397
|
await client?.auth?.set?.({ path: { id }, body: next }).catch?.(() => {})
|
|
1213
1398
|
}
|
|
1214
1399
|
}
|
|
1215
|
-
|
|
1400
|
+
// a dual account's body is read ahead of sending, so a gift that
|
|
1401
|
+
// says it is spent can spend nothing and replay what was asked for
|
|
1402
|
+
let start = alone
|
|
1403
|
+
let orig, forcedGift = false
|
|
1404
|
+
if (opts.body != null) {
|
|
1405
|
+
orig = opts.body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1406
|
+
delete opts.duplex
|
|
1407
|
+
let m = modelOf(orig)
|
|
1408
|
+
if (m.toLowerCase().endsWith("-trial")) {
|
|
1409
|
+
gift ??= await startPlan(s.jwt, s.device).catch(() => null)
|
|
1410
|
+
const raw = giftModel(gift, m)
|
|
1411
|
+
const available = raw && giftUsable(s, gift, raw)
|
|
1412
|
+
if (!available) {
|
|
1413
|
+
const error = { type: raw ? "rate_limit_error" : "invalid_request_error", message: raw ? `ZCode's limited quota for ${raw} is exhausted or temporarily unavailable` : `ZCode has no active limited quota for ${m}` }
|
|
1414
|
+
return kept(new Response(JSON.stringify({ type: "error", error }), { status: raw ? 429 : 400, headers: { "content-type": "application/json" } }))
|
|
1415
|
+
}
|
|
1416
|
+
orig = opts.body = JSON.stringify({ ...JSON.parse(orig), model: raw })
|
|
1417
|
+
m = raw
|
|
1418
|
+
forcedGift = true
|
|
1419
|
+
}
|
|
1420
|
+
start = alone || forcedGift
|
|
1421
|
+
}
|
|
1422
|
+
const swap = (base) => {
|
|
1423
|
+
for (const b of [START_BASE, ZAI_BASE, BIGMODEL_BASE]) {
|
|
1424
|
+
if (url.startsWith(b)) {
|
|
1425
|
+
url = base + url.slice(b.length)
|
|
1426
|
+
break
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
let h = new Headers(opts.headers)
|
|
1216
1431
|
if (start) {
|
|
1217
1432
|
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
1218
1433
|
if (!s.jwt) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1434
|
+
swap(START_BASE)
|
|
1219
1435
|
// the Start Plan is served only to what looks like ZCode's own request
|
|
1220
1436
|
startHeaders(h, s.jwt)
|
|
1221
1437
|
if (opts.body != null) {
|
|
1222
|
-
const text = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1438
|
+
const text = orig ?? (typeof opts.body === "string" ? opts.body : await new Response(opts.body).text())
|
|
1223
1439
|
opts.body = dress(text, s.site, s.device)
|
|
1224
1440
|
delete opts.duplex
|
|
1225
1441
|
h.delete("content-length")
|
|
1226
1442
|
}
|
|
1227
1443
|
} else {
|
|
1228
1444
|
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1445
|
+
swap(s.base)
|
|
1229
1446
|
h.delete("authorization")
|
|
1230
1447
|
h.set("x-api-key", key)
|
|
1231
1448
|
h.set("Authorization", "Bearer " + key)
|
|
1232
1449
|
}
|
|
1233
|
-
|
|
1450
|
+
let res = await fetch(url, { ...opts, headers: h })
|
|
1451
|
+
// a trial entry's gift answer that can't serve the request is
|
|
1452
|
+
// the agent's answer, not the coding plan's: the user picked the
|
|
1453
|
+
// gift, so a spent bucket says 429 (its quota error inside a 200
|
|
1454
|
+
// among them) and anything else goes on as it came. A quota
|
|
1455
|
+
// answer keeps that model off the gift for a minute, so a rate
|
|
1456
|
+
// limit is not knocked twice. A stream's body is never read here.
|
|
1457
|
+
if (start && forcedGift) {
|
|
1458
|
+
const json = (res.headers.get("content-type") ?? "").includes("application/json")
|
|
1459
|
+
if (res.status >= 400 || json) {
|
|
1460
|
+
const text = await res.text()
|
|
1461
|
+
const spent = spentUp(text, res.status)
|
|
1462
|
+
if (!spent && res.status < 400) return kept(res, text)
|
|
1463
|
+
if (spent) {
|
|
1464
|
+
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
|
|
1465
|
+
blocked.set(s.key + "\0" + s.jwt + "\0" + modelOf(orig).toLowerCase(), Date.now() + 60_000)
|
|
1466
|
+
}
|
|
1467
|
+
return spent && res.status < 400 ? kept(new Response(text, { status: 429, headers: res.headers })) : kept(res, text)
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1234
1470
|
// the plan's answer goes on as it came, the account kept: the
|
|
1235
1471
|
// built-in never marked a ZCode account lapsed nor cleared one
|
|
1236
|
-
|
|
1237
|
-
kept.delete("content-length")
|
|
1238
|
-
kept.delete("content-encoding")
|
|
1239
|
-
kept.set("X-Magpie-Sign-In", "kept")
|
|
1240
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: kept })
|
|
1472
|
+
return kept(res)
|
|
1241
1473
|
},
|
|
1242
1474
|
}
|
|
1243
1475
|
},
|
|
@@ -1254,6 +1486,9 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1254
1486
|
const saved = (out) => {
|
|
1255
1487
|
const plan = s.plan || auth?.plan
|
|
1256
1488
|
if (plan && !out.plan) out.plan = plan
|
|
1489
|
+
// MCP reads as a footnote: its window goes last on the card
|
|
1490
|
+
if (Array.isArray(out.windows) && out.windows.some((w) => w.name === "MCP · Month"))
|
|
1491
|
+
out.windows = [...out.windows.filter((w) => w.name !== "MCP · Month"), ...out.windows.filter((w) => w.name === "MCP · Month")]
|
|
1257
1492
|
out.signIn = "kept"
|
|
1258
1493
|
return out
|
|
1259
1494
|
}
|
|
@@ -1269,7 +1504,9 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1269
1504
|
}
|
|
1270
1505
|
return saved(await teamUsage(s, key))
|
|
1271
1506
|
}
|
|
1272
|
-
|
|
1507
|
+
const { start, gift } = await plansOf(s)
|
|
1508
|
+
if (start) return saved(await startUsage(s))
|
|
1509
|
+
if (gift) return saved(await dualUsage(s))
|
|
1273
1510
|
return saved(await codingUsage(s))
|
|
1274
1511
|
} catch (e) {
|
|
1275
1512
|
return saved({ error: e?.message ?? String(e) })
|
|
@@ -1300,4 +1537,4 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1300
1537
|
}
|
|
1301
1538
|
|
|
1302
1539
|
// for tests
|
|
1303
|
-
export const _internal = { entry, limitWindows, termOf, startUsage, routes, teamKeys, ownSignIn, stateOf, dress, PROMPT }
|
|
1540
|
+
export const _internal = { entry, limitWindows, termOf, startUsage, routes, blocked, plansOf, giftOf, dualUsage, spentUp, giftServes, modelOf, teamKeys, ownSignIn, stateOf, dress, PROMPT }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-zcode-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|