tledger 0.2.1 → 0.3.0

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.
@@ -18,10 +18,13 @@ export const RATE_CARD = {
18
18
  };
19
19
 
20
20
  export function normalizeModel(model) {
21
+ // Collapse underscore and whitespace separators to dashes so variants like
22
+ // "gpt-5.4 mini" resolve to their own rate-card entry. Keep in lockstep
23
+ // with normalizeModel in lib/token-ledger-importer.mjs.
21
24
  const value = String(model || "unknown")
22
25
  .trim()
23
26
  .toLowerCase()
24
- .replaceAll("_", "-");
27
+ .replace(/[\s_]+/g, "-");
25
28
  if (RATE_CARD[value]) return value;
26
29
  if (value.startsWith("gpt-5.6-sol")) return "gpt-5.6-sol";
27
30
  if (value.startsWith("gpt-5.6-terra")) return "gpt-5.6-terra";
@@ -1,4 +1,5 @@
1
1
  import {
2
+ eventCredits,
2
3
  normalizeQuotaTimeline,
3
4
  weeklyQuotaObservations,
4
5
  } from "./token-ledger-trend.mjs";
@@ -6,6 +7,11 @@ import {
6
7
  INTERACTIVE_FOOTER,
7
8
  INTERACTIVE_HELP,
8
9
  } from "./token-ledger-controls.mjs";
10
+ import {
11
+ usageBucketsInRange,
12
+ usageCallCount,
13
+ usageThreadIds,
14
+ } from "../lib/token-ledger-usage.mjs";
9
15
 
10
16
  const RESET = "\u001b[0m";
11
17
  const PRIMARY_STYLE = [38, 2, 255, 255, 255];
@@ -78,12 +84,18 @@ function compact(value) {
78
84
  [1_000_000, "M"],
79
85
  [1_000, "K"],
80
86
  ];
81
- for (const [divisor, suffix] of units) {
82
- if (absolute >= divisor) {
83
- const scaled = value / divisor;
84
- const precision = scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
85
- return `${scaled.toFixed(precision)}${suffix}`;
87
+ for (let index = 0; index < units.length; index += 1) {
88
+ const [divisor, suffix] = units[index];
89
+ if (absolute < divisor) continue;
90
+ const scaled = value / divisor;
91
+ const magnitude = Math.abs(scaled);
92
+ const precision = magnitude >= 100 ? 0 : magnitude >= 10 ? 1 : 2;
93
+ // Values that round to 1000 of a unit belong to the next unit up
94
+ // (999,999 → 1.00M, not 1000K).
95
+ if (index > 0 && Number(magnitude.toFixed(precision)) >= 1_000) {
96
+ return compact(Math.sign(value) * divisor * 1_000);
86
97
  }
98
+ return `${scaled.toFixed(precision)}${suffix}`;
87
99
  }
88
100
  return Math.round(value).toLocaleString("en-US");
89
101
  }
@@ -131,8 +143,13 @@ function displayProject(row) {
131
143
  return row.displayProject || row.project || "Unlabelled activity";
132
144
  }
133
145
 
134
- function dateLabel(bounds, range = "day") {
146
+ function isRollingRange(range) {
147
+ return range === "rolling24h" || range === "rolling";
148
+ }
149
+
150
+ function dateLabel(bounds, range = "day", rollingLabel = "1 day") {
135
151
  if (range === "rolling24h") return "LAST 24 HOURS";
152
+ if (range === "rolling") return `LAST ${rollingLabel.toUpperCase()}`;
136
153
  if (range === "week" && bounds.startDateString && bounds.endDateString) {
137
154
  const startParts = bounds.startDateString.split("-").map(Number);
138
155
  const endParts = bounds.endDateString.split("-").map(Number);
@@ -232,30 +249,52 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
232
249
  };
233
250
  }
234
251
 
235
- const inObservedCycle = (event) => {
236
- const eventMs = new Date(event.timestamp).getTime();
237
- return (
238
- Number.isFinite(eventMs) &&
239
- eventMs >= windowStartMs &&
240
- eventMs <= observedThroughMs
252
+ const sumUsage = (events) =>
253
+ events.reduce(
254
+ (acc, event) => {
255
+ const tokens = Number(event.totalTokens) || 0;
256
+ acc.tokens += tokens;
257
+ const credits = eventCredits(event);
258
+ if (Number.isFinite(credits) && credits >= 0) {
259
+ acc.credits += credits;
260
+ acc.ratedTokens += tokens;
261
+ }
262
+ return acc;
263
+ },
264
+ { tokens: 0, credits: 0, ratedTokens: 0 },
241
265
  );
242
- };
243
- const sumTokens = (events) =>
244
- events.reduce((sum, event) => sum + (Number(event.totalTokens) || 0), 0);
245
- const cycleTokens = sumTokens((snapshot.events ?? []).filter(inObservedCycle));
246
- const displayedTokens = sumTokens(displayedEvents.filter(inObservedCycle));
266
+ const cycleEndMs = observedThroughMs + 1;
267
+ const cycle = sumUsage(
268
+ usageBucketsInRange(snapshot, windowStartMs, cycleEndMs),
269
+ );
270
+ const displayed = sumUsage(
271
+ usageBucketsInRange(
272
+ { events: displayedEvents },
273
+ windowStartMs,
274
+ cycleEndMs,
275
+ ),
276
+ );
247
277
  const usedPercent = Math.min(100, Math.max(0, Number(observation.usedPercent) || 0));
248
- const displayedSharePercent = cycleTokens
249
- ? (displayedTokens / cycleTokens) * 100
250
- : null;
278
+ // The weekly meter weights usage by model, token type, and fast mode;
279
+ // rate-card credits carry those weights. Allocate burn by credit share
280
+ // when every event in the cycle is rated, and fall back to raw token
281
+ // share otherwise.
282
+ const creditsUsable =
283
+ cycle.tokens > 0 && cycle.ratedTokens === cycle.tokens && cycle.credits > 0;
284
+ const displayedSharePercent = creditsUsable
285
+ ? (displayed.credits / cycle.credits) * 100
286
+ : cycle.tokens
287
+ ? (displayed.tokens / cycle.tokens) * 100
288
+ : null;
251
289
 
252
290
  return {
253
291
  available: true,
254
292
  usedPercent,
255
293
  remainingPercent: 100 - usedPercent,
256
- cycleTokens,
257
- displayedTokens,
294
+ cycleTokens: cycle.tokens,
295
+ displayedTokens: displayed.tokens,
258
296
  displayedSharePercent,
297
+ shareBasis: creditsUsable ? "credits" : "tokens",
259
298
  estimatedDisplayedBurnPercent:
260
299
  displayedSharePercent === null
261
300
  ? null
@@ -268,8 +307,13 @@ function summary(events) {
268
307
  (sum, event) => sum + (Number(event.totalTokens) || 0),
269
308
  0,
270
309
  );
271
- const calls = events.length;
272
- const threadIds = new Set(events.map((event) => event.threadId).filter(Boolean));
310
+ const calls = events.reduce(
311
+ (sum, event) => sum + usageCallCount(event),
312
+ 0,
313
+ );
314
+ const threadIds = new Set(
315
+ events.flatMap((event) => usageThreadIds(event)),
316
+ );
273
317
  const outputTokens = events.reduce(
274
318
  (sum, event) => sum + (Number(event.outputTokens) || 0),
275
319
  0,
@@ -510,9 +554,15 @@ function snapshotLine(freshness, enabled) {
510
554
 
511
555
  function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
512
556
  const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
513
- const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
557
+ const date = colorize(
558
+ dateLabel(bounds, options.range, options.rollingLabel),
559
+ TEXT_STYLE,
560
+ enabled,
561
+ );
514
562
  const modeLabel = options.range === "rolling24h"
515
563
  ? "24 HOURS"
564
+ : options.range === "rolling"
565
+ ? options.rollingLabel.toUpperCase()
516
566
  : options.range === "week"
517
567
  ? "7 DAYS"
518
568
  : "DAY";
@@ -533,15 +583,17 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
533
583
  ].join(join);
534
584
  if (visibleLength(fullLine) < frameWidth) {
535
585
  const lines = [alignHeader(fullLine)];
536
- if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
586
+ if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
537
587
  return lines;
538
588
  }
539
589
 
540
- const compactDate = dateLabel(bounds, options.range)
590
+ const compactDate = dateLabel(bounds, options.range, options.rollingLabel)
541
591
  .replace(/ 20\d{2}$/, "")
542
592
  .replace(" – ", "–");
543
593
  const compactMode = options.range === "rolling24h"
544
594
  ? "24H"
595
+ : options.range === "rolling"
596
+ ? `${options.rollingAmount}${options.rollingUnit.toUpperCase()}`
545
597
  : options.range === "week"
546
598
  ? "7D"
547
599
  : "DAY";
@@ -556,7 +608,7 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
556
608
  ].join(join);
557
609
  if (visibleLength(compactLine) < frameWidth) {
558
610
  const lines = [alignHeader(compactLine)];
559
- if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
611
+ if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
560
612
  return lines;
561
613
  }
562
614
 
@@ -571,7 +623,7 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
571
623
  compact(stats.projectCount),
572
624
  ].join(" ");
573
625
  const lines = [alignHeader(minimalLine)];
574
- if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
626
+ if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
575
627
  return lines;
576
628
  }
577
629
 
@@ -603,6 +655,14 @@ export function renderTerminal({
603
655
  lines.push("");
604
656
  lines.push(...sidebarLines(stats, frameWidth, enabled, options, quota));
605
657
  }
658
+ if (events.some((event) => event.rangeAllocationEstimated === true)) {
659
+ lines.push("");
660
+ lines.push(colorize(
661
+ "≈ Boundary-spanning compact history is allocated proportionally.",
662
+ SECONDARY_STYLE,
663
+ enabled,
664
+ ));
665
+ }
606
666
  lines.push("");
607
667
  lines.push(
608
668
  colorize(