@kenz1117/dsh-ui-usage-billing 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +196 -105
- package/lib/index.js +132 -4
- package/lib/types/balance.d.ts +19 -0
- package/lib/types/client/TrendChart.d.ts +12 -10
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +6 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/pricing-shared.d.ts +25 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -33,93 +33,93 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
document.head.appendChild(tag);
|
|
34
34
|
}
|
|
35
35
|
var UsageBilling_module_css_default = {
|
|
36
|
-
"
|
|
36
|
+
"panelTitle": "VWh0dG_panelTitle",
|
|
37
|
+
"chartLegendLine": "VWh0dG_chartLegendLine",
|
|
38
|
+
"dashboardModal": "VWh0dG_dashboardModal",
|
|
39
|
+
"emptyRow": "VWh0dG_emptyRow",
|
|
40
|
+
"heroValue": "VWh0dG_heroValue",
|
|
41
|
+
"modelDot": "VWh0dG_modelDot",
|
|
42
|
+
"chartLegendBar": "VWh0dG_chartLegendBar",
|
|
37
43
|
"rateBadge": "VWh0dG_rateBadge",
|
|
38
|
-
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
39
44
|
"bandTagOff": "VWh0dG_bandTagOff",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
45
|
+
"heroSideLabel": "VWh0dG_heroSideLabel",
|
|
46
|
+
"triggerAmountSub": "VWh0dG_triggerAmountSub",
|
|
47
|
+
"healthIdle": "VWh0dG_healthIdle",
|
|
48
|
+
"chartGrid": "VWh0dG_chartGrid",
|
|
42
49
|
"deltaUp": "VWh0dG_deltaUp",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
50
|
+
"healthBadgeOk": "VWh0dG_healthBadgeOk",
|
|
51
|
+
"kpiValue": "VWh0dG_kpiValue",
|
|
52
|
+
"kpiGrid": "VWh0dG_kpiGrid",
|
|
53
|
+
"heroSide": "VWh0dG_heroSide",
|
|
54
|
+
"chartDot": "VWh0dG_chartDot",
|
|
55
|
+
"heroSideItem": "VWh0dG_heroSideItem",
|
|
56
|
+
"chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
|
|
57
|
+
"deltaDown": "VWh0dG_deltaDown",
|
|
58
|
+
"rateBadgeLive": "VWh0dG_rateBadgeLive",
|
|
59
|
+
"pricingChevron": "VWh0dG_pricingChevron",
|
|
60
|
+
"heroLabel": "VWh0dG_heroLabel",
|
|
61
|
+
"trigger": "VWh0dG_trigger",
|
|
62
|
+
"chartAxisLabel": "VWh0dG_chartAxisLabel",
|
|
63
|
+
"modelTable": "VWh0dG_modelTable",
|
|
46
64
|
"chartBar": "VWh0dG_chartBar",
|
|
65
|
+
"modelProvider": "VWh0dG_modelProvider",
|
|
66
|
+
"dashboardBody": "VWh0dG_dashboardBody",
|
|
67
|
+
"chartCrosshair": "VWh0dG_chartCrosshair",
|
|
68
|
+
"triggerAmount": "VWh0dG_triggerAmount",
|
|
69
|
+
"chartTooltip": "VWh0dG_chartTooltip",
|
|
70
|
+
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
71
|
+
"closeButton": "VWh0dG_closeButton",
|
|
72
|
+
"costCol": "VWh0dG_costCol",
|
|
73
|
+
"bandPriceOff": "VWh0dG_bandPriceOff",
|
|
47
74
|
"hero": "VWh0dG_hero",
|
|
48
|
-
"emptyRow": "VWh0dG_emptyRow",
|
|
49
|
-
"dashboardModal": "VWh0dG_dashboardModal",
|
|
50
|
-
"chartLegendBar": "VWh0dG_chartLegendBar",
|
|
51
|
-
"tableScroll": "VWh0dG_tableScroll",
|
|
52
75
|
"heroMain": "VWh0dG_heroMain",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
76
|
+
"chartEmpty": "VWh0dG_chartEmpty",
|
|
77
|
+
"triggerMeta": "VWh0dG_triggerMeta",
|
|
55
78
|
"kpiDetail": "VWh0dG_kpiDetail",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
79
|
+
"pricingChevronOpen": "VWh0dG_pricingChevronOpen",
|
|
80
|
+
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
81
|
+
"healthDot": "VWh0dG_healthDot",
|
|
82
|
+
"planTag": "VWh0dG_planTag",
|
|
83
|
+
"heroSideValue": "VWh0dG_heroSideValue",
|
|
59
84
|
"pricingToggle": "VWh0dG_pricingToggle",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"dashboardSubtitle": "VWh0dG_dashboardSubtitle",
|
|
85
|
+
"triggerDivider": "VWh0dG_triggerDivider",
|
|
86
|
+
"panelHint": "VWh0dG_panelHint",
|
|
87
|
+
"modelCell": "VWh0dG_modelCell",
|
|
88
|
+
"healthOk": "VWh0dG_healthOk",
|
|
89
|
+
"chartWrap": "VWh0dG_chartWrap",
|
|
90
|
+
"bandTag": "VWh0dG_bandTag",
|
|
91
|
+
"kpiGreen": "VWh0dG_kpiGreen",
|
|
68
92
|
"flatTag": "VWh0dG_flatTag",
|
|
93
|
+
"dashboardHead": "VWh0dG_dashboardHead",
|
|
94
|
+
"pricingTable": "VWh0dG_pricingTable",
|
|
95
|
+
"kpiLabel": "VWh0dG_kpiLabel",
|
|
96
|
+
"numCol": "VWh0dG_numCol",
|
|
97
|
+
"tableScroll": "VWh0dG_tableScroll",
|
|
98
|
+
"chartStack": "VWh0dG_chartStack",
|
|
99
|
+
"modelName": "VWh0dG_modelName",
|
|
69
100
|
"dashboard": "VWh0dG_dashboard",
|
|
70
|
-
"
|
|
101
|
+
"rateBadgeBuiltin": "VWh0dG_rateBadgeBuiltin",
|
|
102
|
+
"kpiTile": "VWh0dG_kpiTile",
|
|
71
103
|
"panel": "VWh0dG_panel",
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"kpiGrid": "VWh0dG_kpiGrid",
|
|
75
|
-
"chartSvg": "VWh0dG_chartSvg",
|
|
76
|
-
"closeButton": "VWh0dG_closeButton",
|
|
104
|
+
"bandPrice": "VWh0dG_bandPrice",
|
|
105
|
+
"healthBad": "VWh0dG_healthBad",
|
|
77
106
|
"chartLegend": "VWh0dG_chartLegend",
|
|
78
|
-
"
|
|
79
|
-
"pricingChevron": "VWh0dG_pricingChevron",
|
|
80
|
-
"na": "VWh0dG_na",
|
|
81
|
-
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
82
|
-
"chartStack": "VWh0dG_chartStack",
|
|
107
|
+
"chartLine": "VWh0dG_chartLine",
|
|
83
108
|
"chartTooltipRow": "VWh0dG_chartTooltipRow",
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
109
|
+
"triggerMonth": "VWh0dG_triggerMonth",
|
|
110
|
+
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
111
|
+
"railButton": "VWh0dG_railButton",
|
|
112
|
+
"triggerIcon": "VWh0dG_triggerIcon",
|
|
113
|
+
"healthBadge": "VWh0dG_healthBadge",
|
|
114
|
+
"chartSvg": "VWh0dG_chartSvg",
|
|
115
|
+
"panelHead": "VWh0dG_panelHead",
|
|
88
116
|
"heroMeta": "VWh0dG_heroMeta",
|
|
89
|
-
"
|
|
90
|
-
"kpiValue": "VWh0dG_kpiValue",
|
|
91
|
-
"planTag": "VWh0dG_planTag",
|
|
92
|
-
"heroSideLabel": "VWh0dG_heroSideLabel",
|
|
93
|
-
"chartDot": "VWh0dG_chartDot",
|
|
94
|
-
"healthBad": "VWh0dG_healthBad",
|
|
95
|
-
"kpiGreen": "VWh0dG_kpiGreen",
|
|
96
|
-
"heroSideItem": "VWh0dG_heroSideItem",
|
|
97
|
-
"pricingChevronOpen": "VWh0dG_pricingChevronOpen",
|
|
98
|
-
"dashboardBody": "VWh0dG_dashboardBody",
|
|
99
|
-
"panelHint": "VWh0dG_panelHint",
|
|
117
|
+
"delta": "VWh0dG_delta",
|
|
100
118
|
"chartTooltipDate": "VWh0dG_chartTooltipDate",
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"modelProvider": "VWh0dG_modelProvider",
|
|
106
|
-
"chartGrid": "VWh0dG_chartGrid",
|
|
107
|
-
"healthIdle": "VWh0dG_healthIdle",
|
|
108
|
-
"chartLine": "VWh0dG_chartLine",
|
|
109
|
-
"numCol": "VWh0dG_numCol",
|
|
110
|
-
"rateBadgeLive": "VWh0dG_rateBadgeLive",
|
|
111
|
-
"triggerAmountSub": "VWh0dG_triggerAmountSub",
|
|
112
|
-
"bandPrice": "VWh0dG_bandPrice",
|
|
113
|
-
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
114
|
-
"triggerIcon": "VWh0dG_triggerIcon",
|
|
115
|
-
"dashboardHead": "VWh0dG_dashboardHead",
|
|
116
|
-
"bandPriceOff": "VWh0dG_bandPriceOff",
|
|
117
|
-
"chartTooltip": "VWh0dG_chartTooltip",
|
|
118
|
-
"chartEmpty": "VWh0dG_chartEmpty",
|
|
119
|
-
"modelCell": "VWh0dG_modelCell",
|
|
120
|
-
"modelDot": "VWh0dG_modelDot",
|
|
121
|
-
"chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
|
|
122
|
-
"triggerMeta": "VWh0dG_triggerMeta"
|
|
119
|
+
"triggerToday": "VWh0dG_triggerToday",
|
|
120
|
+
"dashboardRight": "VWh0dG_dashboardRight",
|
|
121
|
+
"dashboardSubtitle": "VWh0dG_dashboardSubtitle",
|
|
122
|
+
"na": "VWh0dG_na"
|
|
123
123
|
};
|
|
124
124
|
/** 运行时实时覆盖:undefined = 用内置目录与内置汇率(默认值降级)。 */
|
|
125
125
|
let liveRate;
|
|
@@ -665,6 +665,12 @@ window.__ModuleLoader__.load({
|
|
|
665
665
|
* Estimate the CNY cost of one model's token usage, mixing the peak and
|
|
666
666
|
* off-peak bands by the given peak share (flat-priced models cost the same in
|
|
667
667
|
* both bands).
|
|
668
|
+
*
|
|
669
|
+
* 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
|
|
670
|
+
* 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
|
|
671
|
+
* peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
|
|
672
|
+
* 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
|
|
673
|
+
* 时段只能按比例估算,而非逐请求判定。
|
|
668
674
|
* @param entry - the catalog entry whose prices apply.
|
|
669
675
|
* @param buckets - token usage counts.
|
|
670
676
|
* @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
|
|
@@ -1062,6 +1068,41 @@ window.__ModuleLoader__.load({
|
|
|
1062
1068
|
const USAGE_STATS_PATH = "/api/billing/usage-stats";
|
|
1063
1069
|
/** Path to the live-pricing endpoint served by this plugin's node half. */
|
|
1064
1070
|
const PRICING_PATH = "/api/billing/pricing";
|
|
1071
|
+
/** Path to the account-balance endpoint served by this plugin's node half. */
|
|
1072
|
+
const BALANCE_PATH = "/api/billing/balance";
|
|
1073
|
+
/** 弹窗打开期间统计与定价的自动刷新间隔(毫秒)。 */
|
|
1074
|
+
const STATS_REFRESH_INTERVAL_MS = 3e4;
|
|
1075
|
+
/**
|
|
1076
|
+
* 本地时区(北京时间)日期戳:与服务端聚合的 dayStamp 一致。不要用
|
|
1077
|
+
* `toISOString()`——那是 UTC,北京时间的凌晨 0-8 点会取到前一天。
|
|
1078
|
+
*/
|
|
1079
|
+
function localDayStamp(time = Date.now()) {
|
|
1080
|
+
const date = new Date(time);
|
|
1081
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1082
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1083
|
+
}
|
|
1084
|
+
/** 本地时区时钟:`HH:MM:SS`。 */
|
|
1085
|
+
function formatClock(time) {
|
|
1086
|
+
const date = new Date(time);
|
|
1087
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1088
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* 高区分度图表色板:趋势图柱、图例与计费表圆点按模型分配。不用模型品牌色
|
|
1092
|
+
* (目录里多为蓝色系,视觉上几乎分不开),保证每个模型一眼可辨。
|
|
1093
|
+
*/
|
|
1094
|
+
const CHART_PALETTE = [
|
|
1095
|
+
"#3b82f6",
|
|
1096
|
+
"#06b6d4",
|
|
1097
|
+
"#8b5cf6",
|
|
1098
|
+
"#f59e0b",
|
|
1099
|
+
"#10b981",
|
|
1100
|
+
"#ef4444",
|
|
1101
|
+
"#ec4899",
|
|
1102
|
+
"#6366f1",
|
|
1103
|
+
"#f97316",
|
|
1104
|
+
"#14b8a6"
|
|
1105
|
+
];
|
|
1065
1106
|
/** Empty snapshot: shown before (or without) real host data — zeros, never fabricated samples. */
|
|
1066
1107
|
const EMPTY_STATS = {
|
|
1067
1108
|
total: {
|
|
@@ -1089,7 +1130,8 @@ window.__ModuleLoader__.load({
|
|
|
1089
1130
|
total: candidate.total ?? EMPTY_STATS.total,
|
|
1090
1131
|
byModel: candidate.byModel ?? {},
|
|
1091
1132
|
byDay: candidate.byDay ?? {},
|
|
1092
|
-
...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {}
|
|
1133
|
+
...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {},
|
|
1134
|
+
...candidate.updatedAt !== void 0 ? { updatedAt: candidate.updatedAt } : {}
|
|
1093
1135
|
};
|
|
1094
1136
|
} catch {
|
|
1095
1137
|
return null;
|
|
@@ -1122,6 +1164,22 @@ window.__ModuleLoader__.load({
|
|
|
1122
1164
|
} catch {}
|
|
1123
1165
|
}
|
|
1124
1166
|
/**
|
|
1167
|
+
* 拉取各提供方账户余额(供模型计费明细表的余额列);失败返回空列表。
|
|
1168
|
+
* @returns the balance rows, or an empty list on any failure.
|
|
1169
|
+
*/
|
|
1170
|
+
async function fetchBalances() {
|
|
1171
|
+
try {
|
|
1172
|
+
const response = await fetch(BALANCE_PATH);
|
|
1173
|
+
if (!response.ok) return [];
|
|
1174
|
+
const text = await response.text();
|
|
1175
|
+
const parsed = JSON.parse(text);
|
|
1176
|
+
if (parsed !== null && typeof parsed === "object" && "balances" in parsed) return parsed.balances;
|
|
1177
|
+
return [];
|
|
1178
|
+
} catch {
|
|
1179
|
+
return [];
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1125
1183
|
* Sidebar footer trigger: compact pill in wide mode, icon in rail mode.
|
|
1126
1184
|
* @param props - framework props plus `wide` column state.
|
|
1127
1185
|
*/
|
|
@@ -1193,15 +1251,30 @@ window.__ModuleLoader__.load({
|
|
|
1193
1251
|
}
|
|
1194
1252
|
/**
|
|
1195
1253
|
* The centered billing dashboard modal.
|
|
1196
|
-
* @param props - stats, locale function, close handler,
|
|
1254
|
+
* @param props - stats, locale function, close handler, model health, balances.
|
|
1197
1255
|
*/
|
|
1198
|
-
function BillingDashboard({ stats, t, onClose, health }) {
|
|
1256
|
+
function BillingDashboard({ stats, t, onClose, health, balances }) {
|
|
1199
1257
|
const { total, byModel, byDay } = stats;
|
|
1200
1258
|
const [pricingOpen, setPricingOpen] = (0, react.useState)(false);
|
|
1201
1259
|
const rateInfo = getRateInfo();
|
|
1260
|
+
const balanceFor = (provider) => balances.find((balance) => normalizeProvider(balance.provider) === normalizeProvider(provider));
|
|
1261
|
+
const renderBalance = (balance) => {
|
|
1262
|
+
if (balance === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1263
|
+
className: UsageBilling_module_css_default.na,
|
|
1264
|
+
children: "—"
|
|
1265
|
+
});
|
|
1266
|
+
if (balance.error === "unconfigured") return t("billing.balanceUnconfigured");
|
|
1267
|
+
if (balance.error === "unauthorized") return t("billing.balanceUnauthorized");
|
|
1268
|
+
if (balance.error === "unreachable") return t("billing.balanceUnreachable");
|
|
1269
|
+
if (balance.totalBalance === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1270
|
+
className: UsageBilling_module_css_default.na,
|
|
1271
|
+
children: "—"
|
|
1272
|
+
});
|
|
1273
|
+
return balance.currency === "USD" ? `$${balance.totalBalance.toFixed(2)}` : formatMoney(balance.totalBalance);
|
|
1274
|
+
};
|
|
1202
1275
|
const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
|
|
1203
1276
|
const dates = Object.keys(byDay).sort();
|
|
1204
|
-
const today = (
|
|
1277
|
+
const today = localDayStamp();
|
|
1205
1278
|
const todayCost = byDay[today]?.cost ?? 0;
|
|
1206
1279
|
const monthPrefix = today.slice(0, 7);
|
|
1207
1280
|
const yearPrefix = today.slice(0, 4);
|
|
@@ -1213,7 +1286,7 @@ window.__ModuleLoader__.load({
|
|
|
1213
1286
|
for (let offset = 6; offset >= 0; offset -= 1) {
|
|
1214
1287
|
const day = /* @__PURE__ */ new Date();
|
|
1215
1288
|
day.setDate(day.getDate() - offset);
|
|
1216
|
-
out.push(day.
|
|
1289
|
+
out.push(localDayStamp(day.getTime()));
|
|
1217
1290
|
}
|
|
1218
1291
|
return out;
|
|
1219
1292
|
}, []);
|
|
@@ -1248,7 +1321,6 @@ window.__ModuleLoader__.load({
|
|
|
1248
1321
|
key,
|
|
1249
1322
|
name: entry.name,
|
|
1250
1323
|
provider: entry.provider,
|
|
1251
|
-
color: resolveToken(entry.colorVar),
|
|
1252
1324
|
calls: data.calls,
|
|
1253
1325
|
input: data.input,
|
|
1254
1326
|
output: data.output,
|
|
@@ -1257,7 +1329,10 @@ window.__ModuleLoader__.load({
|
|
|
1257
1329
|
plan: isSubscriptionPlan(key),
|
|
1258
1330
|
...data.cost > 0 ? { actual: data.cost } : {}
|
|
1259
1331
|
};
|
|
1260
|
-
}).sort((a, b) => (b.actual ?? b.estimated) - (a.actual ?? a.estimated)),
|
|
1332
|
+
}).sort((a, b) => (b.actual ?? b.estimated) - (a.actual ?? a.estimated)).map((row, index) => ({
|
|
1333
|
+
...row,
|
|
1334
|
+
color: CHART_PALETTE[index % CHART_PALETTE.length] ?? "#8b95a3"
|
|
1335
|
+
})), [byModel]);
|
|
1261
1336
|
const estimatedTotal = modelRows.reduce((sum, row) => sum + row.estimated, 0);
|
|
1262
1337
|
const displayTotal = total.cost > 0 ? total.cost : estimatedTotal;
|
|
1263
1338
|
const avgPerCall = total.calls > 0 ? displayTotal / total.calls : 0;
|
|
@@ -1482,13 +1557,9 @@ window.__ModuleLoader__.load({
|
|
|
1482
1557
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1483
1558
|
className: UsageBilling_module_css_default.panelTitle,
|
|
1484
1559
|
children: t("billing.models")
|
|
1485
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1560
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1486
1561
|
className: UsageBilling_module_css_default.panelHint,
|
|
1487
|
-
children:
|
|
1488
|
-
t("billing.estimated"),
|
|
1489
|
-
" · ",
|
|
1490
|
-
t("billing.pricePerM")
|
|
1491
|
-
]
|
|
1562
|
+
children: stats.updatedAt !== void 0 ? `${t("billing.lastUpdated")} ${formatClock(stats.updatedAt)}` : ""
|
|
1492
1563
|
})]
|
|
1493
1564
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1494
1565
|
className: UsageBilling_module_css_default.tableScroll,
|
|
@@ -1514,11 +1585,11 @@ window.__ModuleLoader__.load({
|
|
|
1514
1585
|
}),
|
|
1515
1586
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1516
1587
|
className: UsageBilling_module_css_default.numCol,
|
|
1517
|
-
children: t("billing.
|
|
1588
|
+
children: t("billing.actual")
|
|
1518
1589
|
}),
|
|
1519
1590
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1520
1591
|
className: UsageBilling_module_css_default.numCol,
|
|
1521
|
-
children: t("billing.
|
|
1592
|
+
children: t("billing.balance")
|
|
1522
1593
|
})
|
|
1523
1594
|
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [modelRows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1524
1595
|
colSpan: 7,
|
|
@@ -1554,13 +1625,6 @@ window.__ModuleLoader__.load({
|
|
|
1554
1625
|
className: UsageBilling_module_css_default.numCol,
|
|
1555
1626
|
children: formatPercent(row.cacheHitRate)
|
|
1556
1627
|
}),
|
|
1557
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1558
|
-
className: clsx(UsageBilling_module_css_default.numCol, UsageBilling_module_css_default.costCol),
|
|
1559
|
-
children: row.plan ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1560
|
-
className: UsageBilling_module_css_default.planTag,
|
|
1561
|
-
children: "订阅包含"
|
|
1562
|
-
}) : formatMoney(row.estimated)
|
|
1563
|
-
}),
|
|
1564
1628
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1565
1629
|
className: UsageBilling_module_css_default.numCol,
|
|
1566
1630
|
children: row.plan ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1570,6 +1634,10 @@ window.__ModuleLoader__.load({
|
|
|
1570
1634
|
className: UsageBilling_module_css_default.na,
|
|
1571
1635
|
children: "—"
|
|
1572
1636
|
})
|
|
1637
|
+
}),
|
|
1638
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1639
|
+
className: UsageBilling_module_css_default.numCol,
|
|
1640
|
+
children: renderBalance(balanceFor(row.provider))
|
|
1573
1641
|
})
|
|
1574
1642
|
] }, row.key))] })]
|
|
1575
1643
|
})
|
|
@@ -1703,23 +1771,37 @@ window.__ModuleLoader__.load({
|
|
|
1703
1771
|
const { t, checkModels } = props;
|
|
1704
1772
|
const [stats, setStats] = (0, react.useState)(EMPTY_STATS);
|
|
1705
1773
|
const [health, setHealth] = (0, react.useState)(IDLE_HEALTH);
|
|
1774
|
+
const [balances, setBalances] = (0, react.useState)([]);
|
|
1706
1775
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1707
1776
|
const close = (0, react.useCallback)(() => {
|
|
1708
1777
|
setOpen(false);
|
|
1709
1778
|
}, []);
|
|
1779
|
+
const reloadStats = (0, react.useCallback)(() => {
|
|
1780
|
+
loadUsageStats().then((data) => {
|
|
1781
|
+
if (data !== null) setStats(data);
|
|
1782
|
+
});
|
|
1783
|
+
fetchBalances().then((list) => {
|
|
1784
|
+
if (list.length > 0) setBalances(list);
|
|
1785
|
+
});
|
|
1786
|
+
}, []);
|
|
1710
1787
|
const openDashboard = (0, react.useCallback)(() => {
|
|
1788
|
+
reloadStats();
|
|
1789
|
+
loadLivePricing();
|
|
1711
1790
|
setOpen(true);
|
|
1712
|
-
}, []);
|
|
1791
|
+
}, [reloadStats]);
|
|
1713
1792
|
(0, react.useEffect)(() => {
|
|
1714
|
-
|
|
1715
|
-
loadUsageStats().then((data) => {
|
|
1716
|
-
if (mounted && data !== null) setStats(data);
|
|
1717
|
-
});
|
|
1793
|
+
reloadStats();
|
|
1718
1794
|
loadLivePricing();
|
|
1795
|
+
}, [reloadStats]);
|
|
1796
|
+
(0, react.useEffect)(() => {
|
|
1797
|
+
const timer = setInterval(() => {
|
|
1798
|
+
reloadStats();
|
|
1799
|
+
loadLivePricing();
|
|
1800
|
+
}, STATS_REFRESH_INTERVAL_MS);
|
|
1719
1801
|
return () => {
|
|
1720
|
-
|
|
1802
|
+
clearInterval(timer);
|
|
1721
1803
|
};
|
|
1722
|
-
}, []);
|
|
1804
|
+
}, [reloadStats]);
|
|
1723
1805
|
(0, react.useEffect)(() => {
|
|
1724
1806
|
let mounted = true;
|
|
1725
1807
|
checkModels().then((result) => {
|
|
@@ -1729,7 +1811,7 @@ window.__ModuleLoader__.load({
|
|
|
1729
1811
|
mounted = false;
|
|
1730
1812
|
};
|
|
1731
1813
|
}, [checkModels]);
|
|
1732
|
-
const today = (
|
|
1814
|
+
const today = localDayStamp();
|
|
1733
1815
|
const monthCost = Object.entries(stats.byDay).filter(([date]) => date.startsWith(today.slice(0, 7))).reduce((sum, [, day]) => sum + day.cost, 0);
|
|
1734
1816
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBillingTrigger, {
|
|
1735
1817
|
...props,
|
|
@@ -1740,7 +1822,8 @@ window.__ModuleLoader__.load({
|
|
|
1740
1822
|
stats,
|
|
1741
1823
|
t,
|
|
1742
1824
|
onClose: close,
|
|
1743
|
-
health
|
|
1825
|
+
health,
|
|
1826
|
+
balances
|
|
1744
1827
|
})] });
|
|
1745
1828
|
}
|
|
1746
1829
|
//#endregion
|
|
@@ -1783,7 +1866,11 @@ window.__ModuleLoader__.load({
|
|
|
1783
1866
|
"billing.noData": "暂无计费数据",
|
|
1784
1867
|
"billing.todayRate": "今日汇率",
|
|
1785
1868
|
"billing.rateLive": "实时",
|
|
1786
|
-
"billing.rateBuiltin": "内置"
|
|
1869
|
+
"billing.rateBuiltin": "内置",
|
|
1870
|
+
"billing.balance": "余额",
|
|
1871
|
+
"billing.balanceUnconfigured": "未配置",
|
|
1872
|
+
"billing.balanceUnauthorized": "密钥无效",
|
|
1873
|
+
"billing.balanceUnreachable": "查询失败"
|
|
1787
1874
|
};
|
|
1788
1875
|
const en = {
|
|
1789
1876
|
"billing.title": "Usage",
|
|
@@ -1822,7 +1909,11 @@ window.__ModuleLoader__.load({
|
|
|
1822
1909
|
"billing.noData": "No billing data yet",
|
|
1823
1910
|
"billing.todayRate": "Today rate",
|
|
1824
1911
|
"billing.rateLive": "Live",
|
|
1825
|
-
"billing.rateBuiltin": "Built-in"
|
|
1912
|
+
"billing.rateBuiltin": "Built-in",
|
|
1913
|
+
"billing.balance": "Balance",
|
|
1914
|
+
"billing.balanceUnconfigured": "Not set",
|
|
1915
|
+
"billing.balanceUnauthorized": "Bad key",
|
|
1916
|
+
"billing.balanceUnreachable": "Unavailable"
|
|
1826
1917
|
};
|
|
1827
1918
|
//#endregion
|
|
1828
1919
|
//#region src/client/apply.ts
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
5
|
/** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
|
|
5
6
|
function currentRate() {
|
|
6
7
|
return 6.79;
|
|
@@ -507,6 +508,12 @@ function priceBandCost(band, buckets, currency) {
|
|
|
507
508
|
* Estimate the CNY cost of one model's token usage, mixing the peak and
|
|
508
509
|
* off-peak bands by the given peak share (flat-priced models cost the same in
|
|
509
510
|
* both bands).
|
|
511
|
+
*
|
|
512
|
+
* 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
|
|
513
|
+
* 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
|
|
514
|
+
* peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
|
|
515
|
+
* 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
|
|
516
|
+
* 时段只能按比例估算,而非逐请求判定。
|
|
510
517
|
* @param entry - the catalog entry whose prices apply.
|
|
511
518
|
* @param buckets - token usage counts.
|
|
512
519
|
* @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
|
|
@@ -656,6 +663,101 @@ async function aggregateUsage(persistence, options = {}) {
|
|
|
656
663
|
};
|
|
657
664
|
}
|
|
658
665
|
//#endregion
|
|
666
|
+
//#region lib/types/balance.js
|
|
667
|
+
/**
|
|
668
|
+
* Account-balance queries for the billing dashboard.
|
|
669
|
+
*
|
|
670
|
+
* Only providers with a public balance endpoint can report one. Today that is
|
|
671
|
+
* DeepSeek (`GET https://api.deepseek.com/user/balance`, Bearer 鉴权); the
|
|
672
|
+
* other mainstream providers (OpenAI, 智谱, 通义, Kimi…) expose no standard
|
|
673
|
+
* balance API, so their rows in the model table show an unavailable state.
|
|
674
|
+
* The lookup map below is the extension point for future providers.
|
|
675
|
+
*/
|
|
676
|
+
/** Abort a balance fetch when the upstream hangs beyond this budget. */
|
|
677
|
+
const FETCH_TIMEOUT_MS$1 = 8e3;
|
|
678
|
+
/** DeepSeek 官方余额接口(官方文档 api-docs.deepseek.com/api/get-user-balance)。 */
|
|
679
|
+
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
680
|
+
/** 数字归一化:接口返回的余额是字符串(如 `"110.00"`),统一转 number。 */
|
|
681
|
+
function toNumber(value) {
|
|
682
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
683
|
+
if (typeof value === "string") {
|
|
684
|
+
const parsed = Number(value);
|
|
685
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Query the DeepSeek account balance through the configured credential.
|
|
690
|
+
* @param ctx - host context carrying the credentials seam.
|
|
691
|
+
* @param apiKeyEnv - credential reference resolving the DeepSeek API key.
|
|
692
|
+
* @returns the balance row, or an error row when the key/endpoint misbehaves.
|
|
693
|
+
*/
|
|
694
|
+
async function queryDeepSeek(ctx, apiKeyEnv) {
|
|
695
|
+
const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
|
|
696
|
+
if (hit === void 0) return {
|
|
697
|
+
provider: "deepseek",
|
|
698
|
+
displayName: "DeepSeek",
|
|
699
|
+
error: "unconfigured"
|
|
700
|
+
};
|
|
701
|
+
const controller = new AbortController();
|
|
702
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$1);
|
|
703
|
+
try {
|
|
704
|
+
const response = await fetch(DEEPSEEK_BALANCE_URL, {
|
|
705
|
+
headers: {
|
|
706
|
+
accept: "application/json",
|
|
707
|
+
authorization: `Bearer ${hit.value}`
|
|
708
|
+
},
|
|
709
|
+
signal: controller.signal
|
|
710
|
+
});
|
|
711
|
+
if (response.status === 401 || response.status === 403) return {
|
|
712
|
+
provider: "deepseek",
|
|
713
|
+
displayName: "DeepSeek",
|
|
714
|
+
error: "unauthorized"
|
|
715
|
+
};
|
|
716
|
+
if (!response.ok) return {
|
|
717
|
+
provider: "deepseek",
|
|
718
|
+
displayName: "DeepSeek",
|
|
719
|
+
error: "unreachable"
|
|
720
|
+
};
|
|
721
|
+
const data = await response.json();
|
|
722
|
+
const info = (Array.isArray(data.balance_infos) ? data.balance_infos : [])[0];
|
|
723
|
+
const currency = typeof info?.currency === "string" ? info.currency : void 0;
|
|
724
|
+
const totalBalance = toNumber(info?.total_balance);
|
|
725
|
+
const grantedBalance = toNumber(info?.granted_balance);
|
|
726
|
+
const toppedUpBalance = toNumber(info?.topped_up_balance);
|
|
727
|
+
const isAvailable = typeof data.is_available === "boolean" ? data.is_available : void 0;
|
|
728
|
+
return {
|
|
729
|
+
provider: "deepseek",
|
|
730
|
+
displayName: "DeepSeek",
|
|
731
|
+
...currency !== void 0 ? { currency } : {},
|
|
732
|
+
...totalBalance !== void 0 ? { totalBalance } : {},
|
|
733
|
+
...grantedBalance !== void 0 ? { grantedBalance } : {},
|
|
734
|
+
...toppedUpBalance !== void 0 ? { toppedUpBalance } : {},
|
|
735
|
+
...isAvailable !== void 0 ? { isAvailable } : {}
|
|
736
|
+
};
|
|
737
|
+
} catch {
|
|
738
|
+
return {
|
|
739
|
+
provider: "deepseek",
|
|
740
|
+
displayName: "DeepSeek",
|
|
741
|
+
error: "unreachable"
|
|
742
|
+
};
|
|
743
|
+
} finally {
|
|
744
|
+
clearTimeout(timer);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const QUERIERS = [{
|
|
748
|
+
provider: "deepseek",
|
|
749
|
+
querier: queryDeepSeek
|
|
750
|
+
}];
|
|
751
|
+
/**
|
|
752
|
+
* Query every configured provider's account balance.
|
|
753
|
+
* @param ctx - host context carrying the credentials seam.
|
|
754
|
+
* @param balanceApiKeyEnv - credential reference for the DeepSeek key.
|
|
755
|
+
* @returns the balance rows (one per provider).
|
|
756
|
+
*/
|
|
757
|
+
async function queryBalances(ctx, balanceApiKeyEnv) {
|
|
758
|
+
return await Promise.all(QUERIERS.map(({ querier }) => querier(ctx, balanceApiKeyEnv)));
|
|
759
|
+
}
|
|
760
|
+
//#endregion
|
|
659
761
|
//#region lib/types/pricing-fetch.js
|
|
660
762
|
/**
|
|
661
763
|
* One-shot live pricing refresh for the billing dashboard.
|
|
@@ -820,8 +922,16 @@ async function fetchLivePricing() {
|
|
|
820
922
|
* missing file answers `{ error }` so the dashboard shows zeros, never
|
|
821
923
|
* fabricated samples.
|
|
822
924
|
*/
|
|
925
|
+
/** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
|
|
926
|
+
const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
|
|
927
|
+
/** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
|
|
928
|
+
const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
823
929
|
/** Required services: the web server and the persisted session log store. */
|
|
824
|
-
const inject = [
|
|
930
|
+
const inject = [
|
|
931
|
+
"webServer",
|
|
932
|
+
"sessionPersistence",
|
|
933
|
+
"credentials"
|
|
934
|
+
];
|
|
825
935
|
/**
|
|
826
936
|
* Host plugin body: serve real aggregated usage to the browser dashboard.
|
|
827
937
|
* @param ctx - host context carrying webServer and sessionPersistence.
|
|
@@ -836,9 +946,18 @@ function apply(ctx, config = {}) {
|
|
|
836
946
|
join(homedir(), ".dsh/.dsh-usage-stats.json")
|
|
837
947
|
].filter((path) => typeof path === "string" && path.length > 0);
|
|
838
948
|
let live = { source: "builtin" };
|
|
839
|
-
|
|
840
|
-
live =
|
|
841
|
-
}
|
|
949
|
+
const refreshPricing = async () => {
|
|
950
|
+
live = await fetchLivePricing();
|
|
951
|
+
};
|
|
952
|
+
refreshPricing();
|
|
953
|
+
ctx.effect(() => {
|
|
954
|
+
const timer = setInterval(() => {
|
|
955
|
+
refreshPricing();
|
|
956
|
+
}, PRICING_REFRESH_INTERVAL_MS);
|
|
957
|
+
return () => {
|
|
958
|
+
clearInterval(timer);
|
|
959
|
+
};
|
|
960
|
+
}, "usage-billing: pricing refresh timer");
|
|
842
961
|
ctx.effect(() => ctx.webServer.register({
|
|
843
962
|
kind: "exact",
|
|
844
963
|
path: "/api/billing/pricing",
|
|
@@ -847,6 +966,15 @@ function apply(ctx, config = {}) {
|
|
|
847
966
|
res.end(JSON.stringify(live));
|
|
848
967
|
}
|
|
849
968
|
}), "usage-billing: pricing route");
|
|
969
|
+
ctx.effect(() => ctx.webServer.register({
|
|
970
|
+
kind: "exact",
|
|
971
|
+
path: "/api/billing/balance",
|
|
972
|
+
handler: async (_req, res) => {
|
|
973
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
974
|
+
const balances = await queryBalances(ctx, config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV);
|
|
975
|
+
res.end(JSON.stringify({ balances }));
|
|
976
|
+
}
|
|
977
|
+
}), "usage-billing: balance route");
|
|
850
978
|
ctx.effect(() => ctx.webServer.register({
|
|
851
979
|
kind: "exact",
|
|
852
980
|
path: "/api/billing/usage-stats",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account-balance queries for the billing dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Only providers with a public balance endpoint can report one. Today that is
|
|
5
|
+
* DeepSeek (`GET https://api.deepseek.com/user/balance`, Bearer 鉴权); the
|
|
6
|
+
* other mainstream providers (OpenAI, 智谱, 通义, Kimi…) expose no standard
|
|
7
|
+
* balance API, so their rows in the model table show an unavailable state.
|
|
8
|
+
* The lookup map below is the extension point for future providers.
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { ProviderBalance } from './pricing-shared.ts';
|
|
12
|
+
/**
|
|
13
|
+
* Query every configured provider's account balance.
|
|
14
|
+
* @param ctx - host context carrying the credentials seam.
|
|
15
|
+
* @param balanceApiKeyEnv - credential reference for the DeepSeek key.
|
|
16
|
+
* @returns the balance rows (one per provider).
|
|
17
|
+
*/
|
|
18
|
+
export declare function queryBalances(ctx: Context, balanceApiKeyEnv: string): Promise<readonly ProviderBalance[]>;
|
|
19
|
+
//# sourceMappingURL=balance.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TrendChart: dependency-free SVG
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* TrendChart: dependency-free SVG chart of daily cost + calls.
|
|
3
|
+
*
|
|
4
|
+
* The columns are GROUPED per model — one bar per model per day, each in its
|
|
5
|
+
* brand color, so per-model cost is directly comparable. The blue line is the
|
|
6
|
+
* total call volume across all models, plotted on its own right-hand axis.
|
|
7
|
+
* A hover crosshair shows the day's model breakdown. No chart library — the
|
|
8
|
+
* surface stays self-contained and offline.
|
|
7
9
|
*/
|
|
8
10
|
/** One model's legend identity: key, display name, and brand color. */
|
|
9
11
|
export interface TrendSeriesModel {
|
|
@@ -11,7 +13,7 @@ export interface TrendSeriesModel {
|
|
|
11
13
|
key: string;
|
|
12
14
|
/** Human-readable model name. */
|
|
13
15
|
name: string;
|
|
14
|
-
/** Resolved brand color for the
|
|
16
|
+
/** Resolved brand color for the bar and legend swatch (empty = single-color fallback). */
|
|
15
17
|
color: string;
|
|
16
18
|
}
|
|
17
19
|
/** One day row fed to the chart. */
|
|
@@ -20,15 +22,15 @@ export interface TrendPoint {
|
|
|
20
22
|
date: string;
|
|
21
23
|
/** Total cost that day. */
|
|
22
24
|
cost: number;
|
|
23
|
-
/** API calls that day. */
|
|
25
|
+
/** API calls that day (total across models). */
|
|
24
26
|
calls: number;
|
|
25
|
-
/** Per-model cost that day (stats key → CNY); absent entries
|
|
27
|
+
/** Per-model cost that day (stats key → CNY); absent entries plot zero. */
|
|
26
28
|
byModel?: Readonly<Record<string, number>>;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* Render the daily
|
|
31
|
+
* Render the daily grouped cost bars plus the total-calls line.
|
|
30
32
|
* @param props.data - sorted daily rows (ascending date).
|
|
31
|
-
* @param props.models - the model legend, in
|
|
33
|
+
* @param props.models - the model legend, in bar order.
|
|
32
34
|
*/
|
|
33
35
|
export declare function TrendChart({ data, models }: {
|
|
34
36
|
data: readonly TrendPoint[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Locale dictionaries for the usage billing surface. */
|
|
2
|
-
export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin';
|
|
2
|
+
export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable';
|
|
3
3
|
export declare const NS = "usageBilling";
|
|
4
4
|
export declare const zh: Record<UsageBillingKey, string>;
|
|
5
5
|
export declare const en: Record<UsageBillingKey, string>;
|
|
@@ -115,6 +115,12 @@ export declare function resolveToken(name: string): string;
|
|
|
115
115
|
* Estimate the CNY cost of one model's token usage, mixing the peak and
|
|
116
116
|
* off-peak bands by the given peak share (flat-priced models cost the same in
|
|
117
117
|
* both bands).
|
|
118
|
+
*
|
|
119
|
+
* 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
|
|
120
|
+
* 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
|
|
121
|
+
* peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
|
|
122
|
+
* 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
|
|
123
|
+
* 时段只能按比例估算,而非逐请求判定。
|
|
118
124
|
* @param entry - the catalog entry whose prices apply.
|
|
119
125
|
* @param buckets - token usage counts.
|
|
120
126
|
* @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface UsageBillingConfig {
|
|
|
16
16
|
statsPath?: string;
|
|
17
17
|
/** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
|
|
18
18
|
subscriptionProviders?: string[];
|
|
19
|
+
/** 余额查询用的 DeepSeek 凭据引用(环境变量名);默认 DEEPSEEK_API_KEY。 */
|
|
20
|
+
balanceApiKeyEnv?: string;
|
|
19
21
|
}
|
|
20
22
|
/** Required services: the web server and the persisted session log store. */
|
|
21
23
|
export declare const inject: string[];
|
|
@@ -22,4 +22,29 @@ export interface LivePricing {
|
|
|
22
22
|
/** Overrides keyed by built-in catalog key (present when router matches succeeded). */
|
|
23
23
|
prices?: Record<string, LivePrice>;
|
|
24
24
|
}
|
|
25
|
+
/** 余额查询失败的原因,前端据此显示文案。 */
|
|
26
|
+
export type BalanceError = 'unconfigured' | 'unauthorized' | 'unreachable';
|
|
27
|
+
/** 一个提供方的账户余额(`/api/billing/balance` 的一行)。 */
|
|
28
|
+
export interface ProviderBalance {
|
|
29
|
+
/** 提供方 id(小写,如 `deepseek`),与模型表 provider 匹配用。 */
|
|
30
|
+
provider: string;
|
|
31
|
+
/** 显示名(如 `DeepSeek`)。 */
|
|
32
|
+
displayName: string;
|
|
33
|
+
/** 余额币种(CNY / USD)。 */
|
|
34
|
+
currency?: string;
|
|
35
|
+
/** 总可用余额(含赠金与充值)。 */
|
|
36
|
+
totalBalance?: number;
|
|
37
|
+
/** 未过期赠金余额。 */
|
|
38
|
+
grantedBalance?: number;
|
|
39
|
+
/** 充值余额。 */
|
|
40
|
+
toppedUpBalance?: number;
|
|
41
|
+
/** 余额是否足以继续调用。 */
|
|
42
|
+
isAvailable?: boolean;
|
|
43
|
+
/** 未配置/鉴权失败/网络不可达等失败原因;缺省 = 查询成功。 */
|
|
44
|
+
error?: BalanceError;
|
|
45
|
+
}
|
|
46
|
+
/** Response of `/api/billing/balance` consumed by the dashboard. */
|
|
47
|
+
export interface BalanceResponse {
|
|
48
|
+
balances: readonly ProviderBalance[];
|
|
49
|
+
}
|
|
25
50
|
//# sourceMappingURL=pricing-shared.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-ui-usage-billing",
|
|
3
3
|
"description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|