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/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 name = model.toLowerCase();
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: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
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)}`;