mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17: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:
@ -994,3 +994,49 @@ func TestBillingCacheHitClampedInStats(t *testing.T) {
|
||||
tot["cache_fresh_tokens"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnpricedIsTrackedPerDay pins the day-keyed unpriced tally.
|
||||
//
|
||||
// The lifetime counters cannot answer the question an operator actually has —
|
||||
// "is it STILL happening?" — because they keep the debt from before a rule was
|
||||
// added. After pricing a mispriced source the lifetime number never moves, so a
|
||||
// dashboard reading "unpriced: 6376" is indistinguishable from a system that is
|
||||
// still billing nothing. Day buckets are what make a pricing fix observable: the
|
||||
// offending day keeps its count and the next day starts at zero for it.
|
||||
func TestUnpricedIsTrackedPerDay(t *testing.T) {
|
||||
ps, _ := billingVM(t)
|
||||
// Nothing is priced in the default config, so every request is unpriced.
|
||||
// Two requests on two different UTC days.
|
||||
for _, ms := range []int64{1790000000000, 1790100000000} {
|
||||
ps.Fire(StageRequestEnd, map[string]interface{}{
|
||||
"model": "mystery-model", "source": "nowhere", "key": "***aaaaaa",
|
||||
"ok": true, "prompt_tokens": 10, "completion_tokens": 5,
|
||||
"time": ms,
|
||||
})
|
||||
}
|
||||
st := stateOf(t, ps)
|
||||
byDay, ok := st["unpriced_by_day"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("state has no unpriced_by_day map: %T", st["unpriced_by_day"])
|
||||
}
|
||||
if len(byDay) != 2 {
|
||||
t.Fatalf("expected 2 day buckets, got %d: %v", len(byDay), byDay)
|
||||
}
|
||||
var sum float64
|
||||
for day, bucket := range byDay {
|
||||
b := bucket.(map[string]interface{})
|
||||
sum += b["reqs"].(float64)
|
||||
models := b["models"].(map[string]interface{})
|
||||
if models["mystery-model"].(float64) != 1 {
|
||||
t.Errorf("%s: model tally = %v, want 1", day, models["mystery-model"])
|
||||
}
|
||||
}
|
||||
if sum != 2 {
|
||||
t.Errorf("day buckets sum to %v, want 2 (the lifetime counter must not be double-counted)", sum)
|
||||
}
|
||||
// The lifetime counters still exist and still agree — this is additive, not
|
||||
// a replacement.
|
||||
if st["unpriced_reqs"].(float64) != 2 {
|
||||
t.Errorf("unpriced_reqs = %v, want 2", st["unpriced_reqs"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user