mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(plugins): ccquota 显示每月额度
The page showed a monthly BALANCE ($0.08 left) but no monthly ALLOWANCE, so
"how much do I get and how much of it is gone" had no answer — the most useful
number on a plan that is nearly exhausted.
The vendor's credits endpoint reports what is left and never the cap, so a
percentage is not derivable from it. Two things supply the missing half:
- planId from subscriptions (here individual-goat), mapped to the caps the
vendor publishes ($10 Go, $70 GOAT/Pro/Max).
- totalMonthlyCredits from usage/summary, which counts over the BILLING PERIOD
and so pairs with the monthly allowance rather than with the rolling 5h and
weekly windows.
Because the cap is a table lookup and not an API fact, it is cross-checked
against monthly_used + balance: if the two disagree by more than 2% + $0.50,
the card says the figure may be off rather than confidently printing a wrong
percentage. A planId absent from the table renders the balance with no bar at
all — inventing a denominator would be worse than admitting there isn't one.
Also surfaces what the endpoints were already returning and the page was
dropping: period_end (with a countdown), cancel_at_period_end, plan name, and
the vendor's own belowThreshold flag, which is distinct from "balance > 0" —
an account can hold $3 and still be under their threshold.
Verified on production: both accounts show 每月 100% ($69.94/$70, red bar),
5 小时 0/14, 每周 0.803/35 on one; plan goat, 周期至 2026-10-17 / 2026-10-19.
Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
This commit is contained in:
@ -46,6 +46,26 @@ local REQUIRED_HEADERS = {
|
||||
-- key store). The kernel deliberately does NOT let a plugin read the config, so
|
||||
-- this is the only channel — which also means the key never sits in Lua state
|
||||
-- or in the persisted plugin file.
|
||||
-- Monthly credit caps per planId.
|
||||
--
|
||||
-- The credits endpoint reports what is LEFT but never the cap, so a percentage
|
||||
-- is not derivable from it alone. The caps below come from the vendor's own
|
||||
-- pricing page: every plan costs $10/month and grants a different credit pool
|
||||
-- ($10 Go, $70 GOAT, $70 Pro, $70 Max). totalMonthlyCredits + monthlyCredits
|
||||
-- also sums to the cap, so this table is a CROSS-CHECK on the plan, not the only
|
||||
-- source — if the two disagree, PLAN_CAPS_WAIVED shows it instead of silently
|
||||
-- displaying a wrong percentage.
|
||||
--
|
||||
-- A planId absent from this table renders the balance with no percentage and no
|
||||
-- bar. Guessing a cap for an unknown plan would be worse than showing none.
|
||||
local PLAN_CAPS = {
|
||||
["individual-go"] = 10,
|
||||
["individual-goat"] = 70,
|
||||
["individual-pro"] = 70,
|
||||
["individual-max"] = 70,
|
||||
["team-goat"] = 70,
|
||||
}
|
||||
|
||||
local ACCOUNTS = {}
|
||||
|
||||
-- ---------------------------------------------------------------- helpers
|
||||
@ -125,6 +145,11 @@ local function collectAccount(acct)
|
||||
report.purchased = tonum(c.purchasedCredits)
|
||||
report.free = tonum(c.freeCredits)
|
||||
report.total = report.monthly + report.purchased + report.free
|
||||
-- belowThreshold is the vendor's own "running low" flag, distinct from
|
||||
-- whether any balance remains: an account can hold $3 and still be below
|
||||
-- their configured threshold.
|
||||
report.below_threshold = c.belowThreshold == true
|
||||
report.threshold = tonum(c.creditThreshold)
|
||||
report.limited = wl.limited == true
|
||||
report.exceeded = wl.exceeded
|
||||
report.fiveHour = wl.fiveHour
|
||||
@ -137,7 +162,11 @@ local function collectAccount(acct)
|
||||
report.failures[#report.failures + 1] = "subscriptions: " .. tostring(serr)
|
||||
elseif type(subs) == "table" and type(subs.data) == "table" then
|
||||
report.plan_status = subs.data.status
|
||||
report.plan_price = subs.data.metadata and nil
|
||||
report.plan_id = subs.data.planId
|
||||
report.plan_cap = PLAN_CAPS[tostring(subs.data.planId or "")]
|
||||
report.period_start = subs.data.currentPeriodStart
|
||||
report.period_end = subs.data.currentPeriodEnd
|
||||
report.cancel_at_period_end = subs.data.cancelAtPeriodEnd == true
|
||||
local md = subs.data.metadata
|
||||
if type(md) == "table" then
|
||||
report.plan_name = md.plan or md.name or md.tier
|
||||
@ -152,6 +181,23 @@ local function collectAccount(acct)
|
||||
report.spent = tonum(usage.totalCost)
|
||||
report.tokens = tonum(usage.totalTokens)
|
||||
report.success_rate = tonum(usage.successRate)
|
||||
-- usage/summary counts over the BILLING PERIOD, so this pairs with the
|
||||
-- monthly allowance rather than with the rolling 5h/weekly windows.
|
||||
report.monthly_used = tonum(usage.totalMonthlyCredits)
|
||||
report.period_basis = usage.periodBasis
|
||||
end
|
||||
|
||||
-- Cross-check the configured cap against what the vendor spent plus what is
|
||||
-- left. A mismatch means PLAN_CAPS drifted from the real plan (a price change,
|
||||
-- or a planId this table does not know) — surfaced rather than papered over,
|
||||
-- because a confidently wrong "99% used" is the worst possible reading here.
|
||||
if report.plan_cap and report.monthly_used and report.monthly_used > 0 then
|
||||
local implied = report.monthly_used + report.total
|
||||
local drift = math.abs(implied - report.plan_cap)
|
||||
if drift > (report.plan_cap * 0.02 + 0.5) then
|
||||
report.cap_mismatch = true
|
||||
report.implied_cap = implied
|
||||
end
|
||||
end
|
||||
|
||||
-- An account whose every endpoint failed is an auth problem, not a data
|
||||
@ -213,6 +259,8 @@ PLUGIN.ui = {
|
||||
.ccq-meta{display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px;opacity:.7;margin-top:10px}
|
||||
.ccq-fail{font-size:11.5px;color:#b91c1c;margin-top:8px;word-break:break-word}
|
||||
.ccq-empty{opacity:.6;font-size:13px;padding:8px 0}
|
||||
.ccq-win-nodat{opacity:.5}
|
||||
.ccq-warn-line{font-size:11px;color:#b45309;margin-top:3px}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
@ -232,11 +280,41 @@ PLUGIN.ui = {
|
||||
function stateOf(r){
|
||||
if (r.auth_failed) return {cls:"err", txt:"认证失败"};
|
||||
if (r.exceeded) return {cls:"out", txt:"已超额"};
|
||||
if (r.below_threshold) return {cls:"low", txt:"低于阈值"};
|
||||
var t = Number(r.total)||0;
|
||||
if (t<=0) return {cls:"out", txt:"无余额"};
|
||||
if (t<5) return {cls:"low", txt:"余额偏低"};
|
||||
return {cls:"ok", txt:r.plan_status||"正常"};
|
||||
}
|
||||
// month renders the monthly allowance. Unlike the rolling windows this one is
|
||||
// NOT capped by the vendor: credits reports what is left and usage reports what
|
||||
// was spent, so the cap comes from the plan table (see PLAN_CAPS) and is
|
||||
// cross-checked server-side. With no cap the balance is shown without a bar,
|
||||
// because an invented denominator would be worse than none.
|
||||
function month(r){
|
||||
var used=Number(r.monthly_used)||0, cap=Number(r.plan_cap)||0;
|
||||
var left=Number(r.total)||0;
|
||||
var h='<div class="ccq-win"><div class="ccq-win-h"><span>每月';
|
||||
if(r.period_end){
|
||||
var d=Math.floor((new Date(r.period_end)-Date.now())/86400000);
|
||||
if(d>0) h+=' <span class="ccq-reset" title="'+escH(r.period_end)+'">↻'+d+'d</span>';
|
||||
}
|
||||
h+='</span><span class="ccq-win-n">';
|
||||
if(!(cap>0)){ h+='<span class="ccq-win-nodat" title="未知套餐额度">—</span></span>'; }
|
||||
else{
|
||||
var pctv=Math.floor(used/cap*100+0.5);
|
||||
h+=pctv+'%</span></div>'+
|
||||
'<div class="ccq-bar"><div class="ccq-bar-fill'+(pctv>=100?" over":(pctv>=80?" warn":""))+
|
||||
'" style="width:'+Math.min(100,pctv)+'%"></div></div>'+
|
||||
'<div class="ccq-win-d">$'+used.toFixed(2)+' / $'+cap.toFixed(0)+
|
||||
' · 剩 '+usd(left)+'</div>';
|
||||
if(r.cap_mismatch){
|
||||
h+='</div><div class="ccq-warn-line" title="已用+剩余与套餐额度不符,比例仅供参考">⚠ 额度口径可能有变</div>';
|
||||
} else { h+='</div>'; }
|
||||
return h;
|
||||
}
|
||||
return h+'<div class="ccq-win-d">已用 $'+used.toFixed(2)+' · 剩 '+usd(left)+'</div></div>';
|
||||
}
|
||||
function win(label,w){
|
||||
if(!w) return "";
|
||||
var used=Number(w.used)||0, cap=Number(w.cap)||0;
|
||||
@ -271,9 +349,12 @@ PLUGIN.ui = {
|
||||
(Number(r.purchased)>0?'(含充值 $'+Number(r.purchased).toFixed(2)+')':'')+
|
||||
'</span></div>'+
|
||||
'<div class="ccq-grid">'+
|
||||
win("5 小时", r.fiveHour)+win("每周", r.weekly)+
|
||||
month(r)+win("5 小时", r.fiveHour)+win("每周", r.weekly)+
|
||||
'</div>';
|
||||
var meta=[];
|
||||
if(r.plan_id) meta.push("套餐 "+r.plan_id.replace(/^individual-/,""));
|
||||
if(r.cancel_at_period_end) meta.push("到期不续费");
|
||||
if(r.period_end) meta.push("周期至 "+String(r.period_end).slice(0,10));
|
||||
if(r.requests) meta.push(r.requests.toLocaleString()+" 次请求");
|
||||
if(r.spent) meta.push("累计 "+usd(r.spent));
|
||||
if(r.tokens) meta.push((r.tokens/1e9).toFixed(2)+"B tokens");
|
||||
|
||||
Reference in New Issue
Block a user