tledger 0.3.0 → 0.4.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.
- package/README.md +283 -155
- package/bin/token-ledger-cache-data.mjs +492 -0
- package/bin/token-ledger-cache-image.mjs +7 -1147
- package/bin/token-ledger-cache-sections.mjs +848 -0
- package/bin/token-ledger-cost-terminal.mjs +234 -0
- package/bin/token-ledger-image-layout.mjs +20 -0
- package/bin/token-ledger-image-primitives.mjs +192 -0
- package/bin/token-ledger-report-data.mjs +1159 -0
- package/bin/token-ledger-source-status.mjs +31 -0
- package/bin/token-ledger-terminal.mjs +245 -69
- package/bin/token-ledger-trend-image.mjs +2039 -1599
- package/bin/token-ledger-trend-terminal.mjs +273 -150
- package/bin/token-ledger-trend.mjs +262 -210
- package/bin/token-ledger-tui.mjs +180 -51
- package/bin/token-ledger.mjs +747 -194
- package/docs/durable-ledger-operations.md +198 -0
- package/docs/release-notes-0.4.0.md +41 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-calendar.mjs +225 -0
- package/lib/token-ledger-collection.mjs +100 -0
- package/lib/token-ledger-importer.mjs +3334 -480
- package/lib/token-ledger-labels.mjs +66 -0
- package/lib/token-ledger-ledger.mjs +6056 -0
- package/lib/token-ledger-quota-contract.mjs +38 -0
- package/lib/token-ledger-range-analysis.mjs +120 -0
- package/lib/token-ledger-rates.mjs +330 -0
- package/lib/token-ledger-snapshot.mjs +336 -35
- package/lib/token-ledger-terminal-text.mjs +11 -0
- package/lib/token-ledger-usage.mjs +339 -33
- package/package.json +13 -10
- package/bin/token-ledger-rates.mjs +0 -65
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { buildBurnDayBins, buildUsageTrend, trendModelLabel } from "./token-ledger-trend.mjs";
|
|
2
|
+
import { chooseBinSize } from "./token-ledger-image-layout.mjs";
|
|
2
3
|
import {
|
|
4
|
+
MAX_SAFE_TOKEN_COUNT,
|
|
5
|
+
checkedTokenAdd,
|
|
3
6
|
splitUsageBucketsAtBoundaries,
|
|
7
|
+
tokenValue,
|
|
4
8
|
usageBuckets,
|
|
5
9
|
usageCallCount,
|
|
6
10
|
} from "../lib/token-ledger-usage.mjs";
|
|
11
|
+
import {
|
|
12
|
+
createTimeZoneFormatter,
|
|
13
|
+
formatCalendarDate,
|
|
14
|
+
localDateBoundary,
|
|
15
|
+
localDateString,
|
|
16
|
+
shiftCalendarDate,
|
|
17
|
+
} from "../lib/token-ledger-calendar.mjs";
|
|
18
|
+
import { isFastServiceTier } from "../lib/token-ledger-rates.mjs";
|
|
19
|
+
import { sanitizeTerminalText } from "../lib/token-ledger-terminal-text.mjs";
|
|
20
|
+
import {
|
|
21
|
+
snapshotFreshnessDetail,
|
|
22
|
+
sourceStatusLine,
|
|
23
|
+
} from "./token-ledger-source-status.mjs";
|
|
24
|
+
import { incompleteSourceWarning } from "./token-ledger-terminal.mjs";
|
|
25
|
+
|
|
26
|
+
export { chooseBinSize } from "./token-ledger-image-layout.mjs";
|
|
27
|
+
import { historyScopeLabel } from "../lib/token-ledger-collection.mjs";
|
|
7
28
|
|
|
8
29
|
const RESET = "\u001b[0m";
|
|
9
30
|
const PRIMARY_STYLE = [38, 2, 255, 255, 255];
|
|
@@ -12,9 +33,97 @@ const BORDER_STYLE = [38, 2, 88, 88, 88];
|
|
|
12
33
|
const GRID_STYLE = [38, 2, 72, 72, 72];
|
|
13
34
|
const LINE_STYLE = [1, 38, 2, 255, 236, 168];
|
|
14
35
|
const RESET_LINE_STYLE = [1, 38, 2, 255, 255, 255];
|
|
36
|
+
const TOKEN_SCALE = Symbol("tokenScale");
|
|
37
|
+
|
|
38
|
+
function tokenScale(target) {
|
|
39
|
+
return Number.isFinite(target[TOKEN_SCALE]) && target[TOKEN_SCALE] >= 1
|
|
40
|
+
? target[TOKEN_SCALE]
|
|
41
|
+
: 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setTokenScale(target, scale) {
|
|
45
|
+
Object.defineProperty(target, TOKEN_SCALE, {
|
|
46
|
+
configurable: true,
|
|
47
|
+
enumerable: false,
|
|
48
|
+
value: scale,
|
|
49
|
+
writable: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scaleTokenMap(values, ratio) {
|
|
54
|
+
for (const [model, value] of values) {
|
|
55
|
+
values.set(model, value * ratio);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addBinTokens(bin, model, tokens, fast) {
|
|
60
|
+
if (!(tokens > 0)) return;
|
|
61
|
+
const scale = tokenScale(bin);
|
|
62
|
+
const scaledTokens = tokens / scale;
|
|
63
|
+
bin.totalTokens += scaledTokens;
|
|
64
|
+
bin.values.set(model, (bin.values.get(model) ?? 0) + scaledTokens);
|
|
65
|
+
if (fast) {
|
|
66
|
+
bin.fastValues.set(model, (bin.fastValues.get(model) ?? 0) + scaledTokens);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const scaleFactor = Math.max(1, bin.totalTokens / MAX_SAFE_TOKEN_COUNT);
|
|
70
|
+
if (scaleFactor === 1) return;
|
|
71
|
+
bin.totalTokens = MAX_SAFE_TOKEN_COUNT;
|
|
72
|
+
scaleTokenMap(bin.values, 1 / scaleFactor);
|
|
73
|
+
scaleTokenMap(bin.fastValues, 1 / scaleFactor);
|
|
74
|
+
setTokenScale(bin, scale * scaleFactor);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mergeBinTotals(state, bin) {
|
|
78
|
+
const sourceScale = tokenScale(bin);
|
|
79
|
+
const commonScale = Math.max(state.scale, sourceScale);
|
|
80
|
+
const targetRatio = state.scale / commonScale;
|
|
81
|
+
const sourceRatio = sourceScale / commonScale;
|
|
82
|
+
state.totalTokens *= targetRatio;
|
|
83
|
+
scaleTokenMap(state.values, targetRatio);
|
|
84
|
+
scaleTokenMap(state.fastValues, targetRatio);
|
|
85
|
+
for (const [model, value] of bin.values) {
|
|
86
|
+
state.totalTokens += value * sourceRatio;
|
|
87
|
+
state.values.set(
|
|
88
|
+
model,
|
|
89
|
+
(state.values.get(model) ?? 0) + value * sourceRatio,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
for (const [model, value] of bin.fastValues) {
|
|
93
|
+
state.fastValues.set(
|
|
94
|
+
model,
|
|
95
|
+
(state.fastValues.get(model) ?? 0) + value * sourceRatio,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (bin.estimated) state.estimated = true;
|
|
99
|
+
for (const model of bin.estimatedModels) {
|
|
100
|
+
state.estimatedModels.add(model);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const scaleFactor = Math.max(1, state.totalTokens / MAX_SAFE_TOKEN_COUNT);
|
|
104
|
+
if (scaleFactor > 1) {
|
|
105
|
+
state.totalTokens = MAX_SAFE_TOKEN_COUNT;
|
|
106
|
+
scaleTokenMap(state.values, 1 / scaleFactor);
|
|
107
|
+
scaleTokenMap(state.fastValues, 1 / scaleFactor);
|
|
108
|
+
}
|
|
109
|
+
state.scale = commonScale * scaleFactor;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function alignBinsToScale(bins, scale) {
|
|
113
|
+
for (const bin of bins) {
|
|
114
|
+
const sourceScale = tokenScale(bin);
|
|
115
|
+
if (sourceScale === scale) continue;
|
|
116
|
+
const ratio = sourceScale / scale;
|
|
117
|
+
bin.totalTokens *= ratio;
|
|
118
|
+
scaleTokenMap(bin.values, ratio);
|
|
119
|
+
scaleTokenMap(bin.fastValues, ratio);
|
|
120
|
+
setTokenScale(bin, scale);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
15
123
|
|
|
16
124
|
// Mirrors the SVG renderer's validated categorical palette.
|
|
17
125
|
export const TREND_MODEL_COLORS = {
|
|
126
|
+
Astra: [38, 2, 232, 121, 249],
|
|
18
127
|
Luna: [38, 2, 42, 120, 214],
|
|
19
128
|
Sol: [38, 2, 235, 104, 52],
|
|
20
129
|
Terra: [38, 2, 27, 175, 122],
|
|
@@ -28,6 +137,7 @@ export const TREND_MODEL_COLORS = {
|
|
|
28
137
|
};
|
|
29
138
|
|
|
30
139
|
const MODEL_ORDER = [
|
|
140
|
+
"Astra",
|
|
31
141
|
"Luna",
|
|
32
142
|
"Sol",
|
|
33
143
|
"Terra",
|
|
@@ -39,6 +149,8 @@ const MODEL_ORDER = [
|
|
|
39
149
|
"Unknown",
|
|
40
150
|
];
|
|
41
151
|
|
|
152
|
+
const ATTRIBUTION_MODEL_ORDER = ["Astra", "Luna", "Sol", "Terra"];
|
|
153
|
+
|
|
42
154
|
function colorsEnabled(options = {}) {
|
|
43
155
|
return options.forceColor ??
|
|
44
156
|
(!options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY));
|
|
@@ -49,7 +161,7 @@ function colorize(value, style, enabled) {
|
|
|
49
161
|
}
|
|
50
162
|
|
|
51
163
|
function stripAnsi(value) {
|
|
52
|
-
return
|
|
164
|
+
return sanitizeTerminalText(value);
|
|
53
165
|
}
|
|
54
166
|
|
|
55
167
|
function visibleLength(value) {
|
|
@@ -112,101 +224,11 @@ function modelSort(left, right) {
|
|
|
112
224
|
);
|
|
113
225
|
}
|
|
114
226
|
|
|
115
|
-
function
|
|
116
|
-
return dateString
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function dateStringFromParts(year, month, day) {
|
|
120
|
-
return [year, month, day]
|
|
121
|
-
.map((value, index) =>
|
|
122
|
-
index === 0 ? String(value) : String(value).padStart(2, "0"),
|
|
123
|
-
)
|
|
124
|
-
.join("-");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function shiftCalendarDate(dateString, amount) {
|
|
128
|
-
const [year, month, day] = dateParts(dateString);
|
|
129
|
-
const date = new Date(Date.UTC(year, month - 1, day + amount));
|
|
130
|
-
return dateStringFromParts(
|
|
131
|
-
date.getUTCFullYear(),
|
|
132
|
-
date.getUTCMonth() + 1,
|
|
133
|
-
date.getUTCDate(),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function timeZoneFormatter(timeZone) {
|
|
138
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
139
|
-
timeZone,
|
|
140
|
-
timeZoneName: "longOffset",
|
|
141
|
-
year: "numeric",
|
|
142
|
-
month: "2-digit",
|
|
143
|
-
day: "2-digit",
|
|
144
|
-
hour: "2-digit",
|
|
145
|
-
minute: "2-digit",
|
|
146
|
-
second: "2-digit",
|
|
147
|
-
hourCycle: "h23",
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function timeZoneOffsetMs(instant, formatter) {
|
|
152
|
-
const parts = formatter.formatToParts(instant);
|
|
153
|
-
const value = parts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
|
|
154
|
-
if (value === "GMT") return 0;
|
|
155
|
-
const match = value.match(/^GMT([+-])(\d{2}):?(\d{2})?$/);
|
|
156
|
-
if (!match) return 0;
|
|
157
|
-
const minutes = Number(match[2]) * 60 + Number(match[3] || 0);
|
|
158
|
-
return (match[1] === "+" ? 1 : -1) * minutes * 60 * 1_000;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function zonedMidnight(
|
|
162
|
-
dateString,
|
|
163
|
-
timeZone,
|
|
164
|
-
formatter = timeZoneFormatter(timeZone),
|
|
165
|
-
) {
|
|
166
|
-
const [year, month, day] = dateParts(dateString);
|
|
167
|
-
const utcGuess = Date.UTC(year, month - 1, day);
|
|
168
|
-
const first = new Date(utcGuess - timeZoneOffsetMs(new Date(utcGuess), formatter));
|
|
169
|
-
return new Date(first.getTime() - timeZoneOffsetMs(first, formatter));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function localDateString(
|
|
173
|
-
timestamp,
|
|
174
|
-
timeZone,
|
|
175
|
-
formatter = timeZoneFormatter(timeZone),
|
|
176
|
-
) {
|
|
177
|
-
const parts = formatter.formatToParts(new Date(timestamp));
|
|
178
|
-
const values = Object.fromEntries(
|
|
179
|
-
parts
|
|
180
|
-
.filter((part) => part.type !== "literal")
|
|
181
|
-
.map((part) => [part.type, part.value]),
|
|
182
|
-
);
|
|
183
|
-
return `${values.year}-${values.month}-${values.day}`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function localDateLabel(dateString, timeZone) {
|
|
187
|
-
const date = zonedMidnight(dateString, timeZone);
|
|
188
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
189
|
-
timeZone,
|
|
227
|
+
function localDateLabel(dateString) {
|
|
228
|
+
return formatCalendarDate(dateString, {
|
|
190
229
|
month: "short",
|
|
191
230
|
day: "2-digit",
|
|
192
|
-
})
|
|
193
|
-
.format(date)
|
|
194
|
-
.toUpperCase();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function chooseBinSize(days, width, { minBinWidth = 1, preferDaily = false } = {}) {
|
|
198
|
-
const rangeDays = Number(days);
|
|
199
|
-
const plotWidth = Math.max(1, Number(width) || 1);
|
|
200
|
-
const minimumBinWidth = Math.max(1, Number(minBinWidth) || 1);
|
|
201
|
-
const maxBinCount = Math.max(1, Math.floor(plotWidth / minimumBinWidth));
|
|
202
|
-
const preferredBinSize = preferDaily
|
|
203
|
-
? 1
|
|
204
|
-
: rangeDays <= 14
|
|
205
|
-
? 1
|
|
206
|
-
: plotWidth >= 120
|
|
207
|
-
? 2
|
|
208
|
-
: 3;
|
|
209
|
-
return Math.max(preferredBinSize, Math.ceil(rangeDays / maxBinCount));
|
|
231
|
+
}).toUpperCase();
|
|
210
232
|
}
|
|
211
233
|
|
|
212
234
|
function sortedModelEntries(values) {
|
|
@@ -220,7 +242,12 @@ export function buildActualTokenBins(
|
|
|
220
242
|
bounds,
|
|
221
243
|
days,
|
|
222
244
|
width,
|
|
223
|
-
{
|
|
245
|
+
{
|
|
246
|
+
binSize: forcedBinSize,
|
|
247
|
+
minBinWidth,
|
|
248
|
+
preferDaily,
|
|
249
|
+
events = null,
|
|
250
|
+
} = {},
|
|
224
251
|
) {
|
|
225
252
|
const binSize = forcedBinSize ?? chooseBinSize(days, width, { minBinWidth, preferDaily });
|
|
226
253
|
const binCount = Math.ceil(days / binSize);
|
|
@@ -234,6 +261,8 @@ export function buildActualTokenBins(
|
|
|
234
261
|
fastValues: new Map(),
|
|
235
262
|
totalTokens: 0,
|
|
236
263
|
calls: 0,
|
|
264
|
+
estimated: false,
|
|
265
|
+
estimatedModels: new Set(),
|
|
237
266
|
}));
|
|
238
267
|
const startDate = bounds.startDateString;
|
|
239
268
|
const dateIndexByString = new Map(
|
|
@@ -242,16 +271,16 @@ export function buildActualTokenBins(
|
|
|
242
271
|
index,
|
|
243
272
|
]),
|
|
244
273
|
);
|
|
245
|
-
const dateFormatter =
|
|
274
|
+
const dateFormatter = createTimeZoneFormatter(bounds.timeZone);
|
|
246
275
|
const binBoundaries = [
|
|
247
276
|
bins[0]?.startDateString,
|
|
248
277
|
...bins.map((bin) => bin.endDateString),
|
|
249
278
|
]
|
|
250
279
|
.filter(Boolean)
|
|
251
280
|
.map((dateString) =>
|
|
252
|
-
|
|
281
|
+
localDateBoundary(dateString, bounds.timeZone, dateFormatter).getTime());
|
|
253
282
|
for (const event of splitUsageBucketsAtBoundaries(
|
|
254
|
-
usageBuckets(snapshot),
|
|
283
|
+
events ?? usageBuckets(snapshot),
|
|
255
284
|
binBoundaries,
|
|
256
285
|
)) {
|
|
257
286
|
const timestamp = new Date(event.timestamp).getTime();
|
|
@@ -260,27 +289,42 @@ export function buildActualTokenBins(
|
|
|
260
289
|
const dayIndex = dateIndexByString.get(dateString);
|
|
261
290
|
if (dayIndex === undefined || dayIndex >= days) continue;
|
|
262
291
|
const bin = bins[Math.floor(dayIndex / binSize)];
|
|
263
|
-
|
|
292
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
293
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
294
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
264
295
|
const model = trendModelLabel(event.model);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (event.serviceTier === "priority") {
|
|
269
|
-
bin.fastValues.set(model, (bin.fastValues.get(model) ?? 0) + tokens);
|
|
296
|
+
if (event.rangeAllocationEstimated === true && tokens > 0) {
|
|
297
|
+
bin.estimated = true;
|
|
298
|
+
bin.estimatedModels.add(model);
|
|
270
299
|
}
|
|
300
|
+
bin.calls = checkedTokenAdd(bin.calls, usageCallCount(event), {
|
|
301
|
+
allowFractional,
|
|
302
|
+
});
|
|
303
|
+
addBinTokens(bin, model, tokens, isFastServiceTier(event.serviceTier));
|
|
271
304
|
}
|
|
272
305
|
|
|
273
|
-
const
|
|
274
|
-
|
|
306
|
+
const totalsState = {
|
|
307
|
+
scale: 1,
|
|
308
|
+
totalTokens: 0,
|
|
309
|
+
values: new Map(),
|
|
310
|
+
fastValues: new Map(),
|
|
311
|
+
estimated: false,
|
|
312
|
+
estimatedModels: new Set(),
|
|
313
|
+
};
|
|
275
314
|
for (const bin of bins) {
|
|
276
|
-
|
|
277
|
-
totals.set(model, (totals.get(model) ?? 0) + value);
|
|
278
|
-
}
|
|
279
|
-
for (const [model, value] of bin.fastValues) {
|
|
280
|
-
fastTotals.set(model, (fastTotals.get(model) ?? 0) + value);
|
|
281
|
-
}
|
|
315
|
+
mergeBinTotals(totalsState, bin);
|
|
282
316
|
}
|
|
283
|
-
|
|
317
|
+
alignBinsToScale(bins, totalsState.scale);
|
|
318
|
+
return {
|
|
319
|
+
bins,
|
|
320
|
+
totals: totalsState.values,
|
|
321
|
+
fastTotals: totalsState.fastValues,
|
|
322
|
+
estimated: totalsState.estimated,
|
|
323
|
+
estimatedModels: totalsState.estimatedModels,
|
|
324
|
+
scale: totalsState.scale,
|
|
325
|
+
binSize,
|
|
326
|
+
binCount,
|
|
327
|
+
};
|
|
284
328
|
}
|
|
285
329
|
|
|
286
330
|
function niceCeiling(value) {
|
|
@@ -296,7 +340,12 @@ function allocateSegmentHeights(entries, total, maxValue, plotHeight) {
|
|
|
296
340
|
? Math.max(1, Math.round((total / maxValue) * (plotHeight - 1)))
|
|
297
341
|
: 0;
|
|
298
342
|
if (!barHeight) return [];
|
|
299
|
-
|
|
343
|
+
// Model counters can cap independently when the bin total reaches the
|
|
344
|
+
// safe-token limit. Partition the already-scaled bar across the model
|
|
345
|
+
// entries so capped components cannot make the stack taller than its bin.
|
|
346
|
+
const segmentTotal = entries.reduce((sum, [, value]) => sum + value, 0);
|
|
347
|
+
const partitionTotal = segmentTotal > 0 ? segmentTotal : total;
|
|
348
|
+
const ideal = entries.map(([, value]) => (value / partitionTotal) * barHeight);
|
|
300
349
|
const heights = ideal.map(Math.floor);
|
|
301
350
|
let remainder = barHeight - heights.reduce((sum, value) => sum + value, 0);
|
|
302
351
|
const order = ideal
|
|
@@ -319,11 +368,17 @@ function allocateSegmentHeights(entries, total, maxValue, plotHeight) {
|
|
|
319
368
|
return heights;
|
|
320
369
|
}
|
|
321
370
|
|
|
322
|
-
function sampleQuota(trend, bounds, width) {
|
|
371
|
+
export function sampleQuota(trend, bounds, width) {
|
|
323
372
|
const startMs = bounds.start.getTime();
|
|
324
373
|
const endMs = bounds.end.getTime();
|
|
325
374
|
const points = trend.points ?? [];
|
|
326
375
|
const resets = trend.resets ?? [];
|
|
376
|
+
const lastObservedPoint = [...points]
|
|
377
|
+
.reverse()
|
|
378
|
+
.find((point) => point.observed !== false);
|
|
379
|
+
const observedThroughMs = Number.isFinite(trend.observedThroughMs)
|
|
380
|
+
? Math.min(endMs, trend.observedThroughMs)
|
|
381
|
+
: lastObservedPoint?.timestampMs ?? null;
|
|
327
382
|
const samples = [];
|
|
328
383
|
let pointIndex = 0;
|
|
329
384
|
let resetIndex = 0;
|
|
@@ -332,6 +387,18 @@ function sampleQuota(trend, bounds, width) {
|
|
|
332
387
|
for (let column = 0; column < width; column += 1) {
|
|
333
388
|
const ratio = width <= 1 ? 0 : column / (width - 1);
|
|
334
389
|
const timestampMs = startMs + (endMs - startMs) * ratio;
|
|
390
|
+
if (
|
|
391
|
+
observedThroughMs === null ||
|
|
392
|
+
timestampMs > observedThroughMs
|
|
393
|
+
) {
|
|
394
|
+
samples.push({
|
|
395
|
+
timestampMs,
|
|
396
|
+
point: null,
|
|
397
|
+
reset: false,
|
|
398
|
+
remainingPercent: null,
|
|
399
|
+
});
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
335
402
|
while (
|
|
336
403
|
pointIndex < points.length &&
|
|
337
404
|
points[pointIndex].timestampMs <= timestampMs
|
|
@@ -376,7 +443,7 @@ function lineRow(remainingPercent, plotHeight) {
|
|
|
376
443
|
);
|
|
377
444
|
}
|
|
378
445
|
|
|
379
|
-
function xLabelLine(bins, plotWidth, leftWidth, rightWidth
|
|
446
|
+
function xLabelLine(bins, plotWidth, leftWidth, rightWidth) {
|
|
380
447
|
const labels = Array.from({ length: plotWidth }, () => " ");
|
|
381
448
|
const write = (label, offset) => {
|
|
382
449
|
for (let index = 0; index < label.length; index += 1) {
|
|
@@ -387,7 +454,7 @@ function xLabelLine(bins, plotWidth, leftWidth, rightWidth, timeZone) {
|
|
|
387
454
|
bins.forEach((bin, index) => {
|
|
388
455
|
const start = Math.round((index * plotWidth) / bins.length);
|
|
389
456
|
const end = Math.round(((index + 1) * plotWidth) / bins.length);
|
|
390
|
-
const label = localDateLabel(bin.startDateString
|
|
457
|
+
const label = localDateLabel(bin.startDateString);
|
|
391
458
|
if (end - start >= label.length) {
|
|
392
459
|
write(label, start + Math.floor((end - start - label.length) / 2));
|
|
393
460
|
} else if (index === 0 || index === bins.length - 1 || end - start >= 4) {
|
|
@@ -406,9 +473,26 @@ function frameLine(content, width) {
|
|
|
406
473
|
return `│${fit(content, width - 2)}│`;
|
|
407
474
|
}
|
|
408
475
|
|
|
476
|
+
function wrapAttributionEntries(prefix, entries, width) {
|
|
477
|
+
const lines = [];
|
|
478
|
+
let line = prefix;
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
const separator = line === prefix ? " · " : " ";
|
|
481
|
+
const candidate = `${line}${separator}${entry}`;
|
|
482
|
+
if (visibleLength(candidate) <= width) {
|
|
483
|
+
line = candidate;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
lines.push(line);
|
|
487
|
+
line = entry;
|
|
488
|
+
}
|
|
489
|
+
lines.push(line);
|
|
490
|
+
return lines;
|
|
491
|
+
}
|
|
492
|
+
|
|
409
493
|
function formatAttribution(trend, enabled, width, percentMode) {
|
|
410
494
|
const rows = new Map((trend.models ?? []).map((row) => [row.model, row]));
|
|
411
|
-
const entries =
|
|
495
|
+
const entries = ATTRIBUTION_MODEL_ORDER
|
|
412
496
|
.map((model) => {
|
|
413
497
|
const row = rows.get(model);
|
|
414
498
|
if (!row || !(row.tokensPerBurnPoint > 0)) return null;
|
|
@@ -418,30 +502,20 @@ function formatAttribution(trend, enabled, width, percentMode) {
|
|
|
418
502
|
})
|
|
419
503
|
.filter(Boolean);
|
|
420
504
|
if (!entries.length) return [];
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
);
|
|
427
|
-
const method = colorize(
|
|
428
|
-
`Columns sum to observed meter drops; model split via rate-card credit weights (card ${trend.rateCardAsOf}).`,
|
|
429
|
-
SECONDARY_STYLE,
|
|
430
|
-
enabled,
|
|
431
|
-
);
|
|
432
|
-
return [fit(valueLine, width - 2), fit(method, width - 2)];
|
|
433
|
-
}
|
|
434
|
-
const valueLine = colorize(
|
|
435
|
-
`ESTIMATE ONLY · quota attribution lens · ${entries.join(" ")}`,
|
|
436
|
-
SECONDARY_STYLE,
|
|
437
|
-
enabled,
|
|
505
|
+
const prefix = percentMode
|
|
506
|
+
? "Observed burn rate"
|
|
507
|
+
: "ESTIMATE ONLY · quota attribution lens";
|
|
508
|
+
const valueLines = wrapAttributionEntries(prefix, entries, width - 2).map(
|
|
509
|
+
(line) => fit(colorize(line, SECONDARY_STYLE, enabled), width - 2),
|
|
438
510
|
);
|
|
439
511
|
const method = colorize(
|
|
440
|
-
|
|
512
|
+
percentMode
|
|
513
|
+
? `Columns sum to observed meter drops; model split via rate-card credit weights (card ${trend.rateCardAsOf}).`
|
|
514
|
+
: `Rate-card/token weights, ${trend.rateCardAsOf}; separate from actual-token bars and not official quota math.`,
|
|
441
515
|
SECONDARY_STYLE,
|
|
442
516
|
enabled,
|
|
443
517
|
);
|
|
444
|
-
return [
|
|
518
|
+
return [...valueLines, fit(method, width - 2)];
|
|
445
519
|
}
|
|
446
520
|
|
|
447
521
|
function drainLabelLine(burnBins, plotWidth, leftWidth, rightWidth, enabled) {
|
|
@@ -467,10 +541,14 @@ function drainLabelLine(burnBins, plotWidth, leftWidth, rightWidth, enabled) {
|
|
|
467
541
|
export function renderTrendCombo({
|
|
468
542
|
snapshot,
|
|
469
543
|
bounds,
|
|
470
|
-
trend =
|
|
544
|
+
trend: providedTrend = null,
|
|
471
545
|
days = bounds.rangeDays ?? 7,
|
|
472
546
|
options = {},
|
|
547
|
+
analysis = null,
|
|
548
|
+
snapshotFreshness = null,
|
|
549
|
+
sourceStatus = "unchecked-cache",
|
|
473
550
|
}) {
|
|
551
|
+
const trend = providedTrend ?? analysis?.trend ?? buildUsageTrend(snapshot, bounds, { analysis });
|
|
474
552
|
const enabled = colorsEnabled(options);
|
|
475
553
|
const frameWidth = Math.max(82, Math.min(158, Number(options.width) || 120));
|
|
476
554
|
const innerWidth = frameWidth - 2;
|
|
@@ -478,7 +556,9 @@ export function renderTrendCombo({
|
|
|
478
556
|
const rightWidth = 7;
|
|
479
557
|
const plotWidth = Math.max(36, innerWidth - leftWidth - rightWidth - 2);
|
|
480
558
|
const plotHeight = 11;
|
|
481
|
-
const actual = buildActualTokenBins(snapshot, bounds, days, plotWidth
|
|
559
|
+
const actual = buildActualTokenBins(snapshot, bounds, days, plotWidth, {
|
|
560
|
+
events: analysis?.currentEvents,
|
|
561
|
+
});
|
|
482
562
|
const burn = buildBurnDayBins(trend, bounds, {
|
|
483
563
|
days,
|
|
484
564
|
binSize: actual.binSize,
|
|
@@ -488,6 +568,9 @@ export function renderTrendCombo({
|
|
|
488
568
|
// and shows the observed drop per column in a label row instead.
|
|
489
569
|
const meterUsable = Boolean(trend.available && burn.totalPercent > 0);
|
|
490
570
|
const percentMode = Boolean(options.drain) && meterUsable;
|
|
571
|
+
const meterAvailable = Boolean(trend.available && (trend.points ?? []).length > 0);
|
|
572
|
+
const drainFallback = Boolean(options.drain) && !percentMode;
|
|
573
|
+
const actualEstimated = actual.estimated === true;
|
|
491
574
|
const barBins = percentMode ? burn.bins : actual.bins;
|
|
492
575
|
const binTotal = (bin) => (percentMode ? bin.totalPercent : bin.totalTokens);
|
|
493
576
|
const maxLeft = niceCeiling(
|
|
@@ -561,11 +644,23 @@ export function renderTrendCombo({
|
|
|
561
644
|
}
|
|
562
645
|
|
|
563
646
|
const axisRows = [0, Math.floor(baseline / 2), baseline];
|
|
647
|
+
const history = historyScopeLabel(snapshot);
|
|
648
|
+
const sourceWarning = incompleteSourceWarning(snapshot);
|
|
649
|
+
const actualModeTitle = drainFallback
|
|
650
|
+
? "ACTUAL TOKENS · DRAIN UNAVAILABLE"
|
|
651
|
+
: meterAvailable
|
|
652
|
+
? "ACTUAL TOKENS + WEEKLY QUOTA"
|
|
653
|
+
: "ACTUAL TOKENS";
|
|
654
|
+
const actualModeSubtitle = drainFallback
|
|
655
|
+
? `DRAIN UNAVAILABLE · BARS = raw local tokens${actualEstimated ? " · ≈ marks allocated estimates" : ""}${meterAvailable ? " · LINE = meter remaining" : ""}`
|
|
656
|
+
: meterAvailable
|
|
657
|
+
? `BARS = ${actualEstimated ? "token quantity by model · ≈ marks allocated estimates" : "actual token quantity by model"} · LINE = meter remaining${meterUsable ? " · -% row = observed drain per column" : " · no usable meter drain observed"}`
|
|
658
|
+
: `BARS = ${actualEstimated ? "token quantity by model · ≈ marks allocated estimates" : "actual token quantity by model"} · no account-wide weekly meter observed`;
|
|
564
659
|
const lines = [
|
|
565
660
|
`┌${"─".repeat(frameWidth - 2)}┐`,
|
|
566
661
|
frameLine(
|
|
567
662
|
colorize(
|
|
568
|
-
`TOKEN LEDGER · ${percentMode ? "OBSERVED LIMIT DRAIN + WEEKLY METER" :
|
|
663
|
+
`TOKEN LEDGER · ${percentMode ? "OBSERVED LIMIT DRAIN + WEEKLY METER" : actualModeTitle} · ${localDateLabel(bounds.startDateString)} – ${localDateLabel(bounds.endDateString)} · ${days}D`,
|
|
569
664
|
PRIMARY_STYLE,
|
|
570
665
|
enabled,
|
|
571
666
|
),
|
|
@@ -575,14 +670,30 @@ export function renderTrendCombo({
|
|
|
575
670
|
colorize(
|
|
576
671
|
percentMode
|
|
577
672
|
? "BARS = observed limit % consumed per day by model · LINE = meter remaining · one percent scale"
|
|
578
|
-
:
|
|
579
|
-
|
|
580
|
-
|
|
673
|
+
: actualModeSubtitle,
|
|
674
|
+
SECONDARY_STYLE,
|
|
675
|
+
enabled,
|
|
676
|
+
),
|
|
677
|
+
frameWidth,
|
|
678
|
+
),
|
|
679
|
+
frameLine(
|
|
680
|
+
colorize(
|
|
681
|
+
`SNAPSHOT · ${snapshotFreshnessDetail(snapshotFreshness)}`,
|
|
581
682
|
SECONDARY_STYLE,
|
|
582
683
|
enabled,
|
|
583
684
|
),
|
|
584
685
|
frameWidth,
|
|
585
686
|
),
|
|
687
|
+
frameLine(
|
|
688
|
+
colorize(sourceStatusLine(sourceStatus), SECONDARY_STYLE, enabled),
|
|
689
|
+
frameWidth,
|
|
690
|
+
),
|
|
691
|
+
...(history
|
|
692
|
+
? [frameLine(colorize(history, SECONDARY_STYLE, enabled), frameWidth)]
|
|
693
|
+
: []),
|
|
694
|
+
...(sourceWarning
|
|
695
|
+
? [frameLine(colorize(sourceWarning, SECONDARY_STYLE, enabled), frameWidth)]
|
|
696
|
+
: []),
|
|
586
697
|
`├${"─".repeat(frameWidth - 2)}┤`,
|
|
587
698
|
];
|
|
588
699
|
for (let row = 0; row < plotHeight; row += 1) {
|
|
@@ -593,7 +704,9 @@ export function renderTrendCombo({
|
|
|
593
704
|
? percent(leftValue)
|
|
594
705
|
: compact(leftValue)
|
|
595
706
|
: "";
|
|
596
|
-
const rightLabel = axisRows.includes(row)
|
|
707
|
+
const rightLabel = meterAvailable && axisRows.includes(row)
|
|
708
|
+
? `${Math.round(rightValue)}%`
|
|
709
|
+
: "";
|
|
597
710
|
const content = chart[row]
|
|
598
711
|
.map(({ char, style }) => colorize(char, style, enabled))
|
|
599
712
|
.join("");
|
|
@@ -601,16 +714,21 @@ export function renderTrendCombo({
|
|
|
601
714
|
}
|
|
602
715
|
const axis = `${" ".repeat(leftWidth)}${colorize(`└${"─".repeat(plotWidth)}┘`, BORDER_STYLE, enabled)}${" ".repeat(rightWidth)}`;
|
|
603
716
|
lines.push(frameLine(axis, frameWidth));
|
|
604
|
-
lines.push(frameLine(xLabelLine(barBins, plotWidth, leftWidth, rightWidth
|
|
717
|
+
lines.push(frameLine(xLabelLine(barBins, plotWidth, leftWidth, rightWidth), frameWidth));
|
|
605
718
|
if (!percentMode && meterUsable) {
|
|
606
719
|
lines.push(frameLine(drainLabelLine(burn.bins, plotWidth, leftWidth, rightWidth, enabled), frameWidth));
|
|
607
720
|
lines.push(frameLine(fit("CALENDAR DAY · -% = OBSERVED METER DROP", innerWidth, "center"), frameWidth));
|
|
721
|
+
} else if (drainFallback) {
|
|
722
|
+
lines.push(frameLine(fit("CALENDAR DAY · --drain unavailable; showing raw local tokens", innerWidth, "center"), frameWidth));
|
|
608
723
|
} else {
|
|
609
724
|
lines.push(frameLine(fit("CALENDAR DAY", innerWidth, "center"), frameWidth));
|
|
610
725
|
}
|
|
611
726
|
lines.push(`├${"─".repeat(frameWidth - 2)}┤`);
|
|
612
727
|
|
|
613
|
-
const totalTokens = [...actual.totals.values()].reduce(
|
|
728
|
+
const totalTokens = [...actual.totals.values()].reduce(
|
|
729
|
+
(sum, value) => checkedTokenAdd(sum, value, { allowFractional: true }),
|
|
730
|
+
0,
|
|
731
|
+
);
|
|
614
732
|
const legendModels = percentMode
|
|
615
733
|
? [...burn.totals.keys()].sort(modelSort)
|
|
616
734
|
: [...actual.totals.keys()].sort(modelSort);
|
|
@@ -628,20 +746,25 @@ export function renderTrendCombo({
|
|
|
628
746
|
const fastPart = fastTokens > 0
|
|
629
747
|
? ` · ${percent((fastTokens / actual.totals.get(model)) * 100)} fast`
|
|
630
748
|
: "";
|
|
749
|
+
const estimatedPrefix = actual.estimatedModels?.has(model) ? "≈" : "";
|
|
631
750
|
return colorize(
|
|
632
|
-
`■ ${model} ${compact(actual.totals.get(model))} (${percent((actual.totals.get(model) / totalTokens) * 100)})${fastPart}`,
|
|
751
|
+
`■ ${model} ${estimatedPrefix}${compact(actual.totals.get(model))} (${percent((actual.totals.get(model) / totalTokens) * 100)})${fastPart}`,
|
|
633
752
|
styleForModel(model),
|
|
634
753
|
enabled,
|
|
635
754
|
);
|
|
636
755
|
});
|
|
637
|
-
lines.push(frameLine(colorize(percentMode ? "OBSERVED LIMIT DRAIN BY MODEL · LEFT AXIS" : "ACTUAL TOKEN VOLUME · LEFT AXIS", PRIMARY_STYLE, enabled), frameWidth));
|
|
756
|
+
lines.push(frameLine(colorize(percentMode ? "OBSERVED LIMIT DRAIN BY MODEL · LEFT AXIS" : actualEstimated ? "ACTUAL TOKEN VOLUME BY MODEL · ≈ = ALLOCATED ESTIMATE · LEFT AXIS" : "ACTUAL TOKEN VOLUME · LEFT AXIS", PRIMARY_STYLE, enabled), frameWidth));
|
|
638
757
|
for (let index = 0; index < legend.length; index += 2) {
|
|
639
758
|
const leftLegendWidth = Math.floor((innerWidth - 2) / 2);
|
|
640
759
|
const rightLegendWidth = innerWidth - 2 - leftLegendWidth;
|
|
641
760
|
lines.push(frameLine(`${fit(legend[index], leftLegendWidth)} ${fit(legend[index + 1] ?? "", rightLegendWidth)}`, frameWidth));
|
|
642
761
|
}
|
|
643
|
-
|
|
644
|
-
|
|
762
|
+
if (meterAvailable) {
|
|
763
|
+
lines.push(frameLine(colorize("LINE · OBSERVED WEEKLY QUOTA REMAINING · RIGHT AXIS", LINE_STYLE, enabled), frameWidth));
|
|
764
|
+
lines.push(frameLine(colorize("↟ reset marker returns the line to 100%; it never rises within a cycle", SECONDARY_STYLE, enabled), frameWidth));
|
|
765
|
+
} else {
|
|
766
|
+
lines.push(frameLine(colorize("NO ACCOUNT-WIDE WEEKLY METER OBSERVED", SECONDARY_STYLE, enabled), frameWidth));
|
|
767
|
+
}
|
|
645
768
|
for (const line of formatAttribution(trend, enabled, frameWidth, percentMode)) {
|
|
646
769
|
lines.push(frameLine(line, frameWidth));
|
|
647
770
|
}
|