@lancecheney/dsh-deepseek-balance 0.1.1 → 0.1.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/README.md +10 -0
- package/README.zh.md +10 -0
- package/lib/client.js +37 -10
- package/lib/index.js +152 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) client plu
|
|
|
6
6
|
Spent ¥0.12 | Balance ¥47.17 | Peak | ¥27.0⁺/M output
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
Clicking the badge opens a right-side drawer with full usage stats:
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
9
15
|
## Features
|
|
10
16
|
|
|
11
17
|
- **Balance** — live DeepSeek API balance from `GET /user/balance`, proxied server-side so your API key never reaches the browser.
|
|
@@ -39,6 +45,10 @@ then restart `dsh web` and refresh the page.
|
|
|
39
45
|
|
|
40
46
|
Requires a `DEEPSEEK_API_KEY` configured through the harness credential store (or the `DEEPSEEK_API_KEY` environment variable).
|
|
41
47
|
|
|
48
|
+
## Security
|
|
49
|
+
|
|
50
|
+
The balance is proxied server-side: the browser only ever talks to the local `/api/deepseek-balance` route, which the host half forwards to DeepSeek. Your API key is resolved from the harness credential store (or the `DEEPSEEK_API_KEY` environment variable) and is only used server-side in the `Authorization: Bearer ...` header when calling DeepSeek — it never reaches the browser. The badge only shows a masked preview (`sk-8ce0e*****d899`) so you can tell which key is in use.
|
|
51
|
+
|
|
42
52
|
## Notes
|
|
43
53
|
|
|
44
54
|
- The **Spent** figure is an estimate: all tokens in a session are priced at the current period's rate. Real billing is computed by DeepSeek.
|
package/README.zh.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
消耗 ¥0.12 | 余额 ¥47.17 | 高峰 | ¥27.0⁺/M 输出
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
点开徽章会打开右侧抽屉面板,展示完整用量统计:
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
9
15
|
## 功能
|
|
10
16
|
|
|
11
17
|
- **余额** — 实时 DeepSeek API 余额(`GET /user/balance`),由服务端代理请求,浏览器不会接触你的 API Key。
|
|
@@ -39,6 +45,10 @@ dsh plugin --profile web add @lancecheney/dsh-deepseek-balance
|
|
|
39
45
|
|
|
40
46
|
需要已在 harness 凭据存储中配置 `DEEPSEEK_API_KEY`(或设置 `DEEPSEEK_API_KEY` 环境变量)。
|
|
41
47
|
|
|
48
|
+
## 安全
|
|
49
|
+
|
|
50
|
+
余额接口走服务端代理:浏览器只请求本地 `/api/deepseek-balance` 路由,由 host 半区转发到 DeepSeek。API Key 从 harness 凭据存储(或 `DEEPSEEK_API_KEY` 环境变量)解析,仅在服务端调用 DeepSeek 时放入 `Authorization: Bearer ...` 请求头,绝不进入浏览器。界面只显示脱敏后的预览(`sk-8ce0e*****d899`),方便确认当前用的是哪个密钥。
|
|
51
|
+
|
|
42
52
|
## 说明
|
|
43
53
|
|
|
44
54
|
- **消耗** 是估算值:会话内所有 token 都按当前时段价格计算,实际扣费以 DeepSeek 服务端为准。
|
package/lib/client.js
CHANGED
|
@@ -22,18 +22,41 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
const C = { root: "Dbg1_root", num: "Dbg1_num", sep: "Dbg1_sep", chip: "Dbg1_chip", peak: "Dbg1_peak", offPeak: "Dbg1_offPeak", legacy: "Dbg1_legacy", dimmed: "Dbg1_dimmed", plus: "Dbg1_plus", unit: "Dbg1_unit" };
|
|
23
23
|
const P = { backdrop: "Dp1_backdrop", panel: "Dp1_panel", head: "Dp1_head", avatar: "Dp1_avatar", name: "Dp1_name", key: "Dp1_key", close: "Dp1_close", body: "Dp1_body", stats: "Dp1_stats", statItem: "Dp1_statItem", statValue: "Dp1_statValue", statLabel: "Dp1_statLabel", sepBar: "Dp1_sepBar", balance: "Dp1_balance", balanceBlock: "Dp1_balanceBlock", balanceBlockLabel: "Dp1_balanceBlockLabel", balanceBlockValue: "Dp1_balanceBlockValue", btn: "Dp1_btn", btnPrimary: "Dp1_btnPrimary", section: "Dp1_section", title: "Dp1_title", row: "Dp1_row", twoCol: "Dp1_twoCol", rowTitle: "Dp1_rowTitle", rowTitleGray: "Dp1_rowTitleGray", rowVal: "Dp1_rowVal", rank: "Dp1_rank", monthNav: "Dp1_monthNav", monthGrids: "Dp1_monthGrids", monthGroup: "Dp1_monthGroup", monthGroupTitle: "Dp1_monthGroupTitle", grid: "Dp1_grid", cell: "Dp1_cell", cellFlat: "Dp1_cellFlat", cellPeak: "Dp1_cellPeak", cellOff: "Dp1_cellOff", cellBlue: "Dp1_cellBlue", legend: "Dp1_legend", dot: "Dp1_dot", nameRow: "Dp1_nameRow", edit: "Dp1_edit", editRow: "Dp1_editRow", editInput: "Dp1_editInput" };
|
|
24
24
|
|
|
25
|
-
const MODEL_META = { "deepseek-v4-pro": { label: "DeepSeek-V4-Pro" }, "deepseek-v4-flash": { label: "DeepSeek-
|
|
25
|
+
const MODEL_META = { "deepseek-v4-pro": { label: "DeepSeek-V4-Pro" }, "deepseek-flash": { label: "DeepSeek-Flash" }, "deepseek-v4-flash": { label: "DeepSeek-Flash" }, "deepseek-v4-flash-vision-exp": { label: "DeepSeek-Flash (vision)" } };
|
|
26
26
|
const FALLBACK_PRICING = {
|
|
27
27
|
effectiveFrom: "2026-08-17T00:00:00+08:00",
|
|
28
|
+
weekendFrom: "2026-08-23T00:00:00+08:00",
|
|
28
29
|
currencies: {
|
|
29
|
-
CNY: { symbol: "¥", models: {
|
|
30
|
-
|
|
30
|
+
CNY: { symbol: "¥", models: {
|
|
31
|
+
"deepseek-flash": { eras: [
|
|
32
|
+
{ from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
|
|
33
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }, peak: { hit: 0.10, miss: 3.0, output: 9.0 } },
|
|
34
|
+
{ from: null, legacy: { hit: 0.02, miss: 1.0, output: 2.0 } }
|
|
35
|
+
] },
|
|
36
|
+
"deepseek-v4-pro": { eras: [
|
|
37
|
+
{ from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
|
|
38
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }, peak: { hit: 0.30, miss: 9.0, output: 27.0 } },
|
|
39
|
+
{ from: null, legacy: { hit: 0.025, miss: 3.0, output: 6.0 } }
|
|
40
|
+
] }
|
|
41
|
+
} },
|
|
42
|
+
USD: { symbol: "$", models: {
|
|
43
|
+
"deepseek-flash": { eras: [
|
|
44
|
+
{ from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
|
|
45
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }, peak: { hit: 0.014, miss: 0.44, output: 1.32 } },
|
|
46
|
+
{ from: null, legacy: { hit: 0.0028, miss: 0.14, output: 0.28 } }
|
|
47
|
+
] },
|
|
48
|
+
"deepseek-v4-pro": { eras: [
|
|
49
|
+
{ from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
|
|
50
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }, peak: { hit: 0.044, miss: 1.32, output: 3.96 } },
|
|
51
|
+
{ from: null, legacy: { hit: 0.003625, miss: 0.435, output: 0.87 } }
|
|
52
|
+
] }
|
|
53
|
+
} }
|
|
31
54
|
}
|
|
32
55
|
};
|
|
33
56
|
const DEFAULT_MODEL = "deepseek-v4-pro";
|
|
34
57
|
|
|
35
58
|
function beijingDecimalHour(now) { let h = 0, m = 0; try { const p = new Intl.DateTimeFormat("en-GB", { timeZone: "Asia/Shanghai", hour12: false, hour: "2-digit", minute: "2-digit" }).formatToParts(now); for (const x of p) { if (x.type === "hour") h = Number(x.value); if (x.type === "minute") m = Number(x.value); } } catch { h = now.getHours(); m = now.getMinutes(); } return h + m / 60; }
|
|
36
|
-
function periodFor(now, effectiveFrom) { const eff = Date.parse(effectiveFrom); if (Number.isFinite(eff) && now.getTime() < eff) return "legacy"; const h = beijingDecimalHour(now); return (h >= 9 && h < 12) || (h >= 14 && h < 18) ? "peak" : "offPeak"; }
|
|
59
|
+
function periodFor(now, effectiveFrom, weekendFrom) { const eff = Date.parse(effectiveFrom); if (Number.isFinite(eff) && now.getTime() < eff) return "legacy"; const wk = Date.parse(weekendFrom); const d = new Date(now.getTime() + 8 * 3600 * 1000); const day = d.getUTCDay(); if ((day === 0 || day === 6) && Number.isFinite(wk) && now.getTime() >= wk) return "offPeak"; const h = beijingDecimalHour(now); return (h >= 9 && h < 12) || (h >= 14 && h < 18) ? "peak" : "offPeak"; }
|
|
37
60
|
function firstBalance(data) { const infos = data && data.balance_infos; if (!Array.isArray(infos) || infos.length === 0) return null; return infos.find((r) => r.currency === "CNY") || infos[0]; }
|
|
38
61
|
function fmtPrice(v) { const n = Number(v); return Number.isFinite(n) ? String(n) : String(v); }
|
|
39
62
|
function fmtMoney(v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; if (n <= 0) return "0.00"; if (n < 0.01) return n.toFixed(4); if (n < 1) return n.toFixed(3); return n.toFixed(2); }
|
|
@@ -89,6 +112,7 @@ window.__ModuleLoader__.load({
|
|
|
89
112
|
|
|
90
113
|
const pricing = state.pricing || FALLBACK_PRICING;
|
|
91
114
|
const effectiveFrom = pricing.effectiveFrom || FALLBACK_PRICING.effectiveFrom;
|
|
115
|
+
const weekendFrom = pricing.weekendFrom || FALLBACK_PRICING.weekendFrom;
|
|
92
116
|
const balanceRow = state.phase === "ok" && state.balance ? firstBalance(state.balance) : null;
|
|
93
117
|
const currency = balanceRow && balanceRow.currency ? balanceRow.currency : "CNY";
|
|
94
118
|
const dict = currency === "USD" ? en : zh;
|
|
@@ -102,12 +126,15 @@ window.__ModuleLoader__.load({
|
|
|
102
126
|
const modelId = currentSelection && currentSelection.model ? currentSelection.model : DEFAULT_MODEL;
|
|
103
127
|
const reasoningEffort = currentSelection ? currentSelection.reasoningEffort : void 0;
|
|
104
128
|
|
|
105
|
-
const period = periodFor(new Date(), effectiveFrom);
|
|
129
|
+
const period = periodFor(new Date(), effectiveFrom, weekendFrom);
|
|
106
130
|
const isPeak = period === "peak";
|
|
107
131
|
const isLegacy = period === "legacy";
|
|
108
132
|
const model = models[modelId] || models[DEFAULT_MODEL] || FALLBACK_PRICING.currencies.CNY.models[DEFAULT_MODEL];
|
|
109
|
-
const
|
|
110
|
-
const
|
|
133
|
+
const eras = Array.isArray(model.eras) ? model.eras : [];
|
|
134
|
+
const nowMs = Date.now();
|
|
135
|
+
const era = eras.find((e) => e.from != null && nowMs >= Date.parse(e.from)) || eras[eras.length - 1] || model;
|
|
136
|
+
const p = (period === "peak" ? era.peak : period === "offPeak" ? era.offPeak : era.legacy) || era.peak || era.offPeak || era.legacy;
|
|
137
|
+
const outputPrice = fmtPrice(p ? p.output : "—");
|
|
111
138
|
const cost = state.usage && state.usage.spend ? state.usage.spend.total : null;
|
|
112
139
|
const plus = reasoningEffort === "max" ? "++" : reasoningEffort === "off" ? "" : "+";
|
|
113
140
|
const periodClass = isLegacy ? C.legacy : isPeak ? C.peak : C.offPeak;
|
|
@@ -233,7 +260,7 @@ window.__ModuleLoader__.load({
|
|
|
233
260
|
stat(tr("stat.total"), usageData ? fmtTokens(usageData.totalTokens) : "…"), sepBar("s1"),
|
|
234
261
|
stat(tr("stat.dailyPeak"), usageData ? fmtTokens(usageData.dailyPeakTokens) : "…"), sepBar("s2"),
|
|
235
262
|
stat(tr("stat.longest"), usageData ? fmtDuration(usageData.longestChatMs) : "…"), sepBar("s3"),
|
|
236
|
-
stat(tr("stat.
|
|
263
|
+
stat(tr("stat.todayTokens"), usageData && usageData.days && usageData.days[beijingDayKey(Date.now())] ? fmtTokens(usageData.days[beijingDayKey(Date.now())].tokens) : "…"), sepBar("s4"),
|
|
237
264
|
stat(tr("stat.longStreak"), usageData ? usageData.longestStreak + tr("unit.day") : "…")
|
|
238
265
|
),
|
|
239
266
|
react.createElement("div", { className: P.balance },
|
|
@@ -321,7 +348,7 @@ window.__ModuleLoader__.load({
|
|
|
321
348
|
"balance.unavailable": "余额不可用",
|
|
322
349
|
"panel.stats": "统计", "panel.balance": "余额与消耗", "panel.topup": "跳转API官网", "panel.save": "保存",
|
|
323
350
|
"panel.topSessions": "消耗最多的对话", "panel.month": "每日分布", "panel.monthPeriod": "每日时段分布", "panel.monthTokens": "每日 Token 消耗", "panel.peakSessions": "高峰消耗排行", "panel.modelEffort": "模型与思考强度",
|
|
324
|
-
"stat.total": "累计 Token", "stat.dailyPeak": "每日峰值", "stat.longest": "最长聊天", "stat.
|
|
351
|
+
"stat.total": "累计 Token", "stat.dailyPeak": "每日峰值", "stat.longest": "最长聊天", "stat.longStreak": "最长连续", "stat.todayTokens": "今日 Token", "stat.today": "今日消耗", "stat.cumulative": "累计消耗",
|
|
325
352
|
"unit.day": " 天",
|
|
326
353
|
"legend.flat": "平价", "legend.peak": "高峰", "legend.off": "空闲", "legend.none": "无数据", "legend.less": "少", "legend.more": "多"
|
|
327
354
|
};
|
|
@@ -331,7 +358,7 @@ window.__ModuleLoader__.load({
|
|
|
331
358
|
"balance.unavailable": "balance unavailable",
|
|
332
359
|
"panel.stats": "Stats", "panel.balance": "Balance & Spend", "panel.topup": "API site", "panel.save": "Save",
|
|
333
360
|
"panel.topSessions": "Top conversations", "panel.month": "Daily", "panel.monthPeriod": "Daily period", "panel.monthTokens": "Daily token usage", "panel.peakSessions": "Peak-hour ranking", "panel.modelEffort": "Model & effort",
|
|
334
|
-
"stat.total": "Total tokens", "stat.dailyPeak": "Daily peak", "stat.longest": "Longest chat", "stat.
|
|
361
|
+
"stat.total": "Total tokens", "stat.dailyPeak": "Daily peak", "stat.longest": "Longest chat", "stat.longStreak": "Longest streak", "stat.todayTokens": "Today tokens", "stat.today": "Today", "stat.cumulative": "Total",
|
|
335
362
|
"unit.day": "d",
|
|
336
363
|
"legend.flat": "Flat", "legend.peak": "Peak", "legend.off": "Off-peak", "legend.none": "None", "legend.less": "Less", "legend.more": "More"
|
|
337
364
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
|
-
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { homedir } from "node:os";
|
|
@@ -26,43 +25,85 @@ const PRICING_CN_URL = "https://api-docs.deepseek.com/zh-cn/quick_start/pricing/
|
|
|
26
25
|
const PRICING_US_URL = "https://api-docs.deepseek.com/quick_start/pricing/";
|
|
27
26
|
const PRICING_FETCH_TIMEOUT_MS = 15000;
|
|
28
27
|
|
|
29
|
-
/**
|
|
28
|
+
/** Prices per currency. Each model carries an `eras` list (newest first): the
|
|
29
|
+
* currently effective peak/off-peak table plus historical tiers, so tokens are
|
|
30
|
+
* billed at the price in force when they were used. `from: null` is the
|
|
31
|
+
* pre-peak/off-peak legacy (flat) tier. */
|
|
30
32
|
const FALLBACK_PRICING = {
|
|
31
33
|
effectiveFrom: "2026-08-17T00:00:00+08:00",
|
|
34
|
+
weekendFrom: "2026-08-23T00:00:00+08:00",
|
|
32
35
|
currencies: {
|
|
33
36
|
CNY: {
|
|
34
37
|
symbol: "¥",
|
|
35
38
|
models: {
|
|
36
|
-
"deepseek-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
"deepseek-flash": {
|
|
40
|
+
eras: [
|
|
41
|
+
{ from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
|
|
42
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }, peak: { hit: 0.10, miss: 3.0, output: 9.0 } },
|
|
43
|
+
{ from: null, legacy: { hit: 0.02, miss: 1.0, output: 2.0 } }
|
|
44
|
+
]
|
|
40
45
|
},
|
|
41
46
|
"deepseek-v4-pro": {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
eras: [
|
|
48
|
+
{ from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
|
|
49
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }, peak: { hit: 0.30, miss: 9.0, output: 27.0 } },
|
|
50
|
+
{ from: null, legacy: { hit: 0.025, miss: 3.0, output: 6.0 } }
|
|
51
|
+
]
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
},
|
|
48
55
|
USD: {
|
|
49
56
|
symbol: "$",
|
|
50
57
|
models: {
|
|
51
|
-
"deepseek-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
"deepseek-flash": {
|
|
59
|
+
eras: [
|
|
60
|
+
{ from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
|
|
61
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }, peak: { hit: 0.014, miss: 0.44, output: 1.32 } },
|
|
62
|
+
{ from: null, legacy: { hit: 0.0028, miss: 0.14, output: 0.28 } }
|
|
63
|
+
]
|
|
55
64
|
},
|
|
56
65
|
"deepseek-v4-pro": {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
eras: [
|
|
67
|
+
{ from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
|
|
68
|
+
{ from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }, peak: { hit: 0.044, miss: 1.32, output: 3.96 } },
|
|
69
|
+
{ from: null, legacy: { hit: 0.003625, miss: 0.435, output: 0.87 } }
|
|
70
|
+
]
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
};
|
|
65
76
|
|
|
77
|
+
/** Legacy model names retired/renamed by DeepSeek; their requests are served by
|
|
78
|
+
* DeepSeek-V4.1-Flash and billed at the Flash price. */
|
|
79
|
+
const MODEL_ALIASES = {
|
|
80
|
+
"deepseek-v4-flash": "deepseek-flash",
|
|
81
|
+
"deepseek-v4-flash-vision-exp": "deepseek-flash"
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function resolveModelId(rawModel) {
|
|
85
|
+
const id = typeof rawModel === "string" && rawModel.length > 0 ? rawModel : "";
|
|
86
|
+
return MODEL_ALIASES[id] || id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Index of the price era in force at `time` (eras are newest-first). */
|
|
90
|
+
function priceEraIndex(modelId, time) {
|
|
91
|
+
const m = FALLBACK_PRICING.currencies.CNY.models[modelId];
|
|
92
|
+
if (!m || !Array.isArray(m.eras)) return 0;
|
|
93
|
+
for (let i = 0; i < m.eras.length; i++) {
|
|
94
|
+
const from = m.eras[i].from;
|
|
95
|
+
if (from === null || from === void 0 || time >= Date.parse(from)) return i;
|
|
96
|
+
}
|
|
97
|
+
return m.eras.length - 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Price era for a model at `time`, falling back to legacy/flat when unknown. */
|
|
101
|
+
function priceEraAt(modelId, time) {
|
|
102
|
+
const m = FALLBACK_PRICING.currencies.CNY.models[modelId];
|
|
103
|
+
if (!m || !Array.isArray(m.eras)) return null;
|
|
104
|
+
return m.eras[priceEraIndex(modelId, time)] || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
66
107
|
function json(res, status, body) {
|
|
67
108
|
const payload = JSON.stringify(body);
|
|
68
109
|
res.writeHead(status, {
|
|
@@ -76,7 +117,7 @@ function resolveSettingsSection(ctx) {
|
|
|
76
117
|
try {
|
|
77
118
|
const settings = ctx.get("settings");
|
|
78
119
|
if (settings && typeof settings.get === "function") {
|
|
79
|
-
return settings.get(
|
|
120
|
+
return settings.get("llm-deepseek");
|
|
80
121
|
}
|
|
81
122
|
} catch {}
|
|
82
123
|
return void 0;
|
|
@@ -136,81 +177,93 @@ const number = (match, index) => {
|
|
|
136
177
|
return Number.isFinite(value) ? value : void 0;
|
|
137
178
|
};
|
|
138
179
|
|
|
139
|
-
/** Parse the zh-CN docs page: CNY
|
|
180
|
+
/** Parse the zh-CN docs page: CNY peak/off-peak table (2 columns: flash, pro). */
|
|
140
181
|
function parseCnPricing(html) {
|
|
141
182
|
const text = stripTags(html);
|
|
142
|
-
const
|
|
143
|
-
const re = new RegExp(
|
|
183
|
+
const grab = (label) => {
|
|
184
|
+
const re = new RegExp(label + "\\s*空闲时段\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*高峰时段\\s*([\\d.]+)元\\s*([\\d.]+)元");
|
|
144
185
|
const m = text.match(re);
|
|
145
186
|
if (!m) return void 0;
|
|
146
187
|
return {
|
|
147
|
-
offPeak:
|
|
148
|
-
peak:
|
|
188
|
+
offPeak: [number(m, 1), number(m, 2)],
|
|
189
|
+
peak: [number(m, 3), number(m, 4)]
|
|
149
190
|
};
|
|
150
191
|
};
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let legacy;
|
|
163
|
-
const lm = text.match(/百万tokens输入(缓存命中)\s*([\d.]+)元\s*([\d.]+)元\s*百万tokens输入(缓存未命中)\s*([\d.]+)元\s*([\d.]+)元\s*百万tokens输出\s*([\d.]+)元\s*([\d.]+)元/);
|
|
164
|
-
if (lm) {
|
|
165
|
-
legacy = {
|
|
166
|
-
flash: { hit: number(lm, 1), miss: number(lm, 3), output: number(lm, 5) },
|
|
167
|
-
pro: { hit: number(lm, 2), miss: number(lm, 4), output: number(lm, 6) }
|
|
168
|
-
};
|
|
169
|
-
}
|
|
192
|
+
const hit = grab("百万tokens输入\\s*(缓存命中)");
|
|
193
|
+
const miss = grab("百万tokens输入\\s*(缓存未命中)");
|
|
194
|
+
const out = grab("百万tokens输出");
|
|
195
|
+
if (!hit || !miss || !out) throw new Error("CNY peak/off-peak table not found");
|
|
196
|
+
|
|
197
|
+
const mk = (i) => ({
|
|
198
|
+
offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
|
|
199
|
+
peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
|
|
200
|
+
});
|
|
170
201
|
|
|
171
202
|
return {
|
|
172
|
-
effectiveFrom,
|
|
173
203
|
models: {
|
|
174
|
-
"deepseek-
|
|
175
|
-
"deepseek-v4-pro":
|
|
204
|
+
"deepseek-flash": mk(0),
|
|
205
|
+
"deepseek-v4-pro": mk(1)
|
|
176
206
|
}
|
|
177
207
|
};
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
/** Parse the English docs page: USD
|
|
210
|
+
/** Parse the English docs page: USD peak/off-peak table (2 columns: flash, pro). */
|
|
181
211
|
function parseUsModels(html) {
|
|
182
212
|
const text = stripTags(html);
|
|
183
|
-
const
|
|
184
|
-
const re = new RegExp(
|
|
213
|
+
const grab = (label) => {
|
|
214
|
+
const re = new RegExp(label + "\\s+OFF-PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)", "i");
|
|
185
215
|
const m = text.match(re);
|
|
186
216
|
if (!m) return void 0;
|
|
187
217
|
return {
|
|
188
|
-
offPeak:
|
|
189
|
-
peak:
|
|
218
|
+
offPeak: [number(m, 1), number(m, 2)],
|
|
219
|
+
peak: [number(m, 3), number(m, 4)]
|
|
190
220
|
};
|
|
191
221
|
};
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
pro: { hit: number(lm, 2), miss: number(lm, 4), output: number(lm, 6) }
|
|
202
|
-
};
|
|
203
|
-
}
|
|
222
|
+
const hit = grab("1M INPUT TOKENS\\s*\\(CACHE HIT\\)");
|
|
223
|
+
const miss = grab("1M INPUT TOKENS\\s*\\(CACHE MISS\\)");
|
|
224
|
+
const out = grab("1M OUTPUT TOKENS");
|
|
225
|
+
if (!hit || !miss || !out) throw new Error("USD peak/off-peak table not found");
|
|
226
|
+
|
|
227
|
+
const mk = (i) => ({
|
|
228
|
+
offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
|
|
229
|
+
peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
|
|
230
|
+
});
|
|
204
231
|
|
|
205
232
|
return {
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
models: {
|
|
234
|
+
"deepseek-flash": mk(0),
|
|
235
|
+
"deepseek-v4-pro": mk(1)
|
|
236
|
+
}
|
|
208
237
|
};
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
const pricing = { data: FALLBACK_PRICING, fetchedAt: 0, source: "fallback" };
|
|
212
241
|
let pricingRefresh = null;
|
|
213
242
|
|
|
243
|
+
/** Merge freshly parsed current prices into the era in force right now. */
|
|
244
|
+
function mergeLegacy(models, fallbackModels) {
|
|
245
|
+
const result = {};
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
for (const [id, fb] of Object.entries(fallbackModels)) {
|
|
248
|
+
const parsed = models[id];
|
|
249
|
+
if (!parsed || !Array.isArray(fb.eras)) {
|
|
250
|
+
result[id] = fb;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
let curIdx = fb.eras.length - 1;
|
|
254
|
+
for (let i = 0; i < fb.eras.length; i++) {
|
|
255
|
+
const from = fb.eras[i].from;
|
|
256
|
+
if (from !== null && from !== void 0 && now >= Date.parse(from)) {
|
|
257
|
+
curIdx = i;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const eras = fb.eras.map((era, i) => (i === curIdx ? { ...era, peak: parsed.peak, offPeak: parsed.offPeak } : era));
|
|
262
|
+
result[id] = { ...fb, eras };
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
214
267
|
async function refreshPricing(ctx) {
|
|
215
268
|
if (pricingRefresh) return pricingRefresh;
|
|
216
269
|
pricingRefresh = (async () => {
|
|
@@ -218,6 +271,7 @@ async function refreshPricing(ctx) {
|
|
|
218
271
|
if (ctx?.logger?.warn) ctx.logger.warn(`deepseek-balance: ${message}`);
|
|
219
272
|
};
|
|
220
273
|
let effectiveFrom = FALLBACK_PRICING.effectiveFrom;
|
|
274
|
+
let weekendFrom = FALLBACK_PRICING.weekendFrom;
|
|
221
275
|
let cnModels = FALLBACK_PRICING.currencies.CNY.models;
|
|
222
276
|
let usModels = FALLBACK_PRICING.currencies.USD.models;
|
|
223
277
|
try {
|
|
@@ -228,8 +282,7 @@ async function refreshPricing(ctx) {
|
|
|
228
282
|
if (cnRes.ok) {
|
|
229
283
|
try {
|
|
230
284
|
const parsed = parseCnPricing(await cnRes.text());
|
|
231
|
-
|
|
232
|
-
cnModels = parsed.models;
|
|
285
|
+
cnModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.CNY.models);
|
|
233
286
|
} catch (error) {
|
|
234
287
|
warn(`CNY pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
288
|
}
|
|
@@ -238,7 +291,8 @@ async function refreshPricing(ctx) {
|
|
|
238
291
|
}
|
|
239
292
|
if (usRes.ok) {
|
|
240
293
|
try {
|
|
241
|
-
|
|
294
|
+
const parsed = parseUsModels(await usRes.text());
|
|
295
|
+
usModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.USD.models);
|
|
242
296
|
} catch (error) {
|
|
243
297
|
warn(`USD pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
298
|
}
|
|
@@ -247,6 +301,7 @@ async function refreshPricing(ctx) {
|
|
|
247
301
|
}
|
|
248
302
|
pricing.data = {
|
|
249
303
|
effectiveFrom,
|
|
304
|
+
weekendFrom,
|
|
250
305
|
currencies: {
|
|
251
306
|
CNY: { symbol: "¥", models: cnModels },
|
|
252
307
|
USD: { symbol: "$", models: usModels }
|
|
@@ -333,9 +388,12 @@ function beijingDayKey(ms) {
|
|
|
333
388
|
return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 10);
|
|
334
389
|
}
|
|
335
390
|
|
|
336
|
-
function periodOfUsage(time, effectiveFromMs) {
|
|
391
|
+
function periodOfUsage(time, effectiveFromMs, weekendFromMs) {
|
|
337
392
|
if (time < effectiveFromMs) return "flat";
|
|
338
|
-
const
|
|
393
|
+
const d = new Date(time + 8 * 3600 * 1000);
|
|
394
|
+
const day = d.getUTCDay();
|
|
395
|
+
if ((day === 0 || day === 6) && Number.isFinite(weekendFromMs) && time >= weekendFromMs) return "offPeak";
|
|
396
|
+
const hour = d.getUTCHours();
|
|
339
397
|
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18) ? "peak" : "offPeak";
|
|
340
398
|
}
|
|
341
399
|
|
|
@@ -378,6 +436,7 @@ function aggregateSessions(archivedIds = []) {
|
|
|
378
436
|
if (!existsSync(root)) return summary;
|
|
379
437
|
const effectiveFromMs = Date.parse(pricing.data.effectiveFrom || FALLBACK_PRICING.effectiveFrom);
|
|
380
438
|
if (!Number.isFinite(effectiveFromMs)) return summary;
|
|
439
|
+
const weekendFromMs = Date.parse(pricing.data.weekendFrom || FALLBACK_PRICING.weekendFrom);
|
|
381
440
|
|
|
382
441
|
let wsDirs = [];
|
|
383
442
|
try { wsDirs = readdirSync(root); } catch { return summary; }
|
|
@@ -440,13 +499,16 @@ function aggregateSessions(archivedIds = []) {
|
|
|
440
499
|
else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") usage = r.data.chunk.usage;
|
|
441
500
|
if (usage && time !== null && dedupe.has(r)) {
|
|
442
501
|
const t = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0);
|
|
443
|
-
const
|
|
502
|
+
const modelId = resolveModelId(model);
|
|
503
|
+
const eraIdx = priceEraIndex(modelId, time);
|
|
504
|
+
const era = priceEraAt(modelId, time);
|
|
505
|
+
const period = era && era.legacy ? "flat" : periodOfUsage(time, effectiveFromMs, weekendFromMs);
|
|
444
506
|
const dayKey = beijingDayKey(time);
|
|
445
507
|
tokens += t;
|
|
446
508
|
if (period === "peak") peakTokens += t;
|
|
447
509
|
else if (period === "offPeak") offPeakTokens += t;
|
|
448
510
|
else flatTokens += t;
|
|
449
|
-
const mkModel =
|
|
511
|
+
const mkModel = `${modelId}@${eraIdx}`;
|
|
450
512
|
const b = bucketsFrom(usage);
|
|
451
513
|
dayTokens[dayKey] = dayTokens[dayKey] || {};
|
|
452
514
|
dayTokens[dayKey][mkModel] = dayTokens[dayKey][mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
|
|
@@ -545,10 +607,9 @@ function computeSpend(summary, currency = "CNY") {
|
|
|
545
607
|
pricing.data.currencies?.CNY ||
|
|
546
608
|
FALLBACK_PRICING.currencies.CNY;
|
|
547
609
|
const models = pricingSet.models || {};
|
|
548
|
-
const defaultModel = models[DEFAULT_MODEL] || { legacy: null, peak: { miss: 0, hit: 0, output: 0 }, offPeak: { miss: 0, hit: 0, output: 0 } };
|
|
549
610
|
const priceOf = (tokens, price) => (tokens || 0) * (price || 0) / 1e6;
|
|
550
|
-
const
|
|
551
|
-
if (!
|
|
611
|
+
const eraCost = (b, era) => {
|
|
612
|
+
if (!era) return 0;
|
|
552
613
|
const cost = (bucket, price) => {
|
|
553
614
|
if (!price) return 0;
|
|
554
615
|
return priceOf(bucket.uncachedInputTokens, price.miss)
|
|
@@ -556,22 +617,30 @@ function computeSpend(summary, currency = "CNY") {
|
|
|
556
617
|
+ priceOf(bucket.cacheReadTokens, price.hit)
|
|
557
618
|
+ priceOf(bucket.outputTokens, price.output);
|
|
558
619
|
};
|
|
559
|
-
|
|
620
|
+
if (era.legacy) return cost(b.flat, era.legacy);
|
|
621
|
+
return cost(b.peak, era.peak) + cost(b.offPeak, era.offPeak);
|
|
622
|
+
};
|
|
623
|
+
// Bucket keys are `${modelId}@${eraIndex}` so each token is billed at the
|
|
624
|
+
// price era in force when it was used.
|
|
625
|
+
const bucketCost = (b, key) => {
|
|
626
|
+
const at = typeof key === "string" ? key.lastIndexOf("@") : -1;
|
|
627
|
+
if (at < 0) return 0;
|
|
628
|
+
const modelId = key.slice(0, at);
|
|
629
|
+
const eraIdx = Number(key.slice(at + 1));
|
|
630
|
+
const mm = models[modelId];
|
|
631
|
+
if (!mm || !Array.isArray(mm.eras)) return 0;
|
|
632
|
+
const era = mm.eras[Number.isFinite(eraIdx) ? eraIdx : 0] || mm.eras[mm.eras.length - 1];
|
|
633
|
+
return eraCost(b, era);
|
|
560
634
|
};
|
|
561
635
|
let total = 0;
|
|
562
636
|
for (const [k, b] of Object.entries(summary.byModel || {})) {
|
|
563
|
-
|
|
564
|
-
total += bucketCost(b, models[m] || defaultModel);
|
|
637
|
+
total += bucketCost(b, k);
|
|
565
638
|
}
|
|
566
639
|
const day = (summary.days || {})[beijingDayKey(Date.now())];
|
|
567
640
|
let today = null;
|
|
568
641
|
if (day) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
for (const [m, b] of Object.entries(day.byModel)) today += bucketCost(b, models[m] || defaultModel);
|
|
572
|
-
} else {
|
|
573
|
-
today = bucketCost(day, defaultModel);
|
|
574
|
-
}
|
|
642
|
+
today = 0;
|
|
643
|
+
for (const [k, b] of Object.entries(day.byModel || {})) today += bucketCost(b, k);
|
|
575
644
|
}
|
|
576
645
|
return { total, today, currency };
|
|
577
646
|
}
|
|
@@ -705,7 +774,7 @@ function apply(ctx, config = {}) {
|
|
|
705
774
|
spend: computeSpend({ byModel: s.byModel || {} }, usageCache.currency || "CNY"),
|
|
706
775
|
apiKeyPreview: key,
|
|
707
776
|
tokenName: resolveTokenName(config),
|
|
708
|
-
effectiveFrom: pricing.data.effectiveFrom,
|
|
777
|
+
effectiveFrom: pricing.data.effectiveFrom, weekendFrom: pricing.data.weekendFrom,
|
|
709
778
|
fetchedAt: usageCache.at,
|
|
710
779
|
source: pricing.source
|
|
711
780
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lancecheney/dsh-deepseek-balance",
|
|
3
3
|
"description": "Session-header DeepSeek API balance, per-conversation spend, and peak/off-peak pricing indicator",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|