mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-05 07:02:29 +00:00
feat(plugin): 启用/禁用 + 磁盘列表 + 峰谷定价 + 随核心发布
## 插件管理后端
- PUT /api/plugins/{name} {"enabled":bool} 启用/禁用
- GET /api/plugins/{name} 读源码(编辑器用,与 /state 区分)
- GET /api/plugins 的 on_disk 字段 列出目录里所有 .lua 及其加载态
- validPluginName 提取为共享函数,install/remove/read 三处共用,防止检查漂移
禁用是**运行态开关,不删文件**:插件把线上网关搞坏了、但离修好只差一行时,
运维需要把它移出请求路径而不丢失它(同 systemd mask 而非 remove 的道理)。
它**不跨重启保留**——一个悄悄比操作者意图活得更久的"禁用"本身就是个意外。
Builtin 的判定是「加载的源码与内嵌版本逐字节相同」,而不是「名字匹配」:
被改过的 billing.lua 不能被标成 builtin,否则 UI 会提供覆盖用户改动的操作。
on_disk 列表包含**加载失败**的插件。否则一个语法错误的插件在 UI 上直接消失,
运维看到的现象是"插件不见了"而不是"插件报错了"。
## 峰谷 / 时段定价
commandcode 的 DeepSeek V4 系列就是高峰 01-04 & 06-10 UTC 工作日 2 倍价
(非高峰 17h/天)。静态价目表达不了,而算错方向是**静默**的。
价目条目可带 peak = {multiplier, windows=[{days, hours}]}。命中任一窗口即乘。
★ 用 `os.date("!%H")` 取 **UTC** 小时:provider 费率表按 UTC 标注,而网关跑在
本地时区(本机 Asia/Hong_Kong)。混用本地小时会让峰谷整体偏移 8 小时,
白天算成夜间——比不做峰谷还糟。
## ★ 实现与注释不一致,被判据抓住
applyPeak 最初直接 `price.prompt = price.prompt * m`,注释写「缓存读不翻倍」。
但 costFor 里**缓存读价是从 price.prompt 派生的**,所以原地翻倍会把缓存读
也翻倍——两个折扣被叠在一起,而 provider 从没打算叠。
改成 applyPeak 只**记录**乘数,由 costFor 分段应用:fresh prompt 与 completion
翻倍,cache read 那一项不动。
只靠注释说明意图是不够的:TestBillingPeakDoesNotDoubleCacheRead 立刻红了
(0.006 vs 期望 0.003)。变异回原实现仍是红的。
## 判据(21 个计费测试全绿,新增 5 个峰谷)
窗口恒命中 ×2 / 窗口永不命中保持静态价 / 星期不匹配不命中
(这条正是防"用本地时区整体偏移 8 小时")/ 无 peak 规则向后兼容
/ 缓存读不随峰谷翻倍
后端部分:构建/vet/gofmt 干净,8 个包全绿。
This commit is contained in:
@ -142,7 +142,7 @@ local function priceFor(payload)
|
||||
-- quietly under-reports. It is counted separately and surfaced in the UI.
|
||||
out = {
|
||||
prompt = d.prompt or 0, completion = d.completion or 0,
|
||||
per_request = 0, cache_discount = d.cache_discount,
|
||||
per_request = 0, cache_discount = d.cache_discount, peak = d.peak,
|
||||
}
|
||||
|
||||
-- model dimension (a token price overrides the default's token prices)
|
||||
@ -153,6 +153,7 @@ local function priceFor(payload)
|
||||
if mp.completion ~= nil then out.completion = mp.completion end
|
||||
if mp.per_request ~= nil then out.per_request = out.per_request + mp.per_request end
|
||||
if mp.cache_discount ~= nil then out.cache_discount = mp.cache_discount end
|
||||
if mp.peak ~= nil then out.peak = mp.peak end
|
||||
end
|
||||
|
||||
-- source dimension: usually a flat fee, but may also carry token prices
|
||||
@ -163,6 +164,7 @@ local function priceFor(payload)
|
||||
if sp.completion ~= nil then out.completion = sp.completion end
|
||||
if sp.per_request ~= nil then out.per_request = out.per_request + sp.per_request end
|
||||
if sp.cache_discount ~= nil then out.cache_discount = sp.cache_discount end
|
||||
if sp.peak ~= nil then out.peak = sp.peak end
|
||||
end
|
||||
|
||||
-- key dimension wins over the others (an operator pricing one customer
|
||||
@ -174,10 +176,83 @@ local function priceFor(payload)
|
||||
if kp.completion ~= nil then out.completion = kp.completion end
|
||||
if kp.per_request ~= nil then out.per_request = out.per_request + kp.per_request end
|
||||
if kp.cache_discount ~= nil then out.cache_discount = kp.cache_discount end
|
||||
if kp.peak ~= nil then out.peak = kp.peak end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- ===== 峰谷 / 时段定价 ================================================
|
||||
--
|
||||
-- 有些 provider 按 UTC 时段分价(commandcode 的 DeepSeek V4 系列就是:高峰
|
||||
-- 01-04 & 06-10 UTC 工作日,价格恰好是非高峰的 2 倍)。静态价目无法表达这一点,
|
||||
-- 而算错方向通常是【静默高估或低估】,不会报错——所以这里显式支持。
|
||||
--
|
||||
-- 配置形态(挂在任一维度的价目条目上):
|
||||
--
|
||||
-- "deepseek-v4.1-flash": {
|
||||
-- prompt = 1.5e-7, completion = 6e-7,
|
||||
-- peak = {
|
||||
-- multiplier = 2, -- 高峰时单价乘以它
|
||||
-- windows = [ -- UTC 星期几 = os.date 的 %w(周日=1)
|
||||
-- { days = {2,3,4,5,6}, hours = {{1,2,3},{6,7,8,9}} },
|
||||
-- ],
|
||||
-- },
|
||||
-- }
|
||||
--
|
||||
-- 语义:命中任一 window ⇒ 乘以 multiplier。hours 用 {起,止} 闭区间,跨零点
|
||||
-- 用 {{22,24}} 表示 22:00-24:00(24 是"当天最后一刻")。
|
||||
--
|
||||
-- ★ 为什么用 os.date 的 ! 前缀取 UTC:provider 的费率表按 UTC 标注,而网关
|
||||
-- 跑在本地时区(这台机是 Asia/Hong_Kong)。混用本地小时会让峰谷整体偏移 8
|
||||
-- 小时,白天算成夜间——比不做峰谷还糟。
|
||||
local function inPeakWindow(ev)
|
||||
if ev == nil then return false end
|
||||
local w = ev.windows
|
||||
if type(w) ~= "table" or #w == 0 then return false end
|
||||
local dow = tonumber(os.date("!%w")) or 0 -- 0=Sunday
|
||||
local hour = tonumber(os.date("!%H")) or 0
|
||||
for _, win in ipairs(w) do
|
||||
local days = win.days
|
||||
if type(days) == "table" then
|
||||
local day_ok = false
|
||||
for _, d in ipairs(days) do
|
||||
if tonumber(d) == dow then day_ok = true break end
|
||||
end
|
||||
if not day_ok then goto continue_win end
|
||||
end
|
||||
local hours = win.hours
|
||||
if type(hours) == "table" then
|
||||
for _, h in ipairs(hours) do
|
||||
local lo, hi = tonumber(h[1]), tonumber(h[2])
|
||||
if lo and hi and hour >= lo and hour <= hi then return true end
|
||||
end
|
||||
end
|
||||
::continue_win::
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- applyPeak multiplies a price by the peak rule, if the request lands in a peak
|
||||
-- window. It is a no-op when no rule is configured, so the common case costs one
|
||||
-- nil check.
|
||||
--
|
||||
-- The multiplier is RECORDED, not applied to price.prompt in place. That looks
|
||||
-- like a roundabout way to do it, but applying it there was a real bug: the
|
||||
-- cache-read rate is DERIVED from price.prompt inside costFor, so doubling
|
||||
-- price.prompt silently doubled the cache read too — compounding two separate
|
||||
-- discounts. Keeping the multiplier separate lets costFor scale the fresh-prompt
|
||||
-- and completion legs and leave the cache leg alone, which is what "peak rates
|
||||
-- apply to the token price, cache reads are billed at their own rate" means.
|
||||
local function applyPeak(price)
|
||||
local pk = price.peak
|
||||
if pk == nil then return price end
|
||||
if not inPeakWindow(pk) then return price end
|
||||
local m = tonumber(pk.multiplier) or 1
|
||||
if m <= 0 then return price end
|
||||
price.peak_multiplier = m
|
||||
return price
|
||||
end
|
||||
|
||||
-- costFor computes one request's price.
|
||||
--
|
||||
-- PROMPT CACHE: a cached prompt token is not billed like a fresh one. Almost
|
||||
@ -200,7 +275,7 @@ end
|
||||
-- fresh count never goes negative, which would silently turn a request into
|
||||
-- billable negative tokens.
|
||||
local function costFor(payload, price)
|
||||
price = price or priceFor(payload)
|
||||
price = applyPeak(price or priceFor(payload))
|
||||
local prompt = tonumber(payload.prompt_tokens) or 0
|
||||
local completion = tonumber(payload.completion_tokens) or 0
|
||||
local cacheHit = tonumber(payload.cache_hit_tokens) or 0
|
||||
@ -212,10 +287,15 @@ local function costFor(payload, price)
|
||||
if discount == nil then discount = 0.1 end
|
||||
if discount < 0 then discount = 0 elseif discount > 1 then discount = 1 end
|
||||
|
||||
-- The peak multiplier applies to the freshly-read prompt tokens and the
|
||||
-- completion, but NOT to the cache read: a cache read is a separate upstream
|
||||
-- rate that the off-peak figures already discount, and doubling it would
|
||||
-- stack two discounts the provider never intended to stack.
|
||||
local mult = tonumber(price.peak_multiplier) or 1
|
||||
local fresh = prompt - cacheHit
|
||||
local cost = fresh * price.prompt
|
||||
local cost = fresh * price.prompt * mult
|
||||
+ cacheHit * price.prompt * discount
|
||||
+ completion * price.completion
|
||||
+ completion * price.completion * mult
|
||||
|
||||
local flat = price.per_request
|
||||
if not payload.ok and not plugin.count_failures then
|
||||
|
||||
Reference in New Issue
Block a user