quota: z.ai credit-bucket back-pressure + resource gate + usage accounting (Redmine 490+491)
The loop now consults quota and host state before every dispatch and
DEFERS gated work with a logged reason instead of letting turns die at
the provider (the 2026-08-28 19:00 quota-wall failure mode, replayed as
a test). Adds internal/quota: 5h/weekly credit buckets (provider poll
when z.ai ships an endpoint - fake-server tested - else locally
estimated from the documented credit formula), TZ-aware peak window
(default 01:00-05:00 America/Chicago weekdays, matching the documented
z.ai peak Mon-Fri 14:00-18:00 Singapore), block/defer thresholds, a
read-only load/mem/disk/IO-PSI monitor, an optional redis shared-state
hop (stdlib RESP2 mini-client) so all instances of an account
coordinate, per-class token+credit accounting in loop.jsonl, and
`harness quota status|probe|gate`. Config: [quota] + [resources]
sections; README runbook covers the redis container and deploy-time
cgroup enforcement.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package quota
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Multipliers are the z.ai credit formula coefficients (per 10k tokens,
|
||||
// docs.z.ai/devpack/overview). Family selects by concrete model name; the
|
||||
// flash tier (GLM-5.3-Flash, routed from glm-4.7*) is the cheap one.
|
||||
type Multipliers struct {
|
||||
Input float64
|
||||
CachedInput float64
|
||||
Output float64
|
||||
}
|
||||
|
||||
// Flagship multipliers: GLM-5.3 (and everything auto-routed to it).
|
||||
func FlagshipMultipliers() Multipliers { return Multipliers{Input: 6.9, CachedInput: 1.7, Output: 24} }
|
||||
|
||||
// Flash multipliers: GLM-5.3-Flash (and glm-4.7* routing).
|
||||
func FlashMultipliers() Multipliers { return Multipliers{Input: 2.3, CachedInput: 0.56, Output: 8} }
|
||||
|
||||
// MultipliersFor maps a concrete proxy model name to its credit multiplier
|
||||
// family. Unknown models read as flagship (conservative: overestimate cost
|
||||
// rather than silently burn quota).
|
||||
func MultipliersFor(model string) Multipliers {
|
||||
m := strings.ToLower(model)
|
||||
switch {
|
||||
case strings.Contains(m, "flash"):
|
||||
return FlashMultipliers()
|
||||
default:
|
||||
return FlagshipMultipliers()
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateCredits computes the z.ai credits one turn consumed from its token
|
||||
// usage: (input*in + cached*cache + output*out) / 10000, halved when the
|
||||
// turn ran off-peak (z.ai charges 50% outside peak hours).
|
||||
func EstimateCredits(model string, promptTokens, cachedTokens, completionTokens int, peak bool) float64 {
|
||||
mult := MultipliersFor(model)
|
||||
credits := (float64(promptTokens)*mult.Input +
|
||||
float64(cachedTokens)*mult.CachedInput +
|
||||
float64(completionTokens)*mult.Output) / 10000
|
||||
if !peak {
|
||||
credits /= 2
|
||||
}
|
||||
// Round to 6 decimals: keeps redis INCRBYFLOAT values readable and the
|
||||
// JSONL compact; sub-microcredit noise is meaningless.
|
||||
return math.Round(credits*1e6) / 1e6
|
||||
}
|
||||
|
||||
// EstimateTurnInput is the per-turn usage record the loop hands the gate.
|
||||
type EstimateTurnInput struct {
|
||||
Model string
|
||||
PromptTokens int
|
||||
CachedTokens int
|
||||
CompletionTokens int
|
||||
Peak bool // turn ran inside the peak window
|
||||
}
|
||||
|
||||
// Describe renders a human summary of one turn's credit cost (logs, REPORTs).
|
||||
func (e EstimateTurnInput) Describe() string {
|
||||
return fmt.Sprintf("%s: %d/%d/%d tokens (in/cached/out) %s = %.4f credits",
|
||||
e.Model, e.PromptTokens, e.CachedTokens, e.CompletionTokens,
|
||||
peakTag(e.Peak), EstimateCredits(e.Model, e.PromptTokens, e.CachedTokens, e.CompletionTokens, e.Peak))
|
||||
}
|
||||
|
||||
func peakTag(peak bool) string {
|
||||
if peak {
|
||||
return "peak"
|
||||
}
|
||||
return "off-peak"
|
||||
}
|
||||
Reference in New Issue
Block a user