dsh-usage-badge 0.1.2 → 0.1.4
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 +31 -12
- package/assets/usage-dialog.png +0 -0
- package/docs/design-notes.md +172 -39
- package/lib/buckets.js +115 -0
- package/lib/cache-store.js +633 -0
- package/lib/client.js +1413 -169
- package/lib/fold.js +82 -178
- package/lib/holidays.js +5 -4
- package/lib/index.js +246 -30
- package/lib/official-pricing.js +14 -3
- package/lib/pricing.js +188 -20
- package/package.json +2 -2
package/lib/pricing.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* the same case-insensitive key matching, and the same per-request application of
|
|
8
8
|
* the model multiplier, the context-length tier and the peak/valley rule.
|
|
9
9
|
*
|
|
10
|
+
* Two rules were added on top of that port, both opt-out and both documented in
|
|
11
|
+
* `docs/design-notes.md`: a trailing `*` on a model part makes a family row, and — unless
|
|
12
|
+
* `modelPrefixFallback` is off — an ordinary row also prices its own separator-suffixed
|
|
13
|
+
* variants (`deepseek-flash` covers `deepseek-flash-preview`). `priceUnmatchedModels` can
|
|
14
|
+
* also be switched off, which makes a model no row matches cost nothing rather than being
|
|
15
|
+
* priced by the `default` row.
|
|
16
|
+
*
|
|
10
17
|
* Naming note: every cost function here returns an amount in the *total currency*
|
|
11
18
|
* (`totalCurrency`, CNY by default), not necessarily USD — the sidecar inherited
|
|
12
19
|
* the misleading `usageCostUsd` name, which this port drops.
|
|
@@ -18,9 +25,11 @@
|
|
|
18
25
|
export const PRICE_KEYS = ['inputPerMillion', 'cacheReadPerMillion', 'cacheWritePerMillion', 'outputPerMillion']
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
28
|
+
* The rates a table with no `default` row of its own falls back to, plus the exchange rate the
|
|
29
|
+
* shell used. **Never written to disk** — nothing in this module (or the plugin) persists it, it
|
|
30
|
+
* is only the in-memory fallback so a fresh install shows an estimate instead of zeroes. It is
|
|
31
|
+
* served to the panel as `template` so those rates can be labelled as what they are, and
|
|
32
|
+
* 「应用官方价格」 replaces them with the published list's first line.
|
|
24
33
|
*/
|
|
25
34
|
export const DEFAULT_TEMPLATE = {
|
|
26
35
|
exchangeRate: 6.74,
|
|
@@ -82,6 +91,16 @@ export function normalizePricing(raw) {
|
|
|
82
91
|
exchangeRate: Number(user.exchangeRate) || DEFAULT_TEMPLATE.exchangeRate,
|
|
83
92
|
default: { ...DEFAULT_TEMPLATE.default, ...(user.default ?? {}) },
|
|
84
93
|
overrides: user.overrides && typeof user.overrides === 'object' ? user.overrides : {},
|
|
94
|
+
// Implicit prefix fallback: an authored row also prices the suffixed variants of
|
|
95
|
+
// its own name, so the `deepseek-flash` row the official list writes covers
|
|
96
|
+
// `deepseek-flash-preview` with nothing hand-written. On unless switched off, and
|
|
97
|
+
// only an explicit `false` switches it off.
|
|
98
|
+
modelPrefixFallback: user.modelPrefixFallback !== false,
|
|
99
|
+
// Whether a model that no row matches is priced by the `default` row at all. On by
|
|
100
|
+
// default — pricing everything is what that row is for — and switched off it means
|
|
101
|
+
// "we do not know what this cost": the request contributes tokens and requests but no
|
|
102
|
+
// amount, and the snapshot reports which routes those are rather than a silent ¥0.
|
|
103
|
+
priceUnmatchedModels: user.priceUnmatchedModels !== false,
|
|
85
104
|
timeOfUse: user.timeOfUse,
|
|
86
105
|
multiplier: user.multiplier ?? DEFAULT_TEMPLATE.multiplier,
|
|
87
106
|
contextMultiplier: user.contextMultiplier,
|
|
@@ -98,6 +117,10 @@ export function normalizePricing(raw) {
|
|
|
98
117
|
* `*|model` wildcards. This lets a single row like `"deepseek-v4-flash"` apply to
|
|
99
118
|
* every provider.
|
|
100
119
|
*
|
|
120
|
+
* These are the *exact* keys a route can resolve to. Rows that end a model part with
|
|
121
|
+
* `*` are family rules and are appended after these by {@link rowsForRoute}, because
|
|
122
|
+
* an exact row always outranks a family one.
|
|
123
|
+
*
|
|
101
124
|
* @param {string|null} provider - provider id as recorded in the session log.
|
|
102
125
|
* @param {string|null} model - model id as recorded in the session log.
|
|
103
126
|
* @returns {string[]} candidate override keys, highest priority first.
|
|
@@ -149,6 +172,132 @@ function overrideRow(pricing, key) {
|
|
|
149
172
|
return actual === undefined ? undefined : rows[actual]
|
|
150
173
|
}
|
|
151
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The prefix a family row matches on, or null when the key is not a family row.
|
|
177
|
+
*
|
|
178
|
+
* A trailing `*` on the model part means "every model whose id starts with this", so
|
|
179
|
+
* the row `deepseek-flash-*` prices `deepseek-flash-preview` and any dated or
|
|
180
|
+
* experimental variant of the family without a row per name.
|
|
181
|
+
*
|
|
182
|
+
* The prefix must be non-empty, and that is exactly what keeps a bare `*` inert:
|
|
183
|
+
* `provider|*` already means "any model" through the exact candidate list, and a lone
|
|
184
|
+
* `*` key has never matched anything. The star is not a separator either, so
|
|
185
|
+
* `deepseek-flash-*` does not cover the model `deepseek-flash` itself — write
|
|
186
|
+
* `deepseek-flash*` when the family includes the unsuffixed name.
|
|
187
|
+
*/
|
|
188
|
+
function familyPrefix(pattern) {
|
|
189
|
+
const text = String(pattern)
|
|
190
|
+
return text.endsWith('*') && text.length > 1 ? text.slice(0, -1) : null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* What must follow a prefix for the *implicit* fallback to accept it: a separator.
|
|
195
|
+
*
|
|
196
|
+
* Without this, `gpt-4` would swallow `gpt-4o` — a different model with a different
|
|
197
|
+
* price — and `deepseek-flash` would swallow `deepseek-flashlight`. With it, the
|
|
198
|
+
* fallback only ever reaches `deepseek-flash-preview`, `deepseek.v3`, `qwen:72b`
|
|
199
|
+
* shapes. An explicit `deepseek-flash*` row skips this test: writing the star is how
|
|
200
|
+
* you say "yes, the glued-together names too".
|
|
201
|
+
*/
|
|
202
|
+
const PREFIX_BOUNDARY = /^[-_.:/@]/
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The prefix rows that apply to one route, most specific first.
|
|
206
|
+
*
|
|
207
|
+
* Two kinds end up here, and they share one ranking:
|
|
208
|
+
*
|
|
209
|
+
* - explicit family rows, a model part ending in `*` (`deepseek-flash-*`);
|
|
210
|
+
* - implicit ones, when `modelPrefixFallback` is on: an ordinary row also prices the
|
|
211
|
+
* suffixed variants of its own name, which is what lets the row the official price
|
|
212
|
+
* list writes — `deepseek-flash` — cover `deepseek-flash-preview` with nothing
|
|
213
|
+
* hand-written at all.
|
|
214
|
+
*
|
|
215
|
+
* Ranked by the shape they take — pure model, `provider|model`, `*|model`, the same
|
|
216
|
+
* order the exact keys resolve in — then by prefix length (the narrow `deepseek-flash-*`
|
|
217
|
+
* outranks the broad `deepseek-*`), and explicit wins a tie against implicit. A key that
|
|
218
|
+
* names a different provider, or has more than one `|`, belongs to no shape and is
|
|
219
|
+
* ignored, exactly as it was when it could never match.
|
|
220
|
+
*/
|
|
221
|
+
function familyRows(rows, provider, model, fallback) {
|
|
222
|
+
if (!model) return []
|
|
223
|
+
const routeProvider = String(provider ?? '').toLowerCase()
|
|
224
|
+
const routeModel = String(model).toLowerCase()
|
|
225
|
+
const found = []
|
|
226
|
+
for (const key of Object.keys(rows)) {
|
|
227
|
+
const parts = String(key).split('|')
|
|
228
|
+
if (parts.length > 2) continue
|
|
229
|
+
const modelPart = String(parts.length === 2 ? parts[1] : parts[0])
|
|
230
|
+
const prefix = familyPrefix(modelPart)
|
|
231
|
+
let length
|
|
232
|
+
if (prefix !== null) {
|
|
233
|
+
if (!routeModel.startsWith(prefix.toLowerCase())) continue
|
|
234
|
+
length = prefix.length
|
|
235
|
+
} else if (fallback) {
|
|
236
|
+
const name = modelPart.toLowerCase()
|
|
237
|
+
// The exact key has already had its turn by the time this runs, so an implicit
|
|
238
|
+
// row is always a *strict* prefix plus a separator.
|
|
239
|
+
if (!name || !routeModel.startsWith(name) || !PREFIX_BOUNDARY.test(routeModel.slice(name.length))) continue
|
|
240
|
+
length = name.length
|
|
241
|
+
} else {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
let shape
|
|
245
|
+
if (parts.length === 1) shape = 0
|
|
246
|
+
else if (parts[0] === '*') shape = 2
|
|
247
|
+
else if (parts[0].toLowerCase() === routeProvider) shape = 1
|
|
248
|
+
else continue
|
|
249
|
+
// A row can be `null` in the file — a hand edit, or a delete that left the key behind. The
|
|
250
|
+
// exact-key path has always skipped those; skipping them here too is what keeps a single such
|
|
251
|
+
// row from throwing out of every price lookup and turning `/summary` into a permanent 500.
|
|
252
|
+
const row = rows[key]
|
|
253
|
+
if (!row) continue
|
|
254
|
+
found.push({ shape, length, explicit: prefix === null ? 0 : 1, row })
|
|
255
|
+
}
|
|
256
|
+
return found
|
|
257
|
+
.sort((a, b) => a.shape - b.shape || b.length - a.length || b.explicit - a.explicit)
|
|
258
|
+
.map((entry) => entry.row)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The override rows that price one route, highest priority first.
|
|
263
|
+
*
|
|
264
|
+
* Exact keys come first, in the order {@link matchKeys} establishes, so a table that
|
|
265
|
+
* writes no family row resolves exactly as it did before families existed. Prefix rows
|
|
266
|
+
* follow — same shape order, longest prefix first — which is what lets a family rule
|
|
267
|
+
* cover the models nobody wrote a row for without ever outranking one that somebody did.
|
|
268
|
+
*
|
|
269
|
+
* Memoized per override set and route: this is consulted once per price field of every
|
|
270
|
+
* request record while a day is folded. The entry is only reusable while the override *map*
|
|
271
|
+
* is the same object — adding or removing a key in place would go unnoticed, so callers must
|
|
272
|
+
* treat a document as immutable (every path here re-parses the file instead of editing it).
|
|
273
|
+
* The fallback switch is part of the cache key because two normalized documents can share one
|
|
274
|
+
* override map and differ only by that flag — which is exactly what toggling the switch does
|
|
275
|
+
* when a caller re-normalizes without re-reading.
|
|
276
|
+
*/
|
|
277
|
+
const routeRows = new WeakMap()
|
|
278
|
+
|
|
279
|
+
function rowsForRoute(pricing, provider, model) {
|
|
280
|
+
const rows = pricing.overrides
|
|
281
|
+
if (!rows) return []
|
|
282
|
+
const fallback = pricing.modelPrefixFallback !== false
|
|
283
|
+
let byRoute = routeRows.get(rows)
|
|
284
|
+
if (!byRoute) {
|
|
285
|
+
byRoute = new Map()
|
|
286
|
+
routeRows.set(rows, byRoute)
|
|
287
|
+
}
|
|
288
|
+
const routeKey = `${fallback ? 'p' : 'e'}\u0000${provider ?? ''}\u0000${model ?? ''}`
|
|
289
|
+
const cached = byRoute.get(routeKey)
|
|
290
|
+
if (cached) return cached
|
|
291
|
+
const resolved = []
|
|
292
|
+
for (const key of matchKeys(provider, model)) {
|
|
293
|
+
const row = overrideRow(pricing, key)
|
|
294
|
+
if (row) resolved.push(row)
|
|
295
|
+
}
|
|
296
|
+
const all = resolved.concat(familyRows(rows, provider, model, fallback))
|
|
297
|
+
byRoute.set(routeKey, all)
|
|
298
|
+
return all
|
|
299
|
+
}
|
|
300
|
+
|
|
152
301
|
/**
|
|
153
302
|
* Resolve the four price fields for one route.
|
|
154
303
|
*
|
|
@@ -158,11 +307,11 @@ function overrideRow(pricing, key) {
|
|
|
158
307
|
*/
|
|
159
308
|
export function resolvePrice(pricing, provider, model) {
|
|
160
309
|
const out = {}
|
|
310
|
+
const rows = rowsForRoute(pricing, provider, model)
|
|
161
311
|
for (const k of PRICE_KEYS) {
|
|
162
312
|
out[k] = Number(pricing.default?.[k]) || 0
|
|
163
|
-
for (const
|
|
164
|
-
|
|
165
|
-
if (row && row[k] != null) {
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
if (row[k] != null) {
|
|
166
315
|
out[k] = Number(row[k]) || 0
|
|
167
316
|
break
|
|
168
317
|
}
|
|
@@ -173,36 +322,32 @@ export function resolvePrice(pricing, provider, model) {
|
|
|
173
322
|
|
|
174
323
|
/** Per-model peak/valley rule: the first matching row's own rule, else the global one. */
|
|
175
324
|
export function modelTimeOfUse(pricing, provider, model) {
|
|
176
|
-
for (const
|
|
177
|
-
|
|
178
|
-
if (row && row.timeOfUse) return row.timeOfUse
|
|
325
|
+
for (const row of rowsForRoute(pricing, provider, model)) {
|
|
326
|
+
if (row.timeOfUse) return row.timeOfUse
|
|
179
327
|
}
|
|
180
328
|
return pricing.timeOfUse
|
|
181
329
|
}
|
|
182
330
|
|
|
183
331
|
/** Per-model multiplier applied to the whole request cost (row wins, else global, default 1). */
|
|
184
332
|
export function modelMultiplier(pricing, provider, model) {
|
|
185
|
-
for (const
|
|
186
|
-
|
|
187
|
-
if (row && row.multiplier != null) return Number(row.multiplier) || 1
|
|
333
|
+
for (const row of rowsForRoute(pricing, provider, model)) {
|
|
334
|
+
if (row.multiplier != null) return Number(row.multiplier) || 1
|
|
188
335
|
}
|
|
189
336
|
return Number(pricing.multiplier) || 1
|
|
190
337
|
}
|
|
191
338
|
|
|
192
339
|
/** Per-model context-length tier rule (row wins, else the default-row rule). */
|
|
193
340
|
export function modelContextMultiplier(pricing, provider, model) {
|
|
194
|
-
for (const
|
|
195
|
-
|
|
196
|
-
if (row && row.contextMultiplier != null) return row.contextMultiplier
|
|
341
|
+
for (const row of rowsForRoute(pricing, provider, model)) {
|
|
342
|
+
if (row.contextMultiplier != null) return row.contextMultiplier
|
|
197
343
|
}
|
|
198
344
|
return pricing.contextMultiplier
|
|
199
345
|
}
|
|
200
346
|
|
|
201
347
|
/** The currency a route's prices are quoted in (`cny` unless the row says otherwise). */
|
|
202
348
|
export function resolveCurrency(pricing, provider, model) {
|
|
203
|
-
for (const
|
|
204
|
-
|
|
205
|
-
if (row && row.currency) return row.currency
|
|
349
|
+
for (const row of rowsForRoute(pricing, provider, model)) {
|
|
350
|
+
if (row.currency) return row.currency
|
|
206
351
|
}
|
|
207
352
|
return pricing.default?.currency || 'cny'
|
|
208
353
|
}
|
|
@@ -212,6 +357,23 @@ export function totalCurrencyOf(pricing) {
|
|
|
212
357
|
return pricing.totalCurrency === 'usd' ? 'usd' : 'cny'
|
|
213
358
|
}
|
|
214
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Whether a route has no price at all.
|
|
362
|
+
*
|
|
363
|
+
* True only when `priceUnmatchedModels` is switched off *and* no override row applies to
|
|
364
|
+
* the route. A row that matches counts even when it defines nothing but a multiplier:
|
|
365
|
+
* "matched" means the table has something to say about this model, and the `default` row
|
|
366
|
+
* supplies whatever the row leaves out.
|
|
367
|
+
*
|
|
368
|
+
* @param {object} pricing - the normalized pricing document.
|
|
369
|
+
* @param {string|null} provider - route provider.
|
|
370
|
+
* @param {string|null} model - route model.
|
|
371
|
+
*/
|
|
372
|
+
export function isUnpriced(pricing, provider, model) {
|
|
373
|
+
if (pricing.priceUnmatchedModels !== false) return false
|
|
374
|
+
return rowsForRoute(pricing, provider, model).length === 0
|
|
375
|
+
}
|
|
376
|
+
|
|
215
377
|
/**
|
|
216
378
|
* Convert a cost quoted in `from` into the configured total currency. Same-currency
|
|
217
379
|
* conversion is a no-op, so an all-CNY table needs no exchange rate at all.
|
|
@@ -279,6 +441,7 @@ export function contextMultiplierFor(pricing, provider, model, tokens) {
|
|
|
279
441
|
*/
|
|
280
442
|
export function requestCost(tokens, provider, model, hour, pricing, day) {
|
|
281
443
|
const context = normalizeDay(day)
|
|
444
|
+
if (isUnpriced(pricing, provider, model)) return 0
|
|
282
445
|
const price = resolvePrice(pricing, provider, model)
|
|
283
446
|
const timeMultiplier = multiplierFor(context.weekday, hour, modelTimeOfUse(pricing, provider, model), context.class)
|
|
284
447
|
const modelMultiplierValue = modelMultiplier(pricing, provider, model)
|
|
@@ -294,6 +457,7 @@ export function requestCost(tokens, provider, model, hour, pricing, day) {
|
|
|
294
457
|
*/
|
|
295
458
|
export function legacyCost(tokens, provider, model, hour, pricing, day) {
|
|
296
459
|
const context = normalizeDay(day)
|
|
460
|
+
if (isUnpriced(pricing, provider, model)) return 0
|
|
297
461
|
const price = resolvePrice(pricing, provider, model)
|
|
298
462
|
const timeMultiplier = multiplierFor(context.weekday, hour, modelTimeOfUse(pricing, provider, model), context.class)
|
|
299
463
|
const cost = tokenCost(tokens, price) * timeMultiplier * modelMultiplier(pricing, provider, model)
|
|
@@ -439,7 +603,11 @@ function normalizeDay(day) {
|
|
|
439
603
|
* surcharge and is floored at 1.
|
|
440
604
|
*
|
|
441
605
|
* A public holiday is off-peak for the whole day, which is DeepSeek's published
|
|
442
|
-
* rule — so the holiday check comes
|
|
606
|
+
* rule — so the holiday check comes before every `days` form. A rule can decline that
|
|
607
|
+
* calendar with `honorHolidays: false`, which is what a gateway that keeps charging its
|
|
608
|
+
* own peak windows on a Chinese public holiday needs; the day is then judged by `days`
|
|
609
|
+
* like any other. Weekends need no such flag: they are already per rule through `days`
|
|
610
|
+
* (`"all"`, `"weekend"`, `[6,7]`).
|
|
443
611
|
*
|
|
444
612
|
* @param {number} weekday - ISO weekday (1=Mon … 7=Sun).
|
|
445
613
|
* @param {number} hour - local hour (0–23).
|
|
@@ -448,7 +616,7 @@ function normalizeDay(day) {
|
|
|
448
616
|
*/
|
|
449
617
|
export function multiplierFor(weekday, hour, tou, dayClass = 'normal') {
|
|
450
618
|
if (!tou || !tou.enabled) return 1
|
|
451
|
-
if (dayClass === 'holiday') return 1
|
|
619
|
+
if (dayClass === 'holiday' && tou.honorHolidays !== false) return 1
|
|
452
620
|
if (!dayMatches(weekday, tou.days)) return 1
|
|
453
621
|
for (const range of tou.peakRanges || []) {
|
|
454
622
|
if (!Array.isArray(range) || range.length < 2) continue
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-usage-badge",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DeepSeek Harness 的用量徽标插件:侧边栏底部一行显示当天的 token 总量与估算费用,点开可看 24 小时 / 近 7 日 / 近 30 日 / 近 12
|
|
3
|
+
"version": "0.1.4",
|
|
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 and per-year charts, official DeepSeek pricing and Chinese public holidays.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"repository": {
|