tokelytics 0.3.5 → 0.3.6
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/bin/tokelytics.mjs +140 -70
- package/package.json +1 -1
package/bin/tokelytics.mjs
CHANGED
|
@@ -85,9 +85,20 @@ function mergeDashboardSnapshot(previous, turns, limits, device, now = /* @__PUR
|
|
|
85
85
|
const next = structuredClone(previous ?? emptyDashboardSnapshot(now));
|
|
86
86
|
next.version = 2;
|
|
87
87
|
if (turns.length > 0) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const previousById = new Map(next.recentTurns.map((turn) => [turn.turnId, turn]));
|
|
89
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
90
|
+
const changedTurns = turns.filter((turn) => {
|
|
91
|
+
const prior = previousById.get(turn.turnId);
|
|
92
|
+
if (!prior)
|
|
93
|
+
return true;
|
|
94
|
+
if (sameTurn(prior, turn))
|
|
95
|
+
return false;
|
|
96
|
+
replacements.set(turn.turnId, prior);
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
next.recentTurns = mergeRecentTurns(next.recentTurns, changedTurns);
|
|
100
|
+
next.sessions = mergeSessions(next.sessions, changedTurns, replacements);
|
|
101
|
+
next.rollups = mergeRollups(next.rollups, changedTurns, now, replacements);
|
|
91
102
|
}
|
|
92
103
|
if (limits.length > 0)
|
|
93
104
|
next.limits = mergeLimits(next.limits, limits);
|
|
@@ -130,6 +141,9 @@ function mergeRecentTurns(existing, incoming) {
|
|
|
130
141
|
byId.set(turn.turnId, turn);
|
|
131
142
|
return [...byId.values()].sort((a, b) => a.ts.localeCompare(b.ts)).slice(-SNAPSHOT_RECENT_TURNS);
|
|
132
143
|
}
|
|
144
|
+
function sameTurn(left, right) {
|
|
145
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
146
|
+
}
|
|
133
147
|
function sanitizeTurn(turn) {
|
|
134
148
|
return {
|
|
135
149
|
...turn,
|
|
@@ -156,17 +170,30 @@ function safePath(value) {
|
|
|
156
170
|
const parts = normalized.split("/").filter((part) => part && part !== "." && part !== ".." && !/^[A-Za-z]:$/.test(part));
|
|
157
171
|
return parts.slice(-3).join("/").slice(-200);
|
|
158
172
|
}
|
|
159
|
-
function mergeSessions(existing, turns) {
|
|
173
|
+
function mergeSessions(existing, turns, replacements = /* @__PURE__ */ new Map()) {
|
|
160
174
|
const byId = new Map(existing.map((session) => [session.sessionId, { ...session }]));
|
|
161
175
|
for (const turn of turns) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
const prior = replacements.get(turn.turnId);
|
|
177
|
+
if (prior)
|
|
178
|
+
applySessionTurn(byId, prior, -1);
|
|
179
|
+
applySessionTurn(byId, turn, 1);
|
|
180
|
+
}
|
|
181
|
+
return [...byId.values()].sort((a, b) => b.lastTs.localeCompare(a.lastTs)).slice(0, SNAPSHOT_SESSIONS);
|
|
182
|
+
}
|
|
183
|
+
function applySessionTurn(byId, turn, direction) {
|
|
184
|
+
const session = byId.get(turn.sessionId) ?? emptySession(turn);
|
|
185
|
+
session.turnCount += direction;
|
|
186
|
+
session.inputTokens = Math.max(0, session.inputTokens + direction * turn.inputTokens);
|
|
187
|
+
session.outputTokens = Math.max(0, session.outputTokens + direction * turn.outputTokens);
|
|
188
|
+
session.cacheReadTokens = Math.max(0, session.cacheReadTokens + direction * turn.cacheReadTokens);
|
|
189
|
+
session.cacheCreationTokens = Math.max(0, session.cacheCreationTokens + direction * turn.cacheCreationTokens);
|
|
190
|
+
session.cachedInputTokens = Math.max(0, session.cachedInputTokens + direction * turn.cachedInputTokens);
|
|
191
|
+
session.reasoningTokens = Math.max(0, session.reasoningTokens + direction * turn.reasoningTokens);
|
|
192
|
+
if (session.turnCount <= 0) {
|
|
193
|
+
byId.delete(turn.sessionId);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (direction > 0) {
|
|
170
197
|
if (!session.firstTs || turn.ts < session.firstTs)
|
|
171
198
|
session.firstTs = turn.ts;
|
|
172
199
|
if (!session.lastTs || turn.ts > session.lastTs)
|
|
@@ -178,9 +205,8 @@ function mergeSessions(existing, turns) {
|
|
|
178
205
|
if (turn.model && (!session.model || modelPriority(turn.model) > modelPriority(session.model))) {
|
|
179
206
|
session.model = turn.model;
|
|
180
207
|
}
|
|
181
|
-
byId.set(turn.sessionId, session);
|
|
182
208
|
}
|
|
183
|
-
|
|
209
|
+
byId.set(turn.sessionId, session);
|
|
184
210
|
}
|
|
185
211
|
function emptySession(turn) {
|
|
186
212
|
return {
|
|
@@ -200,48 +226,62 @@ function emptySession(turn) {
|
|
|
200
226
|
reasoningTokens: 0
|
|
201
227
|
};
|
|
202
228
|
}
|
|
203
|
-
function mergeRollups(existing, turns, now) {
|
|
229
|
+
function mergeRollups(existing, turns, now, replacements = /* @__PURE__ */ new Map()) {
|
|
204
230
|
const byId = new Map(existing.map((rollup) => [rollup.id, structuredClone(rollup)]));
|
|
205
231
|
for (const turn of turns) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const rollup = byId.get(id) ?? { id, provider: turn.provider, day, models: {}, hourly: [] };
|
|
211
|
-
const model = turn.model || "unknown";
|
|
212
|
-
const totals = rollup.models[model] ??= {
|
|
213
|
-
model,
|
|
214
|
-
turns: 0,
|
|
215
|
-
inputTokens: 0,
|
|
216
|
-
outputTokens: 0,
|
|
217
|
-
cacheReadTokens: 0,
|
|
218
|
-
cacheCreationTokens: 0,
|
|
219
|
-
cachedInputTokens: 0,
|
|
220
|
-
reasoningTokens: 0
|
|
221
|
-
};
|
|
222
|
-
totals.turns++;
|
|
223
|
-
totals.inputTokens += turn.inputTokens;
|
|
224
|
-
totals.outputTokens += turn.outputTokens;
|
|
225
|
-
totals.cacheReadTokens += turn.cacheReadTokens;
|
|
226
|
-
totals.cacheCreationTokens += turn.cacheCreationTokens;
|
|
227
|
-
totals.cachedInputTokens += turn.cachedInputTokens;
|
|
228
|
-
totals.reasoningTokens += turn.reasoningTokens;
|
|
229
|
-
const hour = hourOf(turn.ts);
|
|
230
|
-
if (hour !== null) {
|
|
231
|
-
const bucket = rollup.hourly.find((item) => item.hour === hour) ?? { hour, output: 0, turns: 0 };
|
|
232
|
-
bucket.output += turn.outputTokens;
|
|
233
|
-
bucket.turns++;
|
|
234
|
-
if (!rollup.hourly.some((item) => item.hour === hour))
|
|
235
|
-
rollup.hourly.push(bucket);
|
|
236
|
-
rollup.hourly.sort((a, b) => a.hour - b.hour);
|
|
237
|
-
}
|
|
238
|
-
byId.set(id, rollup);
|
|
232
|
+
const prior = replacements.get(turn.turnId);
|
|
233
|
+
if (prior)
|
|
234
|
+
applyRollupTurn(byId, prior, -1);
|
|
235
|
+
applyRollupTurn(byId, turn, 1);
|
|
239
236
|
}
|
|
240
237
|
const cutoff = new Date(now);
|
|
241
238
|
cutoff.setUTCDate(cutoff.getUTCDate() - (SNAPSHOT_RETENTION_DAYS - 1));
|
|
242
239
|
const cutoffDay = cutoff.toISOString().slice(0, 10);
|
|
243
240
|
return [...byId.values()].filter((rollup) => rollup.day >= cutoffDay).sort((a, b) => a.day.localeCompare(b.day) || a.provider.localeCompare(b.provider));
|
|
244
241
|
}
|
|
242
|
+
function applyRollupTurn(byId, turn, direction) {
|
|
243
|
+
const day = dayOf(turn.ts);
|
|
244
|
+
if (!day)
|
|
245
|
+
return;
|
|
246
|
+
const id = rollupId(turn.provider, day);
|
|
247
|
+
const rollup = byId.get(id) ?? { id, provider: turn.provider, day, models: {}, hourly: [] };
|
|
248
|
+
const model = turn.model || "unknown";
|
|
249
|
+
const totals = rollup.models[model] ??= {
|
|
250
|
+
model,
|
|
251
|
+
turns: 0,
|
|
252
|
+
inputTokens: 0,
|
|
253
|
+
outputTokens: 0,
|
|
254
|
+
cacheReadTokens: 0,
|
|
255
|
+
cacheCreationTokens: 0,
|
|
256
|
+
cachedInputTokens: 0,
|
|
257
|
+
reasoningTokens: 0
|
|
258
|
+
};
|
|
259
|
+
totals.turns += direction;
|
|
260
|
+
totals.inputTokens = Math.max(0, totals.inputTokens + direction * turn.inputTokens);
|
|
261
|
+
totals.outputTokens = Math.max(0, totals.outputTokens + direction * turn.outputTokens);
|
|
262
|
+
totals.cacheReadTokens = Math.max(0, totals.cacheReadTokens + direction * turn.cacheReadTokens);
|
|
263
|
+
totals.cacheCreationTokens = Math.max(0, totals.cacheCreationTokens + direction * turn.cacheCreationTokens);
|
|
264
|
+
totals.cachedInputTokens = Math.max(0, totals.cachedInputTokens + direction * turn.cachedInputTokens);
|
|
265
|
+
totals.reasoningTokens = Math.max(0, totals.reasoningTokens + direction * turn.reasoningTokens);
|
|
266
|
+
if (totals.turns <= 0)
|
|
267
|
+
delete rollup.models[model];
|
|
268
|
+
const hour = hourOf(turn.ts);
|
|
269
|
+
if (hour !== null) {
|
|
270
|
+
const bucket = rollup.hourly.find((item) => item.hour === hour) ?? { hour, output: 0, turns: 0 };
|
|
271
|
+
bucket.output = Math.max(0, bucket.output + direction * turn.outputTokens);
|
|
272
|
+
bucket.turns += direction;
|
|
273
|
+
if (bucket.turns <= 0) {
|
|
274
|
+
rollup.hourly = rollup.hourly.filter((item) => item.hour !== hour);
|
|
275
|
+
} else if (!rollup.hourly.some((item) => item.hour === hour)) {
|
|
276
|
+
rollup.hourly.push(bucket);
|
|
277
|
+
}
|
|
278
|
+
rollup.hourly.sort((a, b) => a.hour - b.hour);
|
|
279
|
+
}
|
|
280
|
+
if (Object.keys(rollup.models).length === 0)
|
|
281
|
+
byId.delete(id);
|
|
282
|
+
else
|
|
283
|
+
byId.set(id, rollup);
|
|
284
|
+
}
|
|
245
285
|
function mergeLimits(existing, incoming) {
|
|
246
286
|
const byProvider = new Map(existing.map((limit) => [limit.provider, limit]));
|
|
247
287
|
for (const limit of incoming)
|
|
@@ -1802,7 +1842,7 @@ var FirestoreSink = class {
|
|
|
1802
1842
|
};
|
|
1803
1843
|
|
|
1804
1844
|
// src/version.ts
|
|
1805
|
-
var AGENT_VERSION = "0.3.
|
|
1845
|
+
var AGENT_VERSION = "0.3.6";
|
|
1806
1846
|
|
|
1807
1847
|
// src/sync.ts
|
|
1808
1848
|
async function runSync(connectors, sink, state, device, limitCollectors = [], options = {}) {
|
|
@@ -1817,15 +1857,11 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1817
1857
|
let st = state;
|
|
1818
1858
|
const collected = [];
|
|
1819
1859
|
const limits = [];
|
|
1820
|
-
const byProvider = {};
|
|
1821
1860
|
for (const c of connectors) {
|
|
1822
1861
|
const { turns: turns2, limits: nativeLimits2 = [], state: next } = await c.collect(st);
|
|
1823
1862
|
st = next;
|
|
1824
1863
|
if (refreshLimits) limits.push(...nativeLimits2);
|
|
1825
|
-
|
|
1826
|
-
collected.push(t);
|
|
1827
|
-
byProvider[t.provider] = (byProvider[t.provider] ?? 0) + 1;
|
|
1828
|
-
}
|
|
1864
|
+
collected.push(...turns2);
|
|
1829
1865
|
}
|
|
1830
1866
|
if (refreshLimits) {
|
|
1831
1867
|
const providerLimits = await Promise.all(
|
|
@@ -1841,7 +1877,16 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1841
1877
|
}
|
|
1842
1878
|
const unique = /* @__PURE__ */ new Map();
|
|
1843
1879
|
for (const t of collected) unique.set(t.turnId, t);
|
|
1844
|
-
const
|
|
1880
|
+
const collectedTurns = [...unique.values()];
|
|
1881
|
+
const previousTurns = new Map((options.dashboard?.recentTurns ?? []).map((turn) => [turn.turnId, turn]));
|
|
1882
|
+
const turns = collectedTurns.filter((turn) => {
|
|
1883
|
+
const previous = previousTurns.get(turn.turnId);
|
|
1884
|
+
return !previous || !sameTurn(previous, turn);
|
|
1885
|
+
});
|
|
1886
|
+
const changedByProvider = {};
|
|
1887
|
+
for (const turn of turns) {
|
|
1888
|
+
changedByProvider[turn.provider] = (changedByProvider[turn.provider] ?? 0) + 1;
|
|
1889
|
+
}
|
|
1845
1890
|
const nativeLimits = refreshLimits ? dedupeLimits(limits) : [];
|
|
1846
1891
|
const changedLimits = nativeLimits.filter(
|
|
1847
1892
|
(limit) => publication.limitFingerprints?.[limit.provider] !== limitFingerprint(limit)
|
|
@@ -1907,14 +1952,18 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1907
1952
|
publicationError = error;
|
|
1908
1953
|
}
|
|
1909
1954
|
}
|
|
1955
|
+
const scheduledCloudWrite = publication.dashboardDirty && hasBudget && !options.suppressCloudPublish ? Number.isFinite(Date.parse(publication.lastCloudWriteAt ?? "")) ? new Date(
|
|
1956
|
+
Math.max(now.getTime(), Date.parse(publication.lastCloudWriteAt) + cloudSyncIntervalMs)
|
|
1957
|
+
).toISOString() : now.toISOString() : void 0;
|
|
1910
1958
|
return {
|
|
1911
1959
|
processedTurns: turns.length,
|
|
1912
1960
|
processedLimits: changedLimits.length,
|
|
1913
|
-
byProvider,
|
|
1961
|
+
byProvider: changedByProvider,
|
|
1914
1962
|
state: st,
|
|
1915
1963
|
dashboard,
|
|
1916
1964
|
publication,
|
|
1917
1965
|
published,
|
|
1966
|
+
nextCloudWriteAt: scheduledCloudWrite,
|
|
1918
1967
|
publicationError
|
|
1919
1968
|
};
|
|
1920
1969
|
}
|
|
@@ -2005,6 +2054,7 @@ async function createRunner() {
|
|
|
2005
2054
|
byProvider: res.byProvider,
|
|
2006
2055
|
published: res.published,
|
|
2007
2056
|
cloudWritesToday: res.publication.cloudWritesToday ?? 0,
|
|
2057
|
+
nextCloudWriteAt: res.nextCloudWriteAt,
|
|
2008
2058
|
publicationError: res.publicationError
|
|
2009
2059
|
};
|
|
2010
2060
|
}
|
|
@@ -3798,7 +3848,7 @@ function close(server) {
|
|
|
3798
3848
|
}
|
|
3799
3849
|
|
|
3800
3850
|
// src/watch.ts
|
|
3801
|
-
var
|
|
3851
|
+
var TURN_SETTLE_MS = 2500;
|
|
3802
3852
|
var LIMIT_REFRESH_MS = 6e4;
|
|
3803
3853
|
var FALLBACK_SCAN_MS = 5e3;
|
|
3804
3854
|
var DEVICE_HEARTBEAT_MS = 60 * 6e4;
|
|
@@ -3807,6 +3857,7 @@ var MAX_CLOUD_WRITES_PER_DAY = 16;
|
|
|
3807
3857
|
var QUOTA_RETRY_MS = 15 * 6e4;
|
|
3808
3858
|
var QUOTA_RETRY_JITTER_MS = 5 * 6e4;
|
|
3809
3859
|
async function watch2(runner) {
|
|
3860
|
+
const logState = {};
|
|
3810
3861
|
let localServer;
|
|
3811
3862
|
try {
|
|
3812
3863
|
localServer = await startLocalDashboardServer(() => runner.localSnapshot(), runner.uid);
|
|
@@ -3821,7 +3872,7 @@ async function watch2(runner) {
|
|
|
3821
3872
|
deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
|
|
3822
3873
|
cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
|
|
3823
3874
|
maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
|
|
3824
|
-
}, false);
|
|
3875
|
+
}, false, logState);
|
|
3825
3876
|
const paths = runner.watchPaths();
|
|
3826
3877
|
const watcher = esm_default.watch(paths, {
|
|
3827
3878
|
ignoreInitial: true,
|
|
@@ -3835,7 +3886,12 @@ async function watch2(runner) {
|
|
|
3835
3886
|
let running = false;
|
|
3836
3887
|
let scheduledAt = 0;
|
|
3837
3888
|
let cloudBlockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
|
|
3838
|
-
const
|
|
3889
|
+
const takePending = () => {
|
|
3890
|
+
const request = pending;
|
|
3891
|
+
pending = void 0;
|
|
3892
|
+
return request;
|
|
3893
|
+
};
|
|
3894
|
+
const schedule = (request, delay = TURN_SETTLE_MS) => {
|
|
3839
3895
|
pending = mergeRequests(pending, request);
|
|
3840
3896
|
if (running) return;
|
|
3841
3897
|
const target = Date.now() + delay;
|
|
@@ -3845,21 +3901,19 @@ async function watch2(runner) {
|
|
|
3845
3901
|
timer = setTimeout(async () => {
|
|
3846
3902
|
timer = void 0;
|
|
3847
3903
|
scheduledAt = 0;
|
|
3848
|
-
const next =
|
|
3849
|
-
pending = void 0;
|
|
3904
|
+
const next = takePending();
|
|
3850
3905
|
if (!next) return;
|
|
3851
3906
|
running = true;
|
|
3852
|
-
const retryAfterMs = await safeSync(runner, next, Date.now() < cloudBlockedUntil);
|
|
3907
|
+
const retryAfterMs = await safeSync(runner, next, Date.now() < cloudBlockedUntil, logState);
|
|
3853
3908
|
running = false;
|
|
3854
3909
|
if (retryAfterMs) {
|
|
3855
3910
|
cloudBlockedUntil = Date.now() + retryAfterMs;
|
|
3856
3911
|
} else if (Date.now() >= cloudBlockedUntil) {
|
|
3857
3912
|
cloudBlockedUntil = 0;
|
|
3858
3913
|
}
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
schedule(queued, 0);
|
|
3914
|
+
const queued = takePending();
|
|
3915
|
+
if (queued) {
|
|
3916
|
+
schedule(queued, queued.reason === "change" ? TURN_SETTLE_MS : 0);
|
|
3863
3917
|
}
|
|
3864
3918
|
}, Math.max(0, target - Date.now()));
|
|
3865
3919
|
};
|
|
@@ -3899,21 +3953,37 @@ Press Ctrl+C to stop.`);
|
|
|
3899
3953
|
process.once("SIGTERM", stop);
|
|
3900
3954
|
});
|
|
3901
3955
|
}
|
|
3902
|
-
async function safeSync(runner, request, suppressCloudPublish) {
|
|
3956
|
+
async function safeSync(runner, request, suppressCloudPublish, logState) {
|
|
3903
3957
|
try {
|
|
3904
3958
|
const { reason, ...options } = request;
|
|
3905
|
-
const {
|
|
3959
|
+
const {
|
|
3960
|
+
processedTurns,
|
|
3961
|
+
byProvider,
|
|
3962
|
+
published,
|
|
3963
|
+
cloudWritesToday,
|
|
3964
|
+
nextCloudWriteAt,
|
|
3965
|
+
publicationError
|
|
3966
|
+
} = await runner.syncOnce({
|
|
3906
3967
|
...options,
|
|
3907
3968
|
suppressCloudPublish
|
|
3908
3969
|
});
|
|
3909
3970
|
if (processedTurns > 0) {
|
|
3910
3971
|
const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
|
|
3911
3972
|
console.log(
|
|
3912
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail})
|
|
3973
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} completed turn(s) locally (${detail}).`
|
|
3913
3974
|
);
|
|
3914
3975
|
} else if (reason === "startup") {
|
|
3976
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready.`);
|
|
3977
|
+
}
|
|
3978
|
+
if (published) {
|
|
3979
|
+
logState.cloudSchedule = void 0;
|
|
3980
|
+
console.log(
|
|
3981
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] cloud snapshot published (${cloudWritesToday}/${MAX_CLOUD_WRITES_PER_DAY} writes today).`
|
|
3982
|
+
);
|
|
3983
|
+
} else if (nextCloudWriteAt && nextCloudWriteAt !== logState.cloudSchedule) {
|
|
3984
|
+
logState.cloudSchedule = nextCloudWriteAt;
|
|
3915
3985
|
console.log(
|
|
3916
|
-
`
|
|
3986
|
+
`Cloud backup scheduled for ${new Date(nextCloudWriteAt).toLocaleTimeString()} (new turns will be combined into this snapshot).`
|
|
3917
3987
|
);
|
|
3918
3988
|
}
|
|
3919
3989
|
if (publicationError) return cloudRetryDelay(publicationError);
|