mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-21 17:38:00 +00:00
fix: AUTO tier order ascending + stats/CSV export completeness + UI key-view toggle & exit affordance
- scheduler: BuildChain sorts tiers ascending so tier 1 (highest priority) is tried first; previously descending inverted the chain (P10-1..P10-5) - stats/api: handleStatsAPI limit→20000; CSV reads full audit via new Stats.AuditRecords (includes *.old rotation); key_names mapped by keyID() masked key; non-admin filter and keys-csv use masked keys - server: ring buffer NewStats(10000) - ui: dash-row card scroll area moved to table container (fix overflow below card); key view toggle (re-click same key returns to global) + kpi-exit affordance; rec-exit span - chat: chain-failure record now surfaces first failed tier; AUTO comment sync
This commit is contained in:
@ -130,14 +130,15 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
limit := 500
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 5000 {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 20000 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
key := r.URL.Query().Get("key")
|
||||
if reqRole(r.Context()) != "admin" {
|
||||
// user keys may only see their own usage - use full key for internal filtering
|
||||
key = reqKey(r.Context())
|
||||
// user keys may only see their own usage - records and aggregates are
|
||||
// keyed by the masked id (keyID), so filter on that masked form.
|
||||
key = keyID(reqKey(r.Context()))
|
||||
}
|
||||
if r.URL.Query().Get("export") == "csv" {
|
||||
from, _ := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64)
|
||||
@ -150,10 +151,10 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
cw := csv.NewWriter(w)
|
||||
names := map[string]string{}
|
||||
for _, k := range g.core.ListKeys() {
|
||||
names[k.Key] = k.Name
|
||||
names[keyID(k.Key)] = k.Name
|
||||
}
|
||||
_ = cw.Write([]string{"time", "key", "key_name", "type", "model", "source", "status", "ok", "prompt_tokens", "completion_tokens", "latency_ms", "error"})
|
||||
for _, rec := range g.stats.Records(from, to, key) {
|
||||
for _, rec := range g.stats.AuditRecords(from, to, key) {
|
||||
_ = cw.Write([]string{
|
||||
time.UnixMilli(rec.Time).Format(time.RFC3339),
|
||||
rec.Key,
|
||||
@ -180,7 +181,13 @@ if r.URL.Query().Get("export") == "keys-csv" {
|
||||
// Use the same key filtering as the JSON API
|
||||
exportKey := r.URL.Query().Get("key")
|
||||
if reqRole(r.Context()) != "admin" {
|
||||
exportKey = reqKey(r.Context())
|
||||
exportKey = keyID(reqKey(r.Context()))
|
||||
}
|
||||
// by_key rows are keyed by the masked id (keyID); build a masked-id ->
|
||||
// record map so names/roles/models resolve for the export.
|
||||
byMasked := map[string]config.GWKey{}
|
||||
for _, k := range g.core.ListKeys() {
|
||||
byMasked[keyID(k.Key)] = k
|
||||
}
|
||||
snap := g.stats.Snapshot(0, exportKey)
|
||||
byKeyRaw, ok := snap["by_key"].([]StatsRow)
|
||||
@ -188,8 +195,7 @@ if r.URL.Query().Get("export") == "keys-csv" {
|
||||
byKeyRaw = []StatsRow{}
|
||||
}
|
||||
for _, row := range byKeyRaw {
|
||||
key := row.Name
|
||||
keyInfo, found := g.core.FindKey(key)
|
||||
keyInfo, found := byMasked[row.Name]
|
||||
name := ""
|
||||
role := ""
|
||||
models := ""
|
||||
@ -237,7 +243,7 @@ if r.URL.Query().Get("export") == "keys-csv" {
|
||||
snap := g.stats.Snapshot(limit, key)
|
||||
keyNames := map[string]string{}
|
||||
for _, k := range g.core.ListKeys() {
|
||||
keyNames[k.Key] = k.Name
|
||||
keyNames[keyID(k.Key)] = k.Name
|
||||
}
|
||||
snap["key_names"] = keyNames
|
||||
writeJSON(w, http.StatusOK, snap)
|
||||
|
||||
Reference in New Issue
Block a user