Skip to content

zcode 0.1.40: freeFirst's refusals show on the card, and a blocked address rests the whole gift - #545

Open
jinpy666 wants to merge 3 commits into
magpie-community:mainfrom
jinpy666:feat/zcode-freefirst-visibility
Open

jinpy666 wants to merge 3 commits into
magpie-community:mainfrom
jinpy666:feat/zcode-freefirst-visibility

Conversation

@jinpy666

Copy link
Copy Markdown
Contributor

Follows #505 (merged, 0.1.38) and the undiscoverability thread in #536. Run against real accounts: with the machine's address blocked by zcode.z.ai's firewall, a dual account's freeFirst replay worked but was invisible — the gift's balance sat fat while the plan's windows moved, and nothing said why. This makes the refusal visible and the fallback cheap.

The card names the refusal

Every freeFirst gift refusal that the plan answers is recorded per account (refused), and the usage card carries an aside line until the rest is out:

ZCode's gift is refusing requests — freeFirst answers from the coding plan until 14:02 (the network's address is blocked)

The why is specific: the network's address is blocked for a block page, its quota said spent for a spent answer, it answered 500 for anything else.

A blocked address rests the whole gift

A block page is the network's address being refused, not one model's quota — it outlasts a minute. rest(models, ms, why) (the old restModel, parameterized) now rests every model the gift serves for the route's ten minutes on a block page, so the requests behind it go straight to the plan with no doomed round trip each minute; the -Trial entries rest too, answering the plugin's local 429 instead of the same block page. A spent answer keeps its one-minute, one-model rest. Other failures record the card line only.

Tests

Three new cases in dual.test.mjs (red first): a block page answers 200 from the plan, the card names the block until HH:MM, and the next request makes no gift try; a spent 1113 shows the line naming the quota; a 500 shows the line with its status. bun test packages/zcode at 0d2c15c: 185 pass (13 over 0.1.39's 172), check clean. The README's free-first section documents the card line and the rests.

…dress rests the whole gift

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.

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I pulled 0d2c15c and ran it on a Mac: HOME=$(mktemp -d) bun test packages/zcode gives 185 pass, 0 fail (main has 182), and your three new tests pass. Resting the gift's models for longer than a minute on a block page is right, and so is the card line. I'm not merging yet, because of these points:

  1. 3012 is not an address block. startRefused matches both Alibaba Cloud's block page (EDGE_PAGE) and ZCode's JSON code 3012 "request has been blocked due to unusual activity". 3012 is ZCode's risk control on the account: ZCode's own app is refused too (#348), and outside freeFirst startBlocked tells it as START_RESTRICTED, not START_EDGE. On freeGift, the PR sends both to rest(..., "blocked"), and the card then says "the network's address is blocked". The first new test uses a 3012 body as "a firewall block". Please tell them apart (e.g. EDGE_PAGE.test(text) gives "blocked", anything else gives "restricted" with words like "ZCode restricted this account"), and add a test with each body.
    On magpie's side, the gateway did the same mix-up. provider.APIError rewrote any "request has been blocked" body to the firewall hint, so a 3012 was told as an IP block and rested as a plain failure. That is fixed in magpie now (a body carrying ZCodeRestrictedHint keeps its words and rests as restricted). So the plugin's wording is what decides what the user reads.

  2. "Any other failure" says something that isn't happening. For a status ≥400 that isn't spent, refused is set for 60s, but nothing goes into blocked. So the next request still tries the gift first. The card says "freeFirst answers from the coding plan until HH:MM", which is true only for the request that was replayed. Either rest the asked model for that minute too (as spent does), or word this line as what happened ("the gift answered 500 at HH:MM; the plan answered").

  3. A later success doesn't clear refused. If the gift answers 200 within the window, the card still says it is refusing. Delete refused.get(refusedId(s)) on a gift answer < 400 (and when blocked is cleared for the account).

  4. The block rest reaches the -Trial entries, with another name. giftUsable reads blocked, so for the next ten minutes a GLM-5.3-Flash-Trial request gets the plugin's local 429 "ZCode's limited quota for … is exhausted or temporarily unavailable". magpie's gateway reads that 429 as the trial's quota and rests that model for its quota rest, which is longer than your ten minutes, under a quota label. Resting the Trial entries is fine. But the answer should say the gift is blocked from this network (or 3012 restricted), not that its quota is used up. Please add a test: a block page, then a -Trial request, then check the error text.

  5. Minor:

    • refused is never pruned. blocked is swept in rest, so do the same for refused.
    • A busy() cap (the plugin's own concurrency 429) is recorded as "it answered 429", the same as a refusal from ZCode. Leave it out when cap is true.

Point 1 matters most. Points 2 to 4 are about the card and the error saying what is actually true. Happy to re-run it after a push.

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.
@jinpy666

Copy link
Copy Markdown
Contributor Author

All five points addressed on e717472, tests red first.

1. The two block kinds. EDGE_PAGE.test(text) picks the why in the freeGift refusal: a page records blocked and the card reads "the network's address is blocked"; a 3012 records restricted and the card reads "ZCode restricted this account" — matching startBlocked's own START_EDGE/START_RESTRICTED distinction. Both still rest the gift's models for the route's ten minutes (the account can't spend the gift either way). The old firewall test now uses a real page body; a new 3012 test pins the restricted wording and asserts blocked is absent.

2. Other failures hold their line. A ≥400 that isn't spent (and isn't the gift's own cap) rests the asked model for the minute with the record, so "answers from the coding plan until HH:MM" is true for the window, and the 500 test still sees its status named.

3. Success clears. A gift answer under 400 — JSON or stream — deletes the account's refused record; new test: block page, then (block aged) a gift 200, then the card carries no refusing line.

4. The -Trial refusal says the truth. The local 429 consults the fresh record: while the gift is blocked it reads "ZCode's gift is blocked from this network; wait a while, or switch to another network or proxy", while 3012-restricted reads "ZCode's gift is restricted by its risk control (code 3012); only ZCode support lifts it"; the quota wording stays for a genuinely spent bucket. New test pins the blocked wording with no "quota" in it.

5. Minors. refused is pruned of expired entries wherever blocked is swept; a busy() cap records nothing.

bun test packages/zcode at e717472: 189 pass, check clean. The README's free-first section names both wordings and the -Trial case.

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I checked out e717472 and ran it on a Mac mini in a throwaway HOME: bun test packages/zcode (189 of 189 of your tests pass) and bun run check (clean). Then I put your new dual.test.mjs on the old head, 0d2c15c, to see which tests are red there, and added five probes of my own.

Fixed and checked:

  1. 3012 is the account, not the network. "a 3012 says the account is restricted, not the address" is red on 0d2c15c and green now. The card line's "why" tells a block page and a 3012 apart.
  2. The card's "until HH:MM" is now true. rest() puts every gift model in blocked for the window, so the line matches what happens. My probe (a gift 500, then a second request skips the gift and goes straight to the plan) is red on 0d2c15c and green now. Your PR has no test for this, so please add one.
  3. A gift success clears the line. "a gift success clears the card's refusal line" is red on the old head. My probe for a streamed 200 also passes now. Your PR has no test for it, so please add one too.
  4. Pruning, and the busy cap. "the gift's own busy cap is not a refusal for the card" is red on the old head. My pruning probe (an expired entry is gone after the next rest()) passes now.

Still wrong: point 4, a -Trial entry while the gift is refused (index.mjs:2366-2373)

The words are right now, but the reply around them is still a quota reply:

429 {"type":"error","error":{"type":"rate_limit_error","message":"ZCode's gift is restricted by its risk control (code 3012); only ZCode support lifts it"}}
429 {"type":"error","error":{"type":"rate_limit_error","message":"ZCode's gift is blocked from this network; wait a while, or switch to another network or proxy"}}

magpie's gateway decides by the reply, not by the words a person reads (internal/gateway/routing.go, failure()). A 429 whose body says rate_limit_error is a rate limit (rate.?limit matches it), so magpie still rests the route as rate-limited. It also shows that label. The restricted path (restrictedSaid) needs magpie's own sentence in the body, and the edge path (provider.EdgeBlocked) needs the BlockedHint sentence. Neither text is in this reply. This is the mislabelled rest from point 4, with new words.

The fix is to answer the way startBlocked already does, when the gift is refused:

  • why === "restricted" → status 403, type: "permission_error", and the message carries START_RESTRICTED word for word (it is magpie's ZCodeRestrictedHint);
  • why === "blocked" → status 403, permission_error, and the message carries START_EDGE word for word (magpie's BlockedHint).

You can keep your sentence in front, the way startBlocked puts said — START_….

Two more details on the same lines:

  • When raw is unset, the entry has no gift model at all, so its answer is the 400 "no active limited quota for …". Right now a live refusal swaps that for "blocked" or "restricted", so a model the gift never serves is reported as blocked. Pick the refusal text only when raw is set.
  • Please add a test for each kind that checks the status, error.type and the hint sentence, not only the words. They should be red on e717472.

When that's in, say which head to look at and I'll run it again and merge.

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.
@jinpy666

Copy link
Copy Markdown
Contributor Author

Point 4 reworked on 346e902 — head to run: 346e902.

The reply's shape now says what happened, not just its words:

  • Blocked (edge page): 429 invalid_request_error — "ZCode's gift is blocked from this network — the provider's network firewall blocked requests from this IP; wait a while, or switch to another network or proxy" (the plugin's sentence in front, START_EDGE after, as startBlocked composes them).
  • Restricted (3012): 429 invalid_request_error — "ZCode's gift is restricted — ZCode's risk control blocked this account for unusual activity (code 3012); it rests, and the group's other providers are used. If the ZCode app fails on this account too, only ZCode support can lift the block" (START_RESTRICTED).
  • Spent: the local quota reply keeps 429 rate_limit_error with its "limited quota" wording — a genuinely spent bucket rests as a rate limit, as it did.
  • Raw unset (the gift never serves the model): the 400 invalid_request_error "no active limited quota" stands even with a refusal fresh — the swap only happens when raw is set.

A test per kind, each checking status, error.type and the hint sentence; all four are red on e717472 (the restricted and raw-unset kinds answered the old mixed shapes, and the spent kind's test now goes through the freeFirst rest that produces the local reply). bun test packages/zcode at 346e902: 192 pass, check clean. The README names the hints and the type distinction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants