@sixzjd/dsh-usage-dashboard 0.0.0-stage → 0.6.1
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/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.en.md +212 -0
- package/README.md +194 -2
- package/client.js +357 -0
- package/cordis.patch.yml +11 -0
- package/lib/aggregate.js +422 -0
- package/lib/backfill.js +116 -0
- package/lib/catalog.js +230 -0
- package/lib/history.js +150 -0
- package/lib/index.js +2 -0
- package/lib/plugin.js +939 -0
- package/lib/pricing.js +232 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +81 -4
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost model for dsh-usage-dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Prices are USD per 1,000,000 tokens and belong to the MODEL, not to the route:
|
|
5
|
+
* the same model reached through a reseller is priced the same way, and a model
|
|
6
|
+
* with no entry is reported as unpriced instead of receiving a guessed rate.
|
|
7
|
+
*
|
|
8
|
+
* DeepSeek's peak/off-peak table is carried as data (verified against the
|
|
9
|
+
* official announcement effective 2026-08-16T16:00:00Z), never derived as an
|
|
10
|
+
* automatic "half price" rule, and it excludes Chinese statutory holidays.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const PRICING_EPOCH_MS = Date.UTC(2026, 7, 16, 16, 0, 0)
|
|
14
|
+
|
|
15
|
+
// Peak windows are UTC half-open ranges; everything else is off-peak.
|
|
16
|
+
// 01:00–04:00 and 06:00–10:00 UTC = 09:00–12:00 and 14:00–18:00 Beijing.
|
|
17
|
+
export const DEFAULT_PEAK_RULE = Object.freeze({
|
|
18
|
+
weekdays: [1, 2, 3, 4, 5],
|
|
19
|
+
windows: [
|
|
20
|
+
{ startMinute: 60, endMinute: 240 },
|
|
21
|
+
{ startMinute: 360, endMinute: 600 },
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// 2026 arrangement from the State Council notice
|
|
26
|
+
// (https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm).
|
|
27
|
+
// A later year ships with a plugin update or is extended by hand.
|
|
28
|
+
export const CHINA_HOLIDAYS_2026 = Object.freeze([
|
|
29
|
+
'2026-01-01', '2026-01-02', '2026-01-03', '2026-02-15', '2026-02-16', '2026-02-17',
|
|
30
|
+
'2026-02-18', '2026-02-19', '2026-02-20', '2026-02-21', '2026-02-22', '2026-02-23',
|
|
31
|
+
'2026-04-04', '2026-04-05', '2026-04-06', '2026-05-01', '2026-05-02', '2026-05-03',
|
|
32
|
+
'2026-05-04', '2026-05-05', '2026-06-19', '2026-06-20', '2026-06-21', '2026-09-25',
|
|
33
|
+
'2026-09-26', '2026-09-27', '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',
|
|
34
|
+
'2026-10-05', '2026-10-06', '2026-10-07',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Seed price table. `offPeak` is the always-on rate; `peak` only applies to
|
|
39
|
+
* models whose provider publishes a time-of-day table (first-party DeepSeek).
|
|
40
|
+
* Everything the user runs today that is NOT here stays unpriced on purpose:
|
|
41
|
+
* `qfmodel`, `nemotron-*-free` and `gpt-oss-120b` have no public per-token
|
|
42
|
+
* price to inherit, and inventing one would print a plausible wrong number.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_PRICES = Object.freeze([
|
|
45
|
+
{
|
|
46
|
+
modelId: 'deepseek-v4-flash',
|
|
47
|
+
label: 'DeepSeek V4.1 Flash',
|
|
48
|
+
offPeak: { input: '0.15', output: '0.6', cacheRead: '0.003', cacheWrite: '0' },
|
|
49
|
+
peak: { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0' },
|
|
50
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
51
|
+
effectiveFrom: PRICING_EPOCH_MS,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
modelId: 'deepseek-flash',
|
|
55
|
+
label: 'DeepSeek V4.1 Flash',
|
|
56
|
+
offPeak: { input: '0.15', output: '0.6', cacheRead: '0.003', cacheWrite: '0' },
|
|
57
|
+
peak: { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0' },
|
|
58
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
59
|
+
effectiveFrom: PRICING_EPOCH_MS,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
modelId: 'deepseek-v4-pro',
|
|
63
|
+
label: 'DeepSeek V4 Pro',
|
|
64
|
+
offPeak: { input: '0.66', output: '1.98', cacheRead: '0.022', cacheWrite: '0' },
|
|
65
|
+
peak: { input: '1.32', output: '3.96', cacheRead: '0.044', cacheWrite: '0' },
|
|
66
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
67
|
+
effectiveFrom: PRICING_EPOCH_MS,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
modelId: 'deepseek-v4-flash-vision-exp',
|
|
71
|
+
label: 'DeepSeek V4 Flash Vision Exp',
|
|
72
|
+
offPeak: { input: '0.15', output: '0.6', cacheRead: '0.003', cacheWrite: '0' },
|
|
73
|
+
peak: { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0' },
|
|
74
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
75
|
+
effectiveFrom: Date.UTC(2026, 7, 21, 0, 0, 0),
|
|
76
|
+
},
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
const BUCKETS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
80
|
+
|
|
81
|
+
function isRecord(value) {
|
|
82
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Parse a price string into an exact BigInt scaled by 1e12, or null when unusable. */
|
|
86
|
+
function parseRate(raw) {
|
|
87
|
+
if (typeof raw === 'number') {
|
|
88
|
+
if (!Number.isFinite(raw) || raw < 0) return null
|
|
89
|
+
return parseRate(String(raw))
|
|
90
|
+
}
|
|
91
|
+
if (typeof raw !== 'string') return null
|
|
92
|
+
const text = raw.trim()
|
|
93
|
+
if (text === '') return null
|
|
94
|
+
const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(text)
|
|
95
|
+
if (match === null) return null
|
|
96
|
+
const digits = `${match[1]}${match[2] ?? ''}`
|
|
97
|
+
const exponent = match[3] !== undefined ? Number(match[3]) : 0
|
|
98
|
+
const scale = (match[2]?.length ?? 0) - exponent
|
|
99
|
+
return { digits: BigInt(digits), scale }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const RATE_SCALE = 12n
|
|
103
|
+
|
|
104
|
+
/** Normalise a parsed rate to a fixed 1e12 scale so buckets can be summed. */
|
|
105
|
+
function toScaled(parsed) {
|
|
106
|
+
if (parsed === null) return null
|
|
107
|
+
const diff = RATE_SCALE - BigInt(parsed.scale)
|
|
108
|
+
if (diff >= 0n) return parsed.digits * 10n ** diff
|
|
109
|
+
return parsed.digits / 10n ** -diff
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatScaled(scaled) {
|
|
113
|
+
if (scaled === 0n) return '0'
|
|
114
|
+
const negative = scaled < 0n
|
|
115
|
+
const value = (negative ? -scaled : scaled).toString().padStart(Number(RATE_SCALE) + 1, '0')
|
|
116
|
+
const head = value.slice(0, value.length - Number(RATE_SCALE))
|
|
117
|
+
const tail = value.slice(value.length - Number(RATE_SCALE)).replace(/0+$/, '')
|
|
118
|
+
const body = tail === '' ? head : `${head}.${tail}`
|
|
119
|
+
return negative ? `-${body}` : body
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function emptyCost() {
|
|
123
|
+
return {
|
|
124
|
+
currency: 'USD',
|
|
125
|
+
input: '0', output: '0', cacheRead: '0', cacheWrite: '0',
|
|
126
|
+
total: '0',
|
|
127
|
+
priced: 0, unpriced: 0,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function addCost(target, source) {
|
|
132
|
+
for (const bucket of [...BUCKETS, 'total']) {
|
|
133
|
+
const a = parseRate(target[bucket])
|
|
134
|
+
const b = parseRate(source[bucket])
|
|
135
|
+
if (a === null || b === null) continue
|
|
136
|
+
target[bucket] = formatScaled(toScaled(a) + toScaled(b))
|
|
137
|
+
}
|
|
138
|
+
target.priced += source.priced
|
|
139
|
+
target.unpriced += source.unpriced
|
|
140
|
+
return target
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function addCosts(target, source) {
|
|
144
|
+
return addCost(target, source)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Decide the billing band for one instant.
|
|
149
|
+
*
|
|
150
|
+
* Usage recorded before a plan's `effectiveFrom` has no verifiable band, so it
|
|
151
|
+
* fails closed as unpriced rather than inheriting today's rates.
|
|
152
|
+
*/
|
|
153
|
+
export function bandFor(entry, atMs) {
|
|
154
|
+
if (!isRecord(entry)) return null
|
|
155
|
+
const peak = isRecord(entry.peak) ? entry.peak : null
|
|
156
|
+
if (peak === null) return 'off-peak'
|
|
157
|
+
if (Number.isFinite(entry.effectiveFrom) && atMs < entry.effectiveFrom) return null
|
|
158
|
+
const at = new Date(atMs)
|
|
159
|
+
const weekday = at.getUTCDay()
|
|
160
|
+
const rule = isRecord(entry.peakRule) ? entry.peakRule : DEFAULT_PEAK_RULE
|
|
161
|
+
const weekdays = Array.isArray(rule.weekdays) ? rule.weekdays : []
|
|
162
|
+
if (!weekdays.includes(weekday)) return 'off-peak'
|
|
163
|
+
const minute = at.getUTCHours() * 60 + at.getUTCMinutes()
|
|
164
|
+
const holidays = Array.isArray(entry.holidays) ? entry.holidays : []
|
|
165
|
+
const day = at.toISOString().slice(0, 10)
|
|
166
|
+
// A statutory holiday is a full off-peak day: no rule can claim its instants.
|
|
167
|
+
if (holidays.includes(day)) return 'off-peak'
|
|
168
|
+
const windows = Array.isArray(rule.windows) ? rule.windows : []
|
|
169
|
+
for (const window of windows) {
|
|
170
|
+
if (!isRecord(window)) continue
|
|
171
|
+
if (minute >= window.startMinute && minute < window.endMinute) return 'peak'
|
|
172
|
+
}
|
|
173
|
+
return 'off-peak'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Price one usage record. Returns null when the model has no entry at all,
|
|
178
|
+
* which the caller counts as `unpriced` rather than as zero.
|
|
179
|
+
*/
|
|
180
|
+
export function priceUsage(entry, usage, atMs) {
|
|
181
|
+
if (!isRecord(entry)) return null
|
|
182
|
+
const band = bandFor(entry, atMs)
|
|
183
|
+
if (band === null) return null
|
|
184
|
+
const rates = band === 'peak' && isRecord(entry.peak) ? entry.peak : entry.offPeak
|
|
185
|
+
if (!isRecord(rates)) return null
|
|
186
|
+
const out = emptyCost()
|
|
187
|
+
let scaledTotal = 0n
|
|
188
|
+
for (const bucket of BUCKETS) {
|
|
189
|
+
const rate = toScaled(parseRate(rates[bucket]))
|
|
190
|
+
const tokens = BigInt(Math.max(0, Math.trunc(Number(usage?.[bucket]) || 0)))
|
|
191
|
+
if (rate === null) continue
|
|
192
|
+
// price = tokens × ratePerMillion / 1e6, kept exact at 1e12 scale.
|
|
193
|
+
const scaled = (tokens * rate) / 1000000n
|
|
194
|
+
out[bucket] = formatScaled(scaled)
|
|
195
|
+
scaledTotal += scaled
|
|
196
|
+
}
|
|
197
|
+
out.total = formatScaled(scaledTotal)
|
|
198
|
+
out.priced = 1
|
|
199
|
+
return out
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Index a price list by normalised model id. Later rows win, so user edits override defaults. */
|
|
203
|
+
export function buildPriceIndex(entries) {
|
|
204
|
+
const index = new Map()
|
|
205
|
+
if (!Array.isArray(entries)) return index
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (!isRecord(entry) || typeof entry.modelId !== 'string') continue
|
|
208
|
+
const key = entry.modelId.trim().toLowerCase()
|
|
209
|
+
if (key === '') continue
|
|
210
|
+
index.set(key, entry)
|
|
211
|
+
}
|
|
212
|
+
return index
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Last path segment of a `vendor/model` route id, so reseller ids still match. */
|
|
216
|
+
export function normalizeModelId(raw) {
|
|
217
|
+
if (typeof raw !== 'string') return ''
|
|
218
|
+
const trimmed = raw.trim().toLowerCase()
|
|
219
|
+
if (trimmed === '') return ''
|
|
220
|
+
const slash = trimmed.lastIndexOf('/')
|
|
221
|
+
return slash >= 0 ? trimmed.slice(slash + 1) : trimmed
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function lookupPrice(index, modelId) {
|
|
225
|
+
if (index.size === 0) return null
|
|
226
|
+
const exact = index.get(String(modelId ?? '').trim().toLowerCase())
|
|
227
|
+
if (exact !== undefined) return exact
|
|
228
|
+
const short = normalizeModelId(modelId)
|
|
229
|
+
if (short === '') return null
|
|
230
|
+
const byShort = index.get(short)
|
|
231
|
+
return byShort === undefined ? null : byShort
|
|
232
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixzjd/dsh-usage-dashboard",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"displayName": "用量统计",
|
|
4
|
+
"version": "0.6.1",
|
|
5
|
+
"description": "Usage statistics for DeepSeek Harness: daily token usage, request count, cache hit rate and estimated cost as a native settings section, with durable history that survives deleted sessions.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./client.js"
|
|
15
|
+
},
|
|
16
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./locale/en.json": "./locale/en.json",
|
|
19
|
+
"./locale/zh.json": "./locale/zh.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"locale",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"client.js",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"README.md",
|
|
28
|
+
"README.en.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"client": {
|
|
36
|
+
"platform": "web",
|
|
37
|
+
"inject": [
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"compatibility": {
|
|
42
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0",
|
|
43
|
+
"dshReleases": {
|
|
44
|
+
"0.2.0-rc.2": "compatible"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "node --test",
|
|
50
|
+
"test:watch": "node --test --watch",
|
|
51
|
+
"check": "node --check client.js && node --check lib/index.js && npm test"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"dsh-plugin",
|
|
55
|
+
"dsh-bundle",
|
|
56
|
+
"deepseek-harness",
|
|
57
|
+
"deepseek",
|
|
58
|
+
"token-usage",
|
|
59
|
+
"usage-statistics",
|
|
60
|
+
"cost-tracking",
|
|
61
|
+
"cache-hit-rate",
|
|
62
|
+
"analytics"
|
|
63
|
+
],
|
|
64
|
+
"author": "sixzjd",
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/sixzjd/dsh-usage-dashboard.git"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/sixzjd/dsh-usage-dashboard/issues"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/sixzjd/dsh-usage-dashboard#readme",
|
|
73
|
+
"engines": {
|
|
74
|
+
"dsh": ">=0.2.0-rc.2",
|
|
75
|
+
"node": ">=22"
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
}
|
|
83
|
+
}
|