dsh-plugin-usage 0.2.2 → 0.4.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/host/query.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * by-model table and model candidate list) are owned here.
6
6
  */
7
7
  import { dayKey, DAY_MS, bucketKeyFor, stepMs, alignFloor, stepDayKey, MAX_BUCKETS, } from './time.js';
8
+ import { currencyOf } from './settings.js';
8
9
  function emptyBucket(key) {
9
10
  return {
10
11
  key,
@@ -52,7 +53,7 @@ export function query(state, args) {
52
53
  const dailyMap = new Map();
53
54
  const modelMap = new Map();
54
55
  const total = emptyBucket('total');
55
- const seenModels = new Map(); // composite key -> {provider, model} (first composite wins)
56
+ const seenModels = new Map(); // composite key -> {key, provider, model} (first composite wins)
56
57
  let unpricedCalls = 0;
57
58
  // Range-anchored day-step grid: blocks start at the range's first day.
58
59
  const dayAnchor = Number.isFinite(from) ? alignFloor(from, 'day') : null;
@@ -60,14 +61,17 @@ export function query(state, args) {
60
61
  for (const r of state.index ? state.index.recordsInRange(from, to) : state.records) {
61
62
  if (r.time < from || r.time > to)
62
63
  continue;
64
+ // Candidate list first, selection filter second: the dropdown must offer
65
+ // every model that has data in the range, not only the ones already ticked
66
+ // (ticking one used to collapse the list to that single entry).
67
+ const modelKey = compositeModelKey(r);
68
+ if (!seenModels.has(modelKey))
69
+ seenModels.set(modelKey, { key: modelKey, provider: r.provider, model: r.model });
63
70
  if (!modelMatches(r))
64
71
  continue;
65
72
  const cost = state.prices.costOf(r);
66
73
  if (r.totalTokens > 0 && !state.prices.hasPrice(r))
67
74
  unpricedCalls += 1;
68
- const modelKey = compositeModelKey(r);
69
- if (!seenModels.has(modelKey))
70
- seenModels.set(modelKey, { provider: r.provider, model: r.model });
71
75
  const dk = useStepGrid ? stepDayKey(r, dayAnchor, stepDays) : bucketKeyFor(r.time, granularity, stepDays);
72
76
  let db = dailyMap.get(dk);
73
77
  if (db === undefined) {
@@ -145,12 +149,28 @@ export function query(state, args) {
145
149
  heatmap.push(hmMap.get(k) || emptyBucket(k));
146
150
  }
147
151
  }
152
+ // Display currency: the price table is denominated in USD and stays as it is
153
+ // (it is shared, polled, and never mutated per request). Conversion happens
154
+ // here, once, at the output boundary — including the heatmap and the totals,
155
+ // so no two views of the same data disagree. Six decimals keep float tails
156
+ // out of the sums.
157
+ const money = currencyOf(args.currency, state.prices.currency);
158
+ const toDisplay = (v) => (money.perUsd === 1 ? v : Math.round(v * money.perUsd * 1e6) / 1e6);
159
+ for (const b of daily)
160
+ b.cost = toDisplay(b.cost);
161
+ for (const b of byModel)
162
+ b.cost = toDisplay(b.cost);
163
+ total.cost = toDisplay(total.cost);
164
+ if (heatmap !== null)
165
+ for (const b of heatmap)
166
+ b.cost = toDisplay(b.cost);
148
167
  return {
149
168
  daily,
150
169
  byModel,
151
170
  total,
152
171
  models,
153
172
  unpricedCalls,
173
+ currency: { code: money.code, symbol: money.symbol },
154
174
  ...heatmap === null ? {} : { heatmap },
155
175
  backfill: { status: state.backfill.status, total: state.backfill.total, done: state.backfill.done },
156
176
  };
@@ -12,6 +12,45 @@
12
12
  * The store below stays the in-memory view the aggregation code reads.
13
13
  */
14
14
  export const DEFAULT_CURRENCY = '¥';
15
+ /**
16
+ * Currency the price table is denominated in.
17
+ *
18
+ * DEFAULT_PRICES carries DeepSeek's USD list price per 1M tokens (0.27 input /
19
+ * 1.10 output / 0.07 cache-read for `deepseek-chat`, which is exactly 2 / 8 /
20
+ * 0.5 CNY at the fixed 7:1). The table is never rewritten at runtime: a display
21
+ * switch converts on the way out, so a query can never pollute the next one.
22
+ */
23
+ export const PRICE_UNIT_CURRENCY = 'USD';
24
+ /** Display currencies; `perUsd` is how many units of it one USD buys. */
25
+ export const CURRENCIES = {
26
+ USD: { code: 'USD', symbol: '$', perUsd: 1 },
27
+ CNY: { code: 'CNY', symbol: '¥', perUsd: 7 },
28
+ };
29
+ function currencyCodeOf(value) {
30
+ const raw = typeof value === 'string' ? value.trim().toUpperCase() : '';
31
+ if (raw === 'USD' || raw === '$' || raw === 'US$')
32
+ return 'USD';
33
+ if (raw === 'CNY' || raw === '¥' || raw === 'RMB' || raw === 'CN¥')
34
+ return 'CNY';
35
+ return '';
36
+ }
37
+ /**
38
+ * Resolve a configured currency (ISO code or the bare symbol the store has
39
+ * always held) to a CURRENCIES entry. Unknown values fall back to `fallback`,
40
+ * then to the default display currency — never throw on user input.
41
+ *
42
+ * @param {unknown} value
43
+ * @param {unknown} [fallback]
44
+ */
45
+ export function currencyOf(value, fallback) {
46
+ const hit = CURRENCIES[currencyCodeOf(value)];
47
+ if (hit)
48
+ return hit;
49
+ const fb = CURRENCIES[currencyCodeOf(fallback)];
50
+ if (fb)
51
+ return fb;
52
+ return CURRENCIES[currencyCodeOf(DEFAULT_CURRENCY)] || CURRENCIES.CNY;
53
+ }
15
54
  export const DEFAULT_PRICES = {
16
55
  'deepseek-chat': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
17
56
  'deepseek-v3': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Usage Statistics",
4
+ "description": "Token, call and reference-cost statistics by day and by model, with model filters and a yearly heatmap."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "用量统计",
4
+ "description": "按天、按模型查看 Token、调用次数与参考成本,支持模型筛选与年度热力图。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "dsh-plugin-usage",
3
3
  "description": "Token usage statistics for DeepSeek Harness — daily/custom-range views, model filters, call counts and cost, inspired by CC Switch statistics.",
4
- "version": "0.2.2",
4
+ "version": "0.4.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/bennett-dsh/dsh-plugin-usage.git"
9
9
  },
10
10
  "type": "module",
11
+ "icon": "./icon.svg",
11
12
  "main": "lib/host/index.js",
12
13
  "exports": {
13
14
  ".": {
@@ -17,11 +18,14 @@
17
18
  "default": "./lib/client/index.js"
18
19
  },
19
20
  "./cordis.patch.yml": "./cordis.patch.yml",
20
- "./package.json": "./package.json"
21
+ "./package.json": "./package.json",
22
+ "./locale/*.json": "./locale/*.json"
21
23
  },
22
24
  "files": [
23
25
  "lib",
24
26
  "cordis.patch.yml",
27
+ "icon.svg",
28
+ "locale",
25
29
  "image.png",
26
30
  "image-1.png"
27
31
  ],
@@ -47,7 +51,8 @@
47
51
  "inject": [
48
52
  "@deepseek-ai/dsh-client-runtime",
49
53
  "@deepseek-ai/dsh-client-ui-settings",
50
- "@deepseek-ai/dsh-client-locale"
54
+ "@deepseek-ai/dsh-client-locale",
55
+ "@deepseek-ai/dsh-client-ui-conversation"
51
56
  ],
52
57
  "platform": "web"
53
58
  }