From df019e18e7947cee1e32dc0eccbc4ff9c97a2004 Mon Sep 17 00:00:00 2001 From: yetone Date: Thu, 1 Oct 2026 02:58:29 +0800 Subject: [PATCH] =?UTF-8?q?zcode:=20an=20old=20plan=20with=20no=20monthly?= =?UTF-8?q?=20cap=20is=20no=20longer=20taken=20as=20used=20up=20(#366)=20?= =?UTF-8?q?=E2=80=94=20the=20quota=20reply's=20TIME=5FLIMIT=20is=20the=20m?= =?UTF-8?q?onth's=20MCP=20tool=20calls,=20which=20ZCode=20only=20shows=20a?= =?UTF-8?q?nd=20never=20stops=20the=20models=20on,=20but=20magpie=20read?= =?UTF-8?q?=20it=20as=20the=20account's=20monthly=20allowance,=20so=20an?= =?UTF-8?q?=20old=20plan=20reporting=20it=20at=20100%=20had=20its=20ZCode?= =?UTF-8?q?=20models=20skipped=20with=20the=205=20hours=20full.=20It=20is?= =?UTF-8?q?=20now=20shown=20as=20MCP=20=C2=B7=20Month=20and=20set=20aside,?= =?UTF-8?q?=20as=20a=20pasted=20Zhipu=20key's=20already=20was,=20and=20a?= =?UTF-8?q?=20limit=20whose=20whole=20is=200=20(no=20cap)=20counts=20as=20?= =?UTF-8?q?unused.=20TestZCodeNoMonthlyCap=20fails=20without=20the=20chang?= =?UTF-8?q?e.=20go=20vet,=20go=20build=20-tags=20nogui=20(darwin,=20GOOS?= =?UTF-8?q?=3Dlinux),=20GOOS=3Dwindows=20go=20build,=20go=20test=20-tags?= =?UTF-8?q?=20nogui=20./...=20pass=20(the=20Claude=20account=20tests=20pas?= =?UTF-8?q?s=20alone).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/provider/zcode.go | 11 +++++- internal/provider/zcode_quota_test.go | 54 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 internal/provider/zcode_quota_test.go diff --git a/internal/provider/zcode.go b/internal/provider/zcode.go index 62e5bec2..2c92bed9 100644 --- a/internal/provider/zcode.go +++ b/internal/provider/zcode.go @@ -381,15 +381,24 @@ type zhipuLimits struct { // windows are the limits as windows: what is used of the whole when both // are told (the whole less what remains, or the current value), else the -// percentage the vendor gives. +// percentage the vendor gives. TIME_LIMIT is the month's MCP tool calls, +// which ZCode shows but never stops the models on, so it is set aside, as +// is a limit whose whole is told as 0: no cap (an older plan's), which +// the vendor may still give as 100% used. func (d zhipuLimits) windows() []QuotaWindow { out := []QuotaWindow{} for _, x := range d.Limits { span := zcodeSpan(x.Unit, x.Number) w := QuotaWindow{Name: zcodeWindowName(span), Span: span} + if strings.EqualFold(x.Type, "TIME_LIMIT") { + w.Name, w.Aside = "MCP · Month", true + } if x.Percent != nil { w.Used = *x.Percent } + if x.Usage != nil && *x.Usage == 0 { + w.Used, w.Aside = 0, true + } if x.Usage != nil && *x.Usage > 0 { total := *x.Usage switch { diff --git a/internal/provider/zcode_quota_test.go b/internal/provider/zcode_quota_test.go new file mode 100644 index 00000000..d5e32508 --- /dev/null +++ b/internal/provider/zcode_quota_test.go @@ -0,0 +1,54 @@ +package provider + +import ( + "encoding/json" + "strconv" + "testing" + "time" +) + +// TestZCodeNoMonthlyCap: an older GLM Coding plan's reply (#366) — the +// five hours untouched, no week, and the month's MCP tool calls with no +// cap given as 100% used. The account is not out: only the five hours +// count, and the uncapped month reads as nothing used. +func TestZCodeNoMonthlyCap(t *testing.T) { + now := time.Now() + reset := now.Add(3 * time.Hour).UnixMilli() + month := now.Add(20 * 24 * time.Hour).UnixMilli() + var d zhipuLimits + if err := json.Unmarshal([]byte(`{"level":"pro","limits":[ + {"type":"TOKENS_LIMIT","unit":3,"number":5,"percentage":0,"nextResetTime":`+strconv.FormatInt(reset, 10)+`}, + {"type":"TIME_LIMIT","unit":5,"number":1,"usage":0,"currentValue":0,"remaining":0,"percentage":100,"nextResetTime":`+strconv.FormatInt(month, 10)+`}]}`), &d); err != nil { + t.Fatal(err) + } + ws := d.windows() + if len(ws) != 2 || ws[0].Name != "5 hours" || ws[0].Aside || ws[1].Name != "MCP · Month" || !ws[1].Aside || ws[1].Used != 0 { + t.Fatalf("windows: %+v", ws) + } + a := allowanceOf(ws, now) + if used, _ := a.For("glm-5.1", now); used != 0 { + t.Fatalf("used %v, want 0", used) + } + if full := a.Full("glm-5.1", 100, now); !full.IsZero() { + t.Fatalf("full until %v", full) + } + + // the MCP calls used up on a plan that caps them don't stop the models + // either, and still show as used + d = zhipuLimits{} + json.Unmarshal([]byte(`{"limits":[ + {"type":"TOKENS_LIMIT","unit":3,"number":5,"percentage":10,"nextResetTime":`+strconv.FormatInt(reset, 10)+`}, + {"type":"TOKENS_LIMIT","unit":6,"number":1,"percentage":30,"nextResetTime":`+strconv.FormatInt(month, 10)+`}, + {"type":"TIME_LIMIT","unit":5,"number":1,"usage":100,"currentValue":100,"remaining":0,"percentage":100,"nextResetTime":`+strconv.FormatInt(month, 10)+`}]}`), &d) + ws = d.windows() + if len(ws) != 3 || ws[2].Used != 100 || !ws[2].Aside || ws[1].Aside { + t.Fatalf("capped: %+v", ws) + } + a = allowanceOf(ws, now) + if used, _ := a.For("glm-5.1", now); used != 30 { + t.Fatalf("capped used %v, want 30", used) + } + if full := a.Full("glm-5.1", 100, now); !full.IsZero() { + t.Fatalf("capped full until %v", full) + } +}