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/lib/index.js
CHANGED
|
@@ -89,6 +89,7 @@ function aggregateSamples(samples, filter, granularity = "day") {
|
|
|
89
89
|
outputTokens: 0,
|
|
90
90
|
cacheTokens: 0,
|
|
91
91
|
requests: 0,
|
|
92
|
+
failures: 0,
|
|
92
93
|
turns: 0,
|
|
93
94
|
cacheHit: 0,
|
|
94
95
|
cacheMiss: 0,
|
|
@@ -104,11 +105,14 @@ function aggregateSamples(samples, filter, granularity = "day") {
|
|
|
104
105
|
const providerTotals = /* @__PURE__ */ new Map();
|
|
105
106
|
const modelCache = /* @__PURE__ */ new Map();
|
|
106
107
|
const providerCache = /* @__PURE__ */ new Map();
|
|
108
|
+
const modelProviderTotals = /* @__PURE__ */ new Map();
|
|
109
|
+
const providerModelTotals = /* @__PURE__ */ new Map();
|
|
107
110
|
const active = /* @__PURE__ */ new Set();
|
|
108
111
|
const bucketByModel = /* @__PURE__ */ new Map();
|
|
109
112
|
const bucketByProvider = /* @__PURE__ */ new Map();
|
|
110
113
|
const bucketTotals = /* @__PURE__ */ new Map();
|
|
111
114
|
const bucketRequests = /* @__PURE__ */ new Map();
|
|
115
|
+
const bucketFailures = /* @__PURE__ */ new Map();
|
|
112
116
|
const bucketTurns = /* @__PURE__ */ new Map();
|
|
113
117
|
const bucketCacheHit = /* @__PURE__ */ new Map();
|
|
114
118
|
const bucketCacheMiss = /* @__PURE__ */ new Map();
|
|
@@ -121,11 +125,16 @@ function aggregateSamples(samples, filter, granularity = "day") {
|
|
|
121
125
|
continue;
|
|
122
126
|
}
|
|
123
127
|
const total = sample.inputTokens + sample.outputTokens + sample.cacheReadTokens + sample.cacheWriteTokens;
|
|
128
|
+
const hasTokenDelta = sample.inputTokens !== 0 || sample.outputTokens !== 0 || sample.cacheReadTokens !== 0 || sample.cacheWriteTokens !== 0;
|
|
129
|
+
if (sample.failed) {
|
|
130
|
+
out.failures++;
|
|
131
|
+
bucketFailures.set(bucket, (bucketFailures.get(bucket) ?? 0) + 1);
|
|
132
|
+
}
|
|
124
133
|
if (sample.request) {
|
|
125
134
|
out.requests++;
|
|
126
135
|
bucketRequests.set(bucket, (bucketRequests.get(bucket) ?? 0) + 1);
|
|
127
|
-
if (total <= 0) continue;
|
|
128
136
|
}
|
|
137
|
+
if (!hasTokenDelta) continue;
|
|
129
138
|
out.tokens += total;
|
|
130
139
|
out.inputTokens += sample.inputTokens;
|
|
131
140
|
out.outputTokens += sample.outputTokens;
|
|
@@ -141,6 +150,18 @@ function aggregateSamples(samples, filter, granularity = "day") {
|
|
|
141
150
|
modelProviders.set(model, sources);
|
|
142
151
|
modelTotals.set(model, (modelTotals.get(model) ?? 0) + total);
|
|
143
152
|
providerTotals.set(provider, (providerTotals.get(provider) ?? 0) + total);
|
|
153
|
+
let modelSplitMap = modelProviderTotals.get(model);
|
|
154
|
+
if (!modelSplitMap) {
|
|
155
|
+
modelSplitMap = /* @__PURE__ */ new Map();
|
|
156
|
+
modelProviderTotals.set(model, modelSplitMap);
|
|
157
|
+
}
|
|
158
|
+
modelSplitMap.set(provider, (modelSplitMap.get(provider) ?? 0) + total);
|
|
159
|
+
let providerSplitMap = providerModelTotals.get(provider);
|
|
160
|
+
if (!providerSplitMap) {
|
|
161
|
+
providerSplitMap = /* @__PURE__ */ new Map();
|
|
162
|
+
providerModelTotals.set(provider, providerSplitMap);
|
|
163
|
+
}
|
|
164
|
+
providerSplitMap.set(model, (providerSplitMap.get(model) ?? 0) + total);
|
|
144
165
|
const mc = modelCache.get(model) ?? {
|
|
145
166
|
hit: 0,
|
|
146
167
|
miss: 0
|
|
@@ -182,13 +203,14 @@ function aggregateSamples(samples, filter, granularity = "day") {
|
|
|
182
203
|
byModel: byModelObj,
|
|
183
204
|
byProvider: byProviderObj,
|
|
184
205
|
requests: bucketRequests.get(bucket) ?? 0,
|
|
206
|
+
failures: bucketFailures.get(bucket) ?? 0,
|
|
185
207
|
turns: bucketTurns.get(bucket) ?? 0,
|
|
186
208
|
cacheHit: bucketCacheHit.get(bucket) ?? 0,
|
|
187
209
|
cacheMiss: bucketCacheMiss.get(bucket) ?? 0
|
|
188
210
|
});
|
|
189
211
|
}
|
|
190
|
-
out.models = modelsSorted(modelTotals, modelCache, modelProviders);
|
|
191
|
-
out.providers = providersSorted(providerTotals, providerCache);
|
|
212
|
+
out.models = modelsSorted(modelTotals, modelCache, modelProviders, modelProviderTotals);
|
|
213
|
+
out.providers = providersSorted(providerTotals, providerCache, providerModelTotals);
|
|
192
214
|
out.topModel = out.models[0]?.model ?? "";
|
|
193
215
|
out.topProvider = out.providers[0]?.provider ?? "";
|
|
194
216
|
if (out.tokens > 0) {
|
|
@@ -197,7 +219,7 @@ function aggregateSamples(samples, filter, granularity = "day") {
|
|
|
197
219
|
}
|
|
198
220
|
return out;
|
|
199
221
|
}
|
|
200
|
-
function modelsSorted(totals, cache, sources) {
|
|
222
|
+
function modelsSorted(totals, cache, sources, splits) {
|
|
201
223
|
const out = [];
|
|
202
224
|
for (const [model, tokens] of totals) {
|
|
203
225
|
const c = cache.get(model);
|
|
@@ -206,6 +228,7 @@ function modelsSorted(totals, cache, sources) {
|
|
|
206
228
|
model,
|
|
207
229
|
provider: providers.length === 1 ? providers[0] : "",
|
|
208
230
|
providers,
|
|
231
|
+
providerTokens: providerSplit(splits.get(model)),
|
|
209
232
|
tokens,
|
|
210
233
|
percent: 0,
|
|
211
234
|
cacheHit: c?.hit ?? 0,
|
|
@@ -215,7 +238,7 @@ function modelsSorted(totals, cache, sources) {
|
|
|
215
238
|
out.sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
216
239
|
return out;
|
|
217
240
|
}
|
|
218
|
-
function providersSorted(totals, cache) {
|
|
241
|
+
function providersSorted(totals, cache, splits) {
|
|
219
242
|
const out = [];
|
|
220
243
|
for (const [provider, tokens] of totals) {
|
|
221
244
|
const c = cache.get(provider);
|
|
@@ -224,12 +247,30 @@ function providersSorted(totals, cache) {
|
|
|
224
247
|
tokens,
|
|
225
248
|
percent: 0,
|
|
226
249
|
cacheHit: c?.hit ?? 0,
|
|
227
|
-
cacheMiss: c?.miss ?? 0
|
|
250
|
+
cacheMiss: c?.miss ?? 0,
|
|
251
|
+
modelTokens: modelSplit(splits.get(provider))
|
|
228
252
|
});
|
|
229
253
|
}
|
|
230
254
|
out.sort((a, b) => b.tokens - a.tokens);
|
|
231
255
|
return out;
|
|
232
256
|
}
|
|
257
|
+
/** One model's per-provider split, largest first (ties by id for a stable
|
|
258
|
+
* order across refreshes). Empty when the response carries no cross data. */
|
|
259
|
+
function providerSplit(split) {
|
|
260
|
+
if (split === void 0) return [];
|
|
261
|
+
return [...split].map(([provider, tokens]) => ({
|
|
262
|
+
provider,
|
|
263
|
+
tokens
|
|
264
|
+
})).sort((a, b) => b.tokens - a.tokens || a.provider.localeCompare(b.provider));
|
|
265
|
+
}
|
|
266
|
+
/** One provider's per-model split, largest first (ties by name). */
|
|
267
|
+
function modelSplit(split) {
|
|
268
|
+
if (split === void 0) return [];
|
|
269
|
+
return [...split].map(([model, tokens]) => ({
|
|
270
|
+
model,
|
|
271
|
+
tokens
|
|
272
|
+
})).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
273
|
+
}
|
|
233
274
|
//#endregion
|
|
234
275
|
//#region src/store.ts
|
|
235
276
|
/**
|
|
@@ -260,6 +301,7 @@ const usageDayRowSchema = z.object({
|
|
|
260
301
|
cacheReadTokens: z.number(),
|
|
261
302
|
cacheWriteTokens: z.number(),
|
|
262
303
|
requests: z.number(),
|
|
304
|
+
failures: z.number().default(0),
|
|
263
305
|
turns: z.number(),
|
|
264
306
|
lastSeen: z.number()
|
|
265
307
|
});
|
|
@@ -272,6 +314,7 @@ const usageHourRowSchema = z.object({
|
|
|
272
314
|
cacheReadTokens: z.number(),
|
|
273
315
|
cacheWriteTokens: z.number(),
|
|
274
316
|
requests: z.number(),
|
|
317
|
+
failures: z.number().default(0),
|
|
275
318
|
turns: z.number(),
|
|
276
319
|
lastSeen: z.number()
|
|
277
320
|
});
|
|
@@ -297,13 +340,18 @@ const usageHistoryDomain = defineDomain({
|
|
|
297
340
|
liveFirstSeq: z.record(z.string(), z.number()).optional(),
|
|
298
341
|
hoursBackfilledSessions: z.array(z.string()).optional(),
|
|
299
342
|
hoursLiveFirstSeq: z.record(z.string(), z.number()).optional(),
|
|
300
|
-
hoursReady: z.boolean().optional()
|
|
343
|
+
hoursReady: z.boolean().optional(),
|
|
344
|
+
failuresBackfilledSessions: z.array(z.string()).optional(),
|
|
345
|
+
failuresLiveFirstSeq: z.record(z.string(), z.number()).optional(),
|
|
346
|
+
failuresReady: z.boolean().optional()
|
|
301
347
|
}),
|
|
302
348
|
initial: {
|
|
303
349
|
backfilledSessions: [],
|
|
304
350
|
liveFirstSeq: {},
|
|
305
351
|
hoursBackfilledSessions: [],
|
|
306
|
-
hoursLiveFirstSeq: {}
|
|
352
|
+
hoursLiveFirstSeq: {},
|
|
353
|
+
failuresBackfilledSessions: [],
|
|
354
|
+
failuresLiveFirstSeq: {}
|
|
307
355
|
}
|
|
308
356
|
},
|
|
309
357
|
tables: {
|
|
@@ -505,6 +553,78 @@ var UsageStore = class {
|
|
|
505
553
|
this.markChain = write.then(() => void 0, () => void 0);
|
|
506
554
|
return write;
|
|
507
555
|
}
|
|
556
|
+
/** Session ids already replayed into the FAILURE counter (third cursor
|
|
557
|
+
* pair, same reason as the hour pair: the day and hour cursors were
|
|
558
|
+
* already full when the counter arrived). */
|
|
559
|
+
async failuresSeenSessions() {
|
|
560
|
+
await this.ready;
|
|
561
|
+
return new Set(this.cursor()?.failuresBackfilledSessions ?? []);
|
|
562
|
+
}
|
|
563
|
+
/** Per-session seq of the first LIVE-observed event, failure-counter share. */
|
|
564
|
+
async failuresLiveSequences() {
|
|
565
|
+
await this.ready;
|
|
566
|
+
return new Map(Object.entries(this.cursor()?.failuresLiveFirstSeq ?? {}));
|
|
567
|
+
}
|
|
568
|
+
/** Persist failure-pass replay progress (merges; serialized on the same
|
|
569
|
+
* chain as every other cursor write). */
|
|
570
|
+
async markFailuresSeenSessions(ids) {
|
|
571
|
+
const write = this.markChain.then(async () => {
|
|
572
|
+
await this.ready;
|
|
573
|
+
const value = this.requireCursor();
|
|
574
|
+
const seen = new Set(value.failuresBackfilledSessions ?? []);
|
|
575
|
+
for (const id of ids) seen.add(id);
|
|
576
|
+
await this.domainHandle?.global?.set({
|
|
577
|
+
...value,
|
|
578
|
+
backfilledSessions: value.backfilledSessions ?? [],
|
|
579
|
+
failuresBackfilledSessions: [...seen]
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
this.markChain = write.then(() => void 0, () => void 0);
|
|
583
|
+
return write;
|
|
584
|
+
}
|
|
585
|
+
/** Record the failure-pass live boundary per session (earliest wins). */
|
|
586
|
+
async markFailuresLiveSequences(entries) {
|
|
587
|
+
const write = this.markChain.then(async () => {
|
|
588
|
+
await this.ready;
|
|
589
|
+
const value = this.requireCursor();
|
|
590
|
+
const merged = { ...value.failuresLiveFirstSeq ?? {} };
|
|
591
|
+
let changed = false;
|
|
592
|
+
for (const [id, seq] of entries) {
|
|
593
|
+
const prev = merged[id];
|
|
594
|
+
if (prev === void 0 || seq < prev) {
|
|
595
|
+
merged[id] = seq;
|
|
596
|
+
changed = true;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!changed) return;
|
|
600
|
+
await this.domainHandle?.global?.set({
|
|
601
|
+
...value,
|
|
602
|
+
backfilledSessions: value.backfilledSessions ?? [],
|
|
603
|
+
failuresLiveFirstSeq: merged
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
this.markChain = write.then(() => void 0, () => void 0);
|
|
607
|
+
return write;
|
|
608
|
+
}
|
|
609
|
+
/** Whether the failure counter covers every replayable session. */
|
|
610
|
+
async failuresReady() {
|
|
611
|
+
await this.ready;
|
|
612
|
+
return this.cursor()?.failuresReady === true;
|
|
613
|
+
}
|
|
614
|
+
/** Mark the failure history complete; the upgrade pass then never reruns. */
|
|
615
|
+
async markFailuresReady() {
|
|
616
|
+
const write = this.markChain.then(async () => {
|
|
617
|
+
await this.ready;
|
|
618
|
+
const value = this.requireCursor();
|
|
619
|
+
await this.domainHandle?.global?.set({
|
|
620
|
+
...value,
|
|
621
|
+
backfilledSessions: value.backfilledSessions ?? [],
|
|
622
|
+
failuresReady: true
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
this.markChain = write.then(() => void 0, () => void 0);
|
|
626
|
+
return write;
|
|
627
|
+
}
|
|
508
628
|
/** Drop every row and the whole cursor, re-bounding each named session at
|
|
509
629
|
* its WIPE-TIME WATERMARK (the store-rebuild escape hatch for corrupted
|
|
510
630
|
* history or attribution-logic upgrades).
|
|
@@ -532,7 +652,10 @@ var UsageStore = class {
|
|
|
532
652
|
liveFirstSeq,
|
|
533
653
|
hoursBackfilledSessions: [],
|
|
534
654
|
hoursLiveFirstSeq: { ...liveFirstSeq },
|
|
535
|
-
hoursReady: false
|
|
655
|
+
hoursReady: false,
|
|
656
|
+
failuresBackfilledSessions: [],
|
|
657
|
+
failuresLiveFirstSeq: { ...liveFirstSeq },
|
|
658
|
+
failuresReady: false
|
|
536
659
|
});
|
|
537
660
|
for (const key of [...table.keys()]) await table.delete(key);
|
|
538
661
|
for (const key of [...hourTable.keys()]) await hourTable.delete(key);
|
|
@@ -589,6 +712,22 @@ var UsageStore = class {
|
|
|
589
712
|
const target = this.targetsOf(sample);
|
|
590
713
|
await this.recordInto(this.requireHourTable(), hourRowKey(hour, target.provider, target.model), emptyHourRow(hour, target.provider, target.model), sample);
|
|
591
714
|
}
|
|
715
|
+
/** Fold ONLY the failure count of a sample into the day and hour rows. The
|
|
716
|
+
* failure upgrade pass uses this: its replay walks logs the token passes
|
|
717
|
+
* already consumed, so the tokens and the request marker must not be added
|
|
718
|
+
* a second time — only the failed-call counter may move. */
|
|
719
|
+
async recordFailure(sample) {
|
|
720
|
+
await this.record({
|
|
721
|
+
day: sample.day,
|
|
722
|
+
hour: sample.hour,
|
|
723
|
+
model: sample.model,
|
|
724
|
+
inputTokens: 0,
|
|
725
|
+
outputTokens: 0,
|
|
726
|
+
cacheReadTokens: 0,
|
|
727
|
+
cacheWriteTokens: 0,
|
|
728
|
+
failed: true
|
|
729
|
+
});
|
|
730
|
+
}
|
|
592
731
|
/** The row identity a sample lands on: turn markers use the synthetic
|
|
593
732
|
* (turns) model, unlabelled calls the (unknown) one. */
|
|
594
733
|
targetsOf(sample) {
|
|
@@ -666,6 +805,7 @@ function emptyCounters(provider, model) {
|
|
|
666
805
|
cacheReadTokens: 0,
|
|
667
806
|
cacheWriteTokens: 0,
|
|
668
807
|
requests: 0,
|
|
808
|
+
failures: 0,
|
|
669
809
|
turns: 0,
|
|
670
810
|
lastSeen: Date.now()
|
|
671
811
|
};
|
|
@@ -686,6 +826,7 @@ function accumulate(base, sample) {
|
|
|
686
826
|
return {
|
|
687
827
|
...base,
|
|
688
828
|
requests: base.requests + (sample.request ? 1 : 0),
|
|
829
|
+
failures: (base.failures ?? 0) + (sample.failed ? 1 : 0),
|
|
689
830
|
inputTokens: base.inputTokens + sample.inputTokens,
|
|
690
831
|
outputTokens: base.outputTokens + sample.outputTokens,
|
|
691
832
|
cacheReadTokens: base.cacheReadTokens + sample.cacheReadTokens,
|
|
@@ -725,6 +866,10 @@ var UsageFold = class {
|
|
|
725
866
|
cacheWriteTokens: 0,
|
|
726
867
|
turn: true
|
|
727
868
|
};
|
|
869
|
+
if (ev.type === "llm/retry-started") {
|
|
870
|
+
const key = this.keyOf(ev);
|
|
871
|
+
if (key !== null) this.seen.delete(key);
|
|
872
|
+
}
|
|
728
873
|
if (ev.type === "step/start" || ev.type === "llm/retry-started") return {
|
|
729
874
|
day: dayKey(ev.time),
|
|
730
875
|
hour: hourKey(ev.time),
|
|
@@ -739,9 +884,18 @@ var UsageFold = class {
|
|
|
739
884
|
const usage = data.chunk?.type === "usage" ? data.chunk.usage : void 0;
|
|
740
885
|
return usage ? this.replaceSample(ev, usage) : null;
|
|
741
886
|
}
|
|
742
|
-
if (ev.type === "assistant/message") {
|
|
887
|
+
if (ev.type === "assistant/message" || ev.type === "assistant/attempt") {
|
|
743
888
|
const data = ev.data;
|
|
744
|
-
|
|
889
|
+
let usage = ev.type === "assistant/message" ? data.usage : void 0;
|
|
890
|
+
if (usage === void 0 && Array.isArray(data.stream)) for (let i = data.stream.length - 1; i >= 0; i--) {
|
|
891
|
+
const record = data.stream[i];
|
|
892
|
+
if (record?.type === "chunk" && record.chunk?.type === "usage") {
|
|
893
|
+
usage = record.chunk.usage;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (ev.type === "assistant/attempt") return this.attemptSample(ev, usage);
|
|
898
|
+
return usage ? this.replaceSample(ev, usage) : null;
|
|
745
899
|
}
|
|
746
900
|
if (ev.type === "compaction/summary") return this.compactionSample(ev);
|
|
747
901
|
return null;
|
|
@@ -749,21 +903,64 @@ var UsageFold = class {
|
|
|
749
903
|
replaceSample(ev, usage) {
|
|
750
904
|
const usage2 = usage;
|
|
751
905
|
if (!usage2) return null;
|
|
752
|
-
if ((usage2.inputTokens ?? 0) + (usage2.outputTokens ?? 0) + (usage2.cacheReadTokens ?? 0) + (usage2.cacheWriteTokens ?? 0) <= 0) return null;
|
|
753
906
|
const key = this.keyOf(ev);
|
|
754
907
|
const sample = {
|
|
755
908
|
day: dayKey(ev.time),
|
|
756
909
|
hour: hourKey(ev.time),
|
|
757
|
-
inputTokens: usage2.inputTokens,
|
|
758
|
-
outputTokens: usage2.outputTokens,
|
|
910
|
+
inputTokens: usage2.inputTokens ?? 0,
|
|
911
|
+
outputTokens: usage2.outputTokens ?? 0,
|
|
759
912
|
cacheReadTokens: usage2.cacheReadTokens ?? 0,
|
|
760
913
|
cacheWriteTokens: usage2.cacheWriteTokens ?? 0
|
|
761
914
|
};
|
|
915
|
+
const prev = key === null ? void 0 : this.seen.get(key);
|
|
916
|
+
if (!prev && sample.inputTokens + sample.outputTokens + sample.cacheReadTokens + sample.cacheWriteTokens <= 0) return null;
|
|
762
917
|
if (key === null) return sample;
|
|
763
|
-
const prev = this.seen.get(key);
|
|
764
918
|
this.seen.set(key, sample);
|
|
765
|
-
if (prev)
|
|
766
|
-
|
|
919
|
+
if (prev) {
|
|
920
|
+
const correction = {
|
|
921
|
+
day: prev.day,
|
|
922
|
+
hour: prev.hour,
|
|
923
|
+
model: prev.model,
|
|
924
|
+
inputTokens: sample.inputTokens - prev.inputTokens,
|
|
925
|
+
outputTokens: sample.outputTokens - prev.outputTokens,
|
|
926
|
+
cacheReadTokens: sample.cacheReadTokens - prev.cacheReadTokens,
|
|
927
|
+
cacheWriteTokens: sample.cacheWriteTokens - prev.cacheWriteTokens
|
|
928
|
+
};
|
|
929
|
+
sample.day = prev.day;
|
|
930
|
+
sample.hour = prev.hour;
|
|
931
|
+
sample.model = prev.model;
|
|
932
|
+
if (correction.inputTokens === 0 && correction.outputTokens === 0 && correction.cacheReadTokens === 0 && correction.cacheWriteTokens === 0) return null;
|
|
933
|
+
return correction;
|
|
934
|
+
}
|
|
935
|
+
const emitted = { ...sample };
|
|
936
|
+
this.seen.set(key, emitted);
|
|
937
|
+
return emitted;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Fold one `assistant/attempt` — a provider call that failed. Tokens follow
|
|
941
|
+
* the same last-report-wins rule as a committed message (the attempt's
|
|
942
|
+
* stream is the call's final word, so a usage chunk seen mid-stream is
|
|
943
|
+
* replaced or corrected, never added twice), but the call must ALWAYS be
|
|
944
|
+
* counted as one failure: `replaceSample` drops an all-zero first report,
|
|
945
|
+
* which is correct for the token buckets and would silently lose the
|
|
946
|
+
* failure. A failed call that reported no usage therefore contributes a
|
|
947
|
+
* zero-token failure marker instead of a null.
|
|
948
|
+
*/
|
|
949
|
+
attemptSample(ev, usage) {
|
|
950
|
+
const sample = this.replaceSample(ev, usage);
|
|
951
|
+
if (sample) {
|
|
952
|
+
sample.failed = true;
|
|
953
|
+
return sample;
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
day: dayKey(ev.time),
|
|
957
|
+
hour: hourKey(ev.time),
|
|
958
|
+
inputTokens: 0,
|
|
959
|
+
outputTokens: 0,
|
|
960
|
+
cacheReadTokens: 0,
|
|
961
|
+
cacheWriteTokens: 0,
|
|
962
|
+
failed: true
|
|
963
|
+
};
|
|
767
964
|
}
|
|
768
965
|
/**
|
|
769
966
|
* Fold one `compaction/summary` — the one place a provider call's usage
|
|
@@ -906,6 +1103,9 @@ var UsageCollector = class UsageCollector {
|
|
|
906
1103
|
this.store.markHoursLiveSequences(batch).catch(() => {
|
|
907
1104
|
this.status.recordFailures++;
|
|
908
1105
|
});
|
|
1106
|
+
this.store.markFailuresLiveSequences(batch).catch(() => {
|
|
1107
|
+
this.status.recordFailures++;
|
|
1108
|
+
});
|
|
909
1109
|
}
|
|
910
1110
|
});
|
|
911
1111
|
}
|
|
@@ -1029,9 +1229,16 @@ var UsageCollector = class UsageCollector {
|
|
|
1029
1229
|
async backfillHours(persistence, sessions, signal) {
|
|
1030
1230
|
await this.scan(persistence, sessions, signal, "hours");
|
|
1031
1231
|
}
|
|
1032
|
-
/**
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1232
|
+
/** The failure upgrade pass: replay every session the FAILURE cursors have
|
|
1233
|
+
* not seen, counting only failed calls. Neither the day nor the hour
|
|
1234
|
+
* cursor covers this counter (both were full when it arrived), and the
|
|
1235
|
+
* replay must not re-add tokens or requests. */
|
|
1236
|
+
async backfillFailures(persistence, sessions, signal) {
|
|
1237
|
+
await this.scan(persistence, sessions, signal, "failures");
|
|
1238
|
+
}
|
|
1239
|
+
/** Shared replay engine behind {@link backfill}, {@link backfillHours} and
|
|
1240
|
+
* {@link backfillFailures}. `target` selects the cursor pair, the write
|
|
1241
|
+
* function, and the cursor the completed sessions land in. */
|
|
1035
1242
|
async scan(persistence, sessions, signal, target) {
|
|
1036
1243
|
if (this.status.running) return;
|
|
1037
1244
|
if (signal?.aborted) return;
|
|
@@ -1043,16 +1250,16 @@ var UsageCollector = class UsageCollector {
|
|
|
1043
1250
|
if (snap !== null && typeof snap === "object" && "header" in snap && snap.header !== null && typeof snap.header === "object" && typeof snap.header.id === "string") return snap.header;
|
|
1044
1251
|
return item;
|
|
1045
1252
|
}).filter((h) => typeof h?.id === "string" && h.id !== "");
|
|
1046
|
-
const seen = target === "hours" ? await this.store.hoursSeenSessions() : await this.store.seenSessions();
|
|
1253
|
+
const seen = target === "hours" ? await this.store.hoursSeenSessions() : target === "failures" ? await this.store.failuresSeenSessions() : await this.store.seenSessions();
|
|
1047
1254
|
const dayCursorWasEmpty = seen.size === 0;
|
|
1048
|
-
const liveSeq = target === "hours" ? await this.store.hoursLiveSequences() : await this.store.liveSequences();
|
|
1255
|
+
const liveSeq = target === "hours" ? await this.store.hoursLiveSequences() : target === "failures" ? await this.store.failuresLiveSequences() : await this.store.liveSequences();
|
|
1049
1256
|
const targets = headers.filter((h) => !seen.has(h.id));
|
|
1050
1257
|
this.status.total = targets.length;
|
|
1051
1258
|
this.status.done = 0;
|
|
1052
1259
|
const concurrency = this.options.backfillConcurrency ?? 4;
|
|
1053
1260
|
let next = 0;
|
|
1054
1261
|
const completed = [];
|
|
1055
|
-
const writeSample = target === "hours" ? (sample) => this.store.recordHours(sample) : (sample) => this.store.record(sample);
|
|
1262
|
+
const writeSample = target === "hours" ? (sample) => this.store.recordHours(sample) : target === "failures" ? (sample) => this.store.recordFailure(sample) : (sample) => this.store.record(sample);
|
|
1056
1263
|
const flushCompleted = async () => {
|
|
1057
1264
|
if (completed.length === 0) return;
|
|
1058
1265
|
const batch = completed.splice(0, completed.length);
|
|
@@ -1060,8 +1267,13 @@ var UsageCollector = class UsageCollector {
|
|
|
1060
1267
|
await this.store.markHoursSeenSessions(batch);
|
|
1061
1268
|
return;
|
|
1062
1269
|
}
|
|
1270
|
+
if (target === "failures") {
|
|
1271
|
+
await this.store.markFailuresSeenSessions(batch);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1063
1274
|
await this.store.markSeenSessions(batch);
|
|
1064
1275
|
await this.store.markHoursSeenSessions(batch);
|
|
1276
|
+
await this.store.markFailuresSeenSessions(batch);
|
|
1065
1277
|
};
|
|
1066
1278
|
const worker = async () => {
|
|
1067
1279
|
for (;;) {
|
|
@@ -1142,7 +1354,9 @@ var UsageCollector = class UsageCollector {
|
|
|
1142
1354
|
};
|
|
1143
1355
|
await Promise.all(Array.from({ length: Math.min(concurrency, Math.max(1, targets.length)) }, worker));
|
|
1144
1356
|
await flushCompleted();
|
|
1145
|
-
|
|
1357
|
+
const clean = !signal?.aborted && this.status.error === void 0;
|
|
1358
|
+
if ((target === "hours" || dayCursorWasEmpty) && clean) await this.store.markHoursReady();
|
|
1359
|
+
if ((target === "failures" || dayCursorWasEmpty) && clean) await this.store.markFailuresReady();
|
|
1146
1360
|
} finally {
|
|
1147
1361
|
this.status.running = false;
|
|
1148
1362
|
}
|
|
@@ -1167,6 +1381,34 @@ async function lookup(read, timeoutMs) {
|
|
|
1167
1381
|
clearTimeout(timer);
|
|
1168
1382
|
}
|
|
1169
1383
|
}
|
|
1384
|
+
/** Resolve provider route ids to the labels the model selector shows. Both
|
|
1385
|
+
* catalogs are presentational and synchronous on the host runtime, so this is
|
|
1386
|
+
* a plain in-process read rather than a per-request lookup: a missing service,
|
|
1387
|
+
* a throwing adapter, or an unexpected promise all resolve to an empty map and
|
|
1388
|
+
* every surface falls back to the raw historical route id. */
|
|
1389
|
+
function resolveProviderNames(llm) {
|
|
1390
|
+
const names = Object.create(null);
|
|
1391
|
+
if (!llm) return names;
|
|
1392
|
+
try {
|
|
1393
|
+
for (const provider of catalog(llm.listProviders?.())) {
|
|
1394
|
+
const name = provider.name?.trim();
|
|
1395
|
+
if (provider.id && name) names[provider.id] = name;
|
|
1396
|
+
}
|
|
1397
|
+
} catch {}
|
|
1398
|
+
try {
|
|
1399
|
+
for (const entry of catalog(llm.listConfigurableProviders?.())) {
|
|
1400
|
+
const name = entry.displayName?.trim();
|
|
1401
|
+
if (entry.provider && name && names[entry.provider] === void 0) names[entry.provider] = name;
|
|
1402
|
+
}
|
|
1403
|
+
} catch {}
|
|
1404
|
+
return names;
|
|
1405
|
+
}
|
|
1406
|
+
/** Both catalogs are declared synchronous, but a host that publishes them over
|
|
1407
|
+
* a remote face would hand back a promise; treat anything that is not an array
|
|
1408
|
+
* as "no metadata" instead of throwing mid-query. */
|
|
1409
|
+
function catalog(value) {
|
|
1410
|
+
return Array.isArray(value) ? value : [];
|
|
1411
|
+
}
|
|
1170
1412
|
/** Resolve each distinct route once per range query. Unknown/deleted providers
|
|
1171
1413
|
* fall back to their bare historical model names in aggregateSamples(). */
|
|
1172
1414
|
async function resolveModelNames(modelRefs, llm) {
|
|
@@ -1451,7 +1693,7 @@ function granularityOf(range) {
|
|
|
1451
1693
|
async function aggregateRange(store, from, to, granularity = "day", llm) {
|
|
1452
1694
|
const rows = granularity === "hour" ? await store.rangeHourRows(from, to) : await store.rangeRows(from, to);
|
|
1453
1695
|
const names = await resolveModelNames(rows.filter((row) => row.inputTokens + row.outputTokens + row.cacheReadTokens + row.cacheWriteTokens > 0).map((row) => row.model), llm);
|
|
1454
|
-
|
|
1696
|
+
const stats = aggregateSamples((function* () {
|
|
1455
1697
|
for (const row of rows) {
|
|
1456
1698
|
const hour = "hour" in row ? row.hour : void 0;
|
|
1457
1699
|
const day = hour === void 0 ? row.day : hour.slice(0, 10);
|
|
@@ -1475,6 +1717,16 @@ async function aggregateRange(store, from, to, granularity = "day", llm) {
|
|
|
1475
1717
|
cacheWriteTokens: 0,
|
|
1476
1718
|
request: true
|
|
1477
1719
|
};
|
|
1720
|
+
for (let i = 0; i < (row.failures ?? 0); i++) yield {
|
|
1721
|
+
day,
|
|
1722
|
+
hour,
|
|
1723
|
+
model: row.model,
|
|
1724
|
+
inputTokens: 0,
|
|
1725
|
+
outputTokens: 0,
|
|
1726
|
+
cacheReadTokens: 0,
|
|
1727
|
+
cacheWriteTokens: 0,
|
|
1728
|
+
failed: true
|
|
1729
|
+
};
|
|
1478
1730
|
for (let i = 0; i < row.turns; i++) yield {
|
|
1479
1731
|
day,
|
|
1480
1732
|
hour,
|
|
@@ -1489,6 +1741,14 @@ async function aggregateRange(store, from, to, granularity = "day", llm) {
|
|
|
1489
1741
|
from,
|
|
1490
1742
|
to
|
|
1491
1743
|
}, granularity);
|
|
1744
|
+
const resolved = resolveProviderNames(llm);
|
|
1745
|
+
const providerNames = Object.create(null);
|
|
1746
|
+
for (const provider of stats.providers) {
|
|
1747
|
+
const label = resolved[provider.provider];
|
|
1748
|
+
if (label !== void 0) providerNames[provider.provider] = label;
|
|
1749
|
+
}
|
|
1750
|
+
if (Object.keys(providerNames).length > 0) stats.providerNames = providerNames;
|
|
1751
|
+
return stats;
|
|
1492
1752
|
}
|
|
1493
1753
|
/** Build the single fenced prefix route serving the /token-total/api JSON RPC.
|
|
1494
1754
|
* The host mounts it via `ctx.webServer.register` (which takes ONE WebRoute,
|
|
@@ -1591,19 +1851,21 @@ function headerOf(item) {
|
|
|
1591
1851
|
const row = item;
|
|
1592
1852
|
return typeof row?.id === "string" && row.id !== "" ? row : null;
|
|
1593
1853
|
}
|
|
1854
|
+
function inheritedCount(cut) {
|
|
1855
|
+
return typeof cut === "number" && Number.isSafeInteger(cut) && cut >= 0 ? cut : 0;
|
|
1856
|
+
}
|
|
1594
1857
|
/** Read one stored session's events through whichever persistence seam the
|
|
1595
1858
|
* host line exposes (alpha open/read/close wins; rc.1 inspect is the tail). */
|
|
1596
1859
|
async function readEvents(persistence, id, signal) {
|
|
1597
1860
|
if (typeof persistence.open === "function") {
|
|
1598
1861
|
const handle = await persistence.open(id, "read", signal === void 0 ? void 0 : { signal });
|
|
1599
1862
|
try {
|
|
1600
|
-
const
|
|
1601
|
-
const inheritedCut = typeof cut === "number" && Number.isSafeInteger(cut) && cut >= 0 ? cut : 0;
|
|
1863
|
+
const inheritedCut = inheritedCount(handle.inheritedEventCount);
|
|
1602
1864
|
const collected = [];
|
|
1603
1865
|
let offset = 0;
|
|
1604
1866
|
const PAGE = 500;
|
|
1605
1867
|
for (;;) {
|
|
1606
|
-
|
|
1868
|
+
signal?.throwIfAborted();
|
|
1607
1869
|
const slice = await handle.read(offset, PAGE, signal === void 0 ? void 0 : { signal });
|
|
1608
1870
|
const page = slice && slice.events ? slice.events : [];
|
|
1609
1871
|
if (page.length === 0) break;
|
|
@@ -1622,45 +1884,59 @@ async function readEvents(persistence, id, signal) {
|
|
|
1622
1884
|
const inspection = await persistence.inspect(id, signal);
|
|
1623
1885
|
return {
|
|
1624
1886
|
events: inspection.events,
|
|
1625
|
-
inheritedCut: inspection.inheritedEventCount
|
|
1887
|
+
inheritedCut: inheritedCount(inspection.inheritedEventCount)
|
|
1626
1888
|
};
|
|
1627
1889
|
}
|
|
1628
1890
|
throw new Error("no readable persistence seam (need open or inspect)");
|
|
1629
1891
|
}
|
|
1630
1892
|
/** Sum one session's own usage (its main agent) from its log. */
|
|
1631
|
-
async function sumOneSession(persistence, id, signal) {
|
|
1632
|
-
|
|
1893
|
+
async function sumOneSession(persistence, id, signal, live) {
|
|
1894
|
+
signal?.throwIfAborted();
|
|
1895
|
+
const { events, inheritedCut } = typeof live?.snapshotEvents === "function" && typeof live.inheritedEventCount === "number" ? {
|
|
1896
|
+
events: live.snapshotEvents(),
|
|
1897
|
+
inheritedCut: inheritedCount(live.inheritedEventCount)
|
|
1898
|
+
} : await readEvents(persistence, id, signal);
|
|
1633
1899
|
const fold = new UsageFold();
|
|
1634
1900
|
const totals = { ...ZERO };
|
|
1901
|
+
const compaction = { ...ZERO };
|
|
1635
1902
|
for (const ev of events) {
|
|
1636
1903
|
if (inheritedCut > 0 && typeof ev.seq === "number" && ev.seq < inheritedCut) continue;
|
|
1637
1904
|
const sample = fold.fold(ev);
|
|
1638
1905
|
if (sample === null || sample.turn) continue;
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1906
|
+
const buckets = {
|
|
1907
|
+
uncachedInputTokens: sample.inputTokens,
|
|
1908
|
+
outputTokens: sample.outputTokens,
|
|
1909
|
+
cacheReadTokens: sample.cacheReadTokens,
|
|
1910
|
+
cacheWriteTokens: sample.cacheWriteTokens
|
|
1911
|
+
};
|
|
1912
|
+
add(totals, buckets);
|
|
1913
|
+
if (ev.type === "compaction/summary") add(compaction, buckets);
|
|
1643
1914
|
}
|
|
1644
|
-
return
|
|
1915
|
+
return {
|
|
1916
|
+
totals,
|
|
1917
|
+
compaction
|
|
1918
|
+
};
|
|
1645
1919
|
}
|
|
1646
1920
|
var SessionUsageService = class {
|
|
1647
|
-
/**
|
|
1648
|
-
* live event on ANY of them (a subagent's own event changes its ancestors'
|
|
1649
|
-
* totals too) can invalidate exactly the affected trees. */
|
|
1921
|
+
/** Short-lived tree reads; any live event can invalidate tree membership. */
|
|
1650
1922
|
cache = /* @__PURE__ */ new Map();
|
|
1651
|
-
/**
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1923
|
+
/** Any event can introduce a new descendant while an async read is running. */
|
|
1924
|
+
generation = 0;
|
|
1925
|
+
/** Invalidate conservatively: an unseen session may be a newly-created child
|
|
1926
|
+
* of ANY cached root, so membership-only invalidation is not safe. */
|
|
1927
|
+
invalidate(_sessionId) {
|
|
1928
|
+
this.generation++;
|
|
1929
|
+
this.cache.clear();
|
|
1656
1930
|
}
|
|
1657
1931
|
/** Resolve the whole session tree rooted at `rootId` and sum its usage.
|
|
1658
1932
|
* `self` is the root's own usage, `subagents` every descendant's, `total`
|
|
1659
|
-
* the sum.
|
|
1660
|
-
*
|
|
1933
|
+
* the sum. Unreadable logs reject rather than publishing partial totals;
|
|
1934
|
+
* callers can retain their last good display while surfacing the error. */
|
|
1661
1935
|
async tree(persistence, sessions, rootId, signal) {
|
|
1936
|
+
signal?.throwIfAborted();
|
|
1662
1937
|
const hit = this.cache.get(rootId);
|
|
1663
1938
|
if (hit !== void 0 && Date.now() - hit.at < CACHE_TTL_MS) return hit.value;
|
|
1939
|
+
const generation = this.generation;
|
|
1664
1940
|
const headers = await this.listHeaders(persistence, signal);
|
|
1665
1941
|
const children = /* @__PURE__ */ new Map();
|
|
1666
1942
|
for (const header of headers) {
|
|
@@ -1670,10 +1946,10 @@ var SessionUsageService = class {
|
|
|
1670
1946
|
if (bucket === void 0) children.set(parent, [header.id]);
|
|
1671
1947
|
else bucket.push(header.id);
|
|
1672
1948
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1949
|
+
const live = new Map((sessions?.list() ?? []).map((handle) => [handle.id, handle]));
|
|
1950
|
+
for (const handle of live.values()) {
|
|
1951
|
+
const parent = handle.header?.parentSession ?? handle.parentSession;
|
|
1675
1952
|
if (typeof parent !== "string" || parent === "" || parent === handle.id) continue;
|
|
1676
|
-
if (headers.some((h) => h.id === handle.id)) continue;
|
|
1677
1953
|
const bucket = children.get(parent);
|
|
1678
1954
|
if (bucket === void 0) children.set(parent, [handle.id]);
|
|
1679
1955
|
else if (!bucket.includes(handle.id)) bucket.push(handle.id);
|
|
@@ -1690,23 +1966,25 @@ var SessionUsageService = class {
|
|
|
1690
1966
|
queue.push(child);
|
|
1691
1967
|
}
|
|
1692
1968
|
}
|
|
1693
|
-
const
|
|
1969
|
+
const root = await sumOneSession(persistence, rootId, signal, live.get(rootId));
|
|
1970
|
+
const self = root.totals;
|
|
1694
1971
|
const subagents = { ...ZERO };
|
|
1695
|
-
for (const id of descendants) add(subagents, await sumOneSession(persistence, id, signal
|
|
1972
|
+
for (const id of descendants) add(subagents, (await sumOneSession(persistence, id, signal, live.get(id))).totals);
|
|
1696
1973
|
const total = { ...self };
|
|
1697
1974
|
add(total, subagents);
|
|
1698
1975
|
const value = {
|
|
1699
1976
|
self,
|
|
1977
|
+
selfCompaction: root.compaction,
|
|
1700
1978
|
subagents,
|
|
1701
1979
|
total,
|
|
1702
1980
|
subagentSessions: descendants.length
|
|
1703
1981
|
};
|
|
1704
|
-
|
|
1982
|
+
signal?.throwIfAborted();
|
|
1983
|
+
if (generation === this.generation) {
|
|
1705
1984
|
if (this.cache.size >= CACHE_MAX) this.cache.clear();
|
|
1706
1985
|
this.cache.set(rootId, {
|
|
1707
1986
|
at: Date.now(),
|
|
1708
|
-
value
|
|
1709
|
-
members: /* @__PURE__ */ new Set([rootId, ...descendants])
|
|
1987
|
+
value
|
|
1710
1988
|
});
|
|
1711
1989
|
}
|
|
1712
1990
|
return value;
|
|
@@ -1837,6 +2115,28 @@ function apply(ctx, config = {}) {
|
|
|
1837
2115
|
scanController = controller;
|
|
1838
2116
|
await collector.backfillHours(ctx.sessionPersistence, ctx.sessions, controller.signal);
|
|
1839
2117
|
};
|
|
2118
|
+
const ensureFailureHistory = async () => {
|
|
2119
|
+
await store.readyPromise();
|
|
2120
|
+
if (await store.failuresReady()) return;
|
|
2121
|
+
if (!await store.hasAnyDayRows()) return;
|
|
2122
|
+
if (collector.status.error !== void 0) return;
|
|
2123
|
+
const previous = await store.failuresLiveSequences();
|
|
2124
|
+
const boundaries = /* @__PURE__ */ new Map();
|
|
2125
|
+
for (const handle of ctx.sessions.list()) {
|
|
2126
|
+
const seq = handle.seq;
|
|
2127
|
+
if (typeof seq === "number" && Number.isFinite(seq) && seq >= 0) {
|
|
2128
|
+
boundaries.set(handle.id, seq);
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const old = previous.get(handle.id);
|
|
2132
|
+
boundaries.set(handle.id, typeof old === "number" ? old : -1);
|
|
2133
|
+
}
|
|
2134
|
+
await store.markFailuresLiveSequences(boundaries);
|
|
2135
|
+
scanController?.abort();
|
|
2136
|
+
const controller = new AbortController();
|
|
2137
|
+
scanController = controller;
|
|
2138
|
+
await collector.backfillFailures(ctx.sessionPersistence, ctx.sessions, controller.signal);
|
|
2139
|
+
};
|
|
1840
2140
|
ctx.effect(() => {
|
|
1841
2141
|
collector.start();
|
|
1842
2142
|
const onEvent = (session) => {
|
|
@@ -1847,6 +2147,7 @@ function apply(ctx, config = {}) {
|
|
|
1847
2147
|
(async () => {
|
|
1848
2148
|
await rescan();
|
|
1849
2149
|
await ensureHourHistory();
|
|
2150
|
+
await ensureFailureHistory();
|
|
1850
2151
|
})();
|
|
1851
2152
|
return () => {
|
|
1852
2153
|
scanController?.abort();
|