mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 20:27:23 +00:00
refactor(billing): 计费语义下沉到 Lua(内核只解 YAML/传数据)+ 采纳 PR #2 语义
按 issue #1 的修复方向(维护者评论):内核不再持有任何计费语义,billing 逻辑 全部在 `plugins/billing.lua`;内核只负责解 config.yaml、校验 schema、把 profile + 源 base_url 作为**数据**交给插件,再把插件算出的价目通过 `prices` 侧车注入。计费账目仍持久化在插件状态文件(`plugin_dir/.billing.state.json`)。 ## 变更 - **删除** `internal/billing`(compile.go 359 行 + 其 Go 测试); - 新增 `core.compileBillingPrices`:把 profile/sources JSON 化后调用插件导出 `compile_prices`,返回价目表 + 未匹配规则清单(供错误日志点名 URL); - `applyBillingDSL` / 规则编辑器改为走同一个插件编译入口(编辑器 lenient, 半成品规则可保存、可报告); - `billing.lua` 新增 `compile_prices`:URL 归一匹配、首条命中优先、 free/subscription/unpriced/token、USD/M→per-token、peak 窗口、 long_context 边际价、reasoning 可选价、default 策略、未匹配清单。 错误通过载荷 `error` 字段回传(插件导出第二返回值通道不可靠)。 ## 与 PR #2(valuelesser)合并后的语义 - **token 规则不产生任何源条目**:未列价的模型落到 unpriced 统计(可见), 不再"静默计 $0"。比 issue 正文里的 `{per_request:0}` 标记更收口; - `default: free` 由**编译期**给未匹配源写 `{per_request:0}` 标记; `unpriced`/缺省不写。删除无人读取的 `default_mode` 死键; - 保留 `unmatched_rules` 清单(只用于降级应用时点名过期 URL)。 ## 判据(原 Go 单测平移到 Lua + PR #2 的用例) - `internal/lua/billing_compile_test.go`:通配符不标记源、token 规则无源条目、 任意源条目不得携带 token 价(根因直防)、free/subscription 保留有意零价、 reasoning 缺席、long_context 三种形态、peak 形状、URL 归一+首条优先、 lenient 未匹配清单、硬错误 5 例、default 两态; - `internal/lua/billing_test.go`:编译输出驱动真实 priceFor 的 A/B 用例 (旧形状 cost=0 / 新形状 0.75 且 unpriced=0)、default 编译期语义 e2e; - `internal/gateway`:token 规则断言"无源条目"、模型价 per-token 量级; - `internal/core`:过期规则 URL 降级应用(有效规则仍生效)。
This commit is contained in:
@ -224,27 +224,34 @@ func TestBillingRulesPersistAndReachThePlugin(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("prices has no sources map: %#v", prices)
|
||||
}
|
||||
cc, ok := sources["commandcode"]
|
||||
if !ok {
|
||||
t.Fatalf("the rule was saved but never injected: prices has %v, "+
|
||||
"no commandcode entry. Every request on it stays unpriced.", keysOf(sources))
|
||||
}
|
||||
if cc == nil {
|
||||
t.Error("commandcode price entry is nil")
|
||||
// A token rule must NOT produce a source entry at all: priceFor() applies
|
||||
// the source dimension after the model dimension and lets it override the
|
||||
// token rate, so a marker here is what silently zeroed the bill (issue #1 /
|
||||
// PR #2). Models absent from the rule surface as unpriced instead.
|
||||
if entry, ok := sources["commandcode"]; ok {
|
||||
t.Errorf("the token rule produced a source entry %#v — it overrides the "+
|
||||
"model price (issue #1)", entry)
|
||||
}
|
||||
// The free rule must price localzen, and the two must not be conflated.
|
||||
if _, ok := sources["localzen"]; !ok {
|
||||
t.Errorf("the free rule did not reach the plugin: %v", keysOf(sources))
|
||||
}
|
||||
// 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 {
|
||||
// Invariant: only DELIBERATE zero-token entries (mode=free/subscription) may
|
||||
// carry prompt/completion, and they must be exactly 0. In this fixture the
|
||||
// free rule covers localzen, so any other source with token prices — or a
|
||||
// non-zero one — would override model rates (issue #1).
|
||||
for name, raw := range sources {
|
||||
entry, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name == "localzen" { // the explicit free rule of this fixture
|
||||
continue
|
||||
}
|
||||
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)
|
||||
t.Errorf("source %q carries %s=%v: it overrides every model price "+
|
||||
"on that source (issue #1)", name, key, entry[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user