dsh-usage-badge 0.1.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,485 @@
1
+ /**
2
+ * dsh-usage-badge — pricing model and cost math.
3
+ *
4
+ * Ported from the DeepSeek-Harness desktop shell's `usage-sidecar.mjs` so that an
5
+ * existing `$DSH_HOME/storages/usage-pricing.json` keeps behaving identically:
6
+ * the same four override-key shapes (model / provider|model / provider|* / *|model),
7
+ * the same case-insensitive key matching, and the same per-request application of
8
+ * the model multiplier, the context-length tier and the peak/valley rule.
9
+ *
10
+ * Naming note: every cost function here returns an amount in the *total currency*
11
+ * (`totalCurrency`, CNY by default), not necessarily USD — the sidecar inherited
12
+ * the misleading `usageCostUsd` name, which this port drops.
13
+ *
14
+ * @module dsh-usage-badge/pricing
15
+ */
16
+
17
+ /** The price fields resolved per (provider, model). */
18
+ export const PRICE_KEYS = ['inputPerMillion', 'cacheReadPerMillion', 'cacheWritePerMillion', 'outputPerMillion']
19
+
20
+ /**
21
+ * Written to `usage-pricing.json` on first run (only when the file is missing) so
22
+ * a fresh install starts from a working table instead of zero prices. Mirrors the
23
+ * shell's template.
24
+ */
25
+ export const DEFAULT_TEMPLATE = {
26
+ exchangeRate: 6.74,
27
+ default: {
28
+ inputPerMillion: 1.5,
29
+ cacheReadPerMillion: 0.05,
30
+ cacheWritePerMillion: 0,
31
+ outputPerMillion: 4.5,
32
+ currency: 'cny',
33
+ },
34
+ totalCurrency: 'cny',
35
+ multiplier: 1,
36
+ overrides: {
37
+ 'deepseek-v4-flash': {
38
+ inputPerMillion: 1.5,
39
+ cacheReadPerMillion: 0.05,
40
+ cacheWritePerMillion: 0,
41
+ outputPerMillion: 4.5,
42
+ currency: 'cny',
43
+ multiplier: 1,
44
+ timeOfUse: {
45
+ enabled: true,
46
+ peakMultiplier: 2,
47
+ valleyMultiplier: 1,
48
+ peakRanges: [[9, 12], [14, 18]],
49
+ days: 'weekday',
50
+ },
51
+ },
52
+ 'deepseek-v4-pro': {
53
+ inputPerMillion: 4.5,
54
+ cacheReadPerMillion: 0.15,
55
+ cacheWritePerMillion: 0,
56
+ outputPerMillion: 13.5,
57
+ currency: 'cny',
58
+ multiplier: 1,
59
+ timeOfUse: {
60
+ enabled: true,
61
+ peakMultiplier: 2,
62
+ valleyMultiplier: 1,
63
+ peakRanges: [[9, 12], [14, 18]],
64
+ days: 'weekday',
65
+ },
66
+ },
67
+ },
68
+ }
69
+
70
+ /**
71
+ * Normalize a parsed pricing document, applying the template only as a *default
72
+ * row* fallback. Override rows are the user's explicit set and are never merged
73
+ * back from the template — otherwise a model the user deleted would silently
74
+ * reappear.
75
+ *
76
+ * @param {unknown} raw - parsed JSON, or anything when the file is unreadable.
77
+ * @returns {object} a pricing document safe for the cost functions.
78
+ */
79
+ export function normalizePricing(raw) {
80
+ const user = raw && typeof raw === 'object' ? raw : {}
81
+ return {
82
+ exchangeRate: Number(user.exchangeRate) || DEFAULT_TEMPLATE.exchangeRate,
83
+ default: { ...DEFAULT_TEMPLATE.default, ...(user.default ?? {}) },
84
+ overrides: user.overrides && typeof user.overrides === 'object' ? user.overrides : {},
85
+ timeOfUse: user.timeOfUse,
86
+ multiplier: user.multiplier ?? DEFAULT_TEMPLATE.multiplier,
87
+ contextMultiplier: user.contextMultiplier,
88
+ totalCurrency: user.totalCurrency ?? DEFAULT_TEMPLATE.totalCurrency,
89
+ // The legal-holiday calendar the peak/valley rule consults; a separate field
90
+ // from `timeOfUse` because it says which days peak windows may apply to at all.
91
+ holidays: user.holidays,
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Match keys for one request, in resolution priority order: a pure model name
97
+ * (no provider) wins, then the exact `provider|model`, then the `provider|*` and
98
+ * `*|model` wildcards. This lets a single row like `"deepseek-v4-flash"` apply to
99
+ * every provider.
100
+ *
101
+ * @param {string|null} provider - provider id as recorded in the session log.
102
+ * @param {string|null} model - model id as recorded in the session log.
103
+ * @returns {string[]} candidate override keys, highest priority first.
104
+ */
105
+ export function matchKeys(provider, model) {
106
+ const keys = []
107
+ if (model) keys.push(model)
108
+ if (provider && model) keys.push(`${provider}|${model}`)
109
+ if (provider) keys.push(`${provider}|*`)
110
+ if (model) keys.push(`*|${model}`)
111
+ return keys
112
+ }
113
+
114
+ const hasOwnKey = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
115
+
116
+ /**
117
+ * Lowercase → authored-key index, memoized per pricing object.
118
+ *
119
+ * Override rows are matched case-insensitively because a model id reaches us
120
+ * spelled exactly as the session log records it, while the price row is typed by
121
+ * hand: a row named `deepSeek-flash` must still price the model `deepseek-flash`,
122
+ * and the same holds for both sides of `provider|model`. Precedence stays
123
+ * predictable: an exact key always wins, otherwise the first row whose lowercased
124
+ * key matches is used.
125
+ */
126
+ const lowerOverrideKeys = new WeakMap()
127
+
128
+ function overrideKeysByLower(pricing) {
129
+ const rows = pricing.overrides
130
+ if (!rows) return new Map()
131
+ let index = lowerOverrideKeys.get(rows)
132
+ if (!index) {
133
+ index = new Map()
134
+ for (const key of Object.keys(rows)) {
135
+ const lower = key.toLowerCase()
136
+ if (!index.has(lower)) index.set(lower, key)
137
+ }
138
+ lowerOverrideKeys.set(rows, index)
139
+ }
140
+ return index
141
+ }
142
+
143
+ /** The override row for one match key, or undefined when no row applies. */
144
+ function overrideRow(pricing, key) {
145
+ const rows = pricing.overrides
146
+ if (!rows) return undefined
147
+ if (hasOwnKey(rows, key)) return rows[key]
148
+ const actual = overrideKeysByLower(pricing).get(String(key).toLowerCase())
149
+ return actual === undefined ? undefined : rows[actual]
150
+ }
151
+
152
+ /**
153
+ * Resolve the four price fields for one route.
154
+ *
155
+ * Each field is resolved independently: the highest-priority row that *defines*
156
+ * that field wins, so a `provider|model` row may override just the output price
157
+ * while inheriting the rest from a broader row or the default row.
158
+ */
159
+ export function resolvePrice(pricing, provider, model) {
160
+ const out = {}
161
+ for (const k of PRICE_KEYS) {
162
+ out[k] = Number(pricing.default?.[k]) || 0
163
+ for (const key of matchKeys(provider, model)) {
164
+ const row = overrideRow(pricing, key)
165
+ if (row && row[k] != null) {
166
+ out[k] = Number(row[k]) || 0
167
+ break
168
+ }
169
+ }
170
+ }
171
+ return out
172
+ }
173
+
174
+ /** Per-model peak/valley rule: the first matching row's own rule, else the global one. */
175
+ export function modelTimeOfUse(pricing, provider, model) {
176
+ for (const key of matchKeys(provider, model)) {
177
+ const row = overrideRow(pricing, key)
178
+ if (row && row.timeOfUse) return row.timeOfUse
179
+ }
180
+ return pricing.timeOfUse
181
+ }
182
+
183
+ /** Per-model multiplier applied to the whole request cost (row wins, else global, default 1). */
184
+ export function modelMultiplier(pricing, provider, model) {
185
+ for (const key of matchKeys(provider, model)) {
186
+ const row = overrideRow(pricing, key)
187
+ if (row && row.multiplier != null) return Number(row.multiplier) || 1
188
+ }
189
+ return Number(pricing.multiplier) || 1
190
+ }
191
+
192
+ /** Per-model context-length tier rule (row wins, else the default-row rule). */
193
+ export function modelContextMultiplier(pricing, provider, model) {
194
+ for (const key of matchKeys(provider, model)) {
195
+ const row = overrideRow(pricing, key)
196
+ if (row && row.contextMultiplier != null) return row.contextMultiplier
197
+ }
198
+ return pricing.contextMultiplier
199
+ }
200
+
201
+ /** The currency a route's prices are quoted in (`cny` unless the row says otherwise). */
202
+ export function resolveCurrency(pricing, provider, model) {
203
+ for (const key of matchKeys(provider, model)) {
204
+ const row = overrideRow(pricing, key)
205
+ if (row && row.currency) return row.currency
206
+ }
207
+ return pricing.default?.currency || 'cny'
208
+ }
209
+
210
+ /** The currency every summed total is expressed in. */
211
+ export function totalCurrencyOf(pricing) {
212
+ return pricing.totalCurrency === 'usd' ? 'usd' : 'cny'
213
+ }
214
+
215
+ /**
216
+ * Convert a cost quoted in `from` into the configured total currency. Same-currency
217
+ * conversion is a no-op, so an all-CNY table needs no exchange rate at all.
218
+ */
219
+ export function convertCost(cost, from, pricing) {
220
+ const total = totalCurrencyOf(pricing)
221
+ if (from === total) return cost
222
+ const rate = Number(pricing.exchangeRate) || 1
223
+ return from === 'cny' ? cost / rate : cost * rate
224
+ }
225
+
226
+ /** Cost of one token bucket at one resolved price, before any multiplier. */
227
+ export function tokenCost(tokens, price) {
228
+ return (
229
+ ((Number(tokens.input) || 0) / 1e6) * price.inputPerMillion +
230
+ ((Number(tokens.cacheRead) || 0) / 1e6) * price.cacheReadPerMillion +
231
+ ((Number(tokens.cacheWrite) || 0) / 1e6) * price.cacheWritePerMillion +
232
+ ((Number(tokens.output) || 0) / 1e6) * price.outputPerMillion
233
+ )
234
+ }
235
+
236
+ /**
237
+ * The request's input context size used by the tiered-price rule. DSH records
238
+ * uncached input and cache usage as disjoint values; their sum is the input
239
+ * context its own tiered calculation uses.
240
+ */
241
+ export function contextTokens(tokens) {
242
+ return (Number(tokens.input) || 0) + (Number(tokens.cacheRead) || 0) + (Number(tokens.cacheWrite) || 0)
243
+ }
244
+
245
+ /**
246
+ * Parse a token count written either as a number or as a compact string with a
247
+ * K / M / B suffix (case-insensitive, optional whitespace): `128000`, `"128K"`,
248
+ * `"1.5M"`, `"2b"`. Returns NaN for anything unparseable.
249
+ */
250
+ export function parseTokenCount(value) {
251
+ if (typeof value === 'number') return Number.isFinite(value) ? value : NaN
252
+ const text = String(value ?? '').trim()
253
+ if (!text) return NaN
254
+ const m = /^(\d+(?:\.\d+)?)\s*([kKmMbB]?)$/.exec(text)
255
+ if (!m) return NaN
256
+ const suffix = m[2] ? m[2].toLowerCase() : ''
257
+ const scale = suffix === 'k' ? 1e3 : suffix === 'm' ? 1e6 : suffix === 'b' ? 1e9 : 1
258
+ return parseFloat(m[1]) * scale
259
+ }
260
+
261
+ /** The context-tier multiplier for one request: strictly above the threshold, else 1. */
262
+ export function contextMultiplierFor(pricing, provider, model, tokens) {
263
+ const rule = modelContextMultiplier(pricing, provider, model)
264
+ const threshold = parseTokenCount(rule?.threshold)
265
+ const multiplier = Number(rule?.multiplier)
266
+ if (!Number.isFinite(threshold) || threshold <= 0 || !Number.isFinite(multiplier) || multiplier <= 0) return 1
267
+ return contextTokens(tokens) > threshold ? multiplier : 1
268
+ }
269
+
270
+ /**
271
+ * Cost of one per-request usage record, in the total currency. The peak/valley
272
+ * multiplier, the model multiplier and the context tier all apply per request,
273
+ * which is why the raw per-request records are kept rather than only aggregates.
274
+ *
275
+ * @param {{input:number,cacheRead:number,cacheWrite:number,output:number}} tokens - this request's usage.
276
+ * @param {number} hour - local hour (0–23) the request was sent.
277
+ * @param {object} day - `{ date, weekday, class, name }` of the request's day; a bare
278
+ * weekday number is accepted for callers that have no calendar.
279
+ */
280
+ export function requestCost(tokens, provider, model, hour, pricing, day) {
281
+ const context = normalizeDay(day)
282
+ const price = resolvePrice(pricing, provider, model)
283
+ const timeMultiplier = multiplierFor(context.weekday, hour, modelTimeOfUse(pricing, provider, model), context.class)
284
+ const modelMultiplierValue = modelMultiplier(pricing, provider, model)
285
+ const contextMultiplierValue = contextMultiplierFor(pricing, provider, model, tokens)
286
+ const cost = tokenCost(tokens, price) * timeMultiplier * modelMultiplierValue * contextMultiplierValue
287
+ return convertCost(cost, resolveCurrency(pricing, provider, model), pricing)
288
+ }
289
+
290
+ /**
291
+ * Cost for an aggregate-shaped bucket whose per-request records are unavailable
292
+ * (a v1 cache row). It prices the whole bucket flat at the current hour and skips
293
+ * the context tier, matching the shell's pre-context-tier behavior.
294
+ */
295
+ export function legacyCost(tokens, provider, model, hour, pricing, day) {
296
+ const context = normalizeDay(day)
297
+ const price = resolvePrice(pricing, provider, model)
298
+ const timeMultiplier = multiplierFor(context.weekday, hour, modelTimeOfUse(pricing, provider, model), context.class)
299
+ const cost = tokenCost(tokens, price) * timeMultiplier * modelMultiplier(pricing, provider, model)
300
+ return convertCost(cost, resolveCurrency(pricing, provider, model), pricing)
301
+ }
302
+
303
+ /**
304
+ * Per-hour cost array (0…23) for one bucket, in a single pass over its request
305
+ * records so a day chart never rescans the list once per hour.
306
+ */
307
+ export function bucketHourlyCosts(bucket, pricing, day) {
308
+ const context = normalizeDay(day)
309
+ const out = new Array(24).fill(0)
310
+ if (Array.isArray(bucket.usageRecords)) {
311
+ for (const usage of bucket.usageRecords) {
312
+ let hour = Number.isInteger(usage?.hour) ? usage.hour : new Date().getHours()
313
+ if (hour < 0 || hour > 23) hour = new Date().getHours()
314
+ out[hour] += requestCost(usage, bucket.provider, bucket.model, hour, pricing, context)
315
+ }
316
+ return out
317
+ }
318
+ for (let hour = 0; hour < 24; hour++) {
319
+ const tokens = bucket.hourly?.[hour]
320
+ out[hour] = tokens ? legacyCost(tokens, bucket.provider, bucket.model, hour, pricing, context) : 0
321
+ }
322
+ return out
323
+ }
324
+
325
+ /** Total cost of one bucket in the total currency. */
326
+ export function bucketCost(bucket, pricing, day) {
327
+ const context = normalizeDay(day)
328
+ if (!Array.isArray(bucket.hourly) && !Array.isArray(bucket.usageRecords)) {
329
+ return legacyCost(bucket, bucket.provider, bucket.model, new Date().getHours(), pricing, context)
330
+ }
331
+ let total = 0
332
+ for (const hourCost of bucketHourlyCosts(bucket, pricing, context)) total += hourCost
333
+ return total
334
+ }
335
+
336
+ // ── local calendar helpers ───────────────────────────────────────────────────
337
+ // Day and hour bucketing follows the host's local calendar, which is what a
338
+ // "today" figure has to mean. `offsetMinutes()` is re-read per call rather than
339
+ // cached in a module constant so a host that changes zone (or crosses DST) is
340
+ // bucketed correctly on the next pass.
341
+
342
+ /** Minutes east of UTC for the host's current local time. */
343
+ export function offsetMinutes(now = Date.now()) {
344
+ return -new Date(now).getTimezoneOffset()
345
+ }
346
+
347
+ /** Local calendar day key (`YYYY-MM-DD`) for an epoch millisecond timestamp. */
348
+ export function dayKey(ms) {
349
+ const d = new Date(ms + offsetMinutes(ms) * 60000)
350
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`
351
+ }
352
+
353
+ /** Local hour (0–23) for an epoch millisecond timestamp. */
354
+ export function localHour(ms) {
355
+ return new Date(ms + offsetMinutes(ms) * 60000).getUTCHours()
356
+ }
357
+
358
+ /** ISO weekday (1=Monday … 7=Sunday) from a JS Date. */
359
+ export function weekdayOf(d) {
360
+ return ((d.getDay() + 6) % 7) + 1
361
+ }
362
+
363
+ /** ISO weekday (1=Mon … 7=Sun) of a local `YYYY-MM-DD` day key. */
364
+ export function weekdayFromDayKey(dk) {
365
+ const [y, m, d] = String(dk).split('-').map(Number)
366
+ return weekdayOf(new Date(y, m - 1, d))
367
+ }
368
+
369
+ /**
370
+ * The day context every cost function prices against: the local date key, its ISO
371
+ * weekday, and whether the holiday calendar marks it a public holiday.
372
+ *
373
+ * Only two classes exist, because that is all the published policy distinguishes:
374
+ * peak hours are 周一至周五 excluding public holidays, and *every* other hour —
375
+ * "包括周末及中国法定节假日全天" — is off-peak. A weekend the State Council turns
376
+ * into a working day (调休) is still a 周末, so it is deliberately NOT treated as a
377
+ * working day here; doing so would charge peak rates on a day the page calls
378
+ * off-peak in full.
379
+ *
380
+ * @param {string} dateKey - local `YYYY-MM-DD` day key.
381
+ * @param {object} [rules] - resolved holiday rules; absent means "no holiday handling".
382
+ * @returns {{date:string, weekday:number, class:'normal'|'holiday', name:string|null}}
383
+ */
384
+ export function dayContext(dateKey, rules) {
385
+ const weekday = weekdayFromDayKey(dateKey)
386
+ let cls = 'normal'
387
+ let name = null
388
+ if (rules?.enabled) {
389
+ const holidayName = rules.holidays?.get?.(dateKey)
390
+ if (holidayName !== undefined) {
391
+ cls = 'holiday'
392
+ name = holidayName || null
393
+ }
394
+ }
395
+ return { date: dateKey, weekday, class: cls, name }
396
+ }
397
+
398
+ /** A fresh 24-slot hourly token/request series. */
399
+ export function initHourly() {
400
+ return Array.from({ length: 24 }, () => ({ input: 0, cacheRead: 0, cacheWrite: 0, output: 0, requests: 0 }))
401
+ }
402
+
403
+ /**
404
+ * Whether a weekday matches a `days` rule: `"all"` / `"weekday"` / `"weekend"` /
405
+ * an array like `[1,2,3,4,5]`. A missing or unknown rule is lenient and matches
406
+ * every day, which keeps older configs behaving exactly as they did.
407
+ *
408
+ * Matching is on the calendar weekday, which is what the published policy names
409
+ * ("周一至周五"). A public holiday is handled separately, by {@link multiplierFor},
410
+ * because it is off-peak whatever weekday it falls on.
411
+ *
412
+ * @param {number} weekday - ISO weekday (1=Mon … 7=Sun).
413
+ * @param {unknown} days - the rule's `days` field.
414
+ */
415
+ export function dayMatches(weekday, days) {
416
+ if (days == null || days === '') return true
417
+ if (typeof days === 'string') {
418
+ const s = days.trim().toLowerCase()
419
+ if (s === 'all') return true
420
+ if (s === 'weekday' || s === 'workday') return weekday >= 1 && weekday <= 5
421
+ if (s === 'weekend') return weekday >= 6
422
+ return true
423
+ }
424
+ if (Array.isArray(days)) return days.some((d) => Number(d) === weekday)
425
+ return true
426
+ }
427
+
428
+ /** Tolerate a bare weekday number as well as a `{ date, weekday, class }` context. */
429
+ function normalizeDay(day) {
430
+ if (typeof day === 'number') return { date: null, weekday: day, class: 'normal', name: null }
431
+ if (day && typeof day === 'object') return { class: 'normal', name: null, ...day }
432
+ return { date: null, weekday: 1, class: 'normal', name: null }
433
+ }
434
+
435
+ /**
436
+ * Peak/valley multiplier for one local hour on one day. Flat (1) when time-of-use
437
+ * is off or the day does not match the `days` rule; the peak multiplier inside
438
+ * `peakRanges`; the valley multiplier otherwise. The peak multiplier is a
439
+ * surcharge and is floored at 1.
440
+ *
441
+ * A public holiday is off-peak for the whole day, which is DeepSeek's published
442
+ * rule — so the holiday check comes first and overrides every `days` form.
443
+ *
444
+ * @param {number} weekday - ISO weekday (1=Mon … 7=Sun).
445
+ * @param {number} hour - local hour (0–23).
446
+ * @param {object} tou - the resolved time-of-use rule.
447
+ * @param {'normal'|'holiday'} [dayClass] - holiday classification of the day.
448
+ */
449
+ export function multiplierFor(weekday, hour, tou, dayClass = 'normal') {
450
+ if (!tou || !tou.enabled) return 1
451
+ if (dayClass === 'holiday') return 1
452
+ if (!dayMatches(weekday, tou.days)) return 1
453
+ for (const range of tou.peakRanges || []) {
454
+ if (!Array.isArray(range) || range.length < 2) continue
455
+ const [s, e] = range
456
+ if (typeof s === 'number' && typeof e === 'number' && hour >= s && hour < e) {
457
+ return Math.max(1, Number(tou.peakMultiplier) || 1)
458
+ }
459
+ }
460
+ return Number(tou.valleyMultiplier ?? 1) || 0
461
+ }
462
+
463
+ /**
464
+ * The peak/valley state of one day at one hour, for the panel's period indicator.
465
+ *
466
+ * @param {object} pricing - the normalized pricing document.
467
+ * @param {string|null} provider - route provider, for a per-model rule lookup.
468
+ * @param {string|null} model - route model.
469
+ * @param {object|number} day - a `{ date, weekday, class, name }` context.
470
+ * @param {number} [hour] - local hour to evaluate; defaults to the current hour.
471
+ */
472
+ export function timeOfUseState(pricing, provider, model, day, hour = new Date().getHours()) {
473
+ const context = normalizeDay(day)
474
+ const tou = modelTimeOfUse(pricing, provider, model)
475
+ const multiplier = multiplierFor(context.weekday, hour, tou, context.class)
476
+ const peak = multiplier > 1
477
+ return {
478
+ enabled: Boolean(tou && tou.enabled),
479
+ peak,
480
+ label: peak ? 'peak' : 'valley',
481
+ multiplier,
482
+ dayClass: context.class,
483
+ dayName: context.name ?? null,
484
+ }
485
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "dsh-usage-badge",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness 的用量徽标插件:侧边栏底部一行显示当天的 token 总量与估算费用,点开可看 24 小时 / 近 7 日 / 近 30 日 / 近 12 个月用量,按 provider 筛选,并按 DeepSeek 官方定价与中国法定节假日计价。A usage badge plugin for DeepSeek Harness: today's tokens and estimated spend in the sidebar, with 24h / 7d / 30d / 12-month charts, official DeepSeek pricing and Chinese public holidays.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ai-written/dsh-usage-badge.git"
10
+ },
11
+ "homepage": "https://github.com/ai-written/dsh-usage-badge#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ai-written/dsh-usage-badge/issues"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./client": {
20
+ "default": "./lib/client.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "dsh": {
25
+ "manifestVersion": 1,
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "platform": "web"
31
+ }
32
+ },
33
+ "files": [
34
+ "lib",
35
+ "cordis.patch.yml",
36
+ "README.md",
37
+ "LICENSE",
38
+ "assets",
39
+ "docs"
40
+ ],
41
+ "scripts": {
42
+ "test": "node test/manifest.verify.mjs && node test/pricing.verify.mjs && node test/holidays.verify.mjs && node test/official-pricing.verify.mjs && node test/client.render.mjs",
43
+ "test:smoke": "node test/smoke.mjs",
44
+ "prepublishOnly": "npm test"
45
+ },
46
+ "engines": {
47
+ "node": ">=22.15"
48
+ },
49
+ "keywords": [
50
+ "dsh",
51
+ "dsh-plugin",
52
+ "deepseek-harness",
53
+ "usage",
54
+ "cost",
55
+ "badge",
56
+ "billing"
57
+ ],
58
+ "license": "MIT",
59
+ "devDependencies": {
60
+ "react": "^18.3.1",
61
+ "react-dom": "^18.3.1"
62
+ }
63
+ }