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,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,55 +116,40 @@ export function trendModelLabel(value) {
|
|
|
150
116
|
}
|
|
151
117
|
|
|
152
118
|
export function weeklyQuotaObservations(snapshot = {}) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
observation.resetsAt > 0 &&
|
|
166
|
-
Number.isFinite(observation.usedPercent),
|
|
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,
|
|
167
131
|
)
|
|
168
|
-
.map((observation) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
132
|
+
.map((observation) => {
|
|
133
|
+
const timestampMs = finiteTimestamp(observation.timestamp);
|
|
134
|
+
const lastSeenAtMs = finiteTimestamp(observation.lastSeenAt);
|
|
135
|
+
return {
|
|
136
|
+
...observation,
|
|
137
|
+
timestampMs,
|
|
138
|
+
observedThroughMs:
|
|
139
|
+
timestampMs === null
|
|
140
|
+
? lastSeenAtMs
|
|
141
|
+
: Math.max(timestampMs, lastSeenAtMs ?? timestampMs),
|
|
142
|
+
};
|
|
143
|
+
})
|
|
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),
|
|
180
152
|
);
|
|
181
|
-
if (accountScoped.length) {
|
|
182
|
-
observations = accountScoped;
|
|
183
|
-
} else if (observations.some((observation) => observation.limitKey)) {
|
|
184
|
-
const groups = new Map();
|
|
185
|
-
for (const observation of observations) {
|
|
186
|
-
const key = observation.limitKey ?? "anonymous";
|
|
187
|
-
const group = groups.get(key) ?? [];
|
|
188
|
-
group.push(observation);
|
|
189
|
-
groups.set(key, group);
|
|
190
|
-
}
|
|
191
|
-
const accountWide = [...groups.values()].filter((group) =>
|
|
192
|
-
group.every((observation) => !observation.limitName),
|
|
193
|
-
);
|
|
194
|
-
const pool = accountWide.length ? accountWide : [...groups.values()];
|
|
195
|
-
observations = pool.sort((left, right) => right.length - left.length)[0];
|
|
196
|
-
} else {
|
|
197
|
-
const accountWide = observations.filter(
|
|
198
|
-
(observation) => !observation.limitName,
|
|
199
|
-
);
|
|
200
|
-
if (accountWide.length) observations = accountWide;
|
|
201
|
-
}
|
|
202
153
|
|
|
203
154
|
observations.sort(
|
|
204
155
|
(left, right) =>
|
|
@@ -214,6 +165,13 @@ export function weeklyQuotaObservations(snapshot = {}) {
|
|
|
214
165
|
// stale readings from sessions still reporting a superseded window would
|
|
215
166
|
// otherwise be fused into one line, producing meter drain that never happened.
|
|
216
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
|
+
);
|
|
217
175
|
if (!observations.length) return [];
|
|
218
176
|
|
|
219
177
|
const epochs = [];
|
|
@@ -277,6 +235,12 @@ export function normalizeQuotaTimeline(observations) {
|
|
|
277
235
|
: Math.max(usedPercent, observation.usedPercent);
|
|
278
236
|
normalized.push({
|
|
279
237
|
...observation,
|
|
238
|
+
observedThroughMs: Math.min(
|
|
239
|
+
Number.isFinite(observation.observedThroughMs)
|
|
240
|
+
? observation.observedThroughMs
|
|
241
|
+
: observation.timestampMs,
|
|
242
|
+
nextFirstMs,
|
|
243
|
+
),
|
|
280
244
|
cycle,
|
|
281
245
|
reset: !emitted && previousEpoch !== null,
|
|
282
246
|
resetKind,
|
|
@@ -291,46 +255,53 @@ export function normalizeQuotaTimeline(observations) {
|
|
|
291
255
|
}
|
|
292
256
|
|
|
293
257
|
export function eventCredits(event) {
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
event.serviceTier
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const stored = Number(event.rateCardCredits);
|
|
302
|
-
if (event.rateCardCredits !== null && event.rateCardCredits !== undefined) {
|
|
303
|
-
// Stored credits from current snapshots already include the fast-mode
|
|
304
|
-
// multiplier.
|
|
305
|
-
if (Number.isFinite(stored) && stored >= 0) return stored;
|
|
306
|
-
}
|
|
307
|
-
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
|
+
});
|
|
308
265
|
}
|
|
309
266
|
|
|
310
267
|
function eventWeight(event, fallbackCreditsPerToken) {
|
|
311
268
|
const credits = eventCredits(event);
|
|
312
269
|
if (Number.isFinite(credits) && credits > 0) return credits;
|
|
313
|
-
const tokens =
|
|
270
|
+
const tokens = tokenValue(event.totalTokens, {
|
|
271
|
+
allowFractional: event.rangeAllocationEstimated === true,
|
|
272
|
+
});
|
|
314
273
|
return fallbackCreditsPerToken > 0 ? tokens * fallbackCreditsPerToken : tokens;
|
|
315
274
|
}
|
|
316
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
|
+
|
|
317
287
|
function allocateBurn(delta, events, timeZone) {
|
|
318
288
|
if (!(delta > 0)) return { contributions: new Map(), method: "none" };
|
|
319
289
|
|
|
320
|
-
|
|
321
|
-
let ratedTokens = 0;
|
|
290
|
+
const ratedTotals = { credits: 0, tokens: 0, scale: 1 };
|
|
322
291
|
let hasUnrated = false;
|
|
323
292
|
for (const event of events) {
|
|
293
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
324
294
|
const credits = eventCredits(event);
|
|
325
|
-
const
|
|
295
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
296
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
326
297
|
if (Number.isFinite(credits) && credits > 0) {
|
|
327
|
-
|
|
328
|
-
ratedTokens += tokens;
|
|
298
|
+
addRatedTotals(ratedTotals, credits, tokens);
|
|
329
299
|
} else if (tokens > 0) {
|
|
330
300
|
hasUnrated = true;
|
|
331
301
|
}
|
|
332
302
|
}
|
|
333
303
|
|
|
304
|
+
const { credits: ratedCredits, tokens: ratedTokens } = ratedTotals;
|
|
334
305
|
const fallbackCreditsPerToken =
|
|
335
306
|
ratedCredits > 0 && ratedTokens > 0 ? ratedCredits / ratedTokens : 0;
|
|
336
307
|
const weights = new Map();
|
|
@@ -340,12 +311,12 @@ function allocateBurn(delta, events, timeZone) {
|
|
|
340
311
|
const weight = eventWeight(event, fallbackCreditsPerToken);
|
|
341
312
|
if (!(weight > 0)) continue;
|
|
342
313
|
const model = trendModelLabel(event.model);
|
|
343
|
-
weights.set(model, (weights.get(model) ?? 0
|
|
314
|
+
weights.set(model, checkedFiniteAdd(weights.get(model) ?? 0, weight));
|
|
344
315
|
if (timeZone) {
|
|
345
316
|
const day = localDateString(event.timestampMs, timeZone);
|
|
346
|
-
dayWeights.set(day, (dayWeights.get(day) ?? 0
|
|
317
|
+
dayWeights.set(day, checkedFiniteAdd(dayWeights.get(day) ?? 0, weight));
|
|
347
318
|
}
|
|
348
|
-
totalWeight
|
|
319
|
+
totalWeight = checkedFiniteAdd(totalWeight, weight);
|
|
349
320
|
}
|
|
350
321
|
|
|
351
322
|
if (!(totalWeight > 0)) {
|
|
@@ -377,19 +348,24 @@ function allocateBurn(delta, events, timeZone) {
|
|
|
377
348
|
}
|
|
378
349
|
|
|
379
350
|
// Fractions of the span [startMs, endMs) falling on each local calendar day.
|
|
380
|
-
function durationDayShares(startMs, endMs, timeZone) {
|
|
351
|
+
export function durationDayShares(startMs, endMs, timeZone) {
|
|
352
|
+
const formatter = createTimeZoneFormatter(timeZone);
|
|
381
353
|
if (!(endMs > startMs)) {
|
|
382
|
-
return new Map([[localDateString(endMs, timeZone), 1]]);
|
|
354
|
+
return new Map([[localDateString(endMs, timeZone, formatter), 1]]);
|
|
383
355
|
}
|
|
384
356
|
const shares = new Map();
|
|
385
357
|
let cursor = startMs;
|
|
386
358
|
while (cursor < endMs) {
|
|
387
|
-
const day = localDateString(cursor, timeZone);
|
|
388
|
-
const
|
|
359
|
+
const day = localDateString(cursor, timeZone, formatter);
|
|
360
|
+
const nextBoundaryMs = localDateBoundary(
|
|
389
361
|
shiftCalendarDate(day, 1),
|
|
390
362
|
timeZone,
|
|
363
|
+
formatter,
|
|
391
364
|
).getTime();
|
|
392
|
-
|
|
365
|
+
if (!(nextBoundaryMs > cursor)) {
|
|
366
|
+
throw new Error(`Local calendar boundary did not advance after ${day}.`);
|
|
367
|
+
}
|
|
368
|
+
const sliceEnd = Math.min(endMs, nextBoundaryMs);
|
|
393
369
|
shares.set(day, (shares.get(day) ?? 0) + (sliceEnd - cursor));
|
|
394
370
|
cursor = sliceEnd;
|
|
395
371
|
}
|
|
@@ -399,11 +375,27 @@ function durationDayShares(startMs, endMs, timeZone) {
|
|
|
399
375
|
|
|
400
376
|
function tokenTotalsByModel(events) {
|
|
401
377
|
const totals = new Map();
|
|
378
|
+
let scale = 1;
|
|
379
|
+
let totalTokens = 0;
|
|
402
380
|
for (const event of events) {
|
|
403
|
-
|
|
381
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
382
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
383
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
404
384
|
if (!(tokens > 0)) continue;
|
|
405
385
|
const model = trendModelLabel(event.model);
|
|
406
|
-
|
|
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;
|
|
407
399
|
}
|
|
408
400
|
return totals;
|
|
409
401
|
}
|
|
@@ -420,12 +412,6 @@ function cloneAllocations(allocations) {
|
|
|
420
412
|
);
|
|
421
413
|
}
|
|
422
414
|
|
|
423
|
-
function eventsInBounds(events, bounds) {
|
|
424
|
-
const startMs = bounds.start.getTime();
|
|
425
|
-
const endMs = bounds.end.getTime();
|
|
426
|
-
return usageBucketsInRange({ events }, startMs, endMs);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
415
|
function buildModelStats(displayedEvents, intervals, bounds) {
|
|
430
416
|
const rows = new Map();
|
|
431
417
|
const rowFor = (model) => {
|
|
@@ -443,17 +429,26 @@ function buildModelStats(displayedEvents, intervals, bounds) {
|
|
|
443
429
|
};
|
|
444
430
|
|
|
445
431
|
for (const event of displayedEvents) {
|
|
432
|
+
if (event?.invalidTokenRecord === true) continue;
|
|
446
433
|
const model = trendModelLabel(event.model);
|
|
447
434
|
const row = rowFor(model);
|
|
448
|
-
const
|
|
435
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
436
|
+
const tokens = tokenValue(event.totalTokens, { allowFractional });
|
|
449
437
|
const credits = eventCredits(event);
|
|
450
|
-
row.tokens
|
|
438
|
+
row.tokens = checkedTokenAdd(row.tokens, tokens, { allowFractional });
|
|
451
439
|
if (Number.isFinite(credits) && credits >= 0) {
|
|
452
|
-
row.credits
|
|
453
|
-
row.ratedTokens
|
|
440
|
+
row.credits = checkedFiniteAdd(row.credits, credits);
|
|
441
|
+
row.ratedTokens = checkedTokenAdd(row.ratedTokens, tokens, {
|
|
442
|
+
allowFractional,
|
|
443
|
+
});
|
|
454
444
|
}
|
|
455
445
|
const effort = String(event.effort || "unknown").toLowerCase();
|
|
456
|
-
row.efforts.set(
|
|
446
|
+
row.efforts.set(
|
|
447
|
+
effort,
|
|
448
|
+
checkedTokenAdd(row.efforts.get(effort) ?? 0, tokens, {
|
|
449
|
+
allowFractional,
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
457
452
|
}
|
|
458
453
|
|
|
459
454
|
const startMs = bounds.start.getTime();
|
|
@@ -462,8 +457,12 @@ function buildModelStats(displayedEvents, intervals, bounds) {
|
|
|
462
457
|
if (interval.endMs < startMs || interval.endMs >= endMs) continue;
|
|
463
458
|
for (const [model, burnPoints] of interval.contributions) {
|
|
464
459
|
const row = rowFor(model);
|
|
465
|
-
row.burnPoints
|
|
466
|
-
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
|
+
);
|
|
467
466
|
}
|
|
468
467
|
}
|
|
469
468
|
|
|
@@ -496,13 +495,33 @@ function buildModelStats(displayedEvents, intervals, bounds) {
|
|
|
496
495
|
);
|
|
497
496
|
}
|
|
498
497
|
|
|
499
|
-
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) {
|
|
500
519
|
const startMs = bounds.start.getTime();
|
|
501
520
|
const endMs = bounds.end.getTime();
|
|
502
|
-
const displayedEvents =
|
|
503
|
-
const observations =
|
|
504
|
-
|
|
505
|
-
)
|
|
521
|
+
const displayedEvents = rangeAnalysis.currentEvents;
|
|
522
|
+
const observations = rangeAnalysis.quotaObservations.filter(
|
|
523
|
+
(observation) => observation.timestampMs < endMs,
|
|
524
|
+
);
|
|
506
525
|
|
|
507
526
|
if (!observations.length) {
|
|
508
527
|
return {
|
|
@@ -514,23 +533,12 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
514
533
|
sampleCount: 0,
|
|
515
534
|
allocationMethod: "unavailable",
|
|
516
535
|
observedThroughMs: null,
|
|
517
|
-
rateCardAsOf:
|
|
536
|
+
rateCardAsOf: CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
518
537
|
};
|
|
519
538
|
}
|
|
520
539
|
|
|
521
|
-
const sortedEvents =
|
|
522
|
-
usageBuckets(snapshot),
|
|
523
|
-
[
|
|
524
|
-
startMs,
|
|
525
|
-
endMs,
|
|
526
|
-
...observations.flatMap((observation) => [
|
|
527
|
-
observation.cycleStartMs,
|
|
528
|
-
observation.timestampMs,
|
|
529
|
-
]),
|
|
530
|
-
],
|
|
531
|
-
)
|
|
540
|
+
const sortedEvents = rangeAnalysis.trendEvents
|
|
532
541
|
.map((event) => ({ ...event, timestampMs: finiteTimestamp(event.timestamp) }))
|
|
533
|
-
.filter((event) => event.timestampMs !== null && event.timestampMs < endMs)
|
|
534
542
|
.sort((left, right) => left.timestampMs - right.timestampMs);
|
|
535
543
|
const points = [];
|
|
536
544
|
const resets = [];
|
|
@@ -592,7 +600,10 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
592
600
|
methods.add(allocation.method);
|
|
593
601
|
}
|
|
594
602
|
for (const [model, burnPoints] of allocation.contributions) {
|
|
595
|
-
allocations.set(
|
|
603
|
+
allocations.set(
|
|
604
|
+
model,
|
|
605
|
+
checkedFiniteAdd(allocations.get(model) ?? 0, burnPoints),
|
|
606
|
+
);
|
|
596
607
|
}
|
|
597
608
|
if (delta > 0) {
|
|
598
609
|
intervals.push({
|
|
@@ -617,6 +628,7 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
617
628
|
}
|
|
618
629
|
points.push({
|
|
619
630
|
timestampMs: observation.timestampMs,
|
|
631
|
+
observedThroughMs: observation.observedThroughMs,
|
|
620
632
|
cycle: observation.cycle,
|
|
621
633
|
usedPercent: observation.normalizedUsedPercent,
|
|
622
634
|
remainingPercent: 100 - observation.normalizedUsedPercent,
|
|
@@ -645,15 +657,46 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
645
657
|
(point) => point.timestampMs > startMs && point.timestampMs < endMs,
|
|
646
658
|
),
|
|
647
659
|
);
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
660
|
+
|
|
661
|
+
// Repeated equal meter readings are compacted into an observed span. Extend
|
|
662
|
+
// each displayed cycle only through its last real sample; never synthesize a
|
|
663
|
+
// flat line through the unobserved remainder of the report range.
|
|
664
|
+
const sourcePointsByCycle = new Map();
|
|
665
|
+
for (const point of points) {
|
|
666
|
+
const cyclePoints = sourcePointsByCycle.get(point.cycle) ?? [];
|
|
667
|
+
cyclePoints.push(point);
|
|
668
|
+
sourcePointsByCycle.set(point.cycle, cyclePoints);
|
|
656
669
|
}
|
|
670
|
+
const displayedCycles = new Set(displayPoints.map((point) => point.cycle));
|
|
671
|
+
for (const cycle of displayedCycles) {
|
|
672
|
+
const cyclePoints = sourcePointsByCycle.get(cycle) ?? [];
|
|
673
|
+
const displayedCyclePoints = displayPoints.filter(
|
|
674
|
+
(point) => point.cycle === cycle,
|
|
675
|
+
);
|
|
676
|
+
const lastPoint = displayedCyclePoints.at(-1);
|
|
677
|
+
if (!lastPoint || !cyclePoints.length) continue;
|
|
678
|
+
const observedThroughMs = Math.max(
|
|
679
|
+
...cyclePoints.map((point) => point.observedThroughMs),
|
|
680
|
+
);
|
|
681
|
+
const nextResetMs = resets.find((reset) => reset.cycle === cycle + 1)
|
|
682
|
+
?.timestampMs;
|
|
683
|
+
const crossesNextReset = Number.isFinite(nextResetMs) &&
|
|
684
|
+
observedThroughMs >= nextResetMs;
|
|
685
|
+
const endpointMs = Math.min(observedThroughMs, endMs);
|
|
686
|
+
if (!crossesNextReset && endpointMs > lastPoint.timestampMs) {
|
|
687
|
+
displayPoints.push({
|
|
688
|
+
...lastPoint,
|
|
689
|
+
timestampMs: endpointMs,
|
|
690
|
+
observedThroughMs: endpointMs,
|
|
691
|
+
observed: true,
|
|
692
|
+
carried: false,
|
|
693
|
+
confirmation: true,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
displayPoints.sort(
|
|
698
|
+
(left, right) => left.timestampMs - right.timestampMs || left.cycle - right.cycle,
|
|
699
|
+
);
|
|
657
700
|
|
|
658
701
|
const hasUnattributed = methods.has("unattributed");
|
|
659
702
|
let allocationMethod = "unavailable";
|
|
@@ -699,12 +742,21 @@ export function buildUsageTrend(snapshot = {}, bounds) {
|
|
|
699
742
|
).length,
|
|
700
743
|
allocationMethod,
|
|
701
744
|
observedThroughMs:
|
|
702
|
-
[...
|
|
745
|
+
[...displayPoints].reverse().find((point) => point.observed)
|
|
703
746
|
?.timestampMs ?? null,
|
|
704
|
-
rateCardAsOf:
|
|
747
|
+
rateCardAsOf: CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
705
748
|
};
|
|
706
749
|
}
|
|
707
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
|
+
|
|
708
760
|
// Bin observed meter drain into calendar-day (or multi-day) columns in the
|
|
709
761
|
// same percent unit as the meter line. Daily totals are the meter's own
|
|
710
762
|
// observed drops; only the per-model split within a drop and the day
|
|
@@ -741,8 +793,8 @@ export function buildBurnDayBins(trend, bounds, { days, binSize = 1 } = {}) {
|
|
|
741
793
|
)) {
|
|
742
794
|
const share = burnPoints * fraction;
|
|
743
795
|
if (!(share > 0)) continue;
|
|
744
|
-
bin.values.set(model, (bin.values.get(model) ?? 0
|
|
745
|
-
bin.totalPercent
|
|
796
|
+
bin.values.set(model, checkedFiniteAdd(bin.values.get(model) ?? 0, share));
|
|
797
|
+
bin.totalPercent = checkedFiniteAdd(bin.totalPercent, share);
|
|
746
798
|
}
|
|
747
799
|
}
|
|
748
800
|
}
|
|
@@ -751,8 +803,8 @@ export function buildBurnDayBins(trend, bounds, { days, binSize = 1 } = {}) {
|
|
|
751
803
|
let totalPercent = 0;
|
|
752
804
|
for (const bin of bins) {
|
|
753
805
|
for (const [model, value] of bin.values) {
|
|
754
|
-
totals.set(model, (totals.get(model) ?? 0
|
|
755
|
-
totalPercent
|
|
806
|
+
totals.set(model, checkedFiniteAdd(totals.get(model) ?? 0, value));
|
|
807
|
+
totalPercent = checkedFiniteAdd(totalPercent, value);
|
|
756
808
|
}
|
|
757
809
|
}
|
|
758
810
|
return { bins, totals, totalPercent, binSize, binCount };
|