@magpie-community/opencode-zcode-auth 0.1.23 → 0.1.25
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 +5 -0
- package/index.mjs +166 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,11 @@ over Anthropic's Messages API. Provider id: `zcode`.
|
|
|
53
53
|
another's credentials: to use both, add the app's sign-in once for each.
|
|
54
54
|
- **GLM Coding Plan API key**
|
|
55
55
|
- Paste a key (`<id>.<secret>`) and say which site it is from.
|
|
56
|
+
- A key reaches the GLM Coding Plan alone. ZCode's gift plans (the
|
|
57
|
+
Start Plan, ZCode 1024 Dev Day) are served to ZCode's own session,
|
|
58
|
+
which a key doesn't carry: for an account with only a gift plan, the
|
|
59
|
+
card and each refused request (1113) say to sign in with ZCode
|
|
60
|
+
instead.
|
|
56
61
|
|
|
57
62
|
## Where the sign-in is kept
|
|
58
63
|
|
package/index.mjs
CHANGED
|
@@ -437,14 +437,54 @@ async function codingUsage(s) {
|
|
|
437
437
|
if (level) out.plan = "GLM Coding " + level[0].toUpperCase() + level.slice(1)
|
|
438
438
|
out.windows = limitWindows(d)
|
|
439
439
|
const resets = personalResets(root, s.key)
|
|
440
|
+
let subs = null
|
|
440
441
|
try {
|
|
441
|
-
|
|
442
|
+
subs = await call("GET", root + "/api/biz/subscription/list", { auth: s.key })
|
|
443
|
+
Object.assign(out, termOf(subs))
|
|
442
444
|
} catch {}
|
|
443
445
|
const r = await resets
|
|
444
446
|
if (r) out.resets = r
|
|
447
|
+
// a key whose account has no plan in force, read as such, says so
|
|
448
|
+
// rather than showing an empty card: what a key can spend is a GLM
|
|
449
|
+
// Coding Plan alone (a ZCode gift plan is never one, see NO_PLAN_KEY).
|
|
450
|
+
// A list that couldn't be read, a level, a counted window or a plan
|
|
451
|
+
// the sign-in saved keeps the card as it was.
|
|
452
|
+
const valid = (Array.isArray(subs) ? subs : []).some((x) => String(x?.status ?? "").toUpperCase() === "VALID")
|
|
453
|
+
if (subs !== null && !valid && !level && !s.plan && !out.windows.some((w) => !w.aside)) return { error: NO_PLAN_KEY }
|
|
445
454
|
return out
|
|
446
455
|
}
|
|
447
456
|
|
|
457
|
+
// NO_PLAN_KEY is a key with no GLM Coding Plan behind it. ZCode's gift
|
|
458
|
+
// plans (its Start Plan, ZCode 1024 Dev Day) are served by zcode.z.ai
|
|
459
|
+
// to ZCode's own session token, which a sign-in with ZCode (the app's,
|
|
460
|
+
// or Z.ai's or BigModel's in the browser) carries and a key never does:
|
|
461
|
+
// with a key, a gift-only account has nothing to spend, and Z.ai answers
|
|
462
|
+
// each request 1113 (rtbeilin on X, 10-24).
|
|
463
|
+
const NO_PLAN_KEY =
|
|
464
|
+
"this key's account has no GLM Coding Plan; ZCode's gift plans (the Start Plan, ZCode 1024 Dev Day) are spent only with ZCode's sign-in, never with a key — add the account with \"ZCode app's sign-in\" or a ZCode sign-in in the browser"
|
|
465
|
+
|
|
466
|
+
// noPlanKey is a coding plan's 1113 refusal (no plan, no resource
|
|
467
|
+
// package) to a sign-in with no ZCode session, its message followed by
|
|
468
|
+
// NO_PLAN_KEY; anything else goes on as it came. Read only once the
|
|
469
|
+
// answer has failed.
|
|
470
|
+
function noPlanKey(res, text) {
|
|
471
|
+
let v
|
|
472
|
+
try {
|
|
473
|
+
v = JSON.parse(text)
|
|
474
|
+
} catch {
|
|
475
|
+
return null
|
|
476
|
+
}
|
|
477
|
+
// Z.ai names the code in error.code ("1113", as magpie's
|
|
478
|
+
// zhipu_coding_test has it) or error.type (its 401's "1000"), BigModel's
|
|
479
|
+
// envelope in code
|
|
480
|
+
if (![v?.error?.code, v?.error?.type, v?.code].some((c) => String(c ?? "") === "1113")) return null
|
|
481
|
+
const said = first(v?.error?.message, v?.msg, v?.message)
|
|
482
|
+
const out = v?.error && typeof v.error === "object" ? { ...v, error: { ...v.error, message: (said ? said + " — " : "") + NO_PLAN_KEY } } : { ...v, msg: (said ? said + " — " : "") + NO_PLAN_KEY }
|
|
483
|
+
const h = new Headers(res.headers)
|
|
484
|
+
h.set("content-type", "application/json")
|
|
485
|
+
return new Response(JSON.stringify(out), { status: res.status, statusText: res.statusText, headers: h })
|
|
486
|
+
}
|
|
487
|
+
|
|
448
488
|
// startNum reads a number the balance gives as a number or a string.
|
|
449
489
|
function startNum(v) {
|
|
450
490
|
if (isNum(v)) return v
|
|
@@ -1459,6 +1499,110 @@ function startHeaders(h, jwt) {
|
|
|
1459
1499
|
return out
|
|
1460
1500
|
}
|
|
1461
1501
|
|
|
1502
|
+
// Claude Code's tool search, as ZCode's plans take it. Claude Code
|
|
1503
|
+
// (2.1.310) offers a tool named ToolSearch, names its deferred tools in a
|
|
1504
|
+
// system turn, and sends a deferred tool's schema with defer_loading only
|
|
1505
|
+
// once a search loaded it (a tool_reference in a ToolSearch result), with
|
|
1506
|
+
// a DeferredToolPlaceholder that keeps the search on. api.z.ai's
|
|
1507
|
+
// Anthropic endpoint, given a tool named exactly ToolSearch, shows the
|
|
1508
|
+
// model none of the other tools, Bash, Read and Write among them, but
|
|
1509
|
+
// those a tool_reference loaded: a subagent, which hasn't searched,
|
|
1510
|
+
// reports only the deferred tools, and the main turn has to search for
|
|
1511
|
+
// Bash before it can run it (小八 on Discord, 10-24; the beta header
|
|
1512
|
+
// changes nothing, the name alone does). ZCode itself sends no ToolSearch
|
|
1513
|
+
// and no defer_loading. So the search goes as SEARCH_ALIAS, in the tools
|
|
1514
|
+
// and in the turns that called it, the tools Claude Code sent with their
|
|
1515
|
+
// schemas go as ZCode's do, without defer_loading, and the placeholder is
|
|
1516
|
+
// left out; the model's calls to SEARCH_ALIAS come back as ToolSearch
|
|
1517
|
+
// (searchNamed).
|
|
1518
|
+
const SEARCH = "ToolSearch"
|
|
1519
|
+
const SEARCH_ALIAS = "ToolSearchTool"
|
|
1520
|
+
const PLACEHOLDER = "DeferredToolPlaceholder"
|
|
1521
|
+
|
|
1522
|
+
// searchless is a Messages request body without Claude Code's ToolSearch
|
|
1523
|
+
// name, and whether it had the tool; anything else goes as it is.
|
|
1524
|
+
function searchless(text) {
|
|
1525
|
+
if (typeof text !== "string" || !text.includes(`"${SEARCH}"`)) return { text, renamed: false }
|
|
1526
|
+
let b
|
|
1527
|
+
try {
|
|
1528
|
+
b = JSON.parse(text)
|
|
1529
|
+
} catch {
|
|
1530
|
+
return { text, renamed: false }
|
|
1531
|
+
}
|
|
1532
|
+
if (!b || !Array.isArray(b.tools) || !b.tools.some((t) => t?.name === SEARCH)) return { text, renamed: false }
|
|
1533
|
+
// a tool of the alias's own name keeps it; the search can't then be renamed
|
|
1534
|
+
if (b.tools.some((t) => t?.name === SEARCH_ALIAS)) return { text, renamed: false }
|
|
1535
|
+
b.tools = b.tools
|
|
1536
|
+
.filter((t) => t?.name !== PLACEHOLDER)
|
|
1537
|
+
.map((t) => {
|
|
1538
|
+
if (!t || typeof t !== "object") return t
|
|
1539
|
+
const { defer_loading, ...rest } = t
|
|
1540
|
+
return t.name === SEARCH ? { ...rest, name: SEARCH_ALIAS } : rest
|
|
1541
|
+
})
|
|
1542
|
+
if (Array.isArray(b.messages))
|
|
1543
|
+
for (const m of b.messages)
|
|
1544
|
+
if (m?.role === "assistant" && Array.isArray(m.content))
|
|
1545
|
+
for (const x of m.content) if (x?.type === "tool_use" && x.name === SEARCH) x.name = SEARCH_ALIAS
|
|
1546
|
+
if (b.tool_choice?.type === "tool" && b.tool_choice.name === SEARCH) b.tool_choice.name = SEARCH_ALIAS
|
|
1547
|
+
return { text: JSON.stringify(b), renamed: true }
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// searchNamed is an answer to a request searchless renamed, the model's
|
|
1551
|
+
// calls to SEARCH_ALIAS named ToolSearch again, as Claude Code runs them:
|
|
1552
|
+
// a stream's content_block_start events, or a whole message's content.
|
|
1553
|
+
function searchNamed(res) {
|
|
1554
|
+
if (!res.body) return res
|
|
1555
|
+
const type = res.headers.get("content-type") ?? ""
|
|
1556
|
+
const named = (block) => {
|
|
1557
|
+
if (block?.type === "tool_use" && block.name === SEARCH_ALIAS) {
|
|
1558
|
+
block.name = SEARCH
|
|
1559
|
+
return true
|
|
1560
|
+
}
|
|
1561
|
+
return false
|
|
1562
|
+
}
|
|
1563
|
+
const line = (l) => {
|
|
1564
|
+
if (!l.startsWith("data:") || !l.includes(`"${SEARCH_ALIAS}"`)) return l
|
|
1565
|
+
try {
|
|
1566
|
+
const d = JSON.parse(l.slice(5))
|
|
1567
|
+
return named(d?.content_block) ? "data: " + JSON.stringify(d) : l
|
|
1568
|
+
} catch {
|
|
1569
|
+
return l
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (type.includes("text/event-stream")) {
|
|
1573
|
+
const dec = new TextDecoder()
|
|
1574
|
+
const enc = new TextEncoder()
|
|
1575
|
+
let held = ""
|
|
1576
|
+
const body = res.body.pipeThrough(
|
|
1577
|
+
new TransformStream({
|
|
1578
|
+
transform(chunk, ctl) {
|
|
1579
|
+
held += dec.decode(chunk, { stream: true })
|
|
1580
|
+
const i = held.lastIndexOf("\n")
|
|
1581
|
+
if (i < 0) return
|
|
1582
|
+
ctl.enqueue(enc.encode(held.slice(0, i + 1).split("\n").map(line).join("\n")))
|
|
1583
|
+
held = held.slice(i + 1)
|
|
1584
|
+
},
|
|
1585
|
+
flush(ctl) {
|
|
1586
|
+
held += dec.decode()
|
|
1587
|
+
if (held) ctl.enqueue(enc.encode(line(held)))
|
|
1588
|
+
},
|
|
1589
|
+
}),
|
|
1590
|
+
)
|
|
1591
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1592
|
+
}
|
|
1593
|
+
if (!type.includes("json")) return res
|
|
1594
|
+
return res.text().then((text) => {
|
|
1595
|
+
let out = text
|
|
1596
|
+
if (text.includes(`"${SEARCH_ALIAS}"`)) {
|
|
1597
|
+
try {
|
|
1598
|
+
const v = JSON.parse(text)
|
|
1599
|
+
if (Array.isArray(v?.content) && v.content.map(named).some(Boolean)) out = JSON.stringify(v)
|
|
1600
|
+
} catch {}
|
|
1601
|
+
}
|
|
1602
|
+
return new Response(out, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1603
|
+
})
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1462
1606
|
// startURL is a Start Plan request's URL without the query the agent put
|
|
1463
1607
|
// on it: Claude Code asks /v1/messages?beta=true, which magpie passes on,
|
|
1464
1608
|
// and ZCode asks /v1/messages alone.
|
|
@@ -1670,9 +1814,18 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1670
1814
|
// says it is spent can spend nothing and replay what was asked for
|
|
1671
1815
|
let start = alone
|
|
1672
1816
|
let orig, forcedGift = false
|
|
1817
|
+
let renamed = false
|
|
1818
|
+
const done = (res) => (renamed ? searchNamed(res) : res)
|
|
1673
1819
|
if (opts.body != null) {
|
|
1674
1820
|
orig = opts.body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1675
1821
|
delete opts.duplex
|
|
1822
|
+
// Claude Code's tool search goes under another name, or
|
|
1823
|
+
// ZCode's plans show the model no other tool (searchless)
|
|
1824
|
+
const plain = searchless(orig)
|
|
1825
|
+
if (plain.renamed) {
|
|
1826
|
+
orig = opts.body = plain.text
|
|
1827
|
+
renamed = true
|
|
1828
|
+
}
|
|
1676
1829
|
let m = modelOf(orig)
|
|
1677
1830
|
if (m.toLowerCase().endsWith("-trial")) {
|
|
1678
1831
|
gift ??= await startPlan(s.jwt, s.device).catch(() => null)
|
|
@@ -1714,6 +1867,7 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1714
1867
|
} else {
|
|
1715
1868
|
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1716
1869
|
swap(s.base)
|
|
1870
|
+
if (renamed) h.delete("content-length")
|
|
1717
1871
|
h.delete("authorization")
|
|
1718
1872
|
h.set("x-api-key", key)
|
|
1719
1873
|
h.set("Authorization", "Bearer " + key)
|
|
@@ -1726,6 +1880,14 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1726
1880
|
if (startRefused(res.status, text)) return kept(startBlocked(res, text))
|
|
1727
1881
|
res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1728
1882
|
}
|
|
1883
|
+
// a key with no plan behind it: what Z.ai said, and why a
|
|
1884
|
+
// ZCode gift plan can't be had with a key (NO_PLAN_KEY)
|
|
1885
|
+
if (!start && !s.jwt && res.status >= 400) {
|
|
1886
|
+
const text = await res.text()
|
|
1887
|
+
const why = noPlanKey(res, text)
|
|
1888
|
+
if (why) return done(kept(why))
|
|
1889
|
+
res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1890
|
+
}
|
|
1729
1891
|
// a trial entry's gift answer that can't serve the request is
|
|
1730
1892
|
// the agent's answer, not the coding plan's: the user picked the
|
|
1731
1893
|
// gift, so a spent bucket says 429 (its quota error inside a 200
|
|
@@ -1737,17 +1899,17 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1737
1899
|
if (res.status >= 400 || json) {
|
|
1738
1900
|
const text = await res.text()
|
|
1739
1901
|
const spent = spentUp(text, res.status)
|
|
1740
|
-
if (!spent && res.status < 400) return kept(res, text)
|
|
1902
|
+
if (!spent && res.status < 400) return done(kept(res, text))
|
|
1741
1903
|
if (spent) {
|
|
1742
1904
|
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
|
|
1743
1905
|
blocked.set(s.key + "\0" + s.jwt + "\0" + modelOf(orig).toLowerCase(), Date.now() + 60_000)
|
|
1744
1906
|
}
|
|
1745
|
-
return spent && res.status < 400 ? kept(new Response(text, { status: 429, headers: res.headers })) : kept(res, text)
|
|
1907
|
+
return spent && res.status < 400 ? kept(new Response(text, { status: 429, headers: res.headers })) : done(kept(res, text))
|
|
1746
1908
|
}
|
|
1747
1909
|
}
|
|
1748
1910
|
// the plan's answer goes on as it came, the account kept: the
|
|
1749
1911
|
// built-in never marked a ZCode account lapsed nor cleared one
|
|
1750
|
-
return kept(res)
|
|
1912
|
+
return done(kept(res))
|
|
1751
1913
|
},
|
|
1752
1914
|
}
|
|
1753
1915
|
},
|
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.25",
|
|
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",
|