修复用量总计与渠道账本统计

This commit is contained in:
JianFeeeee
2026-10-15 12:43:11 +08:00
parent 226493b5ae
commit eaac0ef6b3
3 changed files with 54 additions and 27 deletions

View File

@ -228,6 +228,26 @@ func incStatus(a *Stat, name string, r Req) {
}
}
// addStat merges an already aggregated row into a total. It deliberately
// includes failed requests without a model: routing can fail before a model is
// resolved, but that request still belongs in the gateway total.
func addStat(dst, src *Stat) {
if dst == nil || src == nil {
return
}
dst.Reqs += src.Reqs
dst.OK += src.OK
dst.Err += src.Err
dst.Tokens += src.Tokens
dst.Prompt += src.Prompt
dst.Compl += src.Compl
dst.LatSum += src.LatSum
dst.FirstByteSum += src.FirstByteSum
if src.LatMax > dst.LatMax {
dst.LatMax = src.LatMax
}
}
// LoadAudit primes the in-memory state from the audit file.
//
// The aggregates (totals, per-key/model/source rows, status counts and the
@ -1219,17 +1239,13 @@ func (s *Stats) Snapshot(limit int, key string) map[string]interface{} {
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
}
// The total is the sum of every recorded request, not the model rows.
// Routing failures are valid requests but may have no resolved model; deriving
// total from byModel silently dropped those 502/503 records. Use byKey, which
// is populated for every request (including an empty key), and keep the same
// scope as the selected key filter.
for _, a := range byKey {
addStat(&total, a)
}
bs := make([]agrRow, 0, len(s.byStatus))
for code, st := range s.byStatus {

View File

@ -560,3 +560,17 @@ func TestAuditRotationThresholds(t *testing.T) {
t.Fatalf("total audit budget = %d, want 256 MiB", total)
}
}
func TestSnapshotTotalIncludesRequestsWithoutModel(t *testing.T) {
s := NewStats(100)
s.Record(Req{Key: "k", Status: 200, OK: true})
s.Record(Req{Key: "k", Model: "m", Status: 200, OK: true, Prompt: 3, Compl: 2})
snap := s.Snapshot(0, "")
total, ok := snap["total"].(Stat)
if !ok {
t.Fatalf("total missing: %#v", snap["total"])
}
if total.Reqs != 2 || total.OK != 2 || total.Tokens != 5 {
t.Fatalf("total must include model-less requests: %#v", total)
}
}

View File

@ -7110,27 +7110,24 @@ function plugin.usage_token_ledger(req)
if key ~= "" then
url = url .. "&key=" .. (key:len() <= 6 and key or ("***" .. key:sub(-6)))
end
local resp, err = __llmsproxy_http({ url = url, method = "GET", timeout_ms = 4000 })
local headers = {}
if key ~= "" then
headers["Authorization"] = "Bearer " .. key
end
local resp, err = __llmsproxy_http({ url = url, method = "GET", timeout_ms = 4000, headers = headers })
if err or not resp or resp.status ~= 200 then
return { status = 200, json = { ok = true, snapshot = { available = false,
error = err or (resp and tostring(resp.status) or "no response") } } }
return { status = 503, json = { ok = false, error = "usage snapshot unavailable: " .. tostring(err or (resp and resp.status or "no response")) } }
end
local ok, rec = pcall(json.decode, resp.body or "")
if not ok or type(rec) ~= "table" then
return { status = 200, json = { ok = true, snapshot = { available = false, error = "stats 无法解析" } } }
end
-- 汇总 token 总量(兼容 stats 的不同形状)
local prompt, completion, requests = 0, 0, 0
local rows = rec.requests or rec.rows or rec.data or {}
if type(rows) == "table" then
for _, r in ipairs(rows) do
if type(r) == "table" then
prompt = prompt + (tonumber(r.prompt_tokens) or 0)
completion = completion + (tonumber(r.completion_tokens) or 0)
requests = requests + 1
end
end
return { status = 502, json = { ok = false, error = "usage snapshot 无法解析" } }
end
-- 汇总 token 总量。/api/stats 的聚合在 `total` 下,不在顶层数组中。
-- 之前错误地遍历 rec.requests/rows/data,导致该端点永远返回 0。
local total = type(rec.total) == "table" and rec.total or {}
local prompt = tonumber(total.prompt_tokens) or 0
local completion = tonumber(total.completion_tokens) or 0
local requests = tonumber(total.reqs) or 0
return { status = 200, json = { ok = true, snapshot = { available = true,
tokens = { prompt = prompt, completion = completion, total = prompt + completion },
requests = requests, raw = rec } } }