mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
feat: AUTO chain rewrite — silent failover+busy skip+pref round-robin+503 tier summary; chain edits reset slot cooldowns (P0/P1); stats by_status + audit jsonl rotation; UI priority-page health badges & status-code card; ctx-menu capture-phase close (outside-press guard); main.go ops warnings; local bundled-Lua verified tests (3 latent bugs fixed); plan.md
This commit is contained in:
@ -3,7 +3,11 @@ package gateway
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@ -56,6 +60,7 @@ type Stats struct {
|
||||
bySrc map[string]*Stat
|
||||
byKeyModel map[string]map[string]*Stat
|
||||
byKeySrc map[string]map[string]*Stat
|
||||
byStatus map[int]*Stat // per http status code aggregates (incl. 402/400)
|
||||
recs []Req
|
||||
maxRecs int
|
||||
auditPath string
|
||||
@ -64,6 +69,15 @@ type Stats struct {
|
||||
|
||||
const hourSec = 3600
|
||||
|
||||
// auditRotateBytes rotates the audit file once it grows past this size (the
|
||||
// file is renamed to <path>.<unix>.old and a fresh one is started); pruning
|
||||
// keeps at most auditKeepOld rotated files. Both are vars so tests can shrink
|
||||
// the threshold.
|
||||
var (
|
||||
auditRotateBytes int64 = 64 << 20
|
||||
auditKeepOld = 10
|
||||
)
|
||||
|
||||
func NewStats(maxRecords int) *Stats {
|
||||
if maxRecords <= 0 {
|
||||
maxRecords = 3000
|
||||
@ -74,6 +88,7 @@ func NewStats(maxRecords int) *Stats {
|
||||
bySrc: map[string]*Stat{},
|
||||
byKeyModel: map[string]map[string]*Stat{},
|
||||
byKeySrc: map[string]map[string]*Stat{},
|
||||
byStatus: map[int]*Stat{},
|
||||
modelHour: map[string]map[int64]int64{},
|
||||
maxRecs: maxRecords,
|
||||
}
|
||||
@ -98,6 +113,10 @@ func inc(m map[string]*Stat, name string, r Req) {
|
||||
a = &Stat{}
|
||||
m[name] = a
|
||||
}
|
||||
incStatus(a, name, r)
|
||||
}
|
||||
|
||||
func incStatus(a *Stat, name string, r Req) {
|
||||
a.Reqs++
|
||||
if r.OK {
|
||||
a.OK++
|
||||
@ -160,6 +179,15 @@ func (s *Stats) Record(r Req) {
|
||||
s.byKeySrc[r.Key] = ks
|
||||
}
|
||||
inc(ks, r.Source, r)
|
||||
if r.Status > 0 {
|
||||
name := strconv.Itoa(r.Status)
|
||||
a := s.byStatus[r.Status]
|
||||
if a == nil {
|
||||
a = &Stat{}
|
||||
s.byStatus[r.Status] = a
|
||||
}
|
||||
incStatus(a, name, r)
|
||||
}
|
||||
// window bucket for quota enforcement (per source-model pair, per unix hour)
|
||||
tok := r.Prompt + r.Compl
|
||||
if tok > 0 && r.Model != "" {
|
||||
@ -187,28 +215,32 @@ func (s *Stats) Record(r Req) {
|
||||
s.recs = s.recs[len(s.recs)-s.maxRecs:]
|
||||
}
|
||||
if s.auditPath != "" {
|
||||
if f, err := os.OpenFile(s.auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err == nil {
|
||||
if b, err := json.Marshal(r); err == nil {
|
||||
_, _ = f.Write(append(b, '\n'))
|
||||
}
|
||||
_ = f.Close()
|
||||
s.rotateAuditLocked()
|
||||
appendAuditLine(s.auditPath, r)
|
||||
}
|
||||
}
|
||||
|
||||
// rotateAuditLocked renames the audit file to <path>.<unix>.old once it
|
||||
// exceeds auditRotateBytes and prunes old files beyond auditKeepOld, keeping
|
||||
// the newest ones. Caller must hold s.mu.
|
||||
func (s *Stats) rotateAuditLocked() {
|
||||
if s.auditPath == "" || auditRotateBytes <= 0 {
|
||||
return
|
||||
}
|
||||
if fi, err := os.Stat(s.auditPath); err == nil && fi.Size() < auditRotateBytes {
|
||||
return
|
||||
}
|
||||
ts := time.Now().Unix()
|
||||
if os.Rename(s.auditPath, fmt.Sprintf("%s.%d.old", s.auditPath, ts)) == nil {
|
||||
old, _ := filepath.Glob(s.auditPath + ".*.old")
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(old)))
|
||||
for i := auditKeepOld; i < len(old); i++ {
|
||||
_ = os.Remove(old[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AppendAudit writes a generic event line (access log entry, login event,
|
||||
// config change, …) to the same audit file without touching the aggregates.
|
||||
func (s *Stats) AppendAudit(obj string, data map[string]interface{}) {
|
||||
s.mu.Lock()
|
||||
path := s.auditPath
|
||||
s.mu.Unlock()
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
row := map[string]interface{}{"obj": obj, "time": time.Now().UnixMilli()}
|
||||
for k, v := range data {
|
||||
row[k] = v
|
||||
}
|
||||
func appendAuditLine(path string, row interface{}) {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
@ -219,6 +251,22 @@ func (s *Stats) AppendAudit(obj string, data map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// AppendAudit writes a generic event line (access log entry, login event,
|
||||
// config change, …) to the same audit file without touching the aggregates.
|
||||
func (s *Stats) AppendAudit(obj string, data map[string]interface{}) {
|
||||
row := map[string]interface{}{"obj": obj, "time": time.Now().UnixMilli()}
|
||||
for k, v := range data {
|
||||
row[k] = v
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.auditPath == "" {
|
||||
return
|
||||
}
|
||||
s.rotateAuditLocked()
|
||||
appendAuditLine(s.auditPath, row)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@ -376,12 +424,22 @@ func (s *Stats) Snapshot(limit int, key string) map[string]interface{} {
|
||||
total.LatMax = a.LatMax
|
||||
}
|
||||
}
|
||||
bs := make([]agrRow, 0, len(s.byStatus))
|
||||
for code := range s.byStatus {
|
||||
bs = append(bs, agrRow{Name: strconv.Itoa(code), Stat: *s.byStatus[code]})
|
||||
}
|
||||
sort.Slice(bs, func(i, j int) bool {
|
||||
ci, _ := strconv.Atoi(bs[i].Name)
|
||||
cj, _ := strconv.Atoi(bs[j].Name)
|
||||
return ci < cj
|
||||
})
|
||||
return map[string]interface{}{
|
||||
"active": s.active,
|
||||
"total": total,
|
||||
"by_key": rows(byKey),
|
||||
"by_model": rows(byModel),
|
||||
"by_source": rows(bySrc),
|
||||
"by_status": bs,
|
||||
"records": append([]Req(nil), recs...),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user