@magpie-community/opencode-zcode-auth 0.1.7 → 0.1.8
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 +32 -2
- package/index.mjs +214 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,12 +54,42 @@ 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 —
|
|
58
|
+
except for a model the account's gift plans also serve, which goes to
|
|
59
|
+
the gift first (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
|
+
**Spending the gift first.** An account with a GLM Coding Plan can also
|
|
66
|
+
hold gift plans from ZCode activities, ones that expire on a fixed date.
|
|
67
|
+
Their allowance would lapse unused if every request went to the coding
|
|
68
|
+
plan, so the plugin reads the gift's balance (per the sign-in's session,
|
|
69
|
+
kept for 10 minutes) and sends a request for a model the gift still has
|
|
70
|
+
quota to spend to the gift first, dressed as ZCode's own. A bucket is
|
|
71
|
+
judged spent only by its `remaining_units` — a missing one is unknown,
|
|
72
|
+
not spent — and a model whose every bucket says none goes straight to
|
|
73
|
+
the coding plan without asking. When the gift can't serve the request —
|
|
74
|
+
any answer 400 or over (its 405, its 401, its 5xx), or its quota error
|
|
75
|
+
(code 1113 "Insufficient balance or no resource package", code 1005
|
|
76
|
+
"exceed quota limit", which it can answer inside an HTTP 200) — the
|
|
77
|
+
plugin replays the request once to the coding plan, as the agent sent
|
|
78
|
+
it. Only a quota answer keeps that model off the gift for a minute; a
|
|
79
|
+
refusal that says nothing about quota is asked again next request. Every
|
|
80
|
+
429 counts as spent, Z.ai's 1302/1303 rate limits among them. A stream's
|
|
81
|
+
body is never read or buffered on the way.
|
|
82
|
+
|
|
83
|
+
## The usage card
|
|
84
|
+
|
|
85
|
+
An account with both plans shows both allowances: the coding plan's
|
|
86
|
+
windows first, then the gift's buckets, each with its own name, models
|
|
87
|
+
and end. The card's plan and term stay the coding plan's. A gift read
|
|
88
|
+
that fails leaves just the coding card — magpie hides a card's windows
|
|
89
|
+
behind an error, so a gift hiccup must not black out a working Coding
|
|
90
|
+
Plan. A gift that goes out of date while requests use it simply stops
|
|
91
|
+
showing.
|
|
92
|
+
|
|
63
93
|
## Models
|
|
64
94
|
|
|
65
95
|
The models are the ones ZCode offers for the account's plan. They come
|
package/index.mjs
CHANGED
|
@@ -130,48 +130,103 @@ const num = (v) => {
|
|
|
130
130
|
return typeof n === "number" && Number.isFinite(n) ? n : undefined
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// startPlan is the
|
|
134
|
-
//
|
|
133
|
+
// startPlan is the gift an account can spend now ({models}), as ZCode reads
|
|
134
|
+
// its balance: several plans can be active at once, and a model routes to
|
|
135
|
+
// the gift when any of them has a live bucket serving it — a bucket past
|
|
136
|
+
// its plan's own end, or one whose remaining_units says none, is skipped
|
|
137
|
+
// rather than asked and refused. models are those live buckets' names.
|
|
135
138
|
async function startPlan(jwt, device) {
|
|
136
139
|
if (!jwt) throw new Error("not signed in to ZCode")
|
|
137
140
|
if (jwtExpired(jwt)) throw new Error(EXPIRED)
|
|
138
141
|
const b = (await call("GET", `${ZCODE}/api/v1/zcode-plan/billing/balance?app_version=${APP_VERSION}`, { auth: "Bearer " + jwt, device })) ?? {}
|
|
139
142
|
const now = num(b.server_time) > 0 ? num(b.server_time) : Date.now() / 1000
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
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 ?? ""))
|
|
144
|
+
const plans = Array.isArray(b.plans) ? b.plans : []
|
|
145
|
+
const balances = Array.isArray(b.balances) ? b.balances : []
|
|
146
|
+
const ms = new Set()
|
|
147
|
+
let any = false
|
|
148
|
+
let name = ""
|
|
149
|
+
for (const p of plans) {
|
|
150
|
+
const st = String(p?.status ?? "").trim().toLowerCase()
|
|
151
|
+
const end = num(p?.ends_at)
|
|
152
|
+
if (st !== "active" || (end > 0 && end <= now)) continue
|
|
153
|
+
any = true
|
|
154
|
+
if (!name) name = String(p?.name ?? "").trim()
|
|
155
|
+
for (const x of balances) {
|
|
156
|
+
if (!same(x, p)) continue
|
|
157
|
+
const exp = num(x.expires_at)
|
|
158
|
+
if (exp > 0 && exp <= now) continue
|
|
159
|
+
// a bucket spends when it has quota: only remaining_units says so, and
|
|
160
|
+
// a model whose every bucket says none drops out of models, so its
|
|
161
|
+
// requests go to the coding plan without first asking the gift
|
|
162
|
+
if ((num(x.remaining_units) ?? 1) <= 0) continue
|
|
163
|
+
for (const c of Array.isArray(x.capabilities) ? x.capabilities : []) {
|
|
164
|
+
const m = String(c ?? "").trim().replace(/^model:/, "").trim()
|
|
165
|
+
if (m) ms.add(m)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
149
168
|
}
|
|
150
|
-
return null
|
|
169
|
+
return any ? { name: name || "Start Plan", models: [...ms] } : null
|
|
151
170
|
}
|
|
152
171
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
172
|
+
// plansOf says where an account's requests go and what more it can spend:
|
|
173
|
+
// {start, gift}. start is what onStart said: an account with no coding plan
|
|
174
|
+
// sends every request to the gift and reads no balance to do it; a team's
|
|
175
|
+
// seat, a sign-in with no ZCode token and a key whose plan-list read found
|
|
176
|
+
// no gift stay on the coding plan. gift is what a coding account also holds:
|
|
177
|
+
// the gift plans ({models}), and its requests spend the gift for a model it
|
|
178
|
+
// serves, until it says it is spent (see blocked). Asked again after 10
|
|
179
|
+
// minutes (a minute when unsure — a plan-list read that failed is retried
|
|
180
|
+
// sooner; a gift read that failed simply leaves gift null until the next
|
|
181
|
+
// ask).
|
|
156
182
|
const routes = new Map()
|
|
157
|
-
async function
|
|
158
|
-
if (
|
|
159
|
-
if (!s.key) return true
|
|
183
|
+
async function plansOf(s) {
|
|
184
|
+
if (isTeam(s) || !s.jwt) return { start: false, gift: null }
|
|
185
|
+
if (!s.key) return { start: true, gift: null }
|
|
160
186
|
const id = s.key + "\0" + s.jwt
|
|
161
187
|
const r = routes.get(id)
|
|
162
|
-
if (r && Date.now() - r.at < r.ttl) return r
|
|
188
|
+
if (r && Date.now() - r.at < r.ttl) return r
|
|
163
189
|
let start = false
|
|
190
|
+
let gift = null
|
|
164
191
|
let sure = false
|
|
165
192
|
try {
|
|
166
|
-
|
|
193
|
+
if (!(await plan(s))) start = true
|
|
194
|
+
else gift = await startPlan(s.jwt, s.device).catch(() => null)
|
|
167
195
|
sure = true
|
|
168
196
|
} catch {
|
|
197
|
+
// the plan list could not be read: a key account takes the dual route
|
|
198
|
+
// (start stays false, the gift is kept, a minute's ttl), so a gift
|
|
199
|
+
// refusal still replays to the coding plan rather than blacking out
|
|
169
200
|
try {
|
|
170
|
-
|
|
201
|
+
gift = await startPlan(s.jwt, s.device)
|
|
202
|
+
sure = false
|
|
171
203
|
} catch {}
|
|
172
204
|
}
|
|
173
|
-
|
|
174
|
-
|
|
205
|
+
const out = { start, gift, at: Date.now(), ttl: sure ? 600_000 : 60_000 }
|
|
206
|
+
routes.set(id, out)
|
|
207
|
+
return out
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// blocked remembers the models a gift said it has none left for: a key+jwt+
|
|
211
|
+
// model for a minute, so one turn that is refused does not ask again.
|
|
212
|
+
const blocked = new Map()
|
|
213
|
+
|
|
214
|
+
// giftServes: the gift's buckets name their models lowercased ("glm-5.3-
|
|
215
|
+
// flash"); what is asked for arrives as the plan lists it ("GLM-5.3-Flash").
|
|
216
|
+
const giftServes = (gift, m) => (gift.models ?? []).some((x) => x.toLowerCase() === m.toLowerCase())
|
|
217
|
+
|
|
218
|
+
// spentUp is the gift saying it has nothing left to spend: its own no-
|
|
219
|
+
// resource-package code 1113, its "exceed quota limit" 1005 — a spent gift
|
|
220
|
+
// can answer a 200 with a JSON body carrying one of those codes. Every 429
|
|
221
|
+
// counts as spent, Z.ai's 1302/1303 rate limits among them.
|
|
222
|
+
function spentUp(text, status) {
|
|
223
|
+
if (status === 429) return true
|
|
224
|
+
let b
|
|
225
|
+
try { b = JSON.parse(text) } catch { return false }
|
|
226
|
+
if (!b || typeof b !== "object") return false
|
|
227
|
+
const code = (v) => String(v ?? "")
|
|
228
|
+
const c1 = code(b.code), c2 = code(b.error?.code)
|
|
229
|
+
return c1 === "1113" || c1 === "1005" || c2 === "1113" || c2 === "1005"
|
|
175
230
|
}
|
|
176
231
|
|
|
177
232
|
// ---- keys ------------------------------------------------------------------------
|
|
@@ -366,10 +421,12 @@ function startNum(v) {
|
|
|
366
421
|
return undefined
|
|
367
422
|
}
|
|
368
423
|
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
424
|
+
// giftOf reads the balance and returns the gift's card fields: its plans and
|
|
425
|
+
// windows. Several plans can be active at once, so every live plan's buckets
|
|
426
|
+
// show, each keeping its own models and end; the plan and end put on the
|
|
427
|
+
// card (for a gift-only account) are the soonest-ending live plan's. null
|
|
428
|
+
// when there is no live plan to show.
|
|
429
|
+
async function giftOf(s) {
|
|
373
430
|
if (!s.jwt) throw new Error("not signed in to ZCode")
|
|
374
431
|
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
375
432
|
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 +439,7 @@ async function startUsage(s) {
|
|
|
382
439
|
if (end > 0 && end <= now && p.status.trim().toLowerCase() === "active") p.status = "expired"
|
|
383
440
|
if (p.status.trim().toLowerCase() === "expired") over.add(p.user_plan_id + "\0" + p.plan_id)
|
|
384
441
|
}
|
|
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)
|
|
442
|
+
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
443
|
const balances = (Array.isArray(b.balances) ? b.balances : [])
|
|
387
444
|
.map((x) => ({ ...x, user_plan_id: str(x?.user_plan_id), plan_id: str(x?.plan_id) }))
|
|
388
445
|
.filter((x) => {
|
|
@@ -395,32 +452,41 @@ async function startUsage(s) {
|
|
|
395
452
|
return keep
|
|
396
453
|
})
|
|
397
454
|
|
|
455
|
+
// every live plan counts as the gift: no plan-name filter decides it,
|
|
456
|
+
// the balance says what the session can spend
|
|
398
457
|
let active = null
|
|
399
458
|
for (const p of plans) {
|
|
400
459
|
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
460
|
const end = startNum(p.ends_at)
|
|
405
|
-
|
|
406
|
-
|
|
461
|
+
if (end > 0 && end <= now) continue
|
|
462
|
+
if (active && !(end > 0 && (!startNum(active.end) || end < startNum(active.end)))) continue
|
|
463
|
+
active = { name: first(str(p.name), "Start Plan"), end, until: end > 0 ? new Date(Math.trunc(end) * 1000).toISOString() : "" }
|
|
407
464
|
}
|
|
408
|
-
if (!active) return
|
|
465
|
+
if (!active) return null
|
|
409
466
|
const out = { plan: active.name, windows: [] }
|
|
410
467
|
if (active.until) Object.assign(out, { until: active.until, renew: "off" })
|
|
468
|
+
const live = plans.filter((p) => {
|
|
469
|
+
if (p.status.trim().toLowerCase() !== "active") return false
|
|
470
|
+
const end = startNum(p.ends_at)
|
|
471
|
+
return !(end > 0 && end <= now)
|
|
472
|
+
})
|
|
411
473
|
for (const x of balances) {
|
|
474
|
+
const owner = live.find((p) => same(x, p))
|
|
475
|
+
if (!owner) continue
|
|
476
|
+
const exp = startNum(x.expires_at)
|
|
477
|
+
if (exp > 0 && exp <= now) continue // a bucket past its own end is over
|
|
412
478
|
const total = startNum(x.total_units), left = startNum(x.remaining_units)
|
|
413
479
|
let used = startNum(x.used_units)
|
|
414
480
|
if (total === undefined && used === undefined && left === undefined) continue
|
|
415
481
|
const models = (Array.isArray(x.capabilities) ? x.capabilities : [])
|
|
416
482
|
.map((c) => String(c ?? "").trim().replace(/^model:/, "").trim()).filter(Boolean)
|
|
417
|
-
const
|
|
483
|
+
const base = first(str(x.show_name), models.join(", "), "Credits")
|
|
484
|
+
const w = { name: base, used: 0, _plan: str(owner.name) }
|
|
418
485
|
if (used === undefined) used = total !== undefined && left !== undefined ? total - left : 0
|
|
419
486
|
if (total > 0) {
|
|
420
487
|
w.used = (100 * used) / total
|
|
421
488
|
w.display = `${compact(used)} / ${compact(total)}`
|
|
422
489
|
}
|
|
423
|
-
const exp = startNum(x.expires_at)
|
|
424
490
|
if (exp > 0) w.resetsAt = new Date(Math.trunc(exp) * 1000).toISOString()
|
|
425
491
|
let span = 0
|
|
426
492
|
for (const p of plans) {
|
|
@@ -440,6 +506,37 @@ async function startUsage(s) {
|
|
|
440
506
|
return out
|
|
441
507
|
}
|
|
442
508
|
|
|
509
|
+
// startUsage is a gift-only account's card: its buckets, or the words
|
|
510
|
+
// for having none to show. The card's header already names the plan, so
|
|
511
|
+
// the windows keep their own names.
|
|
512
|
+
const NO_START = "this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started"
|
|
513
|
+
async function startUsage(s) {
|
|
514
|
+
const g = await giftOf(s)
|
|
515
|
+
if (!g) return { error: NO_START }
|
|
516
|
+
for (const w of g.windows) delete w._plan
|
|
517
|
+
return g
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// dualUsage is an account with both a Coding Plan and a gift: its card shows
|
|
521
|
+
// both allowances, the Coding windows first and the gift's buckets after,
|
|
522
|
+
// each keeping its own models and end. The gift's windows carry their plan's
|
|
523
|
+
// name as a prefix so they are not mistaken for the coding plan's own. A
|
|
524
|
+
// gift read that fails leaves just the Coding card: magpie hides a card's
|
|
525
|
+
// windows behind an error and leaves it out of the menu bar and auto-switch,
|
|
526
|
+
// so erroring here would hide a working Coding Plan over a gift hiccup.
|
|
527
|
+
async function dualUsage(s) {
|
|
528
|
+
const out = await codingUsage(s)
|
|
529
|
+
try {
|
|
530
|
+
const g = await giftOf(s)
|
|
531
|
+
if (g) out.windows = [...out.windows, ...g.windows.map((w) => {
|
|
532
|
+
const p = w._plan
|
|
533
|
+
delete w._plan
|
|
534
|
+
return p && p !== w.name ? { ...w, name: p + " · " + w.name } : w
|
|
535
|
+
})]
|
|
536
|
+
} catch {}
|
|
537
|
+
return out
|
|
538
|
+
}
|
|
539
|
+
|
|
443
540
|
// periodOf reads an entitlement's period: "daily", "weekly", "monthly".
|
|
444
541
|
function periodOf(p) {
|
|
445
542
|
p = p.toLowerCase()
|
|
@@ -989,6 +1086,26 @@ const entry = (m) => ({
|
|
|
989
1086
|
})
|
|
990
1087
|
|
|
991
1088
|
// model is a model as OpenCode's provider hands it to provider.models.
|
|
1089
|
+
// modelOf reads the model a Messages body asks for.
|
|
1090
|
+
function modelOf(text) {
|
|
1091
|
+
try {
|
|
1092
|
+
const m = JSON.parse(text)?.model
|
|
1093
|
+
return typeof m === "string" ? m : ""
|
|
1094
|
+
} catch {
|
|
1095
|
+
return ""
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// kept passes a response on as it came, the account kept: the built-in
|
|
1100
|
+
// never marked a ZCode account lapsed nor cleared one.
|
|
1101
|
+
function kept(res, text) {
|
|
1102
|
+
const h = new Headers(res.headers)
|
|
1103
|
+
h.delete("content-length")
|
|
1104
|
+
h.delete("content-encoding")
|
|
1105
|
+
h.set("X-Magpie-Sign-In", "kept")
|
|
1106
|
+
return new Response(text !== undefined ? text : res.body, { status: res.status, statusText: res.statusText, headers: h })
|
|
1107
|
+
}
|
|
1108
|
+
|
|
992
1109
|
function model(m, url) {
|
|
993
1110
|
const e = entry(m)
|
|
994
1111
|
return {
|
|
@@ -1168,7 +1285,7 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1168
1285
|
async models(provider, { auth } = {}) {
|
|
1169
1286
|
const s = stateOf(auth)
|
|
1170
1287
|
if (!s) return provider.models
|
|
1171
|
-
const start = await
|
|
1288
|
+
const start = await plansOf(s).then((r) => r.start).catch(() => false)
|
|
1172
1289
|
const base = start ? START_BASE : s.base
|
|
1173
1290
|
let ms = modelsOf(await zcodeConfig().catch(() => null), planID(base))
|
|
1174
1291
|
// ZCode's config can't be had: its table stands in, and magpie keeps
|
|
@@ -1196,14 +1313,7 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1196
1313
|
if (!s) throw new Error("not signed in to ZCode")
|
|
1197
1314
|
let url = input instanceof Request ? input.url : String(input)
|
|
1198
1315
|
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
|
-
// the request goes to the plan the account is on, wherever it was made for
|
|
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
|
-
}
|
|
1316
|
+
const { start: alone, gift } = await plansOf(s)
|
|
1207
1317
|
let key = s.key
|
|
1208
1318
|
if (isTeam(s) && !key) {
|
|
1209
1319
|
key = await teamKey(s)
|
|
@@ -1212,32 +1322,79 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1212
1322
|
await client?.auth?.set?.({ path: { id }, body: next }).catch?.(() => {})
|
|
1213
1323
|
}
|
|
1214
1324
|
}
|
|
1215
|
-
|
|
1325
|
+
// a dual account's body is read ahead of sending, so a gift that
|
|
1326
|
+
// says it is spent can spend nothing and replay what was asked for
|
|
1327
|
+
let start = alone
|
|
1328
|
+
let orig
|
|
1329
|
+
if (!alone && gift && opts.body != null) {
|
|
1330
|
+
orig = opts.body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1331
|
+
delete opts.duplex
|
|
1332
|
+
const m = modelOf(orig)
|
|
1333
|
+
// a ZCode sign-in past its end reaches no gift: the coding plan
|
|
1334
|
+
// takes the request, as it did before the gift was known
|
|
1335
|
+
start = !!m && !jwtExpired(s.jwt) && giftServes(gift, m) && Date.now() >= (blocked.get(s.key + "\0" + s.jwt + "\0" + m) ?? 0)
|
|
1336
|
+
}
|
|
1337
|
+
const swap = (base) => {
|
|
1338
|
+
for (const b of [START_BASE, ZAI_BASE, BIGMODEL_BASE]) {
|
|
1339
|
+
if (url.startsWith(b)) {
|
|
1340
|
+
url = base + url.slice(b.length)
|
|
1341
|
+
break
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
let h = new Headers(opts.headers)
|
|
1216
1346
|
if (start) {
|
|
1217
1347
|
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
1218
1348
|
if (!s.jwt) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1349
|
+
swap(START_BASE)
|
|
1219
1350
|
// the Start Plan is served only to what looks like ZCode's own request
|
|
1220
1351
|
startHeaders(h, s.jwt)
|
|
1221
1352
|
if (opts.body != null) {
|
|
1222
|
-
const text = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1353
|
+
const text = orig ?? (typeof opts.body === "string" ? opts.body : await new Response(opts.body).text())
|
|
1223
1354
|
opts.body = dress(text, s.site, s.device)
|
|
1224
1355
|
delete opts.duplex
|
|
1225
1356
|
h.delete("content-length")
|
|
1226
1357
|
}
|
|
1227
1358
|
} else {
|
|
1228
1359
|
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1360
|
+
swap(s.base)
|
|
1229
1361
|
h.delete("authorization")
|
|
1230
1362
|
h.set("x-api-key", key)
|
|
1231
1363
|
h.set("Authorization", "Bearer " + key)
|
|
1232
1364
|
}
|
|
1233
|
-
|
|
1365
|
+
let res = await fetch(url, { ...opts, headers: h })
|
|
1366
|
+
// a dual account's gift answer that can't serve the request goes
|
|
1367
|
+
// to its coding plan instead, as the agent sent it: any status
|
|
1368
|
+
// 400 or over (its 405, its 401, its 5xx — a paid plan is waiting,
|
|
1369
|
+
// and a gift that keeps refusing would otherwise black out the
|
|
1370
|
+
// account), and its quota answers inside a 200. Only a quota
|
|
1371
|
+
// answer keeps that model off the gift for a minute: a refusal
|
|
1372
|
+
// that says nothing about quota is asked again next request.
|
|
1373
|
+
// Every 429 counts as spent, Z.ai's 1302/1303 rate limits among
|
|
1374
|
+
// them — replaying one to the coding plan is the good answer, and
|
|
1375
|
+
// the minute's block keeps a rate limit from being knocked twice.
|
|
1376
|
+
// A stream's body is never read here.
|
|
1377
|
+
if (start && !alone) {
|
|
1378
|
+
const json = (res.headers.get("content-type") ?? "").includes("application/json")
|
|
1379
|
+
if (res.status >= 400 || json) {
|
|
1380
|
+
const text = await res.text()
|
|
1381
|
+
const spent = spentUp(text, res.status)
|
|
1382
|
+
if (!spent && res.status < 400) return kept(res, text)
|
|
1383
|
+
if (spent) {
|
|
1384
|
+
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
|
|
1385
|
+
blocked.set(s.key + "\0" + s.jwt + "\0" + modelOf(orig), Date.now() + 60_000)
|
|
1386
|
+
}
|
|
1387
|
+
swap(s.base)
|
|
1388
|
+
h = new Headers(opts.headers)
|
|
1389
|
+
h.delete("authorization")
|
|
1390
|
+
h.set("x-api-key", key)
|
|
1391
|
+
h.set("Authorization", "Bearer " + key)
|
|
1392
|
+
res = await fetch(url, { ...opts, body: orig, headers: h })
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1234
1395
|
// the plan's answer goes on as it came, the account kept: the
|
|
1235
1396
|
// 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 })
|
|
1397
|
+
return kept(res)
|
|
1241
1398
|
},
|
|
1242
1399
|
}
|
|
1243
1400
|
},
|
|
@@ -1269,7 +1426,9 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1269
1426
|
}
|
|
1270
1427
|
return saved(await teamUsage(s, key))
|
|
1271
1428
|
}
|
|
1272
|
-
|
|
1429
|
+
const { start, gift } = await plansOf(s)
|
|
1430
|
+
if (start) return saved(await startUsage(s))
|
|
1431
|
+
if (gift) return saved(await dualUsage(s))
|
|
1273
1432
|
return saved(await codingUsage(s))
|
|
1274
1433
|
} catch (e) {
|
|
1275
1434
|
return saved({ error: e?.message ?? String(e) })
|
|
@@ -1300,4 +1459,4 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1300
1459
|
}
|
|
1301
1460
|
|
|
1302
1461
|
// for tests
|
|
1303
|
-
export const _internal = { entry, limitWindows, termOf, startUsage, routes, teamKeys, ownSignIn, stateOf, dress, PROMPT }
|
|
1462
|
+
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.8",
|
|
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",
|