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
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./token-ledger-rates.mjs";
|
|
2
|
+
calculateCodexPurchasedCredits,
|
|
3
|
+
CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
4
|
+
} from "../lib/token-ledger-rates.mjs";
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
MAX_SAFE_TOKEN_COUNT,
|
|
7
|
+
checkedFiniteAdd,
|
|
8
|
+
checkedTokenAdd,
|
|
9
|
+
tokenValue,
|
|
10
10
|
} from "../lib/token-ledger-usage.mjs";
|
|
11
|
+
import {
|
|
12
|
+
createTimeZoneFormatter,
|
|
13
|
+
localDateBoundary,
|
|
14
|
+
localDateString,
|
|
15
|
+
shiftCalendarDate,
|
|
16
|
+
todayInTimeZone,
|
|
17
|
+
} from "../lib/token-ledger-calendar.mjs";
|
|
18
|
+
import { buildRangeAnalysis as buildIndexedRangeAnalysis } from "../lib/token-ledger-range-analysis.mjs";
|
|
19
|
+
import {
|
|
20
|
+
quotaIdentityMatchesContract,
|
|
21
|
+
snapshotHasCurrentQuotaIdentityContract,
|
|
22
|
+
} from "../lib/token-ledger-quota-contract.mjs";
|
|
11
23
|
|
|
12
24
|
const WEEK_MINUTES = 10_080;
|
|
13
25
|
const RESET_JITTER_SECONDS = 5 * 60;
|
|
@@ -17,96 +29,41 @@ const MAX_TREND_DAYS = 3_650;
|
|
|
17
29
|
const LONG_GAP_MS = 36 * 60 * 60 * 1_000;
|
|
18
30
|
|
|
19
31
|
const MODEL_SORT_ORDER = new Map([
|
|
20
|
-
["
|
|
21
|
-
["
|
|
22
|
-
["
|
|
23
|
-
["
|
|
24
|
-
["GPT-5.
|
|
25
|
-
["
|
|
26
|
-
["
|
|
27
|
-
["
|
|
28
|
-
["
|
|
29
|
-
["
|
|
32
|
+
["Astra", 0],
|
|
33
|
+
["Luna", 1],
|
|
34
|
+
["Sol", 2],
|
|
35
|
+
["Terra", 3],
|
|
36
|
+
["GPT-5.5", 4],
|
|
37
|
+
["GPT-5.4", 5],
|
|
38
|
+
["Daybreak", 6],
|
|
39
|
+
["Auto review", 7],
|
|
40
|
+
["Other", 8],
|
|
41
|
+
["Unknown", 9],
|
|
42
|
+
["Unattributed", 10],
|
|
30
43
|
]);
|
|
31
44
|
|
|
32
45
|
function finiteTimestamp(value) {
|
|
33
|
-
|
|
46
|
+
if (typeof value !== "string" && !Number.isFinite(value)) return null;
|
|
47
|
+
const timestamp = typeof value === "string"
|
|
48
|
+
? Date.parse(value)
|
|
49
|
+
: new Date(value).getTime();
|
|
34
50
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
function dateStringFromParts(parts) {
|
|
38
|
-
return [parts.year, parts.month, parts.day]
|
|
39
|
-
.map((value, index) =>
|
|
40
|
-
index === 0 ? String(value) : String(value).padStart(2, "0"),
|
|
41
|
-
)
|
|
42
|
-
.join("-");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function shiftCalendarDate(dateString, amount) {
|
|
46
|
-
const [year, month, day] = dateString.split("-").map(Number);
|
|
47
|
-
const date = new Date(Date.UTC(year, month - 1, day + amount));
|
|
48
|
-
return dateStringFromParts({
|
|
49
|
-
year: date.getUTCFullYear(),
|
|
50
|
-
month: date.getUTCMonth() + 1,
|
|
51
|
-
day: date.getUTCDate(),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function offsetAt(instant, timeZone) {
|
|
56
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
57
|
-
timeZone,
|
|
58
|
-
timeZoneName: "longOffset",
|
|
59
|
-
}).formatToParts(instant);
|
|
60
|
-
const value = parts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
|
|
61
|
-
if (value === "GMT") return 0;
|
|
62
|
-
const match = value.match(/^GMT([+-])(\d{2}):?(\d{2})?$/);
|
|
63
|
-
if (!match) return 0;
|
|
64
|
-
const minutes = Number(match[2]) * 60 + Number(match[3] || 0);
|
|
65
|
-
return (match[1] === "+" ? 1 : -1) * minutes * 60 * 1_000;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function zonedMidnight(dateString, timeZone) {
|
|
69
|
-
const [year, month, day] = dateString.split("-").map(Number);
|
|
70
|
-
const utcGuess = Date.UTC(year, month - 1, day);
|
|
71
|
-
let instant = new Date(utcGuess - offsetAt(new Date(utcGuess), timeZone));
|
|
72
|
-
instant = new Date(utcGuess - offsetAt(instant, timeZone));
|
|
73
|
-
return instant;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function localDateString(timestampMs, timeZone) {
|
|
77
|
-
return new Intl.DateTimeFormat("en-CA", {
|
|
78
|
-
timeZone,
|
|
79
|
-
year: "numeric",
|
|
80
|
-
month: "2-digit",
|
|
81
|
-
day: "2-digit",
|
|
82
|
-
}).format(new Date(timestampMs));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function todayInTimeZone(timeZone) {
|
|
86
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
87
|
-
timeZone,
|
|
88
|
-
year: "numeric",
|
|
89
|
-
month: "2-digit",
|
|
90
|
-
day: "2-digit",
|
|
91
|
-
}).formatToParts(new Date());
|
|
92
|
-
const values = Object.fromEntries(
|
|
93
|
-
parts
|
|
94
|
-
.filter((part) => part.type !== "literal")
|
|
95
|
-
.map((part) => [part.type, Number(part.value)]),
|
|
96
|
-
);
|
|
97
|
-
return dateStringFromParts(values);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
53
|
export function multiDayBounds(value, timeZone, rangeDays) {
|
|
101
54
|
const days = Number(rangeDays);
|
|
102
55
|
if (!Number.isSafeInteger(days) || days < 1 || days > MAX_TREND_DAYS) {
|
|
103
56
|
throw new Error(`Trend range must be between 1 and ${MAX_TREND_DAYS} days.`);
|
|
104
57
|
}
|
|
58
|
+
const formatter = createTimeZoneFormatter(timeZone);
|
|
105
59
|
let endDateString = value;
|
|
106
60
|
if (!endDateString || endDateString === "today") {
|
|
107
|
-
endDateString = todayInTimeZone(timeZone);
|
|
61
|
+
endDateString = todayInTimeZone(timeZone, formatter);
|
|
108
62
|
} else if (endDateString === "yesterday") {
|
|
109
|
-
endDateString = shiftCalendarDate(
|
|
63
|
+
endDateString = shiftCalendarDate(
|
|
64
|
+
todayInTimeZone(timeZone, formatter),
|
|
65
|
+
-1,
|
|
66
|
+
);
|
|
110
67
|
}
|
|
111
68
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(endDateString)) {
|
|
112
69
|
throw new Error("Trend end date must be YYYY-MM-DD, today, or yesterday.");
|
|
@@ -125,19 +82,28 @@ export function multiDayBounds(value, timeZone, rangeDays) {
|
|
|
125
82
|
dateString: endDateString,
|
|
126
83
|
startDateString,
|
|
127
84
|
endDateString,
|
|
128
|
-
start:
|
|
129
|
-
end:
|
|
85
|
+
start: localDateBoundary(startDateString, timeZone, formatter),
|
|
86
|
+
end: localDateBoundary(
|
|
87
|
+
shiftCalendarDate(endDateString, 1),
|
|
88
|
+
timeZone,
|
|
89
|
+
formatter,
|
|
90
|
+
),
|
|
130
91
|
timeZone,
|
|
131
92
|
rangeDays: days,
|
|
132
93
|
};
|
|
133
94
|
}
|
|
134
95
|
|
|
135
|
-
function
|
|
136
|
-
return
|
|
96
|
+
export function priorPeriodBounds(bounds, days = bounds.rangeDays) {
|
|
97
|
+
return multiDayBounds(
|
|
98
|
+
shiftCalendarDate(bounds.startDateString, -1),
|
|
99
|
+
bounds.timeZone,
|
|
100
|
+
days,
|
|
101
|
+
);
|
|
137
102
|
}
|
|
138
103
|
|
|
139
104
|
export function trendModelLabel(value) {
|
|
140
105
|
const model = String(value || "unknown").trim().toLowerCase();
|
|
106
|
+
if (model.includes("astra")) return "Astra";
|
|
141
107
|
if (model.includes("luna")) return "Luna";
|
|
142
108
|
if (model.includes("sol")) return "Sol";
|
|
143
109
|
if (model.includes("terra")) return "Terra";
|
|
@@ -150,7 +116,19 @@ export function trendModelLabel(value) {
|
|
|
150
116
|
}
|
|
151
117
|
|
|
152
118
|
export function weeklyQuotaObservations(snapshot = {}) {
|
|
153
|
-
|
|
119
|
+
if (!snapshotHasCurrentQuotaIdentityContract(snapshot)) return [];
|
|
120
|
+
if (!Array.isArray(snapshot.quotaObservations)) return [];
|
|
121
|
+
let observations = snapshot.quotaObservations
|
|
122
|
+
.filter((observation) =>
|
|
123
|
+
observation !== null &&
|
|
124
|
+
typeof observation === "object" &&
|
|
125
|
+
observation.windowMinutes === WEEK_MINUTES &&
|
|
126
|
+
Number.isFinite(observation.resetsAt) &&
|
|
127
|
+
observation.resetsAt > 0 &&
|
|
128
|
+
Number.isFinite(observation.usedPercent) &&
|
|
129
|
+
observation.usedPercent >= 0 &&
|
|
130
|
+
observation.usedPercent <= 100,
|
|
131
|
+
)
|
|
154
132
|
.map((observation) => {
|
|
155
133
|
const timestampMs = finiteTimestamp(observation.timestamp);
|
|
156
134
|
const lastSeenAtMs = finiteTimestamp(observation.lastSeenAt);
|
|
@@ -161,52 +139,17 @@ export function weeklyQuotaObservations(snapshot = {}) {
|
|
|
161
139
|
timestampMs === null
|
|
162
140
|
? lastSeenAtMs
|
|
163
141
|
: Math.max(timestampMs, lastSeenAtMs ?? timestampMs),
|
|
164
|
-
resetsAt: Number(observation.resetsAt),
|
|
165
|
-
usedPercent: Number(observation.usedPercent),
|
|
166
142
|
};
|
|
167
143
|
})
|
|
168
|
-
.filter(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.map((observation) => ({
|
|
177
|
-
...observation,
|
|
178
|
-
usedPercent: clampPercent(observation.usedPercent),
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
// Keep exactly one meter: the account-wide weekly limit. Legacy snapshots
|
|
182
|
-
// tag it with scope: "account"; current snapshots carry a limitKey per
|
|
183
|
-
// limit bucket, where the account-wide bucket has no limitName. Named
|
|
184
|
-
// buckets (per-model limit pools) are separate meters and must not be
|
|
185
|
-
// stitched into this line.
|
|
186
|
-
const accountScoped = observations.filter(
|
|
187
|
-
(observation) => observation.scope === "account",
|
|
144
|
+
.filter((observation) => observation.timestampMs !== null);
|
|
145
|
+
|
|
146
|
+
// Keep exactly one meter: only explicit provider-derived account scope is
|
|
147
|
+
// authoritative. A missing display label does not prove account scope.
|
|
148
|
+
observations = observations.filter(
|
|
149
|
+
(observation) =>
|
|
150
|
+
observation.scope === "account" &&
|
|
151
|
+
quotaIdentityMatchesContract(observation),
|
|
188
152
|
);
|
|
189
|
-
if (accountScoped.length) {
|
|
190
|
-
observations = accountScoped;
|
|
191
|
-
} else if (observations.some((observation) => observation.limitKey)) {
|
|
192
|
-
const groups = new Map();
|
|
193
|
-
for (const observation of observations) {
|
|
194
|
-
const key = observation.limitKey ?? "anonymous";
|
|
195
|
-
const group = groups.get(key) ?? [];
|
|
196
|
-
group.push(observation);
|
|
197
|
-
groups.set(key, group);
|
|
198
|
-
}
|
|
199
|
-
const accountWide = [...groups.values()].filter((group) =>
|
|
200
|
-
group.every((observation) => !observation.limitName),
|
|
201
|
-
);
|
|
202
|
-
const pool = accountWide.length ? accountWide : [...groups.values()];
|
|
203
|
-
observations = pool.sort((left, right) => right.length - left.length)[0];
|
|
204
|
-
} else {
|
|
205
|
-
const accountWide = observations.filter(
|
|
206
|
-
(observation) => !observation.limitName,
|
|
207
|
-
);
|
|
208
|
-
if (accountWide.length) observations = accountWide;
|
|
209
|
-
}
|
|
210
153
|
|
|
211
154
|
observations.sort(
|
|
212
155
|
(left, right) =>
|
|
@@ -222,6 +165,13 @@ export function weeklyQuotaObservations(snapshot = {}) {
|
|
|
222
165
|
// stale readings from sessions still reporting a superseded window would
|
|
223
166
|
// otherwise be fused into one line, producing meter drain that never happened.
|
|
224
167
|
export function normalizeQuotaTimeline(observations) {
|
|
168
|
+
// Some source logs stamp new quota payloads with an old session timestamp.
|
|
169
|
+
// A weekly reading cannot precede its own window. Such an anchor would
|
|
170
|
+
// reorder entire epochs and suppress otherwise valid subsequent readings.
|
|
171
|
+
observations = observations.filter((observation) =>
|
|
172
|
+
observation.timestampMs >=
|
|
173
|
+
(observation.resetsAt - WEEK_MINUTES * 60 - RESET_JITTER_SECONDS) * 1_000,
|
|
174
|
+
);
|
|
225
175
|
if (!observations.length) return [];
|
|
226
176
|
|
|
227
177
|
const epochs = [];
|
|
@@ -305,46 +255,53 @@ export function normalizeQuotaTimeline(observations) {
|
|
|
305
255
|
}
|
|
306
256
|
|
|
307
257
|
export function eventCredits(event) {
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
event.serviceTier
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const stored = Number(event.rateCardCredits);
|
|
316
|
-
if (event.rateCardCredits !== null && event.rateCardCredits !== undefined) {
|
|
317
|
-
// Stored credits from current snapshots already include the fast-mode
|
|
318
|
-
// multiplier.
|
|
319
|
-
if (Number.isFinite(stored) && stored >= 0) return stored;
|
|
320
|
-
}
|
|
321
|
-
return null;
|
|
258
|
+
// Always recompute with the current purchased-credit card. Snapshot values
|
|
259
|
+
// may have been stored under an older card and must not fill a current gap.
|
|
260
|
+
return calculateCodexPurchasedCredits({
|
|
261
|
+
model: event.rateCardModel ?? event.model,
|
|
262
|
+
serviceTier: event.serviceTier,
|
|
263
|
+
usage: event,
|
|
264
|
+
});
|
|
322
265
|
}
|
|
323
266
|
|
|
324
267
|
function eventWeight(event, fallbackCreditsPerToken) {
|
|
325
268
|
const credits = eventCredits(event);
|
|
326
269
|
if (Number.isFinite(credits) && credits > 0) return credits;
|
|
327
|
-
const tokens =
|
|
270
|
+
const tokens = tokenValue(event.totalTokens, {
|
|
271
|
+
allowFractional: event.rangeAllocationEstimated === true,
|
|
272
|
+
});
|
|
328
273
|
return fallbackCreditsPerToken > 0 ? tokens * fallbackCreditsPerToken : tokens;
|
|
329
274
|
}
|
|
330
275
|
|
|
276
|
+
function addRatedTotals(totals, credits, tokens) {
|
|
277
|
+
const scaledTokens = tokens / totals.scale;
|
|
278
|
+
const scaledCredits = credits / totals.scale;
|
|
279
|
+
const nextTokens = totals.tokens + scaledTokens;
|
|
280
|
+
const nextCredits = checkedFiniteAdd(totals.credits, scaledCredits);
|
|
281
|
+
const scaleFactor = Math.max(1, nextTokens / MAX_SAFE_TOKEN_COUNT);
|
|
282
|
+
totals.tokens = nextTokens / scaleFactor;
|
|
283
|
+
totals.credits = nextCredits / scaleFactor;
|
|
284
|
+
totals.scale *= scaleFactor;
|
|
285
|
+
}
|
|
286
|
+
|
|
331
287
|
function allocateBurn(delta, events, timeZone) {
|
|
332
288
|
if (!(delta > 0)) return { contributions: new Map(), method: "none" };
|
|
333
289
|
|
|
334
|
-
|
|
335
|
-
let ratedTokens = 0;
|
|
290
|
+
const ratedTotals = { credits: 0, tokens: 0, scale: 1 };
|
|
336
291
|
let hasUnrated = false;
|
|
337
292
|
for (const event of events) {
|
|
293
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
338
294
|
const credits = eventCredits(event);
|
|
339
|
-
const
|
|
295
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
296
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
340
297
|
if (Number.isFinite(credits) && credits > 0) {
|
|
341
|
-
|
|
342
|
-
ratedTokens += tokens;
|
|
298
|
+
addRatedTotals(ratedTotals, credits, tokens);
|
|
343
299
|
} else if (tokens > 0) {
|
|
344
300
|
hasUnrated = true;
|
|
345
301
|
}
|
|
346
302
|
}
|
|
347
303
|
|
|
304
|
+
const { credits: ratedCredits, tokens: ratedTokens } = ratedTotals;
|
|
348
305
|
const fallbackCreditsPerToken =
|
|
349
306
|
ratedCredits > 0 && ratedTokens > 0 ? ratedCredits / ratedTokens : 0;
|
|
350
307
|
const weights = new Map();
|
|
@@ -354,12 +311,12 @@ function allocateBurn(delta, events, timeZone) {
|
|
|
354
311
|
const weight = eventWeight(event, fallbackCreditsPerToken);
|
|
355
312
|
if (!(weight > 0)) continue;
|
|
356
313
|
const model = trendModelLabel(event.model);
|
|
357
|
-
weights.set(model, (weights.get(model) ?? 0
|
|
314
|
+
weights.set(model, checkedFiniteAdd(weights.get(model) ?? 0, weight));
|
|
358
315
|
if (timeZone) {
|
|
359
316
|
const day = localDateString(event.timestampMs, timeZone);
|
|
360
|
-
dayWeights.set(day, (dayWeights.get(day) ?? 0
|
|
317
|
+
dayWeights.set(day, checkedFiniteAdd(dayWeights.get(day) ?? 0, weight));
|
|
361
318
|
}
|
|
362
|
-
totalWeight
|
|
319
|
+
totalWeight = checkedFiniteAdd(totalWeight, weight);
|
|
363
320
|
}
|
|
364
321
|
|
|
365
322
|
if (!(totalWeight > 0)) {
|
|
@@ -391,19 +348,24 @@ function allocateBurn(delta, events, timeZone) {
|
|
|
391
348
|
}
|
|
392
349
|
|
|
393
350
|
// Fractions of the span [startMs, endMs) falling on each local calendar day.
|
|
394
|
-
function durationDayShares(startMs, endMs, timeZone) {
|
|
351
|
+
export function durationDayShares(startMs, endMs, timeZone) {
|
|
352
|
+
const formatter = createTimeZoneFormatter(timeZone);
|
|
395
353
|
if (!(endMs > startMs)) {
|
|
396
|
-
return new Map([[localDateString(endMs, timeZone), 1]]);
|
|
354
|
+
return new Map([[localDateString(endMs, timeZone, formatter), 1]]);
|
|
397
355
|
}
|
|
398
356
|
const shares = new Map();
|
|
399
357
|
let cursor = startMs;
|
|
400
358
|
while (cursor < endMs) {
|
|
401
|
-
const day = localDateString(cursor, timeZone);
|
|
402
|
-
const
|
|
359
|
+
const day = localDateString(cursor, timeZone, formatter);
|
|
360
|
+
const nextBoundaryMs = localDateBoundary(
|
|
403
361
|
shiftCalendarDate(day, 1),
|
|
404
362
|
timeZone,
|
|
363
|
+
formatter,
|
|
405
364
|
).getTime();
|
|
406
|
-
|
|
365
|
+
if (!(nextBoundaryMs > cursor)) {
|
|
366
|
+
throw new Error(`Local calendar boundary did not advance after ${day}.`);
|
|
367
|
+
}
|
|
368
|
+
const sliceEnd = Math.min(endMs, nextBoundaryMs);
|
|
407
369
|
shares.set(day, (shares.get(day) ?? 0) + (sliceEnd - cursor));
|
|
408
370
|
cursor = sliceEnd;
|
|
409
371
|
}
|
|
@@ -413,11 +375,27 @@ function durationDayShares(startMs, endMs, timeZone) {
|
|
|
413
375
|
|
|
414
376
|
function tokenTotalsByModel(events) {
|
|
415
377
|
const totals = new Map();
|
|
378
|
+
let scale = 1;
|
|
379
|
+
let totalTokens = 0;
|
|
416
380
|
for (const event of events) {
|
|
417
|
-
|
|
381
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
382
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
383
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
418
384
|
if (!(tokens > 0)) continue;
|
|
419
385
|
const model = trendModelLabel(event.model);
|
|
420
|
-
|
|
386
|
+
const contribution = tokens / scale;
|
|
387
|
+
const nextTotal = totalTokens + contribution;
|
|
388
|
+
const scaleFactor = Math.max(1, nextTotal / MAX_SAFE_TOKEN_COUNT);
|
|
389
|
+
if (scaleFactor > 1) {
|
|
390
|
+
for (const [modelName, value] of totals) {
|
|
391
|
+
totals.set(modelName, value / scaleFactor);
|
|
392
|
+
}
|
|
393
|
+
totalTokens /= scaleFactor;
|
|
394
|
+
scale *= scaleFactor;
|
|
395
|
+
}
|
|
396
|
+
const scaledContribution = contribution / scaleFactor;
|
|
397
|
+
totals.set(model, (totals.get(model) ?? 0) + scaledContribution);
|
|
398
|
+
totalTokens += scaledContribution;
|
|
421
399
|
}
|
|
422
400
|
return totals;
|
|
423
401
|
}
|
|
@@ -434,12 +412,6 @@ function cloneAllocations(allocations) {
|
|
|
434
412
|
);
|
|
435
413
|
}
|
|
436
414
|
|
|
437
|
-
function eventsInBounds(events, bounds) {
|
|
438
|
-
const startMs = bounds.start.getTime();
|
|
439
|
-
const endMs = bounds.end.getTime();
|
|
440
|
-
return usageBucketsInRange({ events }, startMs, endMs);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
415
|
function buildModelStats(displayedEvents, intervals, bounds) {
|
|
444
416
|
const rows = new Map();
|
|
445
417
|
const rowFor = (model) => {
|
|
@@ -457,17 +429,26 @@ function buildModelStats(displayedEvents, intervals, bounds) {
|
|
|
457
429
|
};
|
|
458
430
|
|
|
459
431
|
for (const event of displayedEvents) {
|
|
432
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
460
433
|
const model = trendModelLabel(event.model);
|
|
461
434
|
const row = rowFor(model);
|
|
462
|
-
const
|
|
435
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
436
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
463
437
|
const credits = eventCredits(event);
|
|
464
|
-
row.tokens
|
|
438
|
+
row.tokens = checkedTokenAdd(row.tokens, tokens, { allowFractional });
|
|
465
439
|
if (Number.isFinite(credits) && credits >= 0) {
|
|
466
|
-
row.credits
|
|
467
|
-
row.ratedTokens
|
|
440
|
+
row.credits = checkedFiniteAdd(row.credits, credits);
|
|
441
|
+
row.ratedTokens = checkedTokenAdd(row.ratedTokens, tokens, {
|
|
442
|
+
allowFractional,
|
|
443
|
+
});
|
|
468
444
|
}
|
|
469
445
|
const effort = String(event.effort || "unknown").toLowerCase();
|
|
470
|
-
row.efforts.set(
|
|
446
|
+
row.efforts.set(
|
|
447
|
+
effort,
|
|
448
|
+
checkedTokenAdd(row.efforts.get(effort) ?? 0, tokens, {
|
|
449
|
+
allowFractional,
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
471
452
|
}
|
|
472
453
|
|
|
473
454
|
const startMs = bounds.start.getTime();
|
|
@@ -476,8 +457,12 @@ function buildModelStats(displayedEvents, intervals, bounds) {
|
|
|
476
457
|
if (interval.endMs < startMs || interval.endMs >= endMs) continue;
|
|
477
458
|
for (const [model, burnPoints] of interval.contributions) {
|
|
478
459
|
const row = rowFor(model);
|
|
479
|
-
row.burnPoints
|
|
480
|
-
row.attributedTokens
|
|
460
|
+
row.burnPoints = checkedFiniteAdd(row.burnPoints, burnPoints);
|
|
461
|
+
row.attributedTokens = checkedTokenAdd(
|
|
462
|
+
row.attributedTokens,
|
|
463
|
+
interval.modelTokens.get(model) ?? 0,
|
|
464
|
+
{ allowFractional: true },
|
|
465
|
+
);
|
|
481
466
|
}
|
|
482
467
|
}
|
|
483
468
|
|
|
@@ -510,13 +495,33 @@ function buildModelStats(displayedEvents, intervals, bounds) {
|
|
|
510
495
|
);
|
|
511
496
|
}
|
|
512
497
|
|
|
513
|
-
export function
|
|
498
|
+
export function buildRangeAnalysis(
|
|
499
|
+
snapshot = {},
|
|
500
|
+
bounds,
|
|
501
|
+
{ priorBounds = null, includeTrend = true } = {},
|
|
502
|
+
) {
|
|
503
|
+
const quotaObservations = normalizeQuotaTimeline(
|
|
504
|
+
weeklyQuotaObservations(snapshot),
|
|
505
|
+
);
|
|
506
|
+
const indexed = buildIndexedRangeAnalysis(snapshot, bounds, {
|
|
507
|
+
priorBounds,
|
|
508
|
+
quotaObservations,
|
|
509
|
+
});
|
|
510
|
+
return Object.freeze({
|
|
511
|
+
...indexed,
|
|
512
|
+
trend: includeTrend
|
|
513
|
+
? buildUsageTrendFromAnalysis(snapshot, bounds, indexed)
|
|
514
|
+
: null,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildUsageTrendFromAnalysis(snapshot, bounds, rangeAnalysis) {
|
|
514
519
|
const startMs = bounds.start.getTime();
|
|
515
520
|
const endMs = bounds.end.getTime();
|
|
516
|
-
const displayedEvents =
|
|
517
|
-
const observations =
|
|
518
|
-
|
|
519
|
-
)
|
|
521
|
+
const displayedEvents = rangeAnalysis.currentEvents;
|
|
522
|
+
const observations = rangeAnalysis.quotaObservations.filter(
|
|
523
|
+
(observation) => observation.timestampMs < endMs,
|
|
524
|
+
);
|
|
520
525
|
|
|
521
526
|
if (!observations.length) {
|
|
522
527
|
return {
|
|
@@ -528,23 +533,12 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
528
533
|
sampleCount: 0,
|
|
529
534
|
allocationMethod: "unavailable",
|
|
530
535
|
observedThroughMs: null,
|
|
531
|
-
rateCardAsOf:
|
|
536
|
+
rateCardAsOf: CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
532
537
|
};
|
|
533
538
|
}
|
|
534
539
|
|
|
535
|
-
const sortedEvents =
|
|
536
|
-
usageBuckets(snapshot),
|
|
537
|
-
[
|
|
538
|
-
startMs,
|
|
539
|
-
endMs,
|
|
540
|
-
...observations.flatMap((observation) => [
|
|
541
|
-
observation.cycleStartMs,
|
|
542
|
-
observation.timestampMs,
|
|
543
|
-
]),
|
|
544
|
-
],
|
|
545
|
-
)
|
|
540
|
+
const sortedEvents = rangeAnalysis.trendEvents
|
|
546
541
|
.map((event) => ({ ...event, timestampMs: finiteTimestamp(event.timestamp) }))
|
|
547
|
-
.filter((event) => event.timestampMs !== null && event.timestampMs < endMs)
|
|
548
542
|
.sort((left, right) => left.timestampMs - right.timestampMs);
|
|
549
543
|
const points = [];
|
|
550
544
|
const resets = [];
|
|
@@ -606,7 +600,10 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
606
600
|
methods.add(allocation.method);
|
|
607
601
|
}
|
|
608
602
|
for (const [model, burnPoints] of allocation.contributions) {
|
|
609
|
-
allocations.set(
|
|
603
|
+
allocations.set(
|
|
604
|
+
model,
|
|
605
|
+
checkedFiniteAdd(allocations.get(model) ?? 0, burnPoints),
|
|
606
|
+
);
|
|
610
607
|
}
|
|
611
608
|
if (delta > 0) {
|
|
612
609
|
intervals.push({
|
|
@@ -747,10 +744,19 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
747
744
|
observedThroughMs:
|
|
748
745
|
[...displayPoints].reverse().find((point) => point.observed)
|
|
749
746
|
?.timestampMs ?? null,
|
|
750
|
-
rateCardAsOf:
|
|
747
|
+
rateCardAsOf: CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
751
748
|
};
|
|
752
749
|
}
|
|
753
750
|
|
|
751
|
+
export function buildUsageTrend(snapshot = {}, bounds, { analysis = null } = {}) {
|
|
752
|
+
const rangeAnalysis = analysis ?? buildRangeAnalysis(snapshot, bounds);
|
|
753
|
+
return rangeAnalysis.trend ?? buildUsageTrendFromAnalysis(
|
|
754
|
+
snapshot,
|
|
755
|
+
bounds,
|
|
756
|
+
rangeAnalysis,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
754
760
|
// Bin observed meter drain into calendar-day (or multi-day) columns in the
|
|
755
761
|
// same percent unit as the meter line. Daily totals are the meter's own
|
|
756
762
|
// observed drops; only the per-model split within a drop and the day
|
|
@@ -787,8 +793,8 @@ export function buildBurnDayBins(trend, bounds, { days, binSize = 1 } = {}) {
|
|
|
787
793
|
)) {
|
|
788
794
|
const share = burnPoints * fraction;
|
|
789
795
|
if (!(share > 0)) continue;
|
|
790
|
-
bin.values.set(model, (bin.values.get(model) ?? 0
|
|
791
|
-
bin.totalPercent
|
|
796
|
+
bin.values.set(model, checkedFiniteAdd(bin.values.get(model) ?? 0, share));
|
|
797
|
+
bin.totalPercent = checkedFiniteAdd(bin.totalPercent, share);
|
|
792
798
|
}
|
|
793
799
|
}
|
|
794
800
|
}
|
|
@@ -797,8 +803,8 @@ export function buildBurnDayBins(trend, bounds, { days, binSize = 1 } = {}) {
|
|
|
797
803
|
let totalPercent = 0;
|
|
798
804
|
for (const bin of bins) {
|
|
799
805
|
for (const [model, value] of bin.values) {
|
|
800
|
-
totals.set(model, (totals.get(model) ?? 0
|
|
801
|
-
totalPercent
|
|
806
|
+
totals.set(model, checkedFiniteAdd(totals.get(model) ?? 0, value));
|
|
807
|
+
totalPercent = checkedFiniteAdd(totalPercent, value);
|
|
802
808
|
}
|
|
803
809
|
}
|
|
804
810
|
return { bins, totals, totalPercent, binSize, binCount };
|