@sixzjd/dsh-usage-dashboard 0.0.0-stage → 0.6.0

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/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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Usage Meter",
4
+ "description": "View model usage, cache efficiency, and cost statistics."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "用量统计",
4
+ "description": "查看模型使用量、缓存效率与成本统计。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@sixzjd/dsh-usage-dashboard",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "displayName": "用量统计",
4
+ "version": "0.6.0",
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
+ }