usagemax 0.1.1 → 0.3.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 +95 -21
- package/package.json +3 -1
- package/src/archives.js +86 -0
- package/src/cli.js +261 -132
- package/src/core.js +226 -2
- package/src/sources.js +495 -0
package/src/core.js
CHANGED
|
@@ -22,15 +22,103 @@ function text(value, fallback, maximum = 120) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function providerFor(model, source) {
|
|
25
|
-
const
|
|
25
|
+
const displayName = model.toLowerCase().replace(/^\[[^\]]+\]\s*/, "");
|
|
26
|
+
const name = displayName.replace(/^api[-_:]/, "");
|
|
27
|
+
if (name.startsWith("openrouter/")) return "openrouter";
|
|
28
|
+
if (name.startsWith("azure/") || name.startsWith("azure-openai/")) return "azure-openai";
|
|
29
|
+
if (name.startsWith("bedrock/") || name.startsWith("aws/")) return "aws-bedrock";
|
|
30
|
+
if (name.startsWith("vertex/") || name.startsWith("vertex_ai/") || name.startsWith("google/")) return "google";
|
|
26
31
|
if (name.includes("claude")) return "anthropic";
|
|
27
32
|
if (/^(?:gpt|o[1345]|codex)/.test(name) || name.includes("openai")) return "openai";
|
|
28
33
|
if (name.includes("gemini")) return "google";
|
|
29
34
|
if (name.includes("grok")) return "xai";
|
|
30
35
|
if (name.includes("deepseek")) return "deepseek";
|
|
36
|
+
if (name.includes("kimi") || name.includes("moonshot")) return "moonshot";
|
|
37
|
+
if (name.includes("qwen")) return "alibaba";
|
|
38
|
+
if (name.includes("glm") || name.includes("zai") || name.includes("z.ai")) return "zai";
|
|
39
|
+
if (name.includes("mistral") || name.includes("codestral")) return "mistral";
|
|
40
|
+
if (name.includes("llama") || name.includes("meta/")) return "meta";
|
|
41
|
+
if (name.includes("minimax")) return "minimax";
|
|
42
|
+
if (name.includes("command-r") || name.includes("cohere")) return "cohere";
|
|
31
43
|
return source;
|
|
32
44
|
}
|
|
33
45
|
|
|
46
|
+
const snapshotCounterFields = [
|
|
47
|
+
"inputTokens",
|
|
48
|
+
"outputTokens",
|
|
49
|
+
"cacheReadTokens",
|
|
50
|
+
"cacheWriteTokens",
|
|
51
|
+
"reasoningTokens",
|
|
52
|
+
"unclassifiedTokens",
|
|
53
|
+
"totalTokens",
|
|
54
|
+
"costMicros",
|
|
55
|
+
"requests",
|
|
56
|
+
"errors",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function snapshotCounters(value = {}) {
|
|
60
|
+
const inputTokens = number(value.inputTokens);
|
|
61
|
+
const outputTokens = number(value.outputTokens);
|
|
62
|
+
const cacheReadTokens = number(value.cacheReadTokens);
|
|
63
|
+
const cacheWriteTokens = number(value.cacheWriteTokens);
|
|
64
|
+
const reasoningTokens = number(value.reasoningTokens);
|
|
65
|
+
const suppliedTotal = number(value.totalTokens);
|
|
66
|
+
const classified = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens + reasoningTokens;
|
|
67
|
+
const unclassifiedTokens = value.unclassifiedTokens === undefined
|
|
68
|
+
? Math.max(0, suppliedTotal - classified)
|
|
69
|
+
: number(value.unclassifiedTokens);
|
|
70
|
+
return {
|
|
71
|
+
inputTokens,
|
|
72
|
+
outputTokens,
|
|
73
|
+
cacheReadTokens,
|
|
74
|
+
cacheWriteTokens,
|
|
75
|
+
reasoningTokens,
|
|
76
|
+
unclassifiedTokens,
|
|
77
|
+
totalTokens: classified + unclassifiedTokens,
|
|
78
|
+
costMicros: number(value.costMicros, MAX_SAFE_COST_MICROS),
|
|
79
|
+
requests: number(value.requests),
|
|
80
|
+
errors: number(value.errors),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sameCounters(left, right) {
|
|
85
|
+
return snapshotCounterFields.every((field) => left[field] === right[field]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function snapshotKey(source, period, provider, model) {
|
|
89
|
+
return `${source}\u001f${period}\u001f${provider}\u001f${model}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readSnapshotKey(key) {
|
|
93
|
+
const parts = String(key).split("\u001f");
|
|
94
|
+
if (parts.length === 4) return { source: parts[0], period: parts[1], provider: parts[2], model: parts[3] };
|
|
95
|
+
if (parts.length === 3) return { source: parts[0], period: parts[1], provider: providerFor(parts[2], parts[0]), model: parts[2] };
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizedSnapshotRows(report) {
|
|
100
|
+
const rows = new Map();
|
|
101
|
+
for (const row of currentRows(report)) {
|
|
102
|
+
const provider = providerFor(row.model, row.source);
|
|
103
|
+
const key = snapshotKey(row.source, row.period, provider, row.model);
|
|
104
|
+
const current = snapshotCounters(row.current);
|
|
105
|
+
const existing = rows.get(key);
|
|
106
|
+
if (existing) {
|
|
107
|
+
for (const field of snapshotCounterFields) existing.current[field] += current[field];
|
|
108
|
+
} else {
|
|
109
|
+
rows.set(key, {
|
|
110
|
+
key,
|
|
111
|
+
source: row.source,
|
|
112
|
+
period: row.period,
|
|
113
|
+
provider,
|
|
114
|
+
model: row.model,
|
|
115
|
+
current,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return rows;
|
|
120
|
+
}
|
|
121
|
+
|
|
34
122
|
function sha256(value) {
|
|
35
123
|
return createHash("sha256").update(value).digest("hex");
|
|
36
124
|
}
|
|
@@ -74,6 +162,8 @@ function currentRows(report) {
|
|
|
74
162
|
cacheReadTokens: rawAgent.cacheReadTokens,
|
|
75
163
|
cost: rawAgent.totalCost,
|
|
76
164
|
}];
|
|
165
|
+
let allocatedTokens = 0;
|
|
166
|
+
let allocatedCostMicros = 0;
|
|
77
167
|
for (const rawBreakdown of breakdowns) {
|
|
78
168
|
if (!rawBreakdown || typeof rawBreakdown !== "object") continue;
|
|
79
169
|
const model = text(rawBreakdown.modelName, "unknown", 120);
|
|
@@ -82,6 +172,10 @@ function currentRows(report) {
|
|
|
82
172
|
const cacheWriteTokens = number(rawBreakdown.cacheCreationTokens);
|
|
83
173
|
const cacheReadTokens = number(rawBreakdown.cacheReadTokens);
|
|
84
174
|
const costMicros = moneyMicros(rawBreakdown.cost);
|
|
175
|
+
const classifiedTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
176
|
+
const totalTokens = Math.max(classifiedTokens, number(rawBreakdown.totalTokens));
|
|
177
|
+
allocatedTokens += totalTokens;
|
|
178
|
+
allocatedCostMicros += costMicros;
|
|
85
179
|
rows.push({
|
|
86
180
|
key: `${source}\u001f${period}\u001f${model}`,
|
|
87
181
|
source,
|
|
@@ -92,11 +186,29 @@ function currentRows(report) {
|
|
|
92
186
|
outputTokens,
|
|
93
187
|
cacheReadTokens,
|
|
94
188
|
cacheWriteTokens,
|
|
95
|
-
totalTokens
|
|
189
|
+
totalTokens,
|
|
96
190
|
costMicros,
|
|
97
191
|
},
|
|
98
192
|
});
|
|
99
193
|
}
|
|
194
|
+
const residualTokens = Math.max(0, number(rawAgent.totalTokens) - allocatedTokens);
|
|
195
|
+
const residualCostMicros = Math.max(0, moneyMicros(rawAgent.totalCost) - allocatedCostMicros);
|
|
196
|
+
if (residualTokens > 0 || residualCostMicros > 0) {
|
|
197
|
+
rows.push({
|
|
198
|
+
key: `${source}\u001f${period}\u001funattributed`,
|
|
199
|
+
source,
|
|
200
|
+
period,
|
|
201
|
+
model: "unattributed",
|
|
202
|
+
current: {
|
|
203
|
+
inputTokens: 0,
|
|
204
|
+
outputTokens: 0,
|
|
205
|
+
cacheReadTokens: 0,
|
|
206
|
+
cacheWriteTokens: 0,
|
|
207
|
+
totalTokens: residualTokens,
|
|
208
|
+
costMicros: residualCostMicros,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
100
212
|
}
|
|
101
213
|
}
|
|
102
214
|
return rows;
|
|
@@ -159,6 +271,118 @@ export function buildDeltaPlan(report, priorSnapshots, deviceId, pricingVersion
|
|
|
159
271
|
return { plan, regressions };
|
|
160
272
|
}
|
|
161
273
|
|
|
274
|
+
export function buildSnapshotPlan(report, priorSnapshots, {
|
|
275
|
+
bootstrap = false,
|
|
276
|
+
complete = true,
|
|
277
|
+
full = false,
|
|
278
|
+
pricingVersion = "ccusage",
|
|
279
|
+
revision = Date.now(),
|
|
280
|
+
runId = randomPlanId(),
|
|
281
|
+
} = {}) {
|
|
282
|
+
const rows = normalizedSnapshotRows(report);
|
|
283
|
+
const priorRows = new Map();
|
|
284
|
+
const snapshots = priorSnapshots && typeof priorSnapshots === "object" ? priorSnapshots : {};
|
|
285
|
+
for (const [rawKey, rawValue] of Object.entries(snapshots)) {
|
|
286
|
+
const identity = readSnapshotKey(rawKey);
|
|
287
|
+
if (!identity || !/^\d{4}-\d{2}-\d{2}$/.test(identity.period)) continue;
|
|
288
|
+
const key = snapshotKey(identity.source, identity.period, identity.provider, identity.model);
|
|
289
|
+
priorRows.set(key, {
|
|
290
|
+
key,
|
|
291
|
+
...identity,
|
|
292
|
+
current: snapshotCounters(rawValue),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const currentPartitions = new Set([...rows.values()].map((row) => `${row.source}\u001f${row.period}`));
|
|
297
|
+
const allKeys = new Set(rows.keys());
|
|
298
|
+
for (const [key, prior] of priorRows) {
|
|
299
|
+
if (full || currentPartitions.has(`${prior.source}\u001f${prior.period}`)) allKeys.add(key);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const grouped = new Map();
|
|
303
|
+
const nextSnapshots = {};
|
|
304
|
+
const regressions = [];
|
|
305
|
+
for (const key of allKeys) {
|
|
306
|
+
const currentRow = rows.get(key);
|
|
307
|
+
const priorRow = priorRows.get(key);
|
|
308
|
+
const identity = currentRow || priorRow;
|
|
309
|
+
if (!identity) continue;
|
|
310
|
+
const previous = priorRow?.current ?? snapshotCounters();
|
|
311
|
+
const current = currentRow?.current ?? snapshotCounters();
|
|
312
|
+
if (snapshotCounterFields.some((field) => current[field] < previous[field])) regressions.push(key);
|
|
313
|
+
if (!sameCounters(current, snapshotCounters())) nextSnapshots[key] = current;
|
|
314
|
+
const partitionKey = `${identity.source}\u001f${identity.period}`;
|
|
315
|
+
const rowsForPartition = grouped.get(partitionKey) ?? [];
|
|
316
|
+
rowsForPartition.push({
|
|
317
|
+
snapshotKey: key,
|
|
318
|
+
provider: identity.provider,
|
|
319
|
+
model: identity.model,
|
|
320
|
+
previous,
|
|
321
|
+
current,
|
|
322
|
+
costBasis: "estimated",
|
|
323
|
+
contentHash: sha256(`${key}\u001f${JSON.stringify(current)}`),
|
|
324
|
+
lastUsedAt: Date.parse(`${identity.period}T12:00:00.000Z`),
|
|
325
|
+
});
|
|
326
|
+
grouped.set(partitionKey, rowsForPartition);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const partitions = [];
|
|
330
|
+
for (const [partitionKey, partitionRows] of grouped) {
|
|
331
|
+
const [source, day] = partitionKey.split("\u001f");
|
|
332
|
+
const changed = partitionRows.some((row) => !sameCounters(row.previous, row.current));
|
|
333
|
+
if (!bootstrap && !full && !changed) continue;
|
|
334
|
+
partitionRows.sort((left, right) => `${left.provider}/${left.model}`.localeCompare(`${right.provider}/${right.model}`));
|
|
335
|
+
const wireRows = partitionRows.map(({ provider, model, previous, current, costBasis, contentHash, lastUsedAt }) => ({
|
|
336
|
+
provider,
|
|
337
|
+
model,
|
|
338
|
+
previous,
|
|
339
|
+
current,
|
|
340
|
+
costBasis,
|
|
341
|
+
contentHash,
|
|
342
|
+
lastUsedAt,
|
|
343
|
+
}));
|
|
344
|
+
const payloadHash = sha256(JSON.stringify({ source, day, complete, pricingVersion, rows: wireRows }));
|
|
345
|
+
partitions.push({
|
|
346
|
+
partitionId: `${runId}:${sha256(partitionKey).slice(0, 24)}`,
|
|
347
|
+
payloadHash,
|
|
348
|
+
revision,
|
|
349
|
+
source,
|
|
350
|
+
day,
|
|
351
|
+
complete,
|
|
352
|
+
pricingVersion,
|
|
353
|
+
rows: wireRows,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
partitions.sort((left, right) => left.day === right.day ? left.source.localeCompare(right.source) : left.day.localeCompare(right.day));
|
|
357
|
+
return { partitions, nextSnapshots, regressions };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function randomPlanId() {
|
|
361
|
+
return `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function buildSessionPlan(report, deviceId) {
|
|
365
|
+
const sessionRows = Array.isArray(report?.session) ? report.session : [];
|
|
366
|
+
const sessions = new Map();
|
|
367
|
+
for (const row of sessionRows) {
|
|
368
|
+
if (!row || typeof row !== "object") continue;
|
|
369
|
+
const source = text(row.agent, "unknown", 60).toLowerCase();
|
|
370
|
+
const stableIdentity = text(row.period ?? row.sessionId ?? row.id, "", 1000);
|
|
371
|
+
if (!stableIdentity) continue;
|
|
372
|
+
const metadata = row.metadata && typeof row.metadata === "object" ? row.metadata : {};
|
|
373
|
+
const firstActivityAt = Date.parse(metadata.firstActivity ?? metadata.createdAt ?? "");
|
|
374
|
+
const lastActivityAt = Date.parse(metadata.lastActivity ?? row.lastActivity ?? "");
|
|
375
|
+
const sessionKey = sha256(`${deviceId}\u001f${source}\u001f${stableIdentity}`);
|
|
376
|
+
sessions.set(`${source}\u001f${sessionKey}`, {
|
|
377
|
+
source,
|
|
378
|
+
sessionKey,
|
|
379
|
+
...(Number.isFinite(firstActivityAt) ? { firstActivityAt } : {}),
|
|
380
|
+
...(Number.isFinite(lastActivityAt) ? { lastActivityAt } : {}),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return [...sessions.values()].sort((left, right) => `${left.source}/${left.sessionKey}`.localeCompare(`${right.source}/${right.sessionKey}`));
|
|
384
|
+
}
|
|
385
|
+
|
|
162
386
|
export function batchId(deviceId, events) {
|
|
163
387
|
const identity = events.map((event) => event.eventKey).join("\u001f");
|
|
164
388
|
return `cli:${deviceId}:${sha256(identity).slice(0, 32)}`;
|