dsh-token-total 1.0.1 → 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 +2 -1
- package/lib/client.js +303 -22
- package/lib/index.js +196 -11
- 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 +1 -2
- package/lib/types/client/use-session-usage.d.ts +1 -1
- package/lib/types/collector.d.ts +19 -3
- package/lib/types/query.d.ts +8 -0
- package/lib/types/store.d.ts +32 -0
- package/lib/types/wire.d.ts +7 -0
- 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 +2 -2
- package/src/client/TokenTotalPanel.tsx +5 -3
- 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 +7 -11
- package/src/client/use-session-usage.ts +1 -1
- package/src/collector.ts +81 -16
- package/src/index.ts +29 -0
- package/src/query.ts +23 -8
- package/src/routes.ts +6 -0
- package/src/store.ts +136 -0
- package/src/wire.ts +7 -0
package/src/routes.ts
CHANGED
|
@@ -162,6 +162,12 @@ export async function aggregateRange(
|
|
|
162
162
|
for (let i = 0; i < row.requests; i++) {
|
|
163
163
|
yield { day, hour, model: row.model, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, request: true }
|
|
164
164
|
}
|
|
165
|
+
// Failed calls ride the row as a count too. They are folded as their own
|
|
166
|
+
// marker (never as an extra request): the call's request is already
|
|
167
|
+
// counted above, and a zero-token failure must not open a model row.
|
|
168
|
+
for (let i = 0; i < (row.failures ?? 0); i++) {
|
|
169
|
+
yield { day, hour, model: row.model, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, failed: true }
|
|
170
|
+
}
|
|
165
171
|
for (let i = 0; i < row.turns; i++) {
|
|
166
172
|
yield { day, hour, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, turn: true }
|
|
167
173
|
}
|
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
|
|
@@ -70,6 +71,12 @@ export interface TokenTotalRange {
|
|
|
70
71
|
/** Cached prompt tokens: cache reads + cache writes (the cache bucket of `tokens`). */
|
|
71
72
|
cacheTokens: number
|
|
72
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
|
|
73
80
|
turns: number // completed turns
|
|
74
81
|
cacheHit: number
|
|
75
82
|
cacheMiss: number
|