mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
修复用量总计与渠道账本统计
This commit is contained in:
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 } } }
|
||||
|
||||
Reference in New Issue
Block a user