mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17:23 +00:00
fix(billing): 源层零价不再覆盖模型价(issue #1)+ default_mode 生效 + 文档/判据补齐
GitCode issue #1:任何按 `mode: token` 且 URL 非通配符的规则定价的源,计费恒为 0,
且 `unpriced_reqs` 也不增长 —— 完全静默。
## 真因
- `compile.go` 给这类规则写源条目 `{prompt:0, completion:0, per_request:0}`,
本意只是"标记该源已定价"(同源未列入价目表的模型不算 unpriced);
- `billing.lua` 的 priceFor() 源层在模型层之后、且 `sp.prompt ~= nil` 即覆盖;
- 两者叠加 ⇒ 模型价被清成 0,而 `out.priced` 仍为 true ⇒ 连未定价计数都不涨。
## 修法
1. `compile.go`:token 规则的源条目只写 `{per_request: 0}`(只是标记,不参与 token
计价)。`free` / `subscription` 保留全零(那里 0 本身就是语义)。
2. `billing.lua`:
- `priceFor()` 的 `out` 补 `local`(此前是 VM 级全局变量,跨请求残留);
- 实现 `profiles[].default` 的 `free|unpriced` 语义:`free` = 刻意免费(不进
unpriced),`unpriced`(默认)= 未定价债务。此前 compile.go 写 `default_mode`
但**无人读**,两种取值运行时完全等价。
3. `core.go`:应用 profile 时把 `unmatched_sources` 打进日志(WARNING 列出没有
规则命中的源)。该字段此前只写不读,是第二个静默少报点。
4. `docs/plugins.md`:token 价优先级改为与实际实现一致的
`keys > sources > models > default`,并写明"标记已定价只能用 per_request:0"。
## 判据(都先红后绿)
- `internal/billing`:token 规则的源条目**不含** prompt/completion;free/subscription
仍保留显式 0。
- `internal/lua`:同源同模型下旧形状 cost=0(缺陷本身)、新形状 cost=0.75 且
unpriced_reqs=0;未列入价目表的模型仍算"已定价";default_mode 三态。
- `internal/gateway`:`billing_rules_api_test` 的单位断言原本读
`cc["models"]`(恒 false,整块被跳过——从未执行过),改为 `prices["models"]`,
并新增"源条目不得带 token 价"断言。
## 顺带修掉一个"永不执行"的判据
`discover_mutate_test.go` 的变异锚点写成 6 个空格,而 `discover.go` 已是 2 个空格
⇒ 该变异每次都 `anchor not found`,变异 campaign 长期红着。修正锚点后,
"嵌套 metadata 不再读取"这条变异真的被执行并被测试 KILL。
This commit is contained in:
@ -236,19 +236,39 @@ func TestBillingRulesPersistAndReachThePlugin(t *testing.T) {
|
||||
if _, ok := sources["localzen"]; !ok {
|
||||
t.Errorf("the free rule did not reach the plugin: %v", keysOf(sources))
|
||||
}
|
||||
// The injected price must be per-token, i.e. 0.15 USD/M => 1.5e-7.
|
||||
// The source entry exists ONLY as the "this source is priced" marker: it
|
||||
// must not carry token prices, because priceFor() applies the source
|
||||
// dimension after the model dimension and would overwrite the model price
|
||||
// with 0 (GitCode issue #1 — silent, unpriced_reqs does not grow either).
|
||||
if entry, ok := cc.(map[string]interface{}); ok {
|
||||
for _, key := range []string{"prompt", "completion"} {
|
||||
if _, present := entry[key]; present {
|
||||
t.Errorf("the injected source entry carries %s: it will zero the "+
|
||||
"model price (issue #1): %#v", key, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
// The injected MODEL price must be per-token, i.e. 0.15 USD/M => 1.5e-7.
|
||||
// Asserting the exact magnitude catches a unit error (per-million vs
|
||||
// per-token), which would be off by a factor of a million and still look
|
||||
// like "a number".
|
||||
models, ok := cc.(map[string]interface{})["models"].(map[string]interface{})
|
||||
if ok {
|
||||
if m, ok := models["deepseek-v4.1-flash"].(map[string]interface{}); ok {
|
||||
p, _ := m["prompt"].(float64)
|
||||
if p <= 0 || p > 1e-6 {
|
||||
t.Errorf("injected prompt price = %v, want per-token (~1.5e-7); "+
|
||||
"a per-million value here would over-bill by 1e6x", p)
|
||||
}
|
||||
}
|
||||
//
|
||||
// ⚠️ This used to read `cc["models"]`, but `models` lives at
|
||||
// prices["models"], not inside a source entry — so the type assertion was
|
||||
// always false and the whole block was skipped. It had never run, which is
|
||||
// how a per-million unit error could survive a test whose comment claims
|
||||
// to catch exactly that.
|
||||
models, ok := prices["models"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("prices has no models map: %#v", prices)
|
||||
}
|
||||
m, ok := models["deepseek-v4.1-flash"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("the model price was not injected: %v", keysOf(models))
|
||||
}
|
||||
if p, _ := m["prompt"].(float64); p <= 0 || p > 1e-6 {
|
||||
t.Errorf("injected prompt price = %v, want per-token (~1.5e-7); "+
|
||||
"a per-million value here would over-bill by 1e6x", p)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -126,7 +126,12 @@ func TestRealMutants(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "nested metadata no longer read",
|
||||
from: "\tcopyFields(m.Metadata) // nested first",
|
||||
// 锚点必须与 discover.go 逐字节一致:此前这里是 6 个空格,源码已
|
||||
// 变成 2 个空格,于是这条变异永远"找不到锚点"——变异campaign 每次
|
||||
// 都以 anchor not found 失败,而它本该验证"嵌套 metadata 被丢掉"能被
|
||||
// 测试抓住。锚点漂移 = 这条变异从未真正跑过(和 issue #1 里
|
||||
// `cc["models"]` 恒 false 的测试跳过是同一种失效:断言写了但没执行)。
|
||||
from: "\tcopyFields(m.Metadata) // nested first",
|
||||
to: "\tcopyFields(json.RawMessage(nil)) // mutant: nested metadata dropped",
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user