// Package quota is the z.ai coding-plan quota gate (Redmine 490) plus the // read-only system resource monitor (Redmine 491): it tracks plan credit // buckets (5-hour + weekly), computes per-turn credit consumption from the // documented z.ai formula, applies TZ-aware peak-hour scheduling and // back-pressure thresholds, and shares quota state across harness instances // through an optional redis container. The gate NEVER hard-fails the loop: // unknown state is permissive, exhaustion defers work with a logged reason. // // z.ai credit model (docs.z.ai/devpack/overview, 2026-08-29): // // credits = (input*in_mult + cached_input*cache_mult + output*out_mult) / 10000 // // GLM-5.3: 6.9/1.7/24; GLM-5.3-Flash: 2.3/0.56/8. Off-peak hours charge 50%. // Plans: Lite 2,000/10,000, Pro 12,000/60,000, Max 28,000/140,000 credits // (5-hour / weekly). LIVE VERIFICATION of the usage endpoint is OPEN: z.ai // documents the buckets but no public usage REST route exists today // (probed 2026-08-29, see REPORT-20260829-0500-quota); the parser targets // the documented shape and runs against a fake server until z.ai ships it. package quota import ( "encoding/json" "fmt" "time" ) // Bucket IDs for the z.ai coding-plan windows. const ( Bucket5h = "5h" BucketWeekly = "weekly" ) // Bucket is one usage window: credits used against the plan limit and when // the window resets. Used/Limit are in z.ai credits, not tokens or dollars. type Bucket struct { ID string `json:"id"` Used float64 `json:"used"` Limit float64 `json:"limit"` WindowReset time.Time `json:"window_reset"` } // UsedPct is the bucket's consumption ratio in percent of the limit. // A zero/negative limit reads as 0 (never blocks). func (b Bucket) UsedPct() float64 { if b.Limit <= 0 { return 0 } return b.Used / b.Limit * 100 } // QuotaSnapshot is one account's quota state at a point in time, either // polled from the provider (Source "usage_url") or synthesized from locally // estimated consumption against the configured plan limits (Source // "estimate"). type QuotaSnapshot struct { Account string `json:"account"` Source string `json:"source"` FetchedAt time.Time `json:"fetched_at"` Buckets []Bucket `json:"buckets"` } // Bucket returns the bucket with the given id, if present. func (s *QuotaSnapshot) Bucket(id string) (Bucket, bool) { for _, b := range s.Buckets { if b.ID == id { return b, true } } return Bucket{}, false } // MaxUsedPct is the highest consumption ratio across all buckets — the // number back-pressure thresholds compare against. func (s *QuotaSnapshot) MaxUsedPct() float64 { if s == nil { return 0 } max := 0.0 for _, b := range s.Buckets { if p := b.UsedPct(); p > max { max = p } } return max } // providerUsage is the z.ai usage response shape this parser targets. z.ai // has not published the route yet; field aliases (used_credits|credits_used, // total_credits|limit_credits) keep the parser tolerant of either naming. type providerUsage struct { Usage struct { FiveHour providerBucket `json:"five_hour"` Weekly providerBucket `json:"weekly"` } `json:"usage"` } type providerBucket struct { Used any `json:"used_credits"` UsedAlias any `json:"credits_used"` Limit any `json:"total_credits"` LimitAlias any `json:"limit_credits"` ResetAt string `json:"reset_at"` ResetAlias string `json:"reset_time"` } // ParseUsage decodes a provider usage response into a QuotaSnapshot for the // account. It is strict about structure (unknown shapes error so a silent // HTML error page never reads as "quota fine") and lenient about field // naming within the documented buckets. func ParseUsage(account string, fetchedAt time.Time, body []byte) (*QuotaSnapshot, error) { var p providerUsage if err := json.Unmarshal(body, &p); err != nil { return nil, fmt.Errorf("usage decode: %w", err) } num := func(v any) (float64, bool) { switch n := v.(type) { case float64: return n, true case string: var f float64 if _, err := fmt.Sscanf(n, "%g", &f); err == nil { return f, true } } return 0, false } snap := &QuotaSnapshot{Account: account, Source: "usage_url", FetchedAt: fetchedAt} for id, pb := range map[string]providerBucket{Bucket5h: p.Usage.FiveHour, BucketWeekly: p.Usage.Weekly} { used, ok := num(pb.Used) if !ok { used, ok = num(pb.UsedAlias) } if !ok { return nil, fmt.Errorf("usage decode: bucket %q has no used_credits", id) } limit, ok := num(pb.Limit) if !ok { limit, ok = num(pb.LimitAlias) } if !ok { return nil, fmt.Errorf("usage decode: bucket %q has no total_credits", id) } reset := pb.ResetAt if reset == "" { reset = pb.ResetAlias } rt := time.Time{} if reset != "" { var err error rt, err = time.Parse(time.RFC3339, reset) if err != nil { return nil, fmt.Errorf("usage decode: bucket %q reset_at: %w", id, err) } } snap.Buckets = append(snap.Buckets, Bucket{ID: id, Used: used, Limit: limit, WindowReset: rt}) } if len(snap.Buckets) != 2 { return nil, fmt.Errorf("usage decode: want five_hour and weekly buckets, got %d", len(snap.Buckets)) } return snap, nil }