mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 20:27:23 +00:00
fix(billing): 过期规则 URL 不再让整个 profile 失效(降级应用 + 点名 URL)
生产现网发现(issue #1 排查):billing.rules 里一条指向 https://opencode.ai/inference/v1 的规则,在该源 base_url 变成 .../inference/openai/v1 之后匹配不到任何源;严格编译因此让**整个** profile 不生效 —— 账单恒 0、unpriced_reqs 涨到 3.7 万,唯一痕迹是一行启动日志。 - 严格编译失败时降级为 lenient:应用能匹配的规则,其余按 profile default; - 日志用 ERROR 点名**具体哪条 URL** 写错(新增 billing.UnmatchedRuleURLs), 并说明未匹配流量的记账去向; - 判据 TestBillingStaleRuleURLStillAppliesOtherRules:一条过期规则 + 一条有效 规则 ⇒ 有效规则必须仍然生效。
This commit is contained in:
@ -106,6 +106,27 @@ func CompileOpts(profile *config.BillingProfile, sources []config.Source, lenien
|
||||
return prices, nil
|
||||
}
|
||||
|
||||
// UnmatchedRuleURLs lists the non-wildcard rule URLs that match no configured
|
||||
// source. Compile() treats such a URL as a hard error (a typo'd rule prices
|
||||
// nothing), but the CALLER needs the list to say which rule to fix instead of
|
||||
// just "compile failed" — and to explain what the degraded table dropped.
|
||||
func UnmatchedRuleURLs(profile *config.BillingProfile, sources []config.Source) []string {
|
||||
if profile == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for i := range profile.Rules {
|
||||
rule := &profile.Rules[i]
|
||||
if rule.URL == "*" {
|
||||
continue
|
||||
}
|
||||
if len(matchSources(rule.URL, sources)) == 0 {
|
||||
out = append(out, rule.URL)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchSources returns the source names whose base_url matches pattern.
|
||||
// "*" matches every source (used as a catch-all default rule).
|
||||
func matchSources(pattern string, sources []config.Source) []string {
|
||||
|
||||
@ -289,3 +289,28 @@ func TestExplicitZeroModesKeepZeroTokenPrices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmatchedRuleURLs 是给调用方的"哪条规则写错了"清单:严格编译只会说
|
||||
// "compile failed",而运维需要知道具体 URL;降级应用时也要能说明丢了什么。
|
||||
func TestUnmatchedRuleURLs(t *testing.T) {
|
||||
sources := []config.Source{{Name: "mine", BaseURL: "https://mine.example.com/v1"}}
|
||||
profile := &config.BillingProfile{
|
||||
ID: "p",
|
||||
Rules: []config.BillingRule{
|
||||
{URL: "https://mine.example.com/v1", Mode: "free"},
|
||||
{URL: "https://stale.example.com/v1", Mode: "free"},
|
||||
{URL: "*", Mode: "token", Models: map[string]config.BillingToken{
|
||||
"m": {Prompt: "1", Completion: "1"},
|
||||
}},
|
||||
{URL: "https://mine.example.com/v1/", Mode: "free"}, // 尾斜杠归一后匹配
|
||||
},
|
||||
}
|
||||
got := UnmatchedRuleURLs(profile, sources)
|
||||
if len(got) != 1 || got[0] != "https://stale.example.com/v1" {
|
||||
t.Fatalf("unmatched = %v, want only the stale URL (wildcard and trailing-slash "+
|
||||
"variants must not be reported)", got)
|
||||
}
|
||||
if nil != UnmatchedRuleURLs(nil, sources) {
|
||||
t.Error("nil profile must yield nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user