mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47: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:
@ -505,13 +505,22 @@ end
|
||||
|
||||
### 7.1 计费维度
|
||||
|
||||
| 维度 | 用途 | 优先级 |
|
||||
| 维度 | 用途 | token 价覆盖顺序 |
|
||||
|---|---|---|
|
||||
| `sources` | 源的固定价(如按请求计费的转发服务) | 中 |
|
||||
| `models` | 模型的 per-token 价 | 高 |
|
||||
| `default` | 兜底价(通常全 0) | 最低 |
|
||||
| `models` | 模型的 per-token 价 | 中 |
|
||||
| `sources` | 源级价:固定价,或覆盖该源全部模型的 token 价 | 高 |
|
||||
| `keys` | 单个网关 key 的覆盖价 | **最高** |
|
||||
|
||||
**token 价**优先级:`keys` > `models` > `default`。
|
||||
**token 价**优先级:`keys` > `sources` > `models` > `default`。
|
||||
(实现顺序就是"后写覆盖前写":`default` → `models` → `sources` → `keys`;
|
||||
所以**源级 token 价会覆盖模型价**。这是有意的:运营要能给某个源整体改价。)
|
||||
|
||||
⚠️ 正因为 `sources` 会覆盖 `models`,**"标记某源已定价"的条目只能用
|
||||
`{ "per_request": 0 }`**,不能写 `{ "prompt": 0, "completion": 0 }` ——
|
||||
后者会把该源所有模型的 token 价清成 0。编译器为 `mode: token` 的具体 URL
|
||||
规则生成的就是前者(GitCode issue #1)。
|
||||
|
||||
**`per_request` 固定价是叠加的**(不覆盖),所以一个生图模型可以既算 token 又收固定费。
|
||||
**`cache_discount`** 见 §7.7。
|
||||
|
||||
@ -599,6 +608,16 @@ curl -H "Authorization: Bearer $ADMIN_KEY" \
|
||||
|
||||
仪表盘上有 "Unpriced" 卡片。**这个数应该是 0**;不是 0 就去补价目。
|
||||
|
||||
**profile 级兜底**:`profiles[].default` 可设为 `free` 或 `unpriced`(默认):
|
||||
|
||||
| `default` | 未匹配任何规则的源 | `unpriced_reqs` |
|
||||
|---|---|---|
|
||||
| `unpriced`(省略时) | 记 0 价,计入未定价债务 | +1 |
|
||||
| `free` | 记 0 价,但视为**刻意免费** | 不增加 |
|
||||
|
||||
另有 `prices.unmatched_sources`:列出没有规则命中的源,网关在应用 profile 时
|
||||
会以 `[billing] WARNING` 打到日志里 —— 看到它就去补规则(或确认 `default` 选对了)。
|
||||
|
||||
注意"未定价"不等于"免费":这些请求的 `requests` / token 数**照常计入**
|
||||
`total` 与各维度,只有金额是 0。
|
||||
|
||||
|
||||
@ -245,8 +245,23 @@ func applyRule(prices map[string]interface{}, rule *config.BillingRule, srcName
|
||||
// model priced on its own, so the wildcard loses nothing.
|
||||
if rule.URL != "*" {
|
||||
if _, ok := prices["sources"].(map[string]interface{})[srcName]; !ok {
|
||||
// ONLY the marker — deliberately no "prompt"/"completion".
|
||||
//
|
||||
// priceFor() applies the source dimension AFTER the model
|
||||
// dimension, and treats a present source key as an override
|
||||
// (`sp.prompt ~= nil` => out.prompt = sp.prompt). Writing
|
||||
// zeros here therefore rewrote the model price of every model
|
||||
// this rule DOES list to 0 — while out.priced stayed true, so
|
||||
// unpriced_reqs never grew either. Cost 0, no warning: GitCode
|
||||
// issue #1.
|
||||
//
|
||||
// `per_request: 0` is the marker itself: it adds nothing to a
|
||||
// request's cost, but its presence makes the plugin see the
|
||||
// source as priced, which is the only thing this entry is for.
|
||||
// (free/subscription above keep their zero token prices: there
|
||||
// the zero IS the statement.)
|
||||
prices["sources"].(map[string]interface{})[srcName] = map[string]interface{}{
|
||||
"prompt": 0.0, "completion": 0.0, "per_request": 0.0,
|
||||
"per_request": 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,3 +212,80 @@ func TestLongContextStepCompiles(t *testing.T) {
|
||||
t.Error("above=0 must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// A specific-URL token rule marks its source priced, but the entry must carry
|
||||
// ONLY the "this source is priced" marker — not zero token prices.
|
||||
//
|
||||
// Why: `billing.lua`'s priceFor() applies the source dimension AFTER the model
|
||||
// dimension and treats `sp.prompt ~= nil` as an override. A {prompt:0,
|
||||
// completion:0} entry therefore zeroes the model price of every model the rule
|
||||
// DOES list. The symptom is silent: cost is 0 while out.priced is true, so
|
||||
// unpriced_reqs does not grow either (GitCode issue #1).
|
||||
//
|
||||
// The original test only asserted the KEY existed, which froze the broken shape
|
||||
// into a contract; this asserts the entry CONTENT.
|
||||
func TestSpecificURLTokenRuleSourceEntryHasNoZeroTokenPrices(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: "token",
|
||||
Models: map[string]config.BillingToken{
|
||||
"m": {Prompt: "0.15", Completion: "0.60"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
prices, err := Compile(profile, sources)
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
entry, ok := prices["sources"].(map[string]interface{})["mine"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("a specific-url token rule must still mark its own source priced")
|
||||
}
|
||||
for _, key := range []string{"prompt", "completion"} {
|
||||
if _, present := entry[key]; present {
|
||||
t.Errorf("source entry carries %s=%v: priceFor() treats it as an override "+
|
||||
"and zeroes the model price (issue #1)", key, entry[key])
|
||||
}
|
||||
}
|
||||
// per_request: 0 is the marker itself: adding a flat fee must not change
|
||||
// the total, and the key's presence is what tells the plugin "priced".
|
||||
if v, present := entry["per_request"]; !present || v != 0.0 {
|
||||
t.Errorf("source entry must carry per_request:0 as the priced marker, got %#v", entry)
|
||||
}
|
||||
// The model price must still be the real per-token value.
|
||||
m, ok := prices["models"].(map[string]interface{})["m"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("model price missing")
|
||||
}
|
||||
if m["prompt"] != 0.15e-6 {
|
||||
t.Errorf("model prompt price = %v, want 1.5e-7 per token", m["prompt"])
|
||||
}
|
||||
}
|
||||
|
||||
// free / subscription are DELIBERATE zeros: their source entries keep the
|
||||
// zero token prices, because there the zero IS the statement.
|
||||
func TestExplicitZeroModesKeepZeroTokenPrices(t *testing.T) {
|
||||
for _, mode := range []string{"free", "subscription"} {
|
||||
profile := &config.BillingProfile{ID: "p", Rules: []config.BillingRule{{
|
||||
URL: "https://mine.example.com/v1", Mode: mode, Subscription: 10,
|
||||
}}}
|
||||
prices, err := Compile(profile, []config.Source{
|
||||
{Name: "mine", BaseURL: "https://mine.example.com/v1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s compile: %v", mode, err)
|
||||
}
|
||||
entry, ok := prices["sources"].(map[string]interface{})["mine"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("%s: source entry missing", mode)
|
||||
}
|
||||
for _, key := range []string{"prompt", "completion"} {
|
||||
if v, present := entry[key]; !present || v != 0.0 {
|
||||
t.Errorf("%s: entry[%s] = %v (present=%v), want an explicit 0.0",
|
||||
mode, key, v, present)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1065,6 +1065,23 @@ func (c *Core) applyBillingDSL() {
|
||||
return
|
||||
}
|
||||
log.Printf("[billing] profile %q applied (%d rules, %d sources)", profile.ID, len(profile.Rules), len(c.cfg.Sources))
|
||||
// unmatched_sources used to be written by the compiler and read by nobody,
|
||||
// so a profile that silently left half the gateway unpriced looked applied
|
||||
// and healthy. Logging it is the cheap reader: an operator who sees source
|
||||
// names here knows exactly which rules to add (GitCode issue #1 follow-up).
|
||||
if unmatched, ok := prices["unmatched_sources"].([]string); ok && len(unmatched) > 0 {
|
||||
log.Printf("[billing] WARNING %d source(s) match no rule and fall through to the "+
|
||||
"profile default: %v (add rules, or set `default: free` if that is intended)",
|
||||
len(unmatched), unmatched)
|
||||
} else if raw, ok := prices["unmatched_sources"].([]interface{}); ok && len(raw) > 0 {
|
||||
names := make([]string, 0, len(raw))
|
||||
for _, v := range raw {
|
||||
names = append(names, fmt.Sprint(v))
|
||||
}
|
||||
log.Printf("[billing] WARNING %d source(s) match no rule and fall through to the "+
|
||||
"profile default: %v (add rules, or set `default: free` if that is intended)",
|
||||
len(names), names)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateScopeQuotas checks every scope entry's caps before they are stored.
|
||||
|
||||
@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@ -1236,3 +1236,122 @@ func TestLongContextComposesWithCacheDiscount(t *testing.T) {
|
||||
// total = 0.2325
|
||||
approx(t, "step composes with cache discount", got, 0.2325)
|
||||
}
|
||||
|
||||
// ---- 源层零价覆盖模型价(issue #1)---------------------------------------
|
||||
//
|
||||
// 真实缺陷(GitCode issue #1):`compile.go` 给每条**具体 URL** 的 token 规则
|
||||
// 写一条 `{prompt:0, completion:0, per_request:0}` 的源条目,本意只是"标记该源
|
||||
// 已定价";而插件 priceFor() 的源层是**无条件覆盖**模型层:`sp.prompt ~= nil`
|
||||
// 就把模型价改成 0。两条各自正当的规则叠加 ⇒ 每个请求 cost:0,且因为
|
||||
// out.priced=true,连 unpriced_reqs 都不涨 —— 完全静默。
|
||||
//
|
||||
// 这里把两种条目形状都钉住:旧形状必须**真的是错的**(A 行),新形状必须
|
||||
// **保住模型价同时仍算"已定价"**(B 行)。只断言"键存在"是不够的 —— 那正是
|
||||
// 现有测试把错误形状固化成契约的原因。
|
||||
func TestBillingSourceZeroPriceMustNotZeroModelPrice(t *testing.T) {
|
||||
const promptPrice = 1.5e-7 // 0.15 USD/M
|
||||
const completionPrice = 6e-7 // 0.60 USD/M
|
||||
const want = 0.75 // 1e6 * 1.5e-7 + 1e6 * 6e-7
|
||||
|
||||
newPrices := func(sourceEntry map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"default": map[string]interface{}{"prompt": 0.0, "completion": 0.0, "per_request": 0.0},
|
||||
"models": map[string]interface{}{
|
||||
"m": map[string]interface{}{"prompt": promptPrice, "completion": completionPrice},
|
||||
},
|
||||
"sources": map[string]interface{}{"src": sourceEntry},
|
||||
}
|
||||
}
|
||||
run := func(t *testing.T, sourceEntry map[string]interface{}) (cost, unpriced float64) {
|
||||
t.Helper()
|
||||
ps, _ := billingVM(t)
|
||||
_ = ps.SetState("billing", map[string]interface{}{"prices": newPrices(sourceEntry)})
|
||||
ps.Fire(StageRequestEnd, map[string]interface{}{
|
||||
"model": "m", "source": "src", "key": "***p", "ok": true,
|
||||
"prompt_tokens": 1000000.0, "completion_tokens": 1000000.0,
|
||||
"time": 1750000000000,
|
||||
})
|
||||
st := stateOf(t, ps)
|
||||
total := st["total"].(map[string]interface{})
|
||||
return total["cost"].(float64), st["unpriced_reqs"].(float64)
|
||||
}
|
||||
|
||||
// A 行:compile.go 修复前的形状 —— 这是缺陷本身,必须算成 0。
|
||||
cost, unpriced := run(t, map[string]interface{}{
|
||||
"prompt": 0.0, "completion": 0.0, "per_request": 0.0,
|
||||
})
|
||||
approx(t, "旧形状(全零源条目) cost", cost, 0)
|
||||
if unpriced != 0 {
|
||||
t.Errorf("旧形状下 unpriced_reqs = %v,缺陷正是它也为 0(静默)", unpriced)
|
||||
}
|
||||
|
||||
// B 行:修复后的形状 —— 只带 per_request,不得覆盖模型价。
|
||||
cost, unpriced = run(t, map[string]interface{}{"per_request": 0.0})
|
||||
approx(t, "新形状(仅 per_request) cost", cost, want)
|
||||
if unpriced != 0 {
|
||||
t.Errorf("新形状下 unpriced_reqs = %v,want 0(该源仍算已定价)", unpriced)
|
||||
}
|
||||
}
|
||||
|
||||
// 同一源上一个**未列入价目表**的模型:源条目(仅 per_request)让它保持
|
||||
// "已定价",所以不会被计入 unpriced_reqs —— 这正是 compile 写源条目的原意。
|
||||
func TestBillingSourceEntryStillMarksUnlistedModelPriced(t *testing.T) {
|
||||
ps, _ := billingVM(t)
|
||||
_ = ps.SetState("billing", map[string]interface{}{
|
||||
"prices": map[string]interface{}{
|
||||
"default": map[string]interface{}{"prompt": 0.0, "completion": 0.0, "per_request": 0.0},
|
||||
"models": map[string]interface{}{
|
||||
"priced-model": map[string]interface{}{"prompt": 1.5e-7, "completion": 6e-7},
|
||||
},
|
||||
"sources": map[string]interface{}{
|
||||
"src": map[string]interface{}{"per_request": 0.0},
|
||||
},
|
||||
},
|
||||
})
|
||||
ps.Fire(StageRequestEnd, map[string]interface{}{
|
||||
"model": "unlisted-model", "source": "src", "key": "***p", "ok": true,
|
||||
"prompt_tokens": 1000.0, "completion_tokens": 0.0, "time": 1750000000000,
|
||||
})
|
||||
st := stateOf(t, ps)
|
||||
if got := st["unpriced_reqs"].(float64); got != 0 {
|
||||
t.Errorf("unpriced_reqs = %v,want 0:源条目存在即为已定价源", got)
|
||||
}
|
||||
}
|
||||
|
||||
// profile 级 default(compile.go 写入的 default_mode)必须真的生效。
|
||||
//
|
||||
// 真实缺陷(GitCode issue #1 顺带发现):compile.go:54 写 `default_mode`、
|
||||
// :104 写 `unmatched_sources`,但全仓库没有任何读取方 —— 于是 profile 的
|
||||
// `default: free|unpriced` 在运行时完全等价,区别只存在于 YAML 里。
|
||||
// "free" 是**刻意定价为 0**,不该进未定价统计;"unpriced" 才该。
|
||||
func TestBillingProfileDefaultMode(t *testing.T) {
|
||||
cases := []struct {
|
||||
mode string
|
||||
wantUnpriced float64
|
||||
}{
|
||||
{"free", 0},
|
||||
{"unpriced", 1},
|
||||
{"", 1}, // 未声明 = 现有行为(未定价债务)
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ps, _ := billingVM(t)
|
||||
prices := map[string]interface{}{
|
||||
"default": map[string]interface{}{"prompt": 0.0, "completion": 0.0, "per_request": 0.0},
|
||||
}
|
||||
if tc.mode != "" {
|
||||
prices["default_mode"] = tc.mode
|
||||
}
|
||||
_ = ps.SetState("billing", map[string]interface{}{"prices": prices})
|
||||
ps.Fire(StageRequestEnd, map[string]interface{}{
|
||||
"model": "no-rule-model", "source": "no-rule-source", "key": "***p", "ok": true,
|
||||
"prompt_tokens": 1000.0, "completion_tokens": 10.0, "time": 1750000000000,
|
||||
})
|
||||
st := stateOf(t, ps)
|
||||
if got := st["unpriced_reqs"].(float64); got != tc.wantUnpriced {
|
||||
t.Errorf("default_mode=%q: unpriced_reqs = %v, want %v", tc.mode, got, tc.wantUnpriced)
|
||||
}
|
||||
// 无论哪种模式,成本都是 0;区别只在"是否算未定价债务"。
|
||||
approx(t, "default_mode="+tc.mode+" cost",
|
||||
st["total"].(map[string]interface{})["cost"].(float64), 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +189,11 @@ local function priceFor(payload)
|
||||
-- look the same: an unpriced model silently costing 0 is the most dangerous
|
||||
-- failure mode a cost plugin has, because the bill still adds up and just
|
||||
-- quietly under-reports. It is counted separately and surfaced in the UI.
|
||||
out = {
|
||||
-- ★ local 是必须的:此前写的是全局 `out = {`,而 priceFor 每个请求都会调用,
|
||||
-- 于是这个表挂在 VM 级全局变量上跨请求残留/泄漏(GitCode issue #1 顺带发现)。
|
||||
-- 功能上因为每次整体重建而侥幸正确,但它是共享可变状态,任何提前 return 或
|
||||
-- 后续分支忘了重建都会让上一单的价格漏到下一单 —— 计费错误不会报错。
|
||||
local out = {
|
||||
prompt = d.prompt or 0, completion = d.completion or 0,
|
||||
per_request = 0, cache_discount = d.cache_discount, peak = d.peak,
|
||||
-- nil = bill reasoning at the completion rate, which is what every
|
||||
@ -236,6 +240,18 @@ local function priceFor(payload)
|
||||
if kp.cache_discount ~= nil then out.cache_discount = kp.cache_discount end
|
||||
if kp.peak ~= nil then out.peak = kp.peak end
|
||||
end
|
||||
|
||||
-- profile-level default for sources no rule matched.
|
||||
--
|
||||
-- compile.go has always written `default_mode` ("free" | "unpriced") into the
|
||||
-- prices table, but NOTHING read it — so `default: free` and
|
||||
-- `default: unpriced` behaved identically at runtime (both fell through to
|
||||
-- prices.default's zeros and counted as unpriced debt). That is a second
|
||||
-- silent under-report (GitCode issue #1 follow-up). "free" is a DELIBERATE
|
||||
-- zero, so it must be marked priced; anything else stays countable debt.
|
||||
if not out.priced and p.default_mode == "free" then
|
||||
out.priced = true
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user