@magpie-community/opencode-zcode-auth 0.1.23 → 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 +117 -3
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1459,6 +1459,110 @@ function startHeaders(h, jwt) {
|
|
|
1459
1459
|
return out
|
|
1460
1460
|
}
|
|
1461
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
|
+
|
|
1462
1566
|
// startURL is a Start Plan request's URL without the query the agent put
|
|
1463
1567
|
// on it: Claude Code asks /v1/messages?beta=true, which magpie passes on,
|
|
1464
1568
|
// and ZCode asks /v1/messages alone.
|
|
@@ -1670,9 +1774,18 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1670
1774
|
// says it is spent can spend nothing and replay what was asked for
|
|
1671
1775
|
let start = alone
|
|
1672
1776
|
let orig, forcedGift = false
|
|
1777
|
+
let renamed = false
|
|
1778
|
+
const done = (res) => (renamed ? searchNamed(res) : res)
|
|
1673
1779
|
if (opts.body != null) {
|
|
1674
1780
|
orig = opts.body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1675
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
|
+
}
|
|
1676
1789
|
let m = modelOf(orig)
|
|
1677
1790
|
if (m.toLowerCase().endsWith("-trial")) {
|
|
1678
1791
|
gift ??= await startPlan(s.jwt, s.device).catch(() => null)
|
|
@@ -1714,6 +1827,7 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1714
1827
|
} else {
|
|
1715
1828
|
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1716
1829
|
swap(s.base)
|
|
1830
|
+
if (renamed) h.delete("content-length")
|
|
1717
1831
|
h.delete("authorization")
|
|
1718
1832
|
h.set("x-api-key", key)
|
|
1719
1833
|
h.set("Authorization", "Bearer " + key)
|
|
@@ -1737,17 +1851,17 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1737
1851
|
if (res.status >= 400 || json) {
|
|
1738
1852
|
const text = await res.text()
|
|
1739
1853
|
const spent = spentUp(text, res.status)
|
|
1740
|
-
if (!spent && res.status < 400) return kept(res, text)
|
|
1854
|
+
if (!spent && res.status < 400) return done(kept(res, text))
|
|
1741
1855
|
if (spent) {
|
|
1742
1856
|
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
|
|
1743
1857
|
blocked.set(s.key + "\0" + s.jwt + "\0" + modelOf(orig).toLowerCase(), Date.now() + 60_000)
|
|
1744
1858
|
}
|
|
1745
|
-
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))
|
|
1746
1860
|
}
|
|
1747
1861
|
}
|
|
1748
1862
|
// the plan's answer goes on as it came, the account kept: the
|
|
1749
1863
|
// built-in never marked a ZCode account lapsed nor cleared one
|
|
1750
|
-
return kept(res)
|
|
1864
|
+
return done(kept(res))
|
|
1751
1865
|
},
|
|
1752
1866
|
}
|
|
1753
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",
|