dsh-token-total 1.0.0 → 1.0.2
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 +5 -4
- package/lib/client.js +566 -159
- package/lib/index.js +355 -54
- package/lib/types/client/ContextUsageEntry.d.ts +22 -0
- package/lib/types/client/context-meter-core.d.ts +26 -0
- package/lib/types/client/context-takeover.d.ts +4 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/session-usage-core.d.ts +9 -0
- package/lib/types/client/use-session-usage.d.ts +2 -18
- package/lib/types/collector.d.ts +27 -12
- package/lib/types/context-types.d.ts +23 -2
- package/lib/types/model-names.d.ts +6 -0
- package/lib/types/query.d.ts +8 -0
- package/lib/types/session-usage.d.ts +8 -9
- package/lib/types/store.d.ts +32 -0
- package/lib/types/wire.d.ts +30 -1
- package/package.json +1 -1
- package/src/client/ContextUsageEntry.module.css +20 -0
- package/src/client/ContextUsageEntry.tsx +89 -0
- package/src/client/StatsLineEnhanced.tsx +17 -35
- package/src/client/TokenTotalPanel.module.css +72 -5
- package/src/client/TokenTotalPanel.tsx +204 -56
- package/src/client/context-meter-core.ts +54 -0
- package/src/client/context-takeover.ts +35 -0
- package/src/client/index.tsx +21 -3
- package/src/client/locales.ts +4 -0
- package/src/client/session-usage-core.ts +54 -0
- package/src/client/use-session-usage.ts +45 -98
- package/src/collector.ts +138 -67
- package/src/context-types.ts +17 -2
- package/src/index.ts +29 -0
- package/src/model-names.ts +34 -0
- package/src/query.ts +84 -13
- package/src/routes.ts +20 -2
- package/src/session-usage.ts +50 -38
- package/src/store.ts +136 -0
- package/src/wire.ts +24 -1
package/src/store.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface UsageCounters {
|
|
|
34
34
|
cacheReadTokens: number
|
|
35
35
|
cacheWriteTokens: number
|
|
36
36
|
requests: number // usage events (API calls) in the bucket
|
|
37
|
+
/** Provider calls in the bucket that FAILED (error / cancelled before any
|
|
38
|
+
* content): a subset of `requests`, never larger than it. Rows persisted
|
|
39
|
+
* before the counter existed may lack it; the schema normalizes that to 0
|
|
40
|
+
* and every subsequent fold writes the number back. */
|
|
41
|
+
failures: number
|
|
37
42
|
turns: number // completed turns attributed to this model in the bucket
|
|
38
43
|
lastSeen: number // epoch ms of the newest sample
|
|
39
44
|
}
|
|
@@ -57,6 +62,10 @@ export const usageDayRowSchema = z.object({
|
|
|
57
62
|
cacheReadTokens: z.number(),
|
|
58
63
|
cacheWriteTokens: z.number(),
|
|
59
64
|
requests: z.number(),
|
|
65
|
+
// Optional: rows persisted before the failure counter existed must stay
|
|
66
|
+
// readable (the domain format has no migration path, and bumping its
|
|
67
|
+
// version would reject every existing store). A missing value folds as 0.
|
|
68
|
+
failures: z.number().default(0),
|
|
60
69
|
turns: z.number(),
|
|
61
70
|
lastSeen: z.number(),
|
|
62
71
|
})
|
|
@@ -70,6 +79,7 @@ export const usageHourRowSchema = z.object({
|
|
|
70
79
|
cacheReadTokens: z.number(),
|
|
71
80
|
cacheWriteTokens: z.number(),
|
|
72
81
|
requests: z.number(),
|
|
82
|
+
failures: z.number().default(0),
|
|
73
83
|
turns: z.number(),
|
|
74
84
|
lastSeen: z.number(),
|
|
75
85
|
})
|
|
@@ -101,12 +111,21 @@ export const usageHistoryDomain = defineDomain({
|
|
|
101
111
|
hoursBackfilledSessions: z.array(z.string()).optional(),
|
|
102
112
|
hoursLiveFirstSeq: z.record(z.string(), z.number()).optional(),
|
|
103
113
|
hoursReady: z.boolean().optional(),
|
|
114
|
+
// The FAILURE counter arrived after both token cursors were populated,
|
|
115
|
+
// so it needs its own pair too: replaying through the day/hour cursors
|
|
116
|
+
// to recover historic failures would re-add every token. This pass only
|
|
117
|
+
// writes the failed-call counts.
|
|
118
|
+
failuresBackfilledSessions: z.array(z.string()).optional(),
|
|
119
|
+
failuresLiveFirstSeq: z.record(z.string(), z.number()).optional(),
|
|
120
|
+
failuresReady: z.boolean().optional(),
|
|
104
121
|
}),
|
|
105
122
|
initial: {
|
|
106
123
|
backfilledSessions: [] as string[],
|
|
107
124
|
liveFirstSeq: {},
|
|
108
125
|
hoursBackfilledSessions: [] as string[],
|
|
109
126
|
hoursLiveFirstSeq: {},
|
|
127
|
+
failuresBackfilledSessions: [] as string[],
|
|
128
|
+
failuresLiveFirstSeq: {},
|
|
110
129
|
},
|
|
111
130
|
},
|
|
112
131
|
// Two tables, one domain version: storage-domain builds an empty map for a
|
|
@@ -137,6 +156,9 @@ interface UsageCursor {
|
|
|
137
156
|
hoursBackfilledSessions?: string[]
|
|
138
157
|
hoursLiveFirstSeq?: Record<string, number>
|
|
139
158
|
hoursReady?: boolean
|
|
159
|
+
failuresBackfilledSessions?: string[]
|
|
160
|
+
failuresLiveFirstSeq?: Record<string, number>
|
|
161
|
+
failuresReady?: boolean
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
export class UsageStore {
|
|
@@ -375,6 +397,93 @@ export class UsageStore {
|
|
|
375
397
|
return write
|
|
376
398
|
}
|
|
377
399
|
|
|
400
|
+
/** Session ids already replayed into the FAILURE counter (third cursor
|
|
401
|
+
* pair, same reason as the hour pair: the day and hour cursors were
|
|
402
|
+
* already full when the counter arrived). */
|
|
403
|
+
async failuresSeenSessions(): Promise<Set<string>> {
|
|
404
|
+
await this.ready
|
|
405
|
+
return new Set(this.cursor()?.failuresBackfilledSessions ?? [])
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Per-session seq of the first LIVE-observed event, failure-counter share. */
|
|
409
|
+
async failuresLiveSequences(): Promise<Map<string, number>> {
|
|
410
|
+
await this.ready
|
|
411
|
+
return new Map(Object.entries(this.cursor()?.failuresLiveFirstSeq ?? {}))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Persist failure-pass replay progress (merges; serialized on the same
|
|
415
|
+
* chain as every other cursor write). */
|
|
416
|
+
async markFailuresSeenSessions(ids: Iterable<string>): Promise<void> {
|
|
417
|
+
const write = this.markChain.then(async () => {
|
|
418
|
+
await this.ready
|
|
419
|
+
const value = this.requireCursor()
|
|
420
|
+
const seen = new Set(value.failuresBackfilledSessions ?? [])
|
|
421
|
+
for (const id of ids) seen.add(id)
|
|
422
|
+
await this.domainHandle?.global?.set({
|
|
423
|
+
...value,
|
|
424
|
+
backfilledSessions: value.backfilledSessions ?? [],
|
|
425
|
+
failuresBackfilledSessions: [...seen],
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
this.markChain = write.then(
|
|
429
|
+
() => undefined,
|
|
430
|
+
() => undefined,
|
|
431
|
+
)
|
|
432
|
+
return write
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Record the failure-pass live boundary per session (earliest wins). */
|
|
436
|
+
async markFailuresLiveSequences(entries: Iterable<readonly [string, number]>): Promise<void> {
|
|
437
|
+
const write = this.markChain.then(async () => {
|
|
438
|
+
await this.ready
|
|
439
|
+
const value = this.requireCursor()
|
|
440
|
+
const merged: Record<string, number> = { ...(value.failuresLiveFirstSeq ?? {}) }
|
|
441
|
+
let changed = false
|
|
442
|
+
for (const [id, seq] of entries) {
|
|
443
|
+
const prev = merged[id]
|
|
444
|
+
if (prev === undefined || seq < prev) {
|
|
445
|
+
merged[id] = seq
|
|
446
|
+
changed = true
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (!changed) return
|
|
450
|
+
await this.domainHandle?.global?.set({
|
|
451
|
+
...value,
|
|
452
|
+
backfilledSessions: value.backfilledSessions ?? [],
|
|
453
|
+
failuresLiveFirstSeq: merged,
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
this.markChain = write.then(
|
|
457
|
+
() => undefined,
|
|
458
|
+
() => undefined,
|
|
459
|
+
)
|
|
460
|
+
return write
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Whether the failure counter covers every replayable session. */
|
|
464
|
+
async failuresReady(): Promise<boolean> {
|
|
465
|
+
await this.ready
|
|
466
|
+
return this.cursor()?.failuresReady === true
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Mark the failure history complete; the upgrade pass then never reruns. */
|
|
470
|
+
async markFailuresReady(): Promise<void> {
|
|
471
|
+
const write = this.markChain.then(async () => {
|
|
472
|
+
await this.ready
|
|
473
|
+
const value = this.requireCursor()
|
|
474
|
+
await this.domainHandle?.global?.set({
|
|
475
|
+
...value,
|
|
476
|
+
backfilledSessions: value.backfilledSessions ?? [],
|
|
477
|
+
failuresReady: true,
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
this.markChain = write.then(
|
|
481
|
+
() => undefined,
|
|
482
|
+
() => undefined,
|
|
483
|
+
)
|
|
484
|
+
return write
|
|
485
|
+
}
|
|
486
|
+
|
|
378
487
|
/** Drop every row and the whole cursor, re-bounding each named session at
|
|
379
488
|
* its WIPE-TIME WATERMARK (the store-rebuild escape hatch for corrupted
|
|
380
489
|
* history or attribution-logic upgrades).
|
|
@@ -415,6 +524,12 @@ export class UsageStore {
|
|
|
415
524
|
hoursBackfilledSessions: [],
|
|
416
525
|
hoursLiveFirstSeq: { ...liveFirstSeq },
|
|
417
526
|
hoursReady: false,
|
|
527
|
+
// The failure pass resets with the others (this object replaces the
|
|
528
|
+
// global wholesale): its cursor must start empty and its boundary
|
|
529
|
+
// must equal the day boundary for the same reason.
|
|
530
|
+
failuresBackfilledSessions: [],
|
|
531
|
+
failuresLiveFirstSeq: { ...liveFirstSeq },
|
|
532
|
+
failuresReady: false,
|
|
418
533
|
})
|
|
419
534
|
for (const key of [...table.keys()]) await table.delete(key)
|
|
420
535
|
for (const key of [...hourTable.keys()]) await hourTable.delete(key)
|
|
@@ -490,6 +605,23 @@ export class UsageStore {
|
|
|
490
605
|
)
|
|
491
606
|
}
|
|
492
607
|
|
|
608
|
+
/** Fold ONLY the failure count of a sample into the day and hour rows. The
|
|
609
|
+
* failure upgrade pass uses this: its replay walks logs the token passes
|
|
610
|
+
* already consumed, so the tokens and the request marker must not be added
|
|
611
|
+
* a second time — only the failed-call counter may move. */
|
|
612
|
+
async recordFailure(sample: UsageSample): Promise<void> {
|
|
613
|
+
await this.record({
|
|
614
|
+
day: sample.day,
|
|
615
|
+
hour: sample.hour,
|
|
616
|
+
model: sample.model,
|
|
617
|
+
inputTokens: 0,
|
|
618
|
+
outputTokens: 0,
|
|
619
|
+
cacheReadTokens: 0,
|
|
620
|
+
cacheWriteTokens: 0,
|
|
621
|
+
failed: true,
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
|
|
493
625
|
/** The row identity a sample lands on: turn markers use the synthetic
|
|
494
626
|
* (turns) model, unlabelled calls the (unknown) one. */
|
|
495
627
|
private targetsOf(sample: UsageSample): { provider: string; model: string } {
|
|
@@ -578,6 +710,7 @@ function emptyCounters(provider: string, model: string): UsageCounters {
|
|
|
578
710
|
cacheReadTokens: 0,
|
|
579
711
|
cacheWriteTokens: 0,
|
|
580
712
|
requests: 0,
|
|
713
|
+
failures: 0,
|
|
581
714
|
turns: 0,
|
|
582
715
|
lastSeen: Date.now(),
|
|
583
716
|
}
|
|
@@ -597,6 +730,9 @@ function accumulate<T extends UsageCounters>(base: T, sample: UsageSample): T {
|
|
|
597
730
|
return {
|
|
598
731
|
...base,
|
|
599
732
|
requests: base.requests + (sample.request ? 1 : 0),
|
|
733
|
+
// `failures` is missing on rows persisted before the counter existed;
|
|
734
|
+
// fold it as 0 and write the number back so the pass converges to it.
|
|
735
|
+
failures: (base.failures ?? 0) + (sample.failed ? 1 : 0),
|
|
600
736
|
inputTokens: base.inputTokens + sample.inputTokens,
|
|
601
737
|
outputTokens: base.outputTokens + sample.outputTokens,
|
|
602
738
|
cacheReadTokens: base.cacheReadTokens + sample.cacheReadTokens,
|
package/src/wire.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface DailyTokenUsage {
|
|
|
19
19
|
byModel: Record<string, number> // normalized display name -> tokens
|
|
20
20
|
byProvider: Record<string, number> // provider name -> tokens
|
|
21
21
|
requests: number // usage events (API calls)
|
|
22
|
+
failures: number // failed calls in the bucket (subset of requests)
|
|
22
23
|
turns: number // completed turns
|
|
23
24
|
cacheHit: number // cached input tokens in the bucket
|
|
24
25
|
cacheMiss: number // uncached input tokens in the bucket
|
|
@@ -32,6 +33,10 @@ export interface ModelTokenUsage {
|
|
|
32
33
|
provider: string
|
|
33
34
|
/** All source providers; optional for compatibility with older responses. */
|
|
34
35
|
providers?: string[]
|
|
36
|
+
/** Per-provider token split of this one model, largest first — the ranked
|
|
37
|
+
* row's hover detail ("which providers served this model"). Optional so a
|
|
38
|
+
* pre-cross-breakdown host still renders. */
|
|
39
|
+
providerTokens?: Array<{ provider: string; tokens: number }>
|
|
35
40
|
tokens: number
|
|
36
41
|
percent: number // 0..100
|
|
37
42
|
/** Cached input tokens this model served (rate numerator). */
|
|
@@ -47,6 +52,10 @@ export interface ProviderTokenUsage {
|
|
|
47
52
|
percent: number
|
|
48
53
|
cacheHit: number
|
|
49
54
|
cacheMiss: number
|
|
55
|
+
/** Per-model token split this provider served, largest first — the ranked
|
|
56
|
+
* row's hover detail ("which models this provider served"). Optional so a
|
|
57
|
+
* pre-cross-breakdown host still renders. */
|
|
58
|
+
modelTokens?: Array<{ model: string; tokens: number }>
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
/** The full aggregate the panel renders for one time range. */
|
|
@@ -62,6 +71,12 @@ export interface TokenTotalRange {
|
|
|
62
71
|
/** Cached prompt tokens: cache reads + cache writes (the cache bucket of `tokens`). */
|
|
63
72
|
cacheTokens: number
|
|
64
73
|
requests: number // usage events (API calls)
|
|
74
|
+
/** Provider calls in the range that FAILED: the stream ended with an error
|
|
75
|
+
* or the call was cancelled before any content was committed. A subset of
|
|
76
|
+
* `requests` (a failed call was still one call), and — like `requests` —
|
|
77
|
+
* independent of the token buckets: a stream that died mid-flight may
|
|
78
|
+
* still have reported usage. */
|
|
79
|
+
failures: number
|
|
65
80
|
turns: number // completed turns
|
|
66
81
|
cacheHit: number
|
|
67
82
|
cacheMiss: number
|
|
@@ -76,6 +91,11 @@ export interface TokenTotalRange {
|
|
|
76
91
|
daily: DailyTokenUsage[]
|
|
77
92
|
models: ModelTokenUsage[]
|
|
78
93
|
providers: ProviderTokenUsage[]
|
|
94
|
+
/** Provider route id -> the display name the model selector shows. Filled by
|
|
95
|
+
* the host route from the llm service metadata; a provider missing here (a
|
|
96
|
+
* deleted route, an older host) renders as its raw id. Optional so a host
|
|
97
|
+
* that cannot resolve names still serves a valid payload. */
|
|
98
|
+
providerNames?: Record<string, string>
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/** The usage statistics panel aggregate request. */
|
|
@@ -98,8 +118,11 @@ export interface SessionUsageTotals {
|
|
|
98
118
|
* The composer stats row renders this so a delegating session shows its whole
|
|
99
119
|
* spend (the native `tokenUsage` projection only covers the main agent). */
|
|
100
120
|
export interface SessionUsageTree {
|
|
101
|
-
/** The root session's own
|
|
121
|
+
/** The root session's own usage, including logged compaction calls. */
|
|
102
122
|
self: SessionUsageTotals
|
|
123
|
+
/** Root-only compaction spend already included in `self`, but absent from
|
|
124
|
+
* the native `tokenUsage` projection. Optional for older host responses. */
|
|
125
|
+
selfCompaction?: SessionUsageTotals
|
|
103
126
|
/** Sum over every descendant (subagent) session. */
|
|
104
127
|
subagents: SessionUsageTotals
|
|
105
128
|
/** self + subagents. */
|