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/client.js +332 -123
- 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/package.json +1 -1
- 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/query.ts +53 -2
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 }>,
|