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) + } +}