tledger 0.3.1 → 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 -157
- 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 +2025 -1724
- package/bin/token-ledger-trend-terminal.mjs +273 -150
- package/bin/token-ledger-trend.mjs +204 -198
- package/bin/token-ledger-tui.mjs +180 -51
- package/bin/token-ledger.mjs +735 -208
- 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 +3329 -488
- 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
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Cache report analysis. This module owns snapshot parsing and aggregation but
|
|
2
|
+
// has no dependency on either image renderer.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
multiDayBounds,
|
|
6
|
+
trendModelLabel,
|
|
7
|
+
} from "./token-ledger-trend.mjs";
|
|
8
|
+
import { chooseBinSize } from "./token-ledger-image-layout.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createTimeZoneFormatter,
|
|
11
|
+
localDateBoundary,
|
|
12
|
+
localDateString,
|
|
13
|
+
shiftCalendarDate,
|
|
14
|
+
} from "../lib/token-ledger-calendar.mjs";
|
|
15
|
+
import {
|
|
16
|
+
MAX_SAFE_TOKEN_COUNT,
|
|
17
|
+
checkedTokenAdd,
|
|
18
|
+
checkedTokenPartitionAdd,
|
|
19
|
+
splitUsageBucketsAtBoundaries,
|
|
20
|
+
usageBuckets,
|
|
21
|
+
usageBucketsInRange,
|
|
22
|
+
usageCallCount,
|
|
23
|
+
usageDetailedCallCount,
|
|
24
|
+
usageInputCallCount,
|
|
25
|
+
} from "../lib/token-ledger-usage.mjs";
|
|
26
|
+
|
|
27
|
+
const MIN_BIN_WIDTH = 34;
|
|
28
|
+
const MAX_MODEL_ROWS = 6;
|
|
29
|
+
const MAX_FINITE_NUMBER = Number.MAX_VALUE;
|
|
30
|
+
const SCALE_HEADROOM = 1 - Number.EPSILON;
|
|
31
|
+
|
|
32
|
+
function rateFor(inputTokens, cachedInputTokens) {
|
|
33
|
+
return inputTokens > 0 ? (cachedInputTokens / inputTokens) * 100 : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function primitiveString(value) {
|
|
37
|
+
try {
|
|
38
|
+
const text = String.prototype.valueOf.call(value);
|
|
39
|
+
return text === value ? text : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function primitiveNumber(value) {
|
|
46
|
+
try {
|
|
47
|
+
const number = Number.prototype.valueOf.call(value);
|
|
48
|
+
return number === value ? number : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function finiteTimestamp(value) {
|
|
55
|
+
const text = primitiveString(value);
|
|
56
|
+
if (text === null) return null;
|
|
57
|
+
const timestampMs = Date.parse(text);
|
|
58
|
+
return Number.isFinite(timestampMs) ? timestampMs : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parsedNonNegativeFiniteNumber(value) {
|
|
62
|
+
const primitive = primitiveNumber(value);
|
|
63
|
+
const text = primitive === null ? primitiveString(value) : null;
|
|
64
|
+
const number = primitive ?? (
|
|
65
|
+
text === null || text.trim() === "" ? NaN : Number(text)
|
|
66
|
+
);
|
|
67
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scaleToFiniteSum(values) {
|
|
71
|
+
// Values are non-negative and finite; return one common factor for them.
|
|
72
|
+
const ratio = values.reduce(
|
|
73
|
+
(sum, value) => sum + value / MAX_FINITE_NUMBER,
|
|
74
|
+
0,
|
|
75
|
+
);
|
|
76
|
+
const sum = values.reduce((total, value) => total + value, 0);
|
|
77
|
+
return ratio > 1 || !Number.isFinite(sum)
|
|
78
|
+
? SCALE_HEADROOM / Math.max(1, ratio)
|
|
79
|
+
: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function safeModelLabel(value) {
|
|
83
|
+
const model = primitiveString(value);
|
|
84
|
+
return model === null ? "Unknown" : trendModelLabel(model);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cacheBreakdown(event) {
|
|
88
|
+
const parsedReportedTotalTokens = parsedNonNegativeFiniteNumber(
|
|
89
|
+
event.totalTokens,
|
|
90
|
+
);
|
|
91
|
+
const parsedInputTokens = parsedNonNegativeFiniteNumber(event.inputTokens);
|
|
92
|
+
const parsedCachedInputTokens = parsedNonNegativeFiniteNumber(
|
|
93
|
+
event.cachedInputTokens,
|
|
94
|
+
);
|
|
95
|
+
const parsedOutputTokens = parsedNonNegativeFiniteNumber(event.outputTokens);
|
|
96
|
+
const reportedTotalTokens = parsedReportedTotalTokens ?? 0;
|
|
97
|
+
const rawInputTokens = parsedInputTokens ?? 0;
|
|
98
|
+
const outputTokens = parsedOutputTokens ?? 0;
|
|
99
|
+
const rawCachedInputTokens = Math.min(
|
|
100
|
+
rawInputTokens,
|
|
101
|
+
parsedCachedInputTokens ?? 0,
|
|
102
|
+
);
|
|
103
|
+
const componentOverflowed = !Number.isFinite(rawInputTokens + outputTokens);
|
|
104
|
+
const componentScale = scaleToFiniteSum([rawInputTokens, outputTokens]);
|
|
105
|
+
const inputTokens = rawInputTokens * componentScale;
|
|
106
|
+
const componentOutputTokens = outputTokens * componentScale;
|
|
107
|
+
const cachedInputTokens = Math.min(
|
|
108
|
+
inputTokens,
|
|
109
|
+
rawCachedInputTokens * componentScale,
|
|
110
|
+
);
|
|
111
|
+
const componentTotalTokens = Math.min(
|
|
112
|
+
MAX_FINITE_NUMBER,
|
|
113
|
+
inputTokens + componentOutputTokens,
|
|
114
|
+
);
|
|
115
|
+
const totalTokens = reportedTotalTokens > 0
|
|
116
|
+
? reportedTotalTokens
|
|
117
|
+
: componentTotalTokens;
|
|
118
|
+
const hasComponents = inputTokens > 0 || outputTokens > 0;
|
|
119
|
+
const hasReconciledBreakdown = hasComponents && (
|
|
120
|
+
reportedTotalTokens === 0 ||
|
|
121
|
+
componentTotalTokens === reportedTotalTokens ||
|
|
122
|
+
(componentOverflowed && reportedTotalTokens === MAX_FINITE_NUMBER)
|
|
123
|
+
);
|
|
124
|
+
const hasExplicitReconciledZeroBreakdown =
|
|
125
|
+
event.breakdownAvailable === true &&
|
|
126
|
+
parsedReportedTotalTokens === 0 &&
|
|
127
|
+
parsedInputTokens === 0 &&
|
|
128
|
+
parsedCachedInputTokens === 0 &&
|
|
129
|
+
parsedOutputTokens === 0;
|
|
130
|
+
const detailed = event.breakdownAvailable !== false && (
|
|
131
|
+
hasReconciledBreakdown || hasExplicitReconciledZeroBreakdown
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
totalTokens,
|
|
135
|
+
inputTokens,
|
|
136
|
+
cachedInputTokens,
|
|
137
|
+
uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens),
|
|
138
|
+
detailed,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseCacheEvent(value) {
|
|
143
|
+
if (value == null) return null;
|
|
144
|
+
try {
|
|
145
|
+
const timestampMs = finiteTimestamp(value.timestamp);
|
|
146
|
+
if (timestampMs === null) return null;
|
|
147
|
+
return {
|
|
148
|
+
timestampMs,
|
|
149
|
+
model: safeModelLabel(value.model),
|
|
150
|
+
breakdown: cacheBreakdown(value),
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function emptyAggregate() {
|
|
158
|
+
return {
|
|
159
|
+
eventCount: 0,
|
|
160
|
+
detailedEventCount: 0,
|
|
161
|
+
inputEventCount: 0,
|
|
162
|
+
totalTokens: 0,
|
|
163
|
+
detailedTokens: 0,
|
|
164
|
+
unknownBreakdownTokens: 0,
|
|
165
|
+
inputTokens: 0,
|
|
166
|
+
cachedInputTokens: 0,
|
|
167
|
+
uncachedInputTokens: 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const INPUT_SCALE = Symbol("cacheInputScale");
|
|
172
|
+
|
|
173
|
+
function inputScale(target) {
|
|
174
|
+
return Number.isFinite(target[INPUT_SCALE]) && target[INPUT_SCALE] >= 1
|
|
175
|
+
? target[INPUT_SCALE]
|
|
176
|
+
: 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setInputScale(target, scale) {
|
|
180
|
+
Object.defineProperty(target, INPUT_SCALE, {
|
|
181
|
+
configurable: true,
|
|
182
|
+
enumerable: false,
|
|
183
|
+
value: scale,
|
|
184
|
+
writable: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function addInputTotals(target, inputTokens, cachedInputTokens, sourceScale = 1) {
|
|
189
|
+
const targetScale = inputScale(target);
|
|
190
|
+
const normalizedSourceScale = Number.isFinite(sourceScale) && sourceScale >= 1
|
|
191
|
+
? sourceScale
|
|
192
|
+
: 1;
|
|
193
|
+
const commonScale = Math.max(targetScale, normalizedSourceScale);
|
|
194
|
+
const targetRatio = targetScale / commonScale;
|
|
195
|
+
const sourceRatio = normalizedSourceScale / commonScale;
|
|
196
|
+
const input = Number.isFinite(inputTokens) && inputTokens >= 0
|
|
197
|
+
? inputTokens
|
|
198
|
+
: 0;
|
|
199
|
+
const cached = Math.min(
|
|
200
|
+
input,
|
|
201
|
+
Number.isFinite(cachedInputTokens) && cachedInputTokens >= 0
|
|
202
|
+
? cachedInputTokens
|
|
203
|
+
: 0,
|
|
204
|
+
);
|
|
205
|
+
const nextInput = target.inputTokens * targetRatio + input * sourceRatio;
|
|
206
|
+
const nextCached =
|
|
207
|
+
target.cachedInputTokens * targetRatio + cached * sourceRatio;
|
|
208
|
+
const scaleFactor = Math.max(
|
|
209
|
+
1,
|
|
210
|
+
nextInput / MAX_SAFE_TOKEN_COUNT,
|
|
211
|
+
nextCached / MAX_SAFE_TOKEN_COUNT,
|
|
212
|
+
);
|
|
213
|
+
target.inputTokens = nextInput / scaleFactor;
|
|
214
|
+
target.cachedInputTokens = nextCached / scaleFactor;
|
|
215
|
+
target.uncachedInputTokens = Math.max(
|
|
216
|
+
0,
|
|
217
|
+
target.inputTokens - target.cachedInputTokens,
|
|
218
|
+
);
|
|
219
|
+
setInputScale(target, commonScale * scaleFactor);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function alignInputScale(target, commonScale) {
|
|
223
|
+
const currentScale = inputScale(target);
|
|
224
|
+
if (currentScale === commonScale) return;
|
|
225
|
+
const ratio = currentScale / commonScale;
|
|
226
|
+
target.inputTokens *= ratio;
|
|
227
|
+
target.cachedInputTokens *= ratio;
|
|
228
|
+
target.uncachedInputTokens = Math.max(
|
|
229
|
+
0,
|
|
230
|
+
target.inputTokens - target.cachedInputTokens,
|
|
231
|
+
);
|
|
232
|
+
setInputScale(target, commonScale);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function boundedTokenValue(value) {
|
|
236
|
+
return Number.isFinite(value) && value >= 0
|
|
237
|
+
? Math.min(value, MAX_SAFE_TOKEN_COUNT)
|
|
238
|
+
: 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function addBoundedTokens(current, contribution) {
|
|
242
|
+
const sum = boundedTokenValue(current) + boundedTokenValue(contribution);
|
|
243
|
+
return Number.isFinite(sum) && sum <= MAX_SAFE_TOKEN_COUNT
|
|
244
|
+
? sum
|
|
245
|
+
: MAX_SAFE_TOKEN_COUNT;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function addInput(target, breakdown, inputCallCount) {
|
|
249
|
+
addInputTotals(
|
|
250
|
+
target,
|
|
251
|
+
breakdown.inputTokens,
|
|
252
|
+
breakdown.cachedInputTokens,
|
|
253
|
+
);
|
|
254
|
+
target.inputEventCount = checkedTokenAdd(
|
|
255
|
+
target.inputEventCount,
|
|
256
|
+
inputCallCount,
|
|
257
|
+
{ allowFractional: true },
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function finalizeAggregate(aggregate) {
|
|
262
|
+
const uncachedInputTokens = Math.max(
|
|
263
|
+
0,
|
|
264
|
+
aggregate.inputTokens - aggregate.cachedInputTokens,
|
|
265
|
+
);
|
|
266
|
+
const measurementCoveragePercent = aggregate.totalTokens > 0
|
|
267
|
+
? (aggregate.detailedTokens /
|
|
268
|
+
(aggregate.detailedTokens + aggregate.unknownBreakdownTokens)) *
|
|
269
|
+
100
|
|
270
|
+
: aggregate.eventCount > 0
|
|
271
|
+
? (aggregate.detailedEventCount / aggregate.eventCount) * 100
|
|
272
|
+
: null;
|
|
273
|
+
const finalized = {
|
|
274
|
+
...aggregate,
|
|
275
|
+
uncachedInputTokens,
|
|
276
|
+
rate: rateFor(aggregate.inputTokens, aggregate.cachedInputTokens),
|
|
277
|
+
measurementCoveragePercent,
|
|
278
|
+
};
|
|
279
|
+
setInputScale(finalized, inputScale(aggregate));
|
|
280
|
+
return finalized;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function accumulateRange(
|
|
284
|
+
snapshot,
|
|
285
|
+
bounds,
|
|
286
|
+
bins = null,
|
|
287
|
+
dateIndexByString = null,
|
|
288
|
+
sourceEvents = null,
|
|
289
|
+
) {
|
|
290
|
+
const startMs = bounds.start.getTime();
|
|
291
|
+
const endMs = bounds.end.getTime();
|
|
292
|
+
const totals = emptyAggregate();
|
|
293
|
+
const modelTotals = new Map();
|
|
294
|
+
const dateFormatter = bins === null
|
|
295
|
+
? null
|
|
296
|
+
: createTimeZoneFormatter(bounds.timeZone);
|
|
297
|
+
|
|
298
|
+
const boundaries = [
|
|
299
|
+
startMs,
|
|
300
|
+
...((bins ?? []).map((bin) =>
|
|
301
|
+
localDateBoundary(
|
|
302
|
+
bin.endDateString,
|
|
303
|
+
bounds.timeZone,
|
|
304
|
+
dateFormatter,
|
|
305
|
+
).getTime())),
|
|
306
|
+
endMs,
|
|
307
|
+
];
|
|
308
|
+
const events = sourceEvents === null
|
|
309
|
+
? bins === null
|
|
310
|
+
? usageBucketsInRange(snapshot, startMs, endMs)
|
|
311
|
+
: splitUsageBucketsAtBoundaries(usageBuckets(snapshot), boundaries)
|
|
312
|
+
: bins === null
|
|
313
|
+
? sourceEvents
|
|
314
|
+
: splitUsageBucketsAtBoundaries(sourceEvents, boundaries);
|
|
315
|
+
for (const event of events) {
|
|
316
|
+
const parsed = parseCacheEvent(event);
|
|
317
|
+
if (
|
|
318
|
+
parsed === null ||
|
|
319
|
+
parsed.timestampMs < startMs ||
|
|
320
|
+
parsed.timestampMs >= endMs
|
|
321
|
+
) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const { breakdown } = parsed;
|
|
325
|
+
const dateString = dateFormatter === null
|
|
326
|
+
? null
|
|
327
|
+
: localDateString(parsed.timestampMs, bounds.timeZone, dateFormatter);
|
|
328
|
+
const binIndex = dateString === null ? null : dateIndexByString.get(dateString);
|
|
329
|
+
const bin = binIndex === undefined || binIndex === null ? null : bins[binIndex];
|
|
330
|
+
const callCount = usageCallCount(event);
|
|
331
|
+
const detailedCallCount = usageDetailedCallCount(event);
|
|
332
|
+
const inputCallCount = usageInputCallCount(event);
|
|
333
|
+
totals.eventCount = checkedTokenAdd(totals.eventCount, callCount, {
|
|
334
|
+
allowFractional: true,
|
|
335
|
+
});
|
|
336
|
+
totals.totalTokens = addBoundedTokens(
|
|
337
|
+
totals.totalTokens,
|
|
338
|
+
breakdown.totalTokens,
|
|
339
|
+
);
|
|
340
|
+
if (bin) {
|
|
341
|
+
bin.eventCount = checkedTokenAdd(bin.eventCount, callCount, {
|
|
342
|
+
allowFractional: true,
|
|
343
|
+
});
|
|
344
|
+
bin.totalTokens = addBoundedTokens(bin.totalTokens, breakdown.totalTokens);
|
|
345
|
+
}
|
|
346
|
+
checkedTokenPartitionAdd(
|
|
347
|
+
totals,
|
|
348
|
+
boundedTokenValue(breakdown.totalTokens),
|
|
349
|
+
{ detailed: breakdown.detailed },
|
|
350
|
+
);
|
|
351
|
+
if (bin) {
|
|
352
|
+
checkedTokenPartitionAdd(
|
|
353
|
+
bin,
|
|
354
|
+
boundedTokenValue(breakdown.totalTokens),
|
|
355
|
+
{ detailed: breakdown.detailed },
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (!breakdown.detailed) continue;
|
|
359
|
+
|
|
360
|
+
totals.detailedEventCount = checkedTokenAdd(
|
|
361
|
+
totals.detailedEventCount,
|
|
362
|
+
detailedCallCount,
|
|
363
|
+
{ allowFractional: true },
|
|
364
|
+
);
|
|
365
|
+
if (bin) {
|
|
366
|
+
bin.detailedEventCount = checkedTokenAdd(
|
|
367
|
+
bin.detailedEventCount,
|
|
368
|
+
detailedCallCount,
|
|
369
|
+
{ allowFractional: true },
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (!(breakdown.inputTokens > 0)) continue;
|
|
373
|
+
|
|
374
|
+
addInput(totals, breakdown, inputCallCount);
|
|
375
|
+
if (bin) addInput(bin, breakdown, inputCallCount);
|
|
376
|
+
const model = parsed.model;
|
|
377
|
+
const modelAggregate = modelTotals.get(model) ?? {
|
|
378
|
+
model,
|
|
379
|
+
inputTokens: 0,
|
|
380
|
+
cachedInputTokens: 0,
|
|
381
|
+
uncachedInputTokens: 0,
|
|
382
|
+
inputEventCount: 0,
|
|
383
|
+
};
|
|
384
|
+
addInput(modelAggregate, breakdown, inputCallCount);
|
|
385
|
+
modelTotals.set(model, modelAggregate);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const commonScale = Math.max(
|
|
389
|
+
inputScale(totals),
|
|
390
|
+
...[...modelTotals.values()].map(inputScale),
|
|
391
|
+
...(bins ?? []).map(inputScale),
|
|
392
|
+
);
|
|
393
|
+
alignInputScale(totals, commonScale);
|
|
394
|
+
for (const model of modelTotals.values()) {
|
|
395
|
+
alignInputScale(model, commonScale);
|
|
396
|
+
}
|
|
397
|
+
for (const bin of bins ?? []) {
|
|
398
|
+
alignInputScale(bin, commonScale);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const summary = finalizeAggregate(totals);
|
|
402
|
+
summary.models = [...modelTotals.values()]
|
|
403
|
+
.map((model) => finalizeAggregate(model))
|
|
404
|
+
.sort(
|
|
405
|
+
(left, right) =>
|
|
406
|
+
right.inputTokens - left.inputTokens || left.model.localeCompare(right.model),
|
|
407
|
+
);
|
|
408
|
+
return summary;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function aggregateCacheRange(snapshot, bounds, { events = null } = {}) {
|
|
412
|
+
return accumulateRange(snapshot, bounds, null, null, events);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function buildCacheReportData(
|
|
416
|
+
snapshot,
|
|
417
|
+
bounds,
|
|
418
|
+
days,
|
|
419
|
+
plotWidth,
|
|
420
|
+
binSizeOverride = null,
|
|
421
|
+
events = null,
|
|
422
|
+
) {
|
|
423
|
+
const rangeDays = Math.max(1, Number(days) || Number(bounds.rangeDays) || 7);
|
|
424
|
+
// The combined report passes the trend chart's bin size so both charts'
|
|
425
|
+
// columns stay vertically aligned.
|
|
426
|
+
const binSize = binSizeOverride ?? chooseBinSize(rangeDays, plotWidth, {
|
|
427
|
+
minBinWidth: MIN_BIN_WIDTH,
|
|
428
|
+
preferDaily: true,
|
|
429
|
+
});
|
|
430
|
+
const binCount = Math.ceil(rangeDays / binSize);
|
|
431
|
+
const bins = Array.from({ length: binCount }, (_, index) => ({
|
|
432
|
+
...emptyAggregate(),
|
|
433
|
+
startDateString: shiftCalendarDate(
|
|
434
|
+
bounds.startDateString,
|
|
435
|
+
index * binSize,
|
|
436
|
+
),
|
|
437
|
+
endDateString: shiftCalendarDate(
|
|
438
|
+
bounds.startDateString,
|
|
439
|
+
Math.min(rangeDays, (index + 1) * binSize),
|
|
440
|
+
),
|
|
441
|
+
}));
|
|
442
|
+
const dateIndexByString = new Map(
|
|
443
|
+
Array.from({ length: rangeDays }, (_, index) => [
|
|
444
|
+
shiftCalendarDate(bounds.startDateString, index),
|
|
445
|
+
Math.floor(index / binSize),
|
|
446
|
+
]),
|
|
447
|
+
);
|
|
448
|
+
const summary = accumulateRange(
|
|
449
|
+
snapshot,
|
|
450
|
+
bounds,
|
|
451
|
+
bins,
|
|
452
|
+
dateIndexByString,
|
|
453
|
+
events,
|
|
454
|
+
);
|
|
455
|
+
return {
|
|
456
|
+
...summary,
|
|
457
|
+
bins: bins.map((bin) => finalizeAggregate(bin)),
|
|
458
|
+
binSize,
|
|
459
|
+
binCount,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function combinedModelRows(models) {
|
|
464
|
+
if (models.length <= MAX_MODEL_ROWS) return models;
|
|
465
|
+
const visible = models.slice(0, MAX_MODEL_ROWS - 1);
|
|
466
|
+
const remainder = models.slice(MAX_MODEL_ROWS - 1).reduce(
|
|
467
|
+
(row, model) => {
|
|
468
|
+
row.inputTokens += model.inputTokens;
|
|
469
|
+
row.cachedInputTokens += model.cachedInputTokens;
|
|
470
|
+
row.inputEventCount += model.inputEventCount;
|
|
471
|
+
row.uncachedInputTokens = Math.max(
|
|
472
|
+
0,
|
|
473
|
+
row.inputTokens - row.cachedInputTokens,
|
|
474
|
+
);
|
|
475
|
+
return row;
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
model: "Other models",
|
|
479
|
+
inputTokens: 0,
|
|
480
|
+
cachedInputTokens: 0,
|
|
481
|
+
uncachedInputTokens: 0,
|
|
482
|
+
inputEventCount: 0,
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
return [...visible, finalizeAggregate(remainder)];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function priorPeriodSummary(snapshot, bounds, days, events = null) {
|
|
489
|
+
const priorEndDate = shiftCalendarDate(bounds.startDateString, -1);
|
|
490
|
+
const priorBounds = multiDayBounds(priorEndDate, bounds.timeZone, days);
|
|
491
|
+
return aggregateCacheRange(snapshot, priorBounds, { events });
|
|
492
|
+
}
|