@magpie-community/opencode-zcode-auth 0.1.22 → 0.1.24
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/index.mjs +147 -10
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -578,13 +578,16 @@ async function claimHint(s) {
|
|
|
578
578
|
|
|
579
579
|
// startUsage is a gift-only account's card: its buckets, or the words
|
|
580
580
|
// for having none to show. The card's header already names the plan, so
|
|
581
|
-
// the windows keep their own names
|
|
581
|
+
// the windows of one plan keep their own names; with several plans each
|
|
582
|
+
// bucket is named after its plan (see byPlan).
|
|
582
583
|
const NO_START = "this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started"
|
|
583
584
|
async function startUsage(s) {
|
|
584
585
|
const g = await giftOf(s)
|
|
585
586
|
if (!g) return { error: NO_START }
|
|
586
587
|
const best = bestBuckets(g.windows)
|
|
587
|
-
|
|
588
|
+
const label = byPlan(g.windows)
|
|
589
|
+
g.windows = countedFirst(g.windows.map(({ _plan, _spent, ...w }, i) => {
|
|
590
|
+
w.name = label(w.name, _plan)
|
|
588
591
|
if (!w.models?.length) return w
|
|
589
592
|
// a model two plans give (Trust Build's and the Start Plan's Flash)
|
|
590
593
|
// counts on the bucket ZCode spends now; the other stays on the card
|
|
@@ -595,10 +598,28 @@ async function startUsage(s) {
|
|
|
595
598
|
return { ...w, aside: true }
|
|
596
599
|
}
|
|
597
600
|
return { ...w, models: [...models, ...models.map(giftID)] }
|
|
598
|
-
})
|
|
601
|
+
}))
|
|
599
602
|
return g
|
|
600
603
|
}
|
|
601
604
|
|
|
605
|
+
// byPlan names a bucket after its plan ("ZCode 1024 Dev Day · GLM-5.3")
|
|
606
|
+
// when the gift's buckets come from more than one plan: two plans each
|
|
607
|
+
// give a GLM-5.3, and named alike a spent one and a live one can't be
|
|
608
|
+
// told apart (01huadalang, Discord: the new-user plan's spent GLM-5.3
|
|
609
|
+
// showed, the 1024 plan's own GLM-5.3 didn't). One plan's buckets keep
|
|
610
|
+
// their own names, as the card's header names the plan.
|
|
611
|
+
function byPlan(windows) {
|
|
612
|
+
const several = new Set(windows.map((w) => w._plan || "")).size > 1
|
|
613
|
+
return (name, plan) => (several && plan ? plan + " · " + name : name)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// countedFirst puts the buckets a request spends before those set aside
|
|
617
|
+
// (a spent bucket another plan's live one stands in for), each group in
|
|
618
|
+
// the balance's order: magpie's account row shows a card's first two
|
|
619
|
+
// windows and leaves the rest to its tooltip, so a spent plan listed first
|
|
620
|
+
// hid the live plan's allowance for the same model.
|
|
621
|
+
const countedFirst = (ws) => [...ws.filter((w) => !w.aside), ...ws.filter((w) => w.aside)]
|
|
622
|
+
|
|
602
623
|
// bestBuckets is, for each model the gift's buckets serve, the index of
|
|
603
624
|
// the one a request spends: a live one before a spent one, the least
|
|
604
625
|
// used of those. The server picks among an account's buckets itself
|
|
@@ -641,19 +662,21 @@ async function dualUsage(s) {
|
|
|
641
662
|
if (notModels.length) out.windows = out.windows.map((w) => w.aside ? w : { ...w, notModels })
|
|
642
663
|
// Several buckets may serve one model: one spent sibling cannot block it.
|
|
643
664
|
const best = bestBuckets(g.windows)
|
|
644
|
-
|
|
665
|
+
const label = byPlan(g.windows)
|
|
666
|
+
out.windows.push(...countedFirst(g.windows.map(({ _plan: p, _spent, ...w }, i) => {
|
|
645
667
|
const selected = gift.allModels.filter((m) => best.get(m.toLowerCase()) === i)
|
|
646
668
|
const models = selected.map(giftID)
|
|
647
669
|
const scoped = models.length ? { ...w, models } : { ...w, aside: true }
|
|
648
670
|
// the card names a bucket's model as the picker does (GLM-5.3-Flash-Trial);
|
|
649
|
-
// no plan prefix — the card's section
|
|
671
|
+
// no plan prefix while one plan gives them all — the card's section
|
|
672
|
+
// header already says the account; with several, byPlan names each
|
|
650
673
|
let name = w.name
|
|
651
674
|
for (const m of selected) {
|
|
652
675
|
const at = name.toLowerCase().indexOf(m.toLowerCase())
|
|
653
676
|
if (at >= 0) name = name.slice(0, at) + giftID(m) + name.slice(at + m.length)
|
|
654
677
|
}
|
|
655
|
-
return { ...scoped, name }
|
|
656
|
-
}))
|
|
678
|
+
return { ...scoped, name: label(name, p) }
|
|
679
|
+
})))
|
|
657
680
|
}
|
|
658
681
|
} catch {
|
|
659
682
|
if (cached) {
|
|
@@ -1436,6 +1459,110 @@ function startHeaders(h, jwt) {
|
|
|
1436
1459
|
return out
|
|
1437
1460
|
}
|
|
1438
1461
|
|
|
1462
|
+
// Claude Code's tool search, as ZCode's plans take it. Claude Code
|
|
1463
|
+
// (2.1.310) offers a tool named ToolSearch, names its deferred tools in a
|
|
1464
|
+
// system turn, and sends a deferred tool's schema with defer_loading only
|
|
1465
|
+
// once a search loaded it (a tool_reference in a ToolSearch result), with
|
|
1466
|
+
// a DeferredToolPlaceholder that keeps the search on. api.z.ai's
|
|
1467
|
+
// Anthropic endpoint, given a tool named exactly ToolSearch, shows the
|
|
1468
|
+
// model none of the other tools, Bash, Read and Write among them, but
|
|
1469
|
+
// those a tool_reference loaded: a subagent, which hasn't searched,
|
|
1470
|
+
// reports only the deferred tools, and the main turn has to search for
|
|
1471
|
+
// Bash before it can run it (小八 on Discord, 10-24; the beta header
|
|
1472
|
+
// changes nothing, the name alone does). ZCode itself sends no ToolSearch
|
|
1473
|
+
// and no defer_loading. So the search goes as SEARCH_ALIAS, in the tools
|
|
1474
|
+
// and in the turns that called it, the tools Claude Code sent with their
|
|
1475
|
+
// schemas go as ZCode's do, without defer_loading, and the placeholder is
|
|
1476
|
+
// left out; the model's calls to SEARCH_ALIAS come back as ToolSearch
|
|
1477
|
+
// (searchNamed).
|
|
1478
|
+
const SEARCH = "ToolSearch"
|
|
1479
|
+
const SEARCH_ALIAS = "ToolSearchTool"
|
|
1480
|
+
const PLACEHOLDER = "DeferredToolPlaceholder"
|
|
1481
|
+
|
|
1482
|
+
// searchless is a Messages request body without Claude Code's ToolSearch
|
|
1483
|
+
// name, and whether it had the tool; anything else goes as it is.
|
|
1484
|
+
function searchless(text) {
|
|
1485
|
+
if (typeof text !== "string" || !text.includes(`"${SEARCH}"`)) return { text, renamed: false }
|
|
1486
|
+
let b
|
|
1487
|
+
try {
|
|
1488
|
+
b = JSON.parse(text)
|
|
1489
|
+
} catch {
|
|
1490
|
+
return { text, renamed: false }
|
|
1491
|
+
}
|
|
1492
|
+
if (!b || !Array.isArray(b.tools) || !b.tools.some((t) => t?.name === SEARCH)) return { text, renamed: false }
|
|
1493
|
+
// a tool of the alias's own name keeps it; the search can't then be renamed
|
|
1494
|
+
if (b.tools.some((t) => t?.name === SEARCH_ALIAS)) return { text, renamed: false }
|
|
1495
|
+
b.tools = b.tools
|
|
1496
|
+
.filter((t) => t?.name !== PLACEHOLDER)
|
|
1497
|
+
.map((t) => {
|
|
1498
|
+
if (!t || typeof t !== "object") return t
|
|
1499
|
+
const { defer_loading, ...rest } = t
|
|
1500
|
+
return t.name === SEARCH ? { ...rest, name: SEARCH_ALIAS } : rest
|
|
1501
|
+
})
|
|
1502
|
+
if (Array.isArray(b.messages))
|
|
1503
|
+
for (const m of b.messages)
|
|
1504
|
+
if (m?.role === "assistant" && Array.isArray(m.content))
|
|
1505
|
+
for (const x of m.content) if (x?.type === "tool_use" && x.name === SEARCH) x.name = SEARCH_ALIAS
|
|
1506
|
+
if (b.tool_choice?.type === "tool" && b.tool_choice.name === SEARCH) b.tool_choice.name = SEARCH_ALIAS
|
|
1507
|
+
return { text: JSON.stringify(b), renamed: true }
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// searchNamed is an answer to a request searchless renamed, the model's
|
|
1511
|
+
// calls to SEARCH_ALIAS named ToolSearch again, as Claude Code runs them:
|
|
1512
|
+
// a stream's content_block_start events, or a whole message's content.
|
|
1513
|
+
function searchNamed(res) {
|
|
1514
|
+
if (!res.body) return res
|
|
1515
|
+
const type = res.headers.get("content-type") ?? ""
|
|
1516
|
+
const named = (block) => {
|
|
1517
|
+
if (block?.type === "tool_use" && block.name === SEARCH_ALIAS) {
|
|
1518
|
+
block.name = SEARCH
|
|
1519
|
+
return true
|
|
1520
|
+
}
|
|
1521
|
+
return false
|
|
1522
|
+
}
|
|
1523
|
+
const line = (l) => {
|
|
1524
|
+
if (!l.startsWith("data:") || !l.includes(`"${SEARCH_ALIAS}"`)) return l
|
|
1525
|
+
try {
|
|
1526
|
+
const d = JSON.parse(l.slice(5))
|
|
1527
|
+
return named(d?.content_block) ? "data: " + JSON.stringify(d) : l
|
|
1528
|
+
} catch {
|
|
1529
|
+
return l
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (type.includes("text/event-stream")) {
|
|
1533
|
+
const dec = new TextDecoder()
|
|
1534
|
+
const enc = new TextEncoder()
|
|
1535
|
+
let held = ""
|
|
1536
|
+
const body = res.body.pipeThrough(
|
|
1537
|
+
new TransformStream({
|
|
1538
|
+
transform(chunk, ctl) {
|
|
1539
|
+
held += dec.decode(chunk, { stream: true })
|
|
1540
|
+
const i = held.lastIndexOf("\n")
|
|
1541
|
+
if (i < 0) return
|
|
1542
|
+
ctl.enqueue(enc.encode(held.slice(0, i + 1).split("\n").map(line).join("\n")))
|
|
1543
|
+
held = held.slice(i + 1)
|
|
1544
|
+
},
|
|
1545
|
+
flush(ctl) {
|
|
1546
|
+
held += dec.decode()
|
|
1547
|
+
if (held) ctl.enqueue(enc.encode(line(held)))
|
|
1548
|
+
},
|
|
1549
|
+
}),
|
|
1550
|
+
)
|
|
1551
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1552
|
+
}
|
|
1553
|
+
if (!type.includes("json")) return res
|
|
1554
|
+
return res.text().then((text) => {
|
|
1555
|
+
let out = text
|
|
1556
|
+
if (text.includes(`"${SEARCH_ALIAS}"`)) {
|
|
1557
|
+
try {
|
|
1558
|
+
const v = JSON.parse(text)
|
|
1559
|
+
if (Array.isArray(v?.content) && v.content.map(named).some(Boolean)) out = JSON.stringify(v)
|
|
1560
|
+
} catch {}
|
|
1561
|
+
}
|
|
1562
|
+
return new Response(out, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1563
|
+
})
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1439
1566
|
// startURL is a Start Plan request's URL without the query the agent put
|
|
1440
1567
|
// on it: Claude Code asks /v1/messages?beta=true, which magpie passes on,
|
|
1441
1568
|
// and ZCode asks /v1/messages alone.
|
|
@@ -1647,9 +1774,18 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1647
1774
|
// says it is spent can spend nothing and replay what was asked for
|
|
1648
1775
|
let start = alone
|
|
1649
1776
|
let orig, forcedGift = false
|
|
1777
|
+
let renamed = false
|
|
1778
|
+
const done = (res) => (renamed ? searchNamed(res) : res)
|
|
1650
1779
|
if (opts.body != null) {
|
|
1651
1780
|
orig = opts.body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1652
1781
|
delete opts.duplex
|
|
1782
|
+
// Claude Code's tool search goes under another name, or
|
|
1783
|
+
// ZCode's plans show the model no other tool (searchless)
|
|
1784
|
+
const plain = searchless(orig)
|
|
1785
|
+
if (plain.renamed) {
|
|
1786
|
+
orig = opts.body = plain.text
|
|
1787
|
+
renamed = true
|
|
1788
|
+
}
|
|
1653
1789
|
let m = modelOf(orig)
|
|
1654
1790
|
if (m.toLowerCase().endsWith("-trial")) {
|
|
1655
1791
|
gift ??= await startPlan(s.jwt, s.device).catch(() => null)
|
|
@@ -1691,6 +1827,7 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1691
1827
|
} else {
|
|
1692
1828
|
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1693
1829
|
swap(s.base)
|
|
1830
|
+
if (renamed) h.delete("content-length")
|
|
1694
1831
|
h.delete("authorization")
|
|
1695
1832
|
h.set("x-api-key", key)
|
|
1696
1833
|
h.set("Authorization", "Bearer " + key)
|
|
@@ -1714,17 +1851,17 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1714
1851
|
if (res.status >= 400 || json) {
|
|
1715
1852
|
const text = await res.text()
|
|
1716
1853
|
const spent = spentUp(text, res.status)
|
|
1717
|
-
if (!spent && res.status < 400) return kept(res, text)
|
|
1854
|
+
if (!spent && res.status < 400) return done(kept(res, text))
|
|
1718
1855
|
if (spent) {
|
|
1719
1856
|
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
|
|
1720
1857
|
blocked.set(s.key + "\0" + s.jwt + "\0" + modelOf(orig).toLowerCase(), Date.now() + 60_000)
|
|
1721
1858
|
}
|
|
1722
|
-
return spent && res.status < 400 ? kept(new Response(text, { status: 429, headers: res.headers })) : kept(res, text)
|
|
1859
|
+
return spent && res.status < 400 ? kept(new Response(text, { status: 429, headers: res.headers })) : done(kept(res, text))
|
|
1723
1860
|
}
|
|
1724
1861
|
}
|
|
1725
1862
|
// the plan's answer goes on as it came, the account kept: the
|
|
1726
1863
|
// built-in never marked a ZCode account lapsed nor cleared one
|
|
1727
|
-
return kept(res)
|
|
1864
|
+
return done(kept(res))
|
|
1728
1865
|
},
|
|
1729
1866
|
}
|
|
1730
1867
|
},
|
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.24",
|
|
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",
|