dsh-token-total 1.0.3 → 1.0.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/lib/index.js CHANGED
@@ -80,6 +80,7 @@ function aggregateSamples(samples, filter, granularity = "day") {
80
80
  const from = filter.from;
81
81
  const to = filter.to;
82
82
  const buckets = bucketsInRange(from, to, granularity);
83
+ const all = [...samples];
83
84
  const out = {
84
85
  from,
85
86
  to,
@@ -116,7 +117,8 @@ function aggregateSamples(samples, filter, granularity = "day") {
116
117
  const bucketTurns = /* @__PURE__ */ new Map();
117
118
  const bucketCacheHit = /* @__PURE__ */ new Map();
118
119
  const bucketCacheMiss = /* @__PURE__ */ new Map();
119
- for (const sample of samples) {
120
+ const modelAlias = aliasQualifiedModels(new Set(all.map((sample) => modelKeyOf(sample))));
121
+ for (const sample of all) {
120
122
  const bucket = bucketOf(sample, from, to, granularity);
121
123
  if (bucket === null) continue;
122
124
  if (sample.turn) {
@@ -142,9 +144,9 @@ function aggregateSamples(samples, filter, granularity = "day") {
142
144
  out.cacheHit += sample.cacheReadTokens;
143
145
  const missSide = sample.inputTokens + sample.cacheWriteTokens;
144
146
  out.cacheMiss += missSide;
145
- const modelRef = sample.model && sample.model !== "" ? sample.model : "(unknown)";
146
- const provider = providerOf(modelRef);
147
- const model = normalizeModelName(sample.modelName?.trim() || modelNameOf(modelRef));
147
+ const provider = providerOf(sample.model && sample.model !== "" ? sample.model : "(unknown)");
148
+ const modelBase = modelKeyOf(sample);
149
+ const model = modelAlias.get(modelBase) ?? modelBase;
148
150
  const sources = modelProviders.get(model) ?? /* @__PURE__ */ new Set();
149
151
  sources.add(provider);
150
152
  modelProviders.set(model, sources);
@@ -219,6 +221,47 @@ function aggregateSamples(samples, filter, granularity = "day") {
219
221
  }
220
222
  return out;
221
223
  }
224
+ /** The aggregation key of one sample: the resolved display name when the
225
+ * host resolved one, else the bare model ref — normalized like every
226
+ * display name so the key is stable across routes and refreshes. */
227
+ function modelKeyOf(sample) {
228
+ const modelRef = sample.model && sample.model !== "" ? sample.model : "(unknown)";
229
+ return normalizeModelName(sample.modelName?.trim() || modelNameOf(modelRef));
230
+ }
231
+ /**
232
+ * Qualified-name aliases: "workbuddy-ai-plugin/deepseek-v4.1-flash" and
233
+ * "deepseek-v4.1-flash" are the same model reached through a routing
234
+ * namespace, so a name containing "/" merges onto the LONGEST suffix after a
235
+ * "/" that is itself a model key in this dataset (chains like a/b/c → b/c → c
236
+ * resolve transitively). The merge is dataset-relative on purpose: a slash
237
+ * that names no bare model in the data ("GPT/6.1 sol" beside "gpt-6.1-sol")
238
+ * is a genuinely different name and stays its own row — see the
239
+ * distinct-slashes case in tests/model-aggregation.test.mjs.
240
+ */
241
+ function aliasQualifiedModels(names) {
242
+ const alias = /* @__PURE__ */ new Map();
243
+ for (const name of names) {
244
+ if (!name.includes("/")) continue;
245
+ const parts = name.split("/");
246
+ for (let i = 1; i < parts.length; i++) {
247
+ const suffix = parts.slice(i).join("/");
248
+ if (names.has(suffix)) {
249
+ alias.set(name, suffix);
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ for (const name of [...alias.keys()]) {
255
+ let target = alias.get(name);
256
+ const seen = /* @__PURE__ */ new Set([name]);
257
+ while (alias.has(target) && !seen.has(target)) {
258
+ seen.add(target);
259
+ target = alias.get(target);
260
+ }
261
+ alias.set(name, target);
262
+ }
263
+ return alias;
264
+ }
222
265
  function modelsSorted(totals, cache, sources, splits) {
223
266
  const out = [];
224
267
  for (const [model, tokens] of totals) {
@@ -25,10 +25,14 @@ export declare function localDay(offsetDays: number): string;
25
25
  /** 0 = Monday … 6 = Sunday (GitHub-style heatmap rows). */
26
26
  export declare function indexOfDay(day: string): number;
27
27
  export declare function shortDay(day: string): string;
28
- /** X-axis label for a trend bucket key. Day mode keeps the short date; hour
29
- * mode names the day at 00:00 (so a three-day span keeps its day boundaries
30
- * readable) and the hour everywhere else. */
31
- export declare function bucketAxisLabel(key: string, granularity: 'day' | 'hour'): string;
28
+ /** X-axis label for a trend bucket key. `mode` is what the axis READS AS,
29
+ * chosen by the caller from the window it covers:
30
+ * - 'hour' — a single-day hourly span (今日): the date is constant, so the
31
+ * axis shows the bare clock ("8:00");
32
+ * - 'day' — anything spanning several days (3天 hourly, any daily range):
33
+ * the axis shows the short date ("10/22"); on multi-day hourly spans one
34
+ * label marks each day. The tooltip still carries the full date + hour. */
35
+ export declare function bucketAxisLabel(key: string, mode: 'day' | 'hour'): string;
32
36
  /** Tooltip title for a trend bucket key: the date in day mode, date + hour
33
37
  * in hour mode. */
34
38
  export declare function bucketTitle(key: string, granularity: 'day' | 'hour'): string;
@@ -7,7 +7,7 @@
7
7
  /** The locale namespace this plugin owns. */
8
8
  export declare const LOCALE_NS = "tokenTotal";
9
9
  /** The typed translator key union for the panel's locale seat. */
10
- export type TokenTotalKey = 'nav' | 'back' | 'range' | 'rangePreset.today' | 'rangePreset.3' | 'rangePreset.7' | 'rangePreset.30' | 'rangePreset.90' | 'rangeCustom' | 'from' | 'to' | 'refresh' | 'loading' | 'tokens' | 'tokensHint' | 'tokensUnit' | 'inputTokens' | 'outputTokens' | 'cacheTokens' | 'cachedTokens' | 'sessions' | 'requests' | 'requestsFailed' | 'activeDays' | 'cacheRate' | 'cacheRateHint' | 'cacheHitRate' | 'hitRateLegend' | 'topModel' | 'topModelHint' | 'heatmap' | 'heatLess' | 'heatMore' | 'dailyTrend' | 'hourlyTrend' | 'trendLimited' | 'usageRanking' | 'byModel' | 'byProvider' | 'tokensColumn' | 'cacheRateColumn' | 'providerModels' | 'other' | 'total' | 'percent' | 'asOf' | 'empty' | 'stats.counts' | 'stats.tokensPerSecond' | 'stats.cacheHit' | 'stats.cacheMiss' | 'stats.dialog.title' | 'stats.dialog.usageTitle' | 'stats.dialog.llmTime' | 'stats.dialog.toolTime' | 'stats.dialog.ttft' | 'stats.dialog.speed' | 'stats.dialog.main' | 'stats.dialog.subagents' | 'stats.dialog.subagentCount' | 'stats.dialog.treeTotal' | 'message.turnUsage.count' | 'message.turnUsage.cacheHit' | 'message.turnUsage.input' | 'message.turnUsage.cacheRead' | 'message.turnUsage.cacheWrite' | 'message.turnUsage.output';
10
+ export type TokenTotalKey = 'nav' | 'back' | 'range' | 'rangePreset.today' | 'rangePreset.3' | 'rangePreset.7' | 'rangePreset.30' | 'rangePreset.90' | 'rangeCustom' | 'from' | 'to' | 'refresh' | 'loading' | 'tokens' | 'tokensHint' | 'tokensUnit' | 'inputTokens' | 'outputTokens' | 'cacheTokens' | 'cachedTokens' | 'sessions' | 'requests' | 'requestsFailed' | 'activeDays' | 'cacheRate' | 'cacheRateHint' | 'cacheHitRate' | 'hitRateLegend' | 'topModel' | 'topModelHint' | 'heatmap' | 'heatLess' | 'heatMore' | 'dow.mon' | 'dow.tue' | 'dow.wed' | 'dow.thu' | 'dow.fri' | 'dow.sat' | 'dow.sun' | 'month.jan' | 'month.feb' | 'month.mar' | 'month.apr' | 'month.may' | 'month.jun' | 'month.jul' | 'month.aug' | 'month.sep' | 'month.oct' | 'month.nov' | 'month.dec' | 'dailyTrend' | 'hourlyTrend' | 'trendLimited' | 'usageRanking' | 'byModel' | 'byProvider' | 'tokensColumn' | 'cacheRateColumn' | 'providerModels' | 'other' | 'total' | 'percent' | 'asOf' | 'empty' | 'stats.counts' | 'stats.tokensPerSecond' | 'stats.cacheHit' | 'stats.cacheMiss' | 'stats.dialog.title' | 'stats.dialog.usageTitle' | 'stats.dialog.llmTime' | 'stats.dialog.toolTime' | 'stats.dialog.ttft' | 'stats.dialog.speed' | 'stats.dialog.main' | 'stats.dialog.subagents' | 'stats.dialog.subagentCount' | 'stats.dialog.treeTotal' | 'message.turnUsage.count' | 'message.turnUsage.cacheHit' | 'message.turnUsage.input' | 'message.turnUsage.cacheRead' | 'message.turnUsage.cacheWrite' | 'message.turnUsage.output';
11
11
  export declare const en: Record<TokenTotalKey, string>;
12
12
  export declare const zh: Record<TokenTotalKey, string>;
13
13
  export declare const zhTW: Record<TokenTotalKey, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-token-total",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "DSH web plugin: token 用量统计看板 + 单会话(主智能体 + 子智能体)用量统计。提供今日/近3天/近7天/近1月/近3月/自定义筛选、六个汇总卡片、活跃热力图、缓存命中率折线与分模型柱状图、按模型/按供应商 Top5 列表。",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",