mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(billing): 未定价按天统计,让「已修好」和「还在漏」不再长得一样
计费页只有一个终身计数器 unpriced_reqs,实测 6378 —— 占 18981 请求的 34%。
但这个数字回答不了运维唯一会问的问题:「还在漏吗?」
原因是它把两件事混在一起:加规则之前的历史欠账,和加规则之后仍在发生的漏计。
修好一个源之后终身计数纹丝不动,于是仪表盘上「未定价 6378」和一个仍在
什么都没记的系统完全无法区分。
改动:
- 钩子按 UTC 天写入 unpriced_by_day{day:{reqs, models}}。终身计数保留
(报表不该改写历史),天桶是新增的观察维度。
- 页面加「未定价(近 7 天)」一行。为 0 时额外显示「近 7 天无」——这时候
零本身就是结论:加的规则确实生效了。
- 没有天桶数据时返回 null 而不是 0。旧的 state 文件说「未知」,
不该假装有一份干净记录。
生产实测:全部视图 终身 6378 / 近 7 天 2;日视图隐藏终身计数(null)但保留
近 7 天 —— 周期视图原本把两者都置 null,因为那时根本没有天桶。
失败的请求同样计入(上游 502 的 gpt-5.5 也进了天桶):一次失败的请求照样
消耗上游配额,只是没拿到 token,单看 token 维度会漏掉。
判据:两个不同日期的请求必须落到两个天桶且终身计数不重复累加;变异成
「总记今天」即变红(只判首日会漏掉这一点)。
Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
This commit is contained in:
@ -442,6 +442,21 @@ function plugin.on_request_end(payload)
|
||||
s.unpriced_reqs = s.unpriced_reqs + 1
|
||||
local m = payload.model or "?"
|
||||
s.unpriced_models[m] = (s.unpriced_models[m] or 0) + 1
|
||||
|
||||
-- Day-keyed unpriced tally. The lifetime counters above cannot answer the
|
||||
-- question an operator actually has — "is it STILL happening?" — because they
|
||||
-- keep the debt from before a pricing rule was added. After pricing a source
|
||||
-- that was mispriced, the lifetime number never moves, so "unpriced: 6376" on
|
||||
-- a dashboard is indistinguishable from a system that is still billing
|
||||
-- nothing. Counting by day makes the fix observable: the offending day keeps
|
||||
-- its number, new days start at zero.
|
||||
if s.unpriced_by_day == nil then s.unpriced_by_day = {} end
|
||||
-- dayKey wants epoch SECONDS; payload.time is milliseconds.
|
||||
local uday = dayKey(math.floor((tonumber(payload.time) or (os.time() * 1000)) / 1000))
|
||||
local ubucket = s.unpriced_by_day[uday]
|
||||
if ubucket == nil then ubucket = { reqs = 0, models = {} }; s.unpriced_by_day[uday] = ubucket end
|
||||
ubucket.reqs = ubucket.reqs + 1
|
||||
ubucket.models[m] = (ubucket.models[m] or 0) + 1
|
||||
end
|
||||
if s.degraded_reqs == nil then s.degraded_reqs = 0 end
|
||||
-- Degradation is counted here rather than in the chain_step hook because
|
||||
@ -517,7 +532,7 @@ plugin.ui = {
|
||||
global = {
|
||||
"total", "by_source", "by_model", "by_day", "by_day_src",
|
||||
"by_day_model", "by_tier_served", "skip_reasons", "degraded_reqs",
|
||||
"unpriced_models", "unpriced_reqs", "started",
|
||||
"unpriced_models", "unpriced_reqs", "unpriced_by_day", "started",
|
||||
},
|
||||
depth = { by_key = 1, by_day_key = 2 },
|
||||
},
|
||||
@ -593,6 +608,7 @@ plugin.ui = {
|
||||
en: {
|
||||
total: "Total", requests: "Requests", degraded: "Degraded",
|
||||
unpriced: "Unpriced", prompt: "Prompt tokens",
|
||||
unpriced7d: "Unpriced (7d)", unpricedClean: "none in last 7 days",
|
||||
completion: "Completion tokens", failures: "Failures",
|
||||
cacheRate: "Cache hit rate", cacheTokens: "Cache read tokens",
|
||||
perSource: "Per source", perModel: "Per model",
|
||||
@ -614,6 +630,7 @@ plugin.ui = {
|
||||
zh: {
|
||||
total: "总开销", requests: "请求数", degraded: "降级",
|
||||
unpriced: "未定价", prompt: "输入 tokens",
|
||||
unpriced7d: "未定价(近 7 天)", unpricedClean: "近 7 天无",
|
||||
completion: "输出 tokens", failures: "失败",
|
||||
cacheRate: "缓存命中率", cacheTokens: "缓存读取 tokens",
|
||||
perSource: "按源", perModel: "按模型",
|
||||
@ -734,13 +751,35 @@ plugin.ui = {
|
||||
});
|
||||
return out;
|
||||
}
|
||||
// recentUnpriced counts unpriced requests over the last n days. When the
|
||||
// result is zero THAT is the useful signal: every pricing rule added since
|
||||
// then is holding. Null when there is no day data at all (a state file
|
||||
// written before the buckets existed) so the UI can say "unknown" rather than
|
||||
// claim a clean record it cannot support.
|
||||
function recentUnpriced(st, n) {
|
||||
var byDay = st && st.unpriced_by_day;
|
||||
if (!byDay) return null;
|
||||
var cutoff = Date.now() - n * 86400000;
|
||||
var sum = 0, seen = false;
|
||||
Object.keys(byDay).forEach(function (d) {
|
||||
var t = Date.parse(d + "T00:00:00Z");
|
||||
if (isNaN(t)) return;
|
||||
seen = true;
|
||||
if (t >= cutoff) sum += (byDay[d].reqs || 0);
|
||||
});
|
||||
return seen ? sum : null;
|
||||
}
|
||||
|
||||
// rescale returns the state as the selected period presents it. Degraded and
|
||||
// unpriced counters are lifetime-only (there is no day-keyed tally for them)
|
||||
// so they are returned as null and the UI hides them instead of printing a
|
||||
// lifetime number beside a per-window cost — the exact mismatch the period
|
||||
// view exists to remove.
|
||||
function rescale(st) {
|
||||
if (PERIOD === "all" || !st.by_day) return st;
|
||||
if (PERIOD === "all" || !st.by_day) {
|
||||
if (st) st.unpriced_recent = recentUnpriced(st, 7);
|
||||
return st;
|
||||
}
|
||||
var days = daysInWindow(PERIOD, utcToday());
|
||||
var total = zeroBucket();
|
||||
var byDay = {};
|
||||
@ -758,6 +797,10 @@ plugin.ui = {
|
||||
by_day: byDay,
|
||||
degraded_reqs: null,
|
||||
unpriced_reqs: null,
|
||||
// The lifetime unpriced total is null here for the same reason, but the
|
||||
// day buckets are not — they are what lets the card report "0 in the last
|
||||
// 7 days" instead of leaving a settled debt indistinguishable from a leak.
|
||||
unpriced_recent: recentUnpriced(st, 7),
|
||||
};
|
||||
return out;
|
||||
}
|
||||
@ -838,6 +881,15 @@ plugin.ui = {
|
||||
// print a lifetime number next to a per-window cost.
|
||||
view.degraded_reqs != null ? [T.degraded, fmtInt(view.degraded_reqs)] : null,
|
||||
view.unpriced_reqs != null ? [T.unpriced, fmtInt(view.unpriced_reqs)] : null,
|
||||
// "Unpriced (7d)" answers what the lifetime number cannot: is it STILL
|
||||
// happening? A settled debt and a live leak print the same lifetime total.
|
||||
view.unpriced_recent != null
|
||||
? [
|
||||
T.unpriced7d,
|
||||
fmtInt(view.unpriced_recent),
|
||||
view.unpriced_recent === 0 ? T.unpricedClean : null,
|
||||
]
|
||||
: null,
|
||||
[T.prompt, fmtInt(t.prompt_tokens || 0)],
|
||||
[T.completion, fmtInt(t.completion_tokens || 0)],
|
||||
[T.failures, t.failures || 0],
|
||||
|
||||
Reference in New Issue
Block a user