dsh-token-total 1.0.2 → 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/client.js +535 -223
- package/lib/index.js +47 -4
- package/lib/types/client/format.d.ts +8 -4
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/stats-icons.d.ts +4 -0
- package/package.json +1 -1
- package/src/client/ContextUsageEntry.module.css +130 -20
- package/src/client/ContextUsageEntry.tsx +11 -7
- package/src/client/StatsLineEnhanced.module.css +15 -24
- package/src/client/StatsLineEnhanced.tsx +33 -49
- package/src/client/TokenTotalPanel.module.css +259 -99
- package/src/client/TokenTotalPanel.tsx +197 -62
- package/src/client/format.ts +9 -7
- package/src/client/locales.ts +76 -0
- package/src/client/stats-icons.tsx +83 -0
- package/src/query.ts +53 -2
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stat-pill glyphs.
|
|
3
|
+
*
|
|
4
|
+
* The shell freezes its primitives table, and that table's icon names moved
|
|
5
|
+
* with the host line: 0.2.1-rc.2 exports `IconGaugeOutlineRegular` /
|
|
6
|
+
* `IconDatabaseOutlineRegular` (plus the `Medium` weight) and no longer
|
|
7
|
+
* exports the `…Outline16` names this plugin used to import. A missing named
|
|
8
|
+
* export is `undefined` at runtime, and React then throws "Element type is
|
|
9
|
+
* invalid" on the entry's first render — the shell's slot boundary catches it,
|
|
10
|
+
* ABDICATES the entry, and the shadowed official pill silently returns. That
|
|
11
|
+
* failure mode is invisible in types (the package's own declarations still
|
|
12
|
+
* describe the old names) and looks exactly like "the plugin did nothing".
|
|
13
|
+
*
|
|
14
|
+
* So the glyph is resolved from the runtime table by preference order and
|
|
15
|
+
* falls back to an inline drawing of the same shape when no name exists, which
|
|
16
|
+
* keeps one published build rendering on every host line.
|
|
17
|
+
*/
|
|
18
|
+
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
|
19
|
+
|
|
20
|
+
type IconComponent = () => JSX.Element
|
|
21
|
+
|
|
22
|
+
const table = primitives as unknown as Record<string, unknown>
|
|
23
|
+
|
|
24
|
+
/** First exported function component among `names`, or null when the shell
|
|
25
|
+
* exports none of them (an older or newer icon roster). */
|
|
26
|
+
function resolveIcon(...names: readonly string[]): IconComponent | null {
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
const candidate = table[name]
|
|
29
|
+
if (typeof candidate === 'function') return candidate as IconComponent
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolvedGauge = resolveIcon(
|
|
35
|
+
'IconGaugeOutlineRegular',
|
|
36
|
+
'IconGaugeOutlineMedium',
|
|
37
|
+
'IconGaugeOutline16',
|
|
38
|
+
)
|
|
39
|
+
const resolvedDatabase = resolveIcon(
|
|
40
|
+
'IconDatabaseOutlineRegular',
|
|
41
|
+
'IconDatabaseOutlineMedium',
|
|
42
|
+
'IconDatabaseOutline16',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
/** Gauge glyph for the activity pill (counts + throughput). */
|
|
46
|
+
export function GaugeIcon(): JSX.Element {
|
|
47
|
+
if (resolvedGauge !== null) {
|
|
48
|
+
const Icon = resolvedGauge
|
|
49
|
+
return <Icon />
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
53
|
+
<path
|
|
54
|
+
d="M8 2.5a5.5 5.5 0 0 1 5.5 5.5M8 8l2.6-2.6M2.5 8A5.5 5.5 0 0 1 8 2.5"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
strokeWidth="1.3"
|
|
58
|
+
strokeLinecap="round"
|
|
59
|
+
/>
|
|
60
|
+
<circle cx="8" cy="8" r="1" fill="currentColor" />
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Database glyph for the token-usage pill. */
|
|
66
|
+
export function DatabaseIcon(): JSX.Element {
|
|
67
|
+
if (resolvedDatabase !== null) {
|
|
68
|
+
const Icon = resolvedDatabase
|
|
69
|
+
return <Icon />
|
|
70
|
+
}
|
|
71
|
+
return (
|
|
72
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
73
|
+
<ellipse cx="8" cy="3.6" rx="4.6" ry="1.9" fill="none" stroke="currentColor" strokeWidth="1.3" />
|
|
74
|
+
<path
|
|
75
|
+
d="M3.4 3.6v8.8c0 1.05 2.06 1.9 4.6 1.9s4.6-.85 4.6-1.9V3.6M3.4 8c0 1.05 2.06 1.9 4.6 1.9s4.6-.85 4.6-1.9"
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
strokeWidth="1.3"
|
|
79
|
+
strokeLinecap="round"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
)
|
|
83
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -104,6 +104,9 @@ export function aggregateSamples(
|
|
|
104
104
|
const from = filter.from
|
|
105
105
|
const to = filter.to
|
|
106
106
|
const buckets = bucketsInRange(from, to, granularity)
|
|
107
|
+
// Materialize once: the qualified-name pre-pass below and the main fold
|
|
108
|
+
// both walk the samples, and a generator input may only be consumed once.
|
|
109
|
+
const all = [...samples]
|
|
107
110
|
const out: TokenTotalRange = {
|
|
108
111
|
from,
|
|
109
112
|
to,
|
|
@@ -151,7 +154,11 @@ export function aggregateSamples(
|
|
|
151
154
|
const bucketCacheHit = new Map<string, number>()
|
|
152
155
|
const bucketCacheMiss = new Map<string, number>()
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
// One model, one row: every qualified routing name folds onto its bare
|
|
158
|
+
// model key before the first aggregation (see aliasQualifiedModels).
|
|
159
|
+
const modelAlias = aliasQualifiedModels(new Set(all.map((sample) => modelKeyOf(sample))))
|
|
160
|
+
|
|
161
|
+
for (const sample of all) {
|
|
155
162
|
const bucket = bucketOf(sample, from, to, granularity)
|
|
156
163
|
if (bucket === null) continue
|
|
157
164
|
if (sample.turn) {
|
|
@@ -192,7 +199,8 @@ export function aggregateSamples(
|
|
|
192
199
|
out.cacheMiss += missSide
|
|
193
200
|
const modelRef = sample.model && sample.model !== '' ? sample.model : '(unknown)'
|
|
194
201
|
const provider = providerOf(modelRef)
|
|
195
|
-
const
|
|
202
|
+
const modelBase = modelKeyOf(sample)
|
|
203
|
+
const model = modelAlias.get(modelBase) ?? modelBase
|
|
196
204
|
const sources = modelProviders.get(model) ?? new Set<string>()
|
|
197
205
|
sources.add(provider)
|
|
198
206
|
modelProviders.set(model, sources)
|
|
@@ -272,6 +280,49 @@ export function aggregateSamples(
|
|
|
272
280
|
return out
|
|
273
281
|
}
|
|
274
282
|
|
|
283
|
+
/** The aggregation key of one sample: the resolved display name when the
|
|
284
|
+
* host resolved one, else the bare model ref — normalized like every
|
|
285
|
+
* display name so the key is stable across routes and refreshes. */
|
|
286
|
+
function modelKeyOf(sample: UsageSample): string {
|
|
287
|
+
const modelRef = sample.model && sample.model !== '' ? sample.model : '(unknown)'
|
|
288
|
+
return normalizeModelName(sample.modelName?.trim() || modelNameOf(modelRef))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Qualified-name aliases: "workbuddy-ai-plugin/deepseek-v4.1-flash" and
|
|
293
|
+
* "deepseek-v4.1-flash" are the same model reached through a routing
|
|
294
|
+
* namespace, so a name containing "/" merges onto the LONGEST suffix after a
|
|
295
|
+
* "/" that is itself a model key in this dataset (chains like a/b/c → b/c → c
|
|
296
|
+
* resolve transitively). The merge is dataset-relative on purpose: a slash
|
|
297
|
+
* that names no bare model in the data ("GPT/6.1 sol" beside "gpt-6.1-sol")
|
|
298
|
+
* is a genuinely different name and stays its own row — see the
|
|
299
|
+
* distinct-slashes case in tests/model-aggregation.test.mjs.
|
|
300
|
+
*/
|
|
301
|
+
function aliasQualifiedModels(names: Set<string>): Map<string, string> {
|
|
302
|
+
const alias = new Map<string, string>()
|
|
303
|
+
for (const name of names) {
|
|
304
|
+
if (!name.includes('/')) continue
|
|
305
|
+
const parts = name.split('/')
|
|
306
|
+
for (let i = 1; i < parts.length; i++) {
|
|
307
|
+
const suffix = parts.slice(i).join('/')
|
|
308
|
+
if (names.has(suffix)) {
|
|
309
|
+
alias.set(name, suffix)
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const name of [...alias.keys()]) {
|
|
315
|
+
let target = alias.get(name)!
|
|
316
|
+
const seen = new Set([name])
|
|
317
|
+
while (alias.has(target) && !seen.has(target)) {
|
|
318
|
+
seen.add(target)
|
|
319
|
+
target = alias.get(target)!
|
|
320
|
+
}
|
|
321
|
+
alias.set(name, target)
|
|
322
|
+
}
|
|
323
|
+
return alias
|
|
324
|
+
}
|
|
325
|
+
|
|
275
326
|
function modelsSorted(
|
|
276
327
|
totals: Map<string, number>,
|
|
277
328
|
cache: Map<string, { hit: number; miss: number }>,
|