tokelytics 0.1.0 → 0.2.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/bin/tokelytics.mjs +77 -0
- package/package.json +1 -1
package/bin/tokelytics.mjs
CHANGED
|
@@ -147,6 +147,42 @@ function buildRollup(provider, day, turns) {
|
|
|
147
147
|
hourly: [...hours.values()].sort((a, b) => a.hour - b.hour)
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
|
+
var TIMELINE_BUCKET_MS = 5 * 60 * 1e3;
|
|
151
|
+
var TIMELINE_RETENTION_MS = 8 * 24 * 60 * 60 * 1e3;
|
|
152
|
+
function bucketStartMs(ts) {
|
|
153
|
+
const ms = Date.parse(ts);
|
|
154
|
+
if (!Number.isFinite(ms))
|
|
155
|
+
return null;
|
|
156
|
+
return Math.floor(ms / TIMELINE_BUCKET_MS) * TIMELINE_BUCKET_MS;
|
|
157
|
+
}
|
|
158
|
+
function dayOfMs(t) {
|
|
159
|
+
return new Date(t).toISOString().slice(0, 10);
|
|
160
|
+
}
|
|
161
|
+
function buildTimelineBuckets(turns) {
|
|
162
|
+
const map = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const t of turns) {
|
|
164
|
+
const start = bucketStartMs(t.ts);
|
|
165
|
+
if (start === null)
|
|
166
|
+
continue;
|
|
167
|
+
const bucket = map.get(start) ?? { t: start, models: {} };
|
|
168
|
+
const key = t.model || "unknown";
|
|
169
|
+
const m = bucket.models[key] ??= { i: 0, o: 0, cr: 0, cc: 0, ci: 0, rs: 0 };
|
|
170
|
+
m.i += t.inputTokens;
|
|
171
|
+
m.o += t.outputTokens;
|
|
172
|
+
m.cr += t.cacheReadTokens;
|
|
173
|
+
m.cc += t.cacheCreationTokens;
|
|
174
|
+
m.ci += t.cachedInputTokens;
|
|
175
|
+
m.rs += t.reasoningTokens;
|
|
176
|
+
map.set(start, bucket);
|
|
177
|
+
}
|
|
178
|
+
return [...map.values()].sort((a, b) => a.t - b.t);
|
|
179
|
+
}
|
|
180
|
+
function mergeTimeline(existing, recomputed, affectedDays, nowMs) {
|
|
181
|
+
const cutoff = nowMs - TIMELINE_RETENTION_MS;
|
|
182
|
+
const kept = existing.filter((b) => b.t >= cutoff && !affectedDays.has(dayOfMs(b.t)));
|
|
183
|
+
const fresh = recomputed.filter((b) => b.t >= cutoff);
|
|
184
|
+
return [...kept, ...fresh].sort((a, b) => a.t - b.t);
|
|
185
|
+
}
|
|
150
186
|
function affectedKeys(turns) {
|
|
151
187
|
const sessionIds = /* @__PURE__ */ new Set();
|
|
152
188
|
const dayBuckets = /* @__PURE__ */ new Map();
|
|
@@ -915,6 +951,14 @@ var FirestoreRest = class {
|
|
|
915
951
|
});
|
|
916
952
|
if (!res.ok) throw new Error(`Firestore commit failed (${res.status}): ${await res.text()}`);
|
|
917
953
|
}
|
|
954
|
+
/** Fetch a single document's decoded fields, or null if it doesn't exist. */
|
|
955
|
+
async getDoc(name) {
|
|
956
|
+
const res = await fetch(this.url(name), { method: "GET", headers: await this.headers() });
|
|
957
|
+
if (res.status === 404) return null;
|
|
958
|
+
if (!res.ok) throw new Error(`Firestore get failed (${res.status}): ${await res.text()}`);
|
|
959
|
+
const doc = await res.json();
|
|
960
|
+
return doc.fields ? decodeFields(doc.fields) : {};
|
|
961
|
+
}
|
|
918
962
|
/** Run an equality query under a parent path, returning decoded documents. */
|
|
919
963
|
async queryEqual(parentSegments, collectionId, filters) {
|
|
920
964
|
const parent = parentSegments.length ? this.docName(...parentSegments) : this.docsRoot();
|
|
@@ -937,6 +981,22 @@ var FirestoreRest = class {
|
|
|
937
981
|
}
|
|
938
982
|
};
|
|
939
983
|
|
|
984
|
+
// src/sink.ts
|
|
985
|
+
function timelineDaysByProvider(buckets, nowMs) {
|
|
986
|
+
const cutoffDay = new Date(nowMs - TIMELINE_RETENTION_MS).toISOString().slice(0, 10);
|
|
987
|
+
const out = /* @__PURE__ */ new Map();
|
|
988
|
+
for (const { provider, day } of buckets) {
|
|
989
|
+
if (day < cutoffDay) continue;
|
|
990
|
+
let set = out.get(provider);
|
|
991
|
+
if (!set) {
|
|
992
|
+
set = /* @__PURE__ */ new Set();
|
|
993
|
+
out.set(provider, set);
|
|
994
|
+
}
|
|
995
|
+
set.add(day);
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
|
|
940
1000
|
// src/firestore-sink.ts
|
|
941
1001
|
var BATCH = 400;
|
|
942
1002
|
var FirestoreSink = class {
|
|
@@ -986,6 +1046,22 @@ var FirestoreSink = class {
|
|
|
986
1046
|
]);
|
|
987
1047
|
}
|
|
988
1048
|
}
|
|
1049
|
+
async recomputeTimelines(buckets) {
|
|
1050
|
+
const now = Date.now();
|
|
1051
|
+
for (const [provider, days] of timelineDaysByProvider(buckets, now)) {
|
|
1052
|
+
const recomputed = [];
|
|
1053
|
+
for (const day of days) {
|
|
1054
|
+
recomputed.push(...buildTimelineBuckets(await this.turnsForBucket(provider, day)));
|
|
1055
|
+
}
|
|
1056
|
+
const name = this.fs.docName("users", this.uid, "timelines", provider);
|
|
1057
|
+
const existingDoc = await this.fs.getDoc(name);
|
|
1058
|
+
const existing = existingDoc?.["buckets"] ?? [];
|
|
1059
|
+
const merged = mergeTimeline(existing, recomputed, days, now);
|
|
1060
|
+
await this.fs.upsert([
|
|
1061
|
+
{ name, fields: { provider, buckets: merged, updatedAt: new Date(now).toISOString() } }
|
|
1062
|
+
]);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
989
1065
|
async touchDevice(meta) {
|
|
990
1066
|
await this.fs.upsert([
|
|
991
1067
|
{ name: this.fs.docName("users", this.uid, "devices", meta.deviceId), fields: { ...meta } }
|
|
@@ -1014,6 +1090,7 @@ async function runSync(connectors, sink, state, device) {
|
|
|
1014
1090
|
const { sessionIds, dayBuckets } = affectedKeys(turns);
|
|
1015
1091
|
await sink.recomputeSessions(sessionIds);
|
|
1016
1092
|
await sink.recomputeRollups(dayBuckets);
|
|
1093
|
+
await sink.recomputeTimelines(dayBuckets);
|
|
1017
1094
|
}
|
|
1018
1095
|
await sink.touchDevice({ ...device, lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1019
1096
|
return { newTurns: turns.length, byProvider, state: st };
|