@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.
Files changed (2) hide show
  1. package/index.mjs +117 -3
  2. 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.23",
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",