mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
202 lines
4.5 KiB
Go
202 lines
4.5 KiB
Go
package gateway
|
|
|
|
import (
|
|
"sync"
|
|
)
|
|
|
|
// Req is one recorded gateway request (audit trail + per-key/per-model stats).
|
|
type Req struct {
|
|
Time int64 `json:"time"` // unix milliseconds
|
|
Key string `json:"key"` // gateway key id
|
|
Type string `json:"type"` // chat | stream | image
|
|
Model string `json:"model"` // effective model used upstream
|
|
// Source provider/source name
|
|
Source string `json:"source"`
|
|
// Prompt prompt tokens
|
|
Prompt int64 `json:"prompt_tokens"`
|
|
// Compl completion tokens
|
|
Compl int64 `json:"completion_tokens"`
|
|
// LatMs total handling time ms
|
|
LatMs int64 `json:"latency_ms"`
|
|
OK bool `json:"ok"`
|
|
// Status http status code
|
|
Status int `json:"status"`
|
|
// Err short error message
|
|
Err string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Stat aggregates counters for one dimension row.
|
|
type Stat struct {
|
|
Reqs int64 `json:"reqs"`
|
|
OK int64 `json:"ok"`
|
|
Err int64 `json:"err"`
|
|
Tokens int64 `json:"tokens"`
|
|
Prompt int64 `json:"prompt_tokens"`
|
|
Compl int64 `json:"completion_tokens"`
|
|
LatSum int64 `json:"latency_sum_ms"`
|
|
LatMax int64 `json:"latency_max_ms"`
|
|
}
|
|
|
|
type agrRow struct {
|
|
Name string `json:"name"`
|
|
Stat
|
|
}
|
|
|
|
// Stats collects per-key / per-model / per-source aggregates plus a bounded
|
|
// ring of raw request records, all guarded by one mutex.
|
|
type Stats struct {
|
|
mu sync.Mutex
|
|
active int64
|
|
byKey map[string]*Stat
|
|
byModel map[string]*Stat
|
|
bySrc map[string]*Stat
|
|
byKeyModel map[string]map[string]*Stat
|
|
byKeySrc map[string]map[string]*Stat
|
|
recs []Req
|
|
maxRecs int
|
|
}
|
|
|
|
func NewStats(maxRecords int) *Stats {
|
|
if maxRecords <= 0 {
|
|
maxRecords = 3000
|
|
}
|
|
return &Stats{
|
|
byKey: map[string]*Stat{},
|
|
byModel: map[string]*Stat{},
|
|
bySrc: map[string]*Stat{},
|
|
byKeyModel: map[string]map[string]*Stat{},
|
|
byKeySrc: map[string]map[string]*Stat{},
|
|
maxRecs: maxRecords,
|
|
}
|
|
}
|
|
|
|
// Begin accounts an in-flight request; the returned func must be called once
|
|
// the request finished (defer ok).
|
|
func (s *Stats) Begin() func() {
|
|
s.mu.Lock()
|
|
s.active++
|
|
s.mu.Unlock()
|
|
return func() {
|
|
s.mu.Lock()
|
|
s.active--
|
|
s.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func inc(m map[string]*Stat, name string, r Req) {
|
|
a := m[name]
|
|
if a == nil {
|
|
a = &Stat{}
|
|
m[name] = a
|
|
}
|
|
a.Reqs++
|
|
if r.OK {
|
|
a.OK++
|
|
} else {
|
|
a.Err++
|
|
}
|
|
a.Tokens += r.Prompt + r.Compl
|
|
a.Prompt += r.Prompt
|
|
a.Compl += r.Compl
|
|
a.LatSum += r.LatMs
|
|
if r.LatMs > a.LatMax {
|
|
a.LatMax = r.LatMs
|
|
}
|
|
}
|
|
|
|
// Record appends a finished request to the aggregates and ring buffer.
|
|
func (s *Stats) Record(r Req) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
inc(s.byKey, r.Key, r)
|
|
inc(s.byModel, r.Model, r)
|
|
inc(s.bySrc, r.Source, r)
|
|
km := s.byKeyModel[r.Key]
|
|
if km == nil {
|
|
km = map[string]*Stat{}
|
|
s.byKeyModel[r.Key] = km
|
|
}
|
|
inc(km, r.Model, r)
|
|
ks := s.byKeySrc[r.Key]
|
|
if ks == nil {
|
|
ks = map[string]*Stat{}
|
|
s.byKeySrc[r.Key] = ks
|
|
}
|
|
inc(ks, r.Source, r)
|
|
s.recs = append(s.recs, r)
|
|
if len(s.recs) > s.maxRecs {
|
|
s.recs = s.recs[len(s.recs)-s.maxRecs:]
|
|
}
|
|
}
|
|
|
|
func rows(m map[string]*Stat) []StatsRow {
|
|
out := make([]StatsRow, 0, len(m))
|
|
for k, v := range m {
|
|
out = append(out, StatsRow{Name: k, Stat: *v})
|
|
}
|
|
for i := 1; i < len(out); i++ {
|
|
for j := i; j > 0 && out[j].Reqs > out[j-1].Reqs; j-- {
|
|
out[j], out[j-1] = out[j-1], out[j]
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// StatsRow is one aggregated row for the dashboard.
|
|
type StatsRow struct {
|
|
Name string `json:"name"`
|
|
Stat
|
|
}
|
|
|
|
// Snapshot returns the whole dashboard payload; when key != "" the records
|
|
// and aggregate views are restricted to that gateway key.
|
|
func (s *Stats) Snapshot(limit int, key string) map[string]interface{} {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if limit <= 0 {
|
|
limit = s.maxRecs
|
|
}
|
|
start := 0
|
|
if len(s.recs) > limit {
|
|
start = len(s.recs) - limit
|
|
}
|
|
recs := s.recs[start:]
|
|
if key != "" {
|
|
filt := recs[:0]
|
|
for _, r := range recs {
|
|
if r.Key == key {
|
|
filt = append(filt, r)
|
|
}
|
|
}
|
|
recs = filt
|
|
}
|
|
var total Stat
|
|
var byModel, byKey, bySrc map[string]*Stat
|
|
if key == "" {
|
|
byKey, byModel, bySrc = s.byKey, s.byModel, s.bySrc
|
|
} else {
|
|
byKey = map[string]*Stat{key: s.byKey[key]}
|
|
byModel = s.byKeyModel[key]
|
|
bySrc = s.byKeySrc[key]
|
|
}
|
|
for _, a := range byModel {
|
|
total.Reqs += a.Reqs
|
|
total.OK += a.OK
|
|
total.Err += a.Err
|
|
total.Tokens += a.Tokens
|
|
total.Prompt += a.Prompt
|
|
total.Compl += a.Compl
|
|
total.LatSum += a.LatSum
|
|
if a.LatMax > total.LatMax {
|
|
total.LatMax = a.LatMax
|
|
}
|
|
}
|
|
return map[string]interface{}{
|
|
"active": s.active,
|
|
"total": total,
|
|
"by_key": rows(byKey),
|
|
"by_model": rows(byModel),
|
|
"by_source": rows(bySrc),
|
|
"records": append([]Req(nil), recs...),
|
|
}
|
|
} |