feat(auto): quota-aware AUTO slot scheduling (model tiers, hour/week/month resets) + key scope periods; fix key/slot create/delete UX

This commit is contained in:
root
2026-08-09 11:07:38 +08:00
parent 3408c9cb1f
commit 859d310ad3
9 changed files with 1065 additions and 162 deletions

View File

@ -2,6 +2,7 @@ package gateway
import (
"sync"
"time"
)
// Req is one recorded gateway request (audit trail + per-key/per-model stats).
@ -54,8 +55,11 @@ type Stats struct {
byKeySrc map[string]map[string]*Stat
recs []Req
maxRecs int
modelHour map[string]map[int64]int64 // model -> unix-hour bucket -> tokens
}
const hourSec = 3600
func NewStats(maxRecords int) *Stats {
if maxRecords <= 0 {
maxRecords = 3000
@ -66,6 +70,7 @@ func NewStats(maxRecords int) *Stats {
bySrc: map[string]*Stat{},
byKeyModel: map[string]map[string]*Stat{},
byKeySrc: map[string]map[string]*Stat{},
modelHour: map[string]map[int64]int64{},
maxRecs: maxRecords,
}
}
@ -123,12 +128,99 @@ func (s *Stats) Record(r Req) {
s.byKeySrc[r.Key] = ks
}
inc(ks, r.Source, r)
// window bucket for quota enforcement (per model, per unix hour)
tok := r.Prompt + r.Compl
if tok > 0 && r.Model != "" {
h := r.Time / hourSec
hm := s.modelHour[r.Model]
if hm == nil {
hm = map[int64]int64{}
s.modelHour[r.Model] = hm
}
hm[h] += tok
if len(hm) > 24*40 {
for k := range hm {
if k < h-24*40 {
delete(hm, k)
}
}
}
}
s.recs = append(s.recs, r)
if len(s.recs) > s.maxRecs {
s.recs = s.recs[len(s.recs)-s.maxRecs:]
}
}
// ModelTokens returns the tokens consumed per model for one gateway key id
// (used for per-model token quota enforcement).
func (s *Stats) ModelTokens(key string) map[string]int64 {
s.mu.Lock()
defer s.mu.Unlock()
out := map[string]int64{}
for k, v := range s.byKeyModel[key] {
out[k] = v.Tokens
}
return out
}
// KeyTokens returns the total tokens consumed by one gateway key id.
func (s *Stats) KeyTokens(key string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
a := s.byKey[key]
if a == nil {
return 0
}
return a.Tokens
}
// AutoPeriodSeconds maps a quota reset period to its window length in
// seconds. "" → 0 (never resets); "hour" → 1h; "week" → 7d; "month" → 30d;
// "nhour" → Hours (>=1) hours.
func AutoPeriodSeconds(period string, hours int64) int64 {
switch period {
case "hour":
return hourSec
case "week":
return 7 * 24 * hourSec
case "month":
return 30 * 24 * hourSec
case "nhour":
if hours < 1 {
hours = 1
}
return hours * hourSec
}
return 0
}
// WindowTokens returns the tokens billed for the model within the last `sec`
// seconds (0 = since forever).
func (s *Stats) WindowTokens(model string, sec int64) int64 {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().Unix()
hm := s.modelHour[model]
if len(hm) == 0 {
return 0
}
var total int64
if sec <= 0 {
for _, v := range hm {
total += v
}
return total
}
cut := now - sec
for h, v := range hm {
if h*hourSec >= cut {
total += v
}
}
return total
}
func rows(m map[string]*Stat) []StatsRow {
out := make([]StatsRow, 0, len(m))
for k, v := range m {