dsh-token-total 1.0.0 → 1.0.1
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 +3 -3
- package/lib/client.js +273 -147
- package/lib/index.js +159 -43
- package/lib/types/client/session-usage-core.d.ts +10 -0
- package/lib/types/client/use-session-usage.d.ts +2 -18
- package/lib/types/collector.d.ts +8 -9
- package/lib/types/context-types.d.ts +23 -2
- package/lib/types/model-names.d.ts +6 -0
- package/lib/types/session-usage.d.ts +8 -9
- package/lib/types/wire.d.ts +23 -1
- package/package.json +1 -1
- package/src/client/StatsLineEnhanced.tsx +15 -33
- package/src/client/TokenTotalPanel.module.css +72 -5
- package/src/client/TokenTotalPanel.tsx +199 -53
- package/src/client/session-usage-core.ts +58 -0
- package/src/client/use-session-usage.ts +45 -98
- package/src/collector.ts +57 -51
- package/src/context-types.ts +17 -2
- package/src/model-names.ts +34 -0
- package/src/query.ts +61 -5
- package/src/routes.ts +14 -2
- package/src/session-usage.ts +50 -38
- package/src/wire.ts +17 -1
package/src/session-usage.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import type {
|
|
27
27
|
UsageSessionEvent,
|
|
28
|
+
UsageSessionHandle,
|
|
28
29
|
UsageSessionHeader,
|
|
29
30
|
UsageSessionPersistence,
|
|
30
31
|
UsageSessionSnapshot,
|
|
@@ -68,6 +69,10 @@ function headerOf(item: UsageSessionHeader | UsageSessionSnapshot): UsageSession
|
|
|
68
69
|
return typeof row?.id === 'string' && row.id !== '' ? row : null
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function inheritedCount(cut: unknown): number {
|
|
73
|
+
return typeof cut === 'number' && Number.isSafeInteger(cut) && cut >= 0 ? cut : 0
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
/** Read one stored session's events through whichever persistence seam the
|
|
72
77
|
* host line exposes (alpha open/read/close wins; rc.1 inspect is the tail). */
|
|
73
78
|
async function readEvents(
|
|
@@ -78,13 +83,12 @@ async function readEvents(
|
|
|
78
83
|
if (typeof persistence.open === 'function') {
|
|
79
84
|
const handle = await persistence.open(id, 'read', signal === undefined ? undefined : { signal })
|
|
80
85
|
try {
|
|
81
|
-
const
|
|
82
|
-
const inheritedCut = typeof cut === 'number' && Number.isSafeInteger(cut) && cut >= 0 ? cut : 0
|
|
86
|
+
const inheritedCut = inheritedCount(handle.inheritedEventCount)
|
|
83
87
|
const collected: UsageSessionEvent[] = []
|
|
84
88
|
let offset = 0
|
|
85
89
|
const PAGE = 500
|
|
86
90
|
for (;;) {
|
|
87
|
-
|
|
91
|
+
signal?.throwIfAborted()
|
|
88
92
|
const slice = await handle.read(offset, PAGE, signal === undefined ? undefined : { signal })
|
|
89
93
|
const page = ((slice && slice.events) ? slice.events : []) as readonly UsageSessionEvent[]
|
|
90
94
|
if (page.length === 0) break
|
|
@@ -98,7 +102,7 @@ async function readEvents(
|
|
|
98
102
|
}
|
|
99
103
|
if (typeof persistence.inspect === 'function') {
|
|
100
104
|
const inspection = await persistence.inspect(id, signal)
|
|
101
|
-
return { events: inspection.events, inheritedCut: inspection.inheritedEventCount
|
|
105
|
+
return { events: inspection.events, inheritedCut: inheritedCount(inspection.inheritedEventCount) }
|
|
102
106
|
}
|
|
103
107
|
throw new Error('no readable persistence seam (need open or inspect)')
|
|
104
108
|
}
|
|
@@ -108,49 +112,59 @@ async function sumOneSession(
|
|
|
108
112
|
persistence: UsageSessionPersistence,
|
|
109
113
|
id: string,
|
|
110
114
|
signal?: AbortSignal,
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
live?: UsageSessionHandle,
|
|
116
|
+
): Promise<{ totals: SessionUsageTotals; compaction: SessionUsageTotals }> {
|
|
117
|
+
signal?.throwIfAborted()
|
|
118
|
+
const { events, inheritedCut } = typeof live?.snapshotEvents === 'function'
|
|
119
|
+
&& typeof live.inheritedEventCount === 'number'
|
|
120
|
+
? { events: live.snapshotEvents(), inheritedCut: inheritedCount(live.inheritedEventCount) }
|
|
121
|
+
: await readEvents(persistence, id, signal)
|
|
113
122
|
const fold = new UsageFold()
|
|
114
123
|
const totals: SessionUsageTotals = { ...ZERO }
|
|
124
|
+
const compaction: SessionUsageTotals = { ...ZERO }
|
|
115
125
|
for (const ev of events) {
|
|
116
126
|
if (inheritedCut > 0 && typeof ev.seq === 'number' && ev.seq < inheritedCut) continue
|
|
117
127
|
const sample = fold.fold(ev)
|
|
118
128
|
if (sample === null || sample.turn) continue
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
const buckets: SessionUsageTotals = {
|
|
130
|
+
uncachedInputTokens: sample.inputTokens,
|
|
131
|
+
outputTokens: sample.outputTokens,
|
|
132
|
+
cacheReadTokens: sample.cacheReadTokens,
|
|
133
|
+
cacheWriteTokens: sample.cacheWriteTokens,
|
|
134
|
+
}
|
|
135
|
+
add(totals, buckets)
|
|
136
|
+
if (ev.type === 'compaction/summary') add(compaction, buckets)
|
|
123
137
|
}
|
|
124
|
-
return totals
|
|
138
|
+
return { totals, compaction }
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
export class SessionUsageService {
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
*
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (entry.members.has(sessionId)) this.cache.delete(rootId)
|
|
139
|
-
}
|
|
142
|
+
/** Short-lived tree reads; any live event can invalidate tree membership. */
|
|
143
|
+
private cache = new Map<string, { at: number; value: SessionUsageTree }>()
|
|
144
|
+
/** Any event can introduce a new descendant while an async read is running. */
|
|
145
|
+
private generation = 0
|
|
146
|
+
|
|
147
|
+
/** Invalidate conservatively: an unseen session may be a newly-created child
|
|
148
|
+
* of ANY cached root, so membership-only invalidation is not safe. */
|
|
149
|
+
invalidate(_sessionId: string): void {
|
|
150
|
+
this.generation++
|
|
151
|
+
this.cache.clear()
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/** Resolve the whole session tree rooted at `rootId` and sum its usage.
|
|
143
155
|
* `self` is the root's own usage, `subagents` every descendant's, `total`
|
|
144
|
-
* the sum.
|
|
145
|
-
*
|
|
156
|
+
* the sum. Unreadable logs reject rather than publishing partial totals;
|
|
157
|
+
* callers can retain their last good display while surfacing the error. */
|
|
146
158
|
async tree(
|
|
147
159
|
persistence: UsageSessionPersistence,
|
|
148
160
|
sessions: UsageSessionStore | undefined,
|
|
149
161
|
rootId: string,
|
|
150
162
|
signal?: AbortSignal,
|
|
151
163
|
): Promise<SessionUsageTree> {
|
|
164
|
+
signal?.throwIfAborted()
|
|
152
165
|
const hit = this.cache.get(rootId)
|
|
153
166
|
if (hit !== undefined && Date.now() - hit.at < CACHE_TTL_MS) return hit.value
|
|
167
|
+
const generation = this.generation
|
|
154
168
|
|
|
155
169
|
const headers = await this.listHeaders(persistence, signal)
|
|
156
170
|
const children = new Map<string, string[]>()
|
|
@@ -163,10 +177,10 @@ export class SessionUsageService {
|
|
|
163
177
|
}
|
|
164
178
|
// A brand-new live subagent may not be materialized in persistence yet;
|
|
165
179
|
// the live store still knows its parent, so fold those in too.
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
const live = new Map((sessions?.list() ?? []).map((handle) => [handle.id, handle]))
|
|
181
|
+
for (const handle of live.values()) {
|
|
182
|
+
const parent = handle.header?.parentSession ?? handle.parentSession
|
|
168
183
|
if (typeof parent !== 'string' || parent === '' || parent === handle.id) continue
|
|
169
|
-
if (headers.some((h) => h.id === handle.id)) continue
|
|
170
184
|
const bucket = children.get(parent)
|
|
171
185
|
if (bucket === undefined) children.set(parent, [handle.id])
|
|
172
186
|
else if (!bucket.includes(handle.id)) bucket.push(handle.id)
|
|
@@ -185,23 +199,21 @@ export class SessionUsageService {
|
|
|
185
199
|
}
|
|
186
200
|
}
|
|
187
201
|
|
|
188
|
-
const
|
|
202
|
+
const root = await sumOneSession(persistence, rootId, signal, live.get(rootId))
|
|
203
|
+
const self = root.totals
|
|
189
204
|
const subagents: SessionUsageTotals = { ...ZERO }
|
|
190
205
|
for (const id of descendants) {
|
|
191
|
-
const one = await sumOneSession(persistence, id, signal
|
|
192
|
-
add(subagents, one)
|
|
206
|
+
const one = await sumOneSession(persistence, id, signal, live.get(id))
|
|
207
|
+
add(subagents, one.totals)
|
|
193
208
|
}
|
|
194
209
|
const total: SessionUsageTotals = { ...self }
|
|
195
210
|
add(total, subagents)
|
|
196
|
-
const value: SessionUsageTree = { self, subagents, total, subagentSessions: descendants.length }
|
|
211
|
+
const value: SessionUsageTree = { self, selfCompaction: root.compaction, subagents, total, subagentSessions: descendants.length }
|
|
197
212
|
|
|
198
|
-
|
|
213
|
+
signal?.throwIfAborted()
|
|
214
|
+
if (generation === this.generation) {
|
|
199
215
|
if (this.cache.size >= CACHE_MAX) this.cache.clear()
|
|
200
|
-
this.cache.set(rootId, {
|
|
201
|
-
at: Date.now(),
|
|
202
|
-
value,
|
|
203
|
-
members: new Set([rootId, ...descendants]),
|
|
204
|
-
})
|
|
216
|
+
this.cache.set(rootId, { at: Date.now(), value })
|
|
205
217
|
}
|
|
206
218
|
return value
|
|
207
219
|
}
|
package/src/wire.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface ModelTokenUsage {
|
|
|
32
32
|
provider: string
|
|
33
33
|
/** All source providers; optional for compatibility with older responses. */
|
|
34
34
|
providers?: string[]
|
|
35
|
+
/** Per-provider token split of this one model, largest first — the ranked
|
|
36
|
+
* row's hover detail ("which providers served this model"). Optional so a
|
|
37
|
+
* pre-cross-breakdown host still renders. */
|
|
38
|
+
providerTokens?: Array<{ provider: string; tokens: number }>
|
|
35
39
|
tokens: number
|
|
36
40
|
percent: number // 0..100
|
|
37
41
|
/** Cached input tokens this model served (rate numerator). */
|
|
@@ -47,6 +51,10 @@ export interface ProviderTokenUsage {
|
|
|
47
51
|
percent: number
|
|
48
52
|
cacheHit: number
|
|
49
53
|
cacheMiss: number
|
|
54
|
+
/** Per-model token split this provider served, largest first — the ranked
|
|
55
|
+
* row's hover detail ("which models this provider served"). Optional so a
|
|
56
|
+
* pre-cross-breakdown host still renders. */
|
|
57
|
+
modelTokens?: Array<{ model: string; tokens: number }>
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/** The full aggregate the panel renders for one time range. */
|
|
@@ -76,6 +84,11 @@ export interface TokenTotalRange {
|
|
|
76
84
|
daily: DailyTokenUsage[]
|
|
77
85
|
models: ModelTokenUsage[]
|
|
78
86
|
providers: ProviderTokenUsage[]
|
|
87
|
+
/** Provider route id -> the display name the model selector shows. Filled by
|
|
88
|
+
* the host route from the llm service metadata; a provider missing here (a
|
|
89
|
+
* deleted route, an older host) renders as its raw id. Optional so a host
|
|
90
|
+
* that cannot resolve names still serves a valid payload. */
|
|
91
|
+
providerNames?: Record<string, string>
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
/** The usage statistics panel aggregate request. */
|
|
@@ -98,8 +111,11 @@ export interface SessionUsageTotals {
|
|
|
98
111
|
* The composer stats row renders this so a delegating session shows its whole
|
|
99
112
|
* spend (the native `tokenUsage` projection only covers the main agent). */
|
|
100
113
|
export interface SessionUsageTree {
|
|
101
|
-
/** The root session's own
|
|
114
|
+
/** The root session's own usage, including logged compaction calls. */
|
|
102
115
|
self: SessionUsageTotals
|
|
116
|
+
/** Root-only compaction spend already included in `self`, but absent from
|
|
117
|
+
* the native `tokenUsage` projection. Optional for older host responses. */
|
|
118
|
+
selfCompaction?: SessionUsageTotals
|
|
103
119
|
/** Sum over every descendant (subagent) session. */
|
|
104
120
|
subagents: SessionUsageTotals
|
|
105
121
|
/** self + subagents. */
|