From c8e2209e2f62d3275ab8714be1a401811568a6e5 Mon Sep 17 00:00:00 2001
From: jinpy666 <5868671+jinpy666@users.noreply.github.com>
Date: Sat, 31 Oct 2026 10:34:02 +0800
Subject: [PATCH 1/4] zcode 0.1.40: freeFirst's refusals show on the card, and
a blocked address rests the whole gift
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A freeFirst replay answers from the plan silently, so a gift sitting
idle beside a fat balance — its address blocked by the vendor's firewall,
its quota spent, its host failing — looks like the option doing nothing.
Every refusal is now recorded and the usage card names it until the rest
is out: freeFirst answers from the coding plan until 14:02 (the
network's address is blocked / its quota said spent / it answered 500). A
block page rests every model the gift serves for the route's ten minutes
— an address block outlasts a minute — so the fallback costs no round
trips while it holds; a spent answer rests its one model for a minute,
as before.
---
packages/zcode/README.md | 10 ++++++-
packages/zcode/dual.test.mjs | 53 ++++++++++++++++++++++++++++++++++++
packages/zcode/index.mjs | 50 +++++++++++++++++++++++++++-------
3 files changed, 102 insertions(+), 11 deletions(-)
diff --git a/packages/zcode/README.md b/packages/zcode/README.md
index a4d21df1..78bd89a9 100644
--- a/packages/zcode/README.md
+++ b/packages/zcode/README.md
@@ -168,7 +168,15 @@ behind it go straight to the plan. A gift that cannot be asked at all —
the network, the host — is answered from the plan the same way, the
route read dropped for the next request, while a caller's own abort
stays an abort. The agent sees the plan's answer
-either way. And the `-Trial` entries keep their own strict semantics —
+either way. When a refusal is fresh, the usage card says so — **"ZCode's
+gift is refusing requests — freeFirst answers from the coding plan until
+14:02 (the network's address is blocked)"** — so a gift sitting idle
+beside a fat balance is explained, not silent. A block page rests every
+model the gift serves for the route's ten minutes (an address block
+outlasts a minute), a spent answer rests its one model for a minute, and
+any other failure is named with its status; while a rest is on, the
+requests behind it go straight to the plan. And the `-Trial` entries
+keep their own strict semantics —
asking one is still an explicit pick of the gift alone, never the plan;
a spent one answers 429 with freeFirst on, as it does without it.
diff --git a/packages/zcode/dual.test.mjs b/packages/zcode/dual.test.mjs
index 725473df..cc3c91eb 100644
--- a/packages/zcode/dual.test.mjs
+++ b/packages/zcode/dual.test.mjs
@@ -36,6 +36,7 @@ const real = globalThis.fetch
beforeEach(async () => {
_internal.routes.clear()
_internal.blocked.clear()
+ _internal.refused.clear()
sent.length = 0
for (const k of Object.keys(answers)) delete answers[k]
globalThis.fetch = async (input, init = {}) => {
@@ -386,6 +387,58 @@ test("an abort while the gift is in flight stays an abort", async () => {
expect(tries.plan).toBe(0)
})
+test("a firewall block on the gift shows on the card and rests the gift's models", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ error: { message: "request has been blocked due to unusual activity", code: 3012 } }), { status: 405, headers: { "content-type": "application/json" } })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", model: "GLM-5.3-Flash", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ expect(res.status).toBe(200)
+ expect(JSON.parse(sent.at(-1).body).model).toBe("GLM-5.3-Flash")
+ // while the block is fresh the gift is skipped entirely: no gift try,
+ // the plan answers straight
+ const before = sent.filter((c) => c.origin === "https://zcode.z.ai" && c.path.endsWith("/messages")).length
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ expect(sent.filter((c) => c.origin === "https://zcode.z.ai" && c.path.endsWith("/messages")).length).toBe(before)
+ // the card says why the gift sits idle beside its balance
+ const u = await h.usage(async () => coding, { id: "zcode" })
+ const line = (u.windows ?? []).find((w) => w.aside && /refusing requests/.test(w.name))
+ expect(line.display).toMatch(/coding plan until \d\d:\d\d/)
+ expect(line.display).toContain("blocked")
+})
+
+test("a spent answer names itself on the card for its minute", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ error: { message: "Insufficient balance or no resource package", code: 1113 } }), { status: 429, headers: { "content-type": "application/json" } })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ expect(res.status).toBe(200)
+ const u = await h.usage(async () => coding, { id: "zcode" })
+ const line = (u.windows ?? []).find((w) => w.aside && /refusing requests/.test(w.name))
+ expect(line.display).toMatch(/coding plan until \d\d:\d\d/)
+ expect(line.display).toContain("spent")
+})
+
+test("a gift failure the plan answers shows on the card with the status", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response("boom", { status: 500 })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ expect(res.status).toBe(200)
+ const u = await h.usage(async () => coding, { id: "zcode" })
+ const line = (u.windows ?? []).find((w) => w.aside && /refusing requests/.test(w.name))
+ expect(line.display).toMatch(/coding plan until \d\d:\d\d/)
+ expect(line.display).toContain("500")
+})
+
for (const [name, paid, gift] of [
["a full paid week leaves an available gift usable", 100, 25],
["a spent gift leaves the paid plan usable", 20, 100],
diff --git a/packages/zcode/index.mjs b/packages/zcode/index.mjs
index 79b54b03..b20cc229 100644
--- a/packages/zcode/index.mjs
+++ b/packages/zcode/index.mjs
@@ -257,6 +257,23 @@ async function plansOf(s) {
// model for a minute, so one turn that is refused does not ask again.
const blocked = new Map()
+// refused remembers the account's gift answering a freeFirst request with
+// a refusal: the replay answered from the plan, and the usage card says so
+// until the rest is out — a gift sitting idle beside a fat balance needs
+// the why. A blocked address says so for the route's ten minutes, a spent
+// answer for its minute, any other answer with its status.
+const refused = new Map()
+const REFUSED_TTL = 600_000
+const refusedId = (s) => s.key + "\0" + s.jwt
+const refusedWhy = (r) => ({ blocked: "the network's address is blocked", spent: "its quota said spent" }[r.why] ?? r.why)
+function refusedLine(s) {
+ const r = refused.get(refusedId(s))
+ if (!r || Date.now() >= r.until) return null
+ const d = new Date(r.until)
+ const hhmm = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0")
+ return { name: "ZCode's gift is refusing requests", used: 0, aside: true, display: `freeFirst answers from the coding plan until ${hhmm} (${refusedWhy(r)})` }
+}
+
// giftServes: the gift's buckets name their models lowercased ("glm-5.3-
// flash"); what is asked for arrives as the plan lists it ("GLM-5.3-Flash").
const giftServes = (gift, m) => (gift.models ?? []).some((x) => x.toLowerCase() === m.toLowerCase())
@@ -2385,14 +2402,18 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
const askedUrl = url
const place = workPlace(h)
const sid = sessionOf(h.get("x-session-id"), s.device)
- // restModel is the gift saying this model has nothing left: the
- // model rests a minute (blocked) and the route is dropped, so
- // the requests behind it re-read the balance and go straight to
- // the plan.
- const restModel = () => {
+ // rest is the gift saying this model — or, with a blocked
+ // address, every model it serves — has nothing useful right
+ // now: each model rests for ms, the route read is dropped, and
+ // the refusal is recorded for the usage card to say why the
+ // gift sits idle.
+ const rest = (models, ms, why) => {
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
- blocked.set(s.key + "\0" + s.jwt + "\0" + asked.toLowerCase(), Date.now() + 60_000)
- routes.delete(s.key + "\0" + s.jwt)
+ const id = refusedId(s)
+ const until = Date.now() + ms
+ for (const m of models) blocked.set(id + "\0" + m.toLowerCase(), until)
+ routes.delete(id)
+ refused.set(id, { until, why })
}
// replayOnPlan is free first's answer to a gift that refused
// what its own balance read had promised: the model as the
@@ -2476,7 +2497,11 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
// none) reads its plans again on the next request, so a
// coding plan that is there after all is used (#369)
if (freeGift) {
- restModel()
+ // a block page is the network's address being refused, not
+ // one model's quota: every model the gift serves rests for
+ // the route's ten minutes, so the fallback costs nothing
+ // while the address is blocked, and the card says why
+ rest(giftNames(known), REFUSED_TTL, "blocked")
return done(kept(await replayOnPlan()))
}
if (s.key && !forcedGift) routes.delete(s.key + "\0" + s.jwt)
@@ -2527,11 +2552,12 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
const cap = busy(res.status, text)
const spent = !cap && spentUp(text, res.status)
if (spent) {
- restModel()
+ rest([asked], 60_000, "spent")
return done(kept(await replayOnPlan()))
}
if (res.status >= 400) {
if (!cap && (res.status === 401 || res.status === 403)) routes.delete(s.key + "\0" + s.jwt)
+ refused.set(refusedId(s), { until: Date.now() + 60_000, why: "it answered " + res.status })
return done(kept(await replayOnPlan()))
}
return done(kept(res, text))
@@ -2597,6 +2623,10 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
// -Trial models, said on the card rather than left out unsaid
const need = start ? null : giftSignIn(s, auth?.type === "api")
if (need) card.windows = [...(card.windows ?? []), need]
+ // a gift refusing freeFirst's requests while its balance looks
+ // fat: the card says the plan is answering, and until when
+ const refusal = refusedLine(s)
+ if (refusal) card.windows = [...(card.windows ?? []), refusal]
}
return card
} catch (e) {
@@ -2660,4 +2690,4 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
// config sets what zcodeConfig has read, for tests: null is ZCode's config
// out of reach, undefined forgets it
const config = (cfg) => (cfgCache = cfg === undefined ? null : { cfg, at: Date.now() })
-export const _internal = { config, entry, limitWindows, termOf, startUsage, routes, blocked, plansOf, giftOf, dualUsage, claimHint, claimDaily, spentUp, giftServes, modelOf, teamKeys, ownSignIn, stateOf, located, dress, PROMPT, marked, keyDevice, sessionOf, busy, BUSY_WAITS, DEFAULT_CONCURRENCY, DEFAULT_ACCOUNTS }
+export const _internal = { config, entry, limitWindows, termOf, startUsage, routes, blocked, refused, plansOf, giftOf, dualUsage, claimHint, claimDaily, spentUp, giftServes, modelOf, teamKeys, ownSignIn, stateOf, located, dress, PROMPT, marked, keyDevice, sessionOf, busy, BUSY_WAITS, DEFAULT_CONCURRENCY, DEFAULT_ACCOUNTS }
From 8c47e43244a8041983501531e47b581c43ecaa83 Mon Sep 17 00:00:00 2001
From: jinpy666 <5868671+jinpy666@users.noreply.github.com>
Date: Sat, 31 Oct 2026 11:30:42 +0800
Subject: [PATCH 2/4] zcode: the card and the -Trial refusal say which kind of
block it is
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Review: a 3012 body is ZCode's risk control on the account, not the
address — the edge firewall page is — and the card said the address for
both. EDGE_PAGE now picks the why: a page reads 'the network's address
is blocked', a 3012 reads 'ZCode restricted this account'. The card's
'answers from the coding plan until' also said more than it did for
other failures, which rested nothing: the asked model now rests the
minute with the record, a success clears the record, the gift's own
waited-out busy cap is not recorded, and refused is pruned with blocked.
A -Trial request refused while the gift is blocked or restricted says
that instead of a quota the model has not spent.
---
packages/zcode/README.md | 21 ++++++-----
packages/zcode/dual.test.mjs | 68 +++++++++++++++++++++++++++++++++++-
packages/zcode/index.mjs | 35 ++++++++++++++-----
3 files changed, 107 insertions(+), 17 deletions(-)
diff --git a/packages/zcode/README.md b/packages/zcode/README.md
index 78bd89a9..ebdb64ab 100644
--- a/packages/zcode/README.md
+++ b/packages/zcode/README.md
@@ -170,15 +170,20 @@ route read dropped for the next request, while a caller's own abort
stays an abort. The agent sees the plan's answer
either way. When a refusal is fresh, the usage card says so — **"ZCode's
gift is refusing requests — freeFirst answers from the coding plan until
-14:02 (the network's address is blocked)"** — so a gift sitting idle
-beside a fat balance is explained, not silent. A block page rests every
-model the gift serves for the route's ten minutes (an address block
-outlasts a minute), a spent answer rests its one model for a minute, and
-any other failure is named with its status; while a rest is on, the
-requests behind it go straight to the plan. And the `-Trial` entries
-keep their own strict semantics —
+14:02 (…)"** — so a gift sitting idle
+beside a fat balance is explained, not silent. The why tells the two
+block kinds apart: a block page from the edge firewall says "the
+network's address is blocked" (every model the gift serves rests for the
+route's ten minutes — an address block outlasts a minute), ZCode's own
+3012 says "ZCode restricted this account", a spent answer rests its one
+model for a minute as "its quota said spent", and any other failure is
+named with its status; while a rest is on, the
+requests behind it go straight to the plan. The `-Trial` entries keep
+their own strict semantics —
asking one is still an explicit pick of the gift alone, never the plan;
-a spent one answers 429 with freeFirst on, as it does without it.
+a spent one answers 429 with freeFirst on, as it does without it, and
+one asked while the gift is blocked or restricted says that, not a
+quota.
## A few at once
diff --git a/packages/zcode/dual.test.mjs b/packages/zcode/dual.test.mjs
index cc3c91eb..93334e7e 100644
--- a/packages/zcode/dual.test.mjs
+++ b/packages/zcode/dual.test.mjs
@@ -390,7 +390,7 @@ test("an abort while the gift is in flight stays an abort", async () => {
test("a firewall block on the gift shows on the card and rests the gift's models", async () => {
planAndBalance()
answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
- answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ error: { message: "request has been blocked due to unusual activity", code: 3012 } }), { status: 405, headers: { "content-type": "application/json" } })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response('
405errors.aliyun.com', { status: 405 })
answers["/api/anthropic/v1/messages"] = ok({ type: "message", model: "GLM-5.3-Flash", content: [] })
const h = await freePlugin({ freeFirst: true })
const opts = await h.loader(async () => coding, { id: "zcode" })
@@ -439,6 +439,72 @@ test("a gift failure the plan answers shows on the card with the status", async
expect(line.display).toContain("500")
})
+test("a 3012 says the account is restricted, not the address", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ error: { message: "request has been blocked due to unusual activity", code: 3012 } }), { status: 405, headers: { "content-type": "application/json" } })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", model: "GLM-5.3-Flash", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ expect(res.status).toBe(200)
+ const u = await h.usage(async () => coding, { id: "zcode" })
+ const line = (u.windows ?? []).find((w) => w.aside && /refusing requests/.test(w.name))
+ expect(line.display).toContain("restricted")
+ expect(line.display).not.toContain("blocked")
+})
+
+test("a blocked gift's -Trial entry says the network is blocked, not the quota", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response('405errors.aliyun.com', { status: 405 })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash-Trial") })
+ expect(res.status).toBe(429)
+ const said = await res.json()
+ expect(said.error.message).toContain("blocked from this network")
+ expect(said.error.message).not.toContain("quota")
+})
+
+test("a gift success clears the card's refusal line", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response('405errors.aliyun.com', { status: 405 })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ // the block out, the gift answers: the line goes with it
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ _internal.blocked.set(KEY + "\0" + jwt + "\0glm-5.3-flash", Date.now() - 1)
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ const u = await h.usage(async () => coding, { id: "zcode" })
+ expect((u.windows ?? []).find((w) => w.aside && /refusing requests/.test(w.name))).toBeUndefined()
+})
+
+test("the gift's own busy cap is not a refusal for the card", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ const waits = _internal.BUSY_WAITS.splice(0)
+ _internal.BUSY_WAITS.push(1)
+ try {
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ code: "3008", msg: "user concurrency limit exceeded" }), { status: 429, headers: { "content-type": "application/json" } })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ expect(res.status).toBe(200)
+ const u = await h.usage(async () => coding, { id: "zcode" })
+ expect((u.windows ?? []).find((w) => w.aside && /refusing requests/.test(w.name))).toBeUndefined()
+ } finally {
+ _internal.BUSY_WAITS.length = 0
+ _internal.BUSY_WAITS.push(...waits)
+ }
+})
+
for (const [name, paid, gift] of [
["a full paid week leaves an available gift usable", 100, 25],
["a spent gift leaves the paid plan usable", 20, 100],
diff --git a/packages/zcode/index.mjs b/packages/zcode/index.mjs
index b20cc229..b8825445 100644
--- a/packages/zcode/index.mjs
+++ b/packages/zcode/index.mjs
@@ -265,7 +265,7 @@ const blocked = new Map()
const refused = new Map()
const REFUSED_TTL = 600_000
const refusedId = (s) => s.key + "\0" + s.jwt
-const refusedWhy = (r) => ({ blocked: "the network's address is blocked", spent: "its quota said spent" }[r.why] ?? r.why)
+const refusedWhy = (r) => ({ blocked: "the network's address is blocked", restricted: "ZCode restricted this account", spent: "its quota said spent" }[r.why] ?? r.why)
function refusedLine(s) {
const r = refused.get(refusedId(s))
if (!r || Date.now() >= r.until) return null
@@ -2368,7 +2368,18 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
const raw = giftModel(gift, m)
const available = raw && giftUsable(s, gift, raw)
if (!available) {
- const error = { type: raw ? "rate_limit_error" : "invalid_request_error", message: raw ? `ZCode's limited quota for ${raw} is exhausted or temporarily unavailable` : `ZCode has no active limited quota for ${m}` }
+ // the refused record says why the model rests: a blocked
+ // address or a restricted account is not the quota, and
+ // the agent should read the truth
+ const r = refused.get(refusedId(s))
+ const live = r && Date.now() < r.until
+ const quota = raw ? `ZCode's limited quota for ${raw} is exhausted or temporarily unavailable` : `ZCode has no active limited quota for ${m}`
+ const said = live && r.why === "blocked"
+ ? "ZCode's gift is blocked from this network; wait a while, or switch to another network or proxy"
+ : live && r.why === "restricted"
+ ? "ZCode's gift is restricted by its risk control (code 3012); only ZCode support lifts it"
+ : quota
+ const error = { type: raw ? "rate_limit_error" : "invalid_request_error", message: said }
return kept(new Response(JSON.stringify({ type: "error", error }), { status: raw ? 429 : 400, headers: { "content-type": "application/json" } }))
}
orig = opts.body = JSON.stringify({ ...JSON.parse(orig), model: raw })
@@ -2409,6 +2420,7 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
// gift sits idle.
const rest = (models, ms, why) => {
for (const [k, until] of blocked) if (Date.now() >= until) blocked.delete(k)
+ for (const [k, r] of refused) if (Date.now() >= r.until) refused.delete(k)
const id = refusedId(s)
const until = Date.now() + ms
for (const m of models) blocked.set(id + "\0" + m.toLowerCase(), until)
@@ -2497,11 +2509,12 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
// none) reads its plans again on the next request, so a
// coding plan that is there after all is used (#369)
if (freeGift) {
- // a block page is the network's address being refused, not
- // one model's quota: every model the gift serves rests for
- // the route's ten minutes, so the fallback costs nothing
- // while the address is blocked, and the card says why
- rest(giftNames(known), REFUSED_TTL, "blocked")
+ // what the card says must say what happened: a block page
+ // is the network's address being refused (every model the
+ // gift serves rests for the route's ten minutes, so the
+ // fallback costs nothing while it holds); ZCode's own 3012
+ // is the account restricted, not the address
+ rest(giftNames(known), REFUSED_TTL, EDGE_PAGE.test(text) ? "blocked" : "restricted")
return done(kept(await replayOnPlan()))
}
if (s.key && !forcedGift) routes.delete(s.key + "\0" + s.jwt)
@@ -2557,11 +2570,17 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
}
if (res.status >= 400) {
if (!cap && (res.status === 401 || res.status === 403)) routes.delete(s.key + "\0" + s.jwt)
- refused.set(refusedId(s), { until: Date.now() + 60_000, why: "it answered " + res.status })
+ // the card's line must hold for the rest it declares: the
+ // asked model rests the minute with the record. The gift's
+ // own waited-out cap is a turn not come, not a refusal.
+ if (!cap) rest([asked], 60_000, "it answered " + res.status)
return done(kept(await replayOnPlan()))
}
+ refused.delete(refusedId(s))
return done(kept(res, text))
}
+ // a stream's 200 is the gift answering again
+ refused.delete(refusedId(s))
}
// the plan's answer goes on as it came, the account kept: the
// built-in never marked a ZCode account lapsed nor cleared one
From 28e92fe1bbac38007d4c01186b0f1e0d2d5a3f04 Mon Sep 17 00:00:00 2001
From: jinpy666 <5868671+jinpy666@users.noreply.github.com>
Date: Sat, 31 Oct 2026 12:15:30 +0800
Subject: [PATCH 3/4] zcode: a blocked or restricted -Trial refusal is not a
rate limit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Review: the words were right but the reply's shape was still a quota
one, and magpie's gateway classifies by the reply, not the words — a
429 whose error.type says rate_limit_error rests the route as
rate-limited. A -Trial refusal while the gift is blocked or restricted
now carries invalid_request_error and the block's own hint, the way
startBlocked words them: START_EDGE for a blocked address,
START_RESTRICTED for a 3012, the plugin's sentence in front. A model
the gift never serves keeps its no-quota 400 even with a refusal
fresh, and each kind is pinned by a test checking status, error.type
and the hint sentence.
---
packages/zcode/README.md | 7 +++--
packages/zcode/dual.test.mjs | 52 +++++++++++++++++++++++++++++++++++-
packages/zcode/index.mjs | 23 +++++++++-------
3 files changed, 69 insertions(+), 13 deletions(-)
diff --git a/packages/zcode/README.md b/packages/zcode/README.md
index ebdb64ab..f2ef18ef 100644
--- a/packages/zcode/README.md
+++ b/packages/zcode/README.md
@@ -182,8 +182,11 @@ requests behind it go straight to the plan. The `-Trial` entries keep
their own strict semantics —
asking one is still an explicit pick of the gift alone, never the plan;
a spent one answers 429 with freeFirst on, as it does without it, and
-one asked while the gift is blocked or restricted says that, not a
-quota.
+one asked while the gift is blocked or restricted says that with the
+block's own hint ("…the provider's network firewall blocked requests
+from this IP…" / "…ZCode's risk control blocked this account…") and an
+error type that is not a rate limit, so the gateway rests the route by
+what happened; a model the gift never serves keeps its no-quota 400.
## A few at once
diff --git a/packages/zcode/dual.test.mjs b/packages/zcode/dual.test.mjs
index 93334e7e..69ec1011 100644
--- a/packages/zcode/dual.test.mjs
+++ b/packages/zcode/dual.test.mjs
@@ -465,10 +465,60 @@ test("a blocked gift's -Trial entry says the network is blocked, not the quota",
const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash-Trial") })
expect(res.status).toBe(429)
const said = await res.json()
- expect(said.error.message).toContain("blocked from this network")
+ // an address block is not a rate limit: magpie classifies by the type
+ expect(said.error.type).toBe("invalid_request_error")
+ expect(said.error.message).toContain("network firewall blocked requests from this IP")
expect(said.error.message).not.toContain("quota")
})
+test("a restricted gift's -Trial entry says the account is restricted", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ error: { message: "request has been blocked due to unusual activity", code: 3012 } }), { status: 405, headers: { "content-type": "application/json" } })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash-Trial") })
+ expect(res.status).toBe(429)
+ const said = await res.json()
+ expect(said.error.type).toBe("invalid_request_error")
+ expect(said.error.message).toContain("risk control blocked this account")
+})
+
+test("a spent gift's -Trial entry keeps its quota reply", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ // the freeFirst plain request hears the gift say its quota is spent, and
+ // the trial entry after it is answered from that rest, locally
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response(JSON.stringify({ error: { message: "Insufficient balance or no resource package", code: 1113 } }), { status: 429, headers: { "content-type": "application/json" } })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash-Trial") })
+ expect(res.status).toBe(429)
+ const said = await res.json()
+ expect(said.error.type).toBe("rate_limit_error")
+ expect(said.error.message).toContain("limited quota")
+})
+
+test("a -Trial entry the gift never serves keeps its no-quota 400", async () => {
+ planAndBalance()
+ answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
+ answers["/api/v1/zcode-plan/anthropic/v1/messages"] = new Response('405errors.aliyun.com', { status: 405 })
+ answers["/api/anthropic/v1/messages"] = ok({ type: "message", content: [] })
+ const h = await freePlugin({ freeFirst: true })
+ const opts = await h.loader(async () => coding, { id: "zcode" })
+ await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.3-Flash") })
+ const res = await opts.fetch(MSG, { method: "POST", headers: { "content-type": "application/json" }, body: body("GLM-5.2-Trial") })
+ expect(res.status).toBe(400)
+ const said = await res.json()
+ expect(said.error.type).toBe("invalid_request_error")
+ expect(said.error.message).toContain("no active limited quota")
+ expect(said.error.message).not.toContain("blocked")
+})
+
test("a gift success clears the card's refusal line", async () => {
planAndBalance()
answers["/api/monitor/usage/quota/limit"] = ok({ limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, percentage: 20 }] })
diff --git a/packages/zcode/index.mjs b/packages/zcode/index.mjs
index b8825445..51118c41 100644
--- a/packages/zcode/index.mjs
+++ b/packages/zcode/index.mjs
@@ -2369,17 +2369,20 @@ export async function ZCodeAuthPlugin({ client }, options = {}) {
const available = raw && giftUsable(s, gift, raw)
if (!available) {
// the refused record says why the model rests: a blocked
- // address or a restricted account is not the quota, and
- // the agent should read the truth
+ // address or a restricted account is not a rate limit and
+ // not the quota — the reply carries the hint magpie's
+ // gateway classifies the rest by. A model the gift never
+ // serves keeps its own no-quota 400.
const r = refused.get(refusedId(s))
- const live = r && Date.now() < r.until
- const quota = raw ? `ZCode's limited quota for ${raw} is exhausted or temporarily unavailable` : `ZCode has no active limited quota for ${m}`
- const said = live && r.why === "blocked"
- ? "ZCode's gift is blocked from this network; wait a while, or switch to another network or proxy"
- : live && r.why === "restricted"
- ? "ZCode's gift is restricted by its risk control (code 3012); only ZCode support lifts it"
- : quota
- const error = { type: raw ? "rate_limit_error" : "invalid_request_error", message: said }
+ const blocked = raw && r && Date.now() < r.until && (r.why === "blocked" || r.why === "restricted")
+ const said = blocked && r.why === "blocked"
+ ? `ZCode's gift is blocked from this network — ${START_EDGE}`
+ : blocked
+ ? `ZCode's gift is restricted — ${START_RESTRICTED}`
+ : raw
+ ? `ZCode's limited quota for ${raw} is exhausted or temporarily unavailable`
+ : `ZCode has no active limited quota for ${m}`
+ const error = { type: blocked || !raw ? "invalid_request_error" : "rate_limit_error", message: said }
return kept(new Response(JSON.stringify({ type: "error", error }), { status: raw ? 429 : 400, headers: { "content-type": "application/json" } }))
}
orig = opts.body = JSON.stringify({ ...JSON.parse(orig), model: raw })
From 1aa15e48ead501e7389f25cf2f8b5420c5db1dc3 Mon Sep 17 00:00:00 2001
From: jinpy666 <5868671+jinpy666@users.noreply.github.com>
Date: Sun, 1 Nov 2026 23:39:41 +0800
Subject: [PATCH 4/4] zcode 0.1.41: rebased on 0.1.40's maxAccounts
---
packages/zcode/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/zcode/package.json b/packages/zcode/package.json
index 0d190377..7652248c 100644
--- a/packages/zcode/package.json
+++ b/packages/zcode/package.json
@@ -1,6 +1,6 @@
{
"name": "@magpie-community/opencode-zcode-auth",
- "version": "0.1.40",
+ "version": "0.1.41",
"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, and claims its gift plans daily",
"type": "module",
"main": "./index.mjs",