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/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