modelmeter-collect 0.9.0 → 0.10.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 +12 -2
- package/cli.mjs +50 -6
- package/lib.mjs +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,23 @@ It dedupes (state in `~/.modelmeter/collector-state.json`), so every run is safe
|
|
|
12
12
|
Not covered: the **ChatGPT** consumer app (no per-message token data exists) and **Cursor**
|
|
13
13
|
on a Pro plan (usage stays on Cursor's servers).
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Try it, no account
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# See where your Claude Code / Codex tokens go - signals + tips, runs locally:
|
|
19
|
+
npx modelmeter-collect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
With no token configured this reads your local logs and prints your cache reuse, top MCP
|
|
23
|
+
server, context-bloat, and recommendations. Nothing is sent.
|
|
24
|
+
|
|
25
|
+
## Track it over time
|
|
16
26
|
|
|
17
27
|
```bash
|
|
18
28
|
# 1. Grab an ingest token from the Providers tab at https://modelmeter.dev, then:
|
|
19
29
|
npx modelmeter-collect init mm_live_xxxxxxxx
|
|
20
30
|
|
|
21
|
-
# 2. Backfill the last couple of weeks:
|
|
31
|
+
# 2. Backfill the last couple of weeks (now that a token is saved, it reports usage):
|
|
22
32
|
npx modelmeter-collect
|
|
23
33
|
|
|
24
34
|
# Preview what would be sent, without sending:
|
package/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
claudeSessionSummary,
|
|
24
24
|
codexSessionSummary,
|
|
25
25
|
buildLocalRecommendations,
|
|
26
|
+
summarizeSignals,
|
|
26
27
|
} from './lib.mjs'
|
|
27
28
|
|
|
28
29
|
const HOME = homedir()
|
|
@@ -72,8 +73,9 @@ Commands:
|
|
|
72
73
|
status, and exactly what would be sent. Add --recommendations for
|
|
73
74
|
local optimization tips (cache, MCP, output, context bloat), or
|
|
74
75
|
--payload for the raw JSON (token counts only, never transcript text).
|
|
75
|
-
(none)
|
|
76
|
-
|
|
76
|
+
(none) No token yet? Shows your local optimization signals + tips from
|
|
77
|
+
Claude Code / Codex logs, no account needed. With a token saved,
|
|
78
|
+
it scans and reports usage (deduped, safe to run repeatedly).
|
|
77
79
|
|
|
78
80
|
It sends only model names and token counts. Never your prompts, never your keys.
|
|
79
81
|
Get an ingest token from the Providers tab at https://modelmeter.dev`)
|
|
@@ -185,7 +187,7 @@ function recentSessionFiles(dir, cutoffMs, limit) {
|
|
|
185
187
|
.map((x) => x.p)
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
function
|
|
190
|
+
function readSummaries(cutoffMs) {
|
|
189
191
|
const summaries = []
|
|
190
192
|
const sources = [
|
|
191
193
|
[join(HOME, '.claude', 'projects'), claudeSessionSummary],
|
|
@@ -203,6 +205,27 @@ function printRecommendations(cutoffMs) {
|
|
|
203
205
|
if (summary) summaries.push(summary)
|
|
204
206
|
}
|
|
205
207
|
}
|
|
208
|
+
return summaries
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function pct(x) {
|
|
212
|
+
return `${Math.round(x * 100)}%`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A glance at the optimization signals (the same dimensions ModelMeter surfaces), so
|
|
216
|
+
// the command shows value, not just discovery.
|
|
217
|
+
function printSignals(summaries) {
|
|
218
|
+
if (summaries.length === 0) return
|
|
219
|
+
const s = summarizeSignals(summaries)
|
|
220
|
+
console.log(`\nOptimization signals (last 14 days, ${s.sessions} sessions, computed locally):`)
|
|
221
|
+
console.log(` cache reuse: ${pct(s.cacheRate)}`)
|
|
222
|
+
console.log(` output share: ${pct(s.outputShare)}`)
|
|
223
|
+
if (s.reasoningShare >= 0.005) console.log(` reasoning: ${pct(s.reasoningShare)} of output`)
|
|
224
|
+
if (s.topMcp) console.log(` top MCP: ${s.topMcp} (${pct(s.topMcpShare)} of tool tokens)`)
|
|
225
|
+
console.log(` context bloat: ${s.bloatedSessions} session${s.bloatedSessions === 1 ? '' : 's'} grew past 2x`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function printRecommendations(summaries) {
|
|
206
229
|
console.log('\nLocal recommendations (computed from your logs, nothing sent):')
|
|
207
230
|
const recs = buildLocalRecommendations(summaries)
|
|
208
231
|
if (recs.length === 0) {
|
|
@@ -233,7 +256,7 @@ if (cmd === 'doctor') {
|
|
|
233
256
|
}),
|
|
234
257
|
)
|
|
235
258
|
if (args.includes('--recommendations')) {
|
|
236
|
-
printRecommendations(cutoffMs)
|
|
259
|
+
printRecommendations(readSummaries(cutoffMs))
|
|
237
260
|
process.exit(0)
|
|
238
261
|
}
|
|
239
262
|
if (args.includes('--payload')) {
|
|
@@ -241,6 +264,7 @@ if (cmd === 'doctor') {
|
|
|
241
264
|
process.env.MODELMETER_DRYRUN = '1'
|
|
242
265
|
await runCollector() // prints the exact payload (counts only), then exits
|
|
243
266
|
} else {
|
|
267
|
+
printSignals(readSummaries(cutoffMs))
|
|
244
268
|
console.log(
|
|
245
269
|
'\nRun `npx modelmeter-collect doctor --recommendations` for local optimization tips, or',
|
|
246
270
|
)
|
|
@@ -249,5 +273,25 @@ if (cmd === 'doctor') {
|
|
|
249
273
|
}
|
|
250
274
|
}
|
|
251
275
|
|
|
252
|
-
// Default: scan and report.
|
|
253
|
-
|
|
276
|
+
// Default: with a token configured, scan and report (the hook / scheduled path).
|
|
277
|
+
// Without one, this is a new user trying it - show the local value, no account needed.
|
|
278
|
+
const cfg = readConfig()
|
|
279
|
+
const configured = Boolean(process.env.MODELMETER_TOKEN || cfg.token)
|
|
280
|
+
if (configured) {
|
|
281
|
+
await runCollector()
|
|
282
|
+
} else {
|
|
283
|
+
const cutoffMs = Date.now() - 14 * 86_400_000
|
|
284
|
+
const summaries = readSummaries(cutoffMs)
|
|
285
|
+
console.log('ModelMeter: where your Claude Code and Codex tokens go. Local, token counts only.\n')
|
|
286
|
+
if (summaries.length === 0) {
|
|
287
|
+
console.log('No Claude Code or Codex session logs found in the last 14 days.')
|
|
288
|
+
console.log('Use one of those tools, then run `npx modelmeter-collect` again.')
|
|
289
|
+
process.exit(0)
|
|
290
|
+
}
|
|
291
|
+
printSignals(summaries)
|
|
292
|
+
printRecommendations(summaries)
|
|
293
|
+
console.log('\nThis ran entirely on your machine. Nothing was sent.')
|
|
294
|
+
console.log('Track it over time, across repos, or for your team: get a token at')
|
|
295
|
+
console.log('https://modelmeter.dev, then `npx modelmeter-collect init <token>`.')
|
|
296
|
+
process.exit(0)
|
|
297
|
+
}
|
package/lib.mjs
CHANGED
|
@@ -589,3 +589,45 @@ export function buildLocalRecommendations(summaries) {
|
|
|
589
589
|
|
|
590
590
|
return recs
|
|
591
591
|
}
|
|
592
|
+
|
|
593
|
+
// Headline optimization signals (metrics, not recommendations) for `doctor` to show
|
|
594
|
+
// at a glance: the same dimensions ModelMeter surfaces, computed locally.
|
|
595
|
+
export function summarizeSignals(summaries) {
|
|
596
|
+
const list = (summaries || []).filter(Boolean)
|
|
597
|
+
let uncached = 0
|
|
598
|
+
let cacheRead = 0
|
|
599
|
+
let cacheCreate = 0
|
|
600
|
+
let output = 0
|
|
601
|
+
let reasoning = 0
|
|
602
|
+
let bloated = 0
|
|
603
|
+
const tools = {}
|
|
604
|
+
for (const s of list) {
|
|
605
|
+
uncached += s.uncached
|
|
606
|
+
cacheRead += s.cacheRead
|
|
607
|
+
cacheCreate += s.cacheCreate
|
|
608
|
+
output += s.output
|
|
609
|
+
reasoning += s.reasoning || 0
|
|
610
|
+
const b = sessionBloat(s.contextSeq)
|
|
611
|
+
if (b && b.last5 > b.first5 * 2 && b.last5 > 30_000) bloated++
|
|
612
|
+
for (const [g, v] of Object.entries(s.tools || {})) {
|
|
613
|
+
tools[g] = (tools[g] || 0) + v.tokens
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const inputTotal = uncached + cacheRead + cacheCreate
|
|
617
|
+
const total = inputTotal + output
|
|
618
|
+
const toolArr = Object.entries(tools)
|
|
619
|
+
.map(([tool, tok]) => ({ tool, tok }))
|
|
620
|
+
.sort((a, b) => b.tok - a.tok)
|
|
621
|
+
const toolTotal = toolArr.reduce((n, t) => n + t.tok, 0)
|
|
622
|
+
const topMcp = toolArr.find((t) => t.tool.startsWith('mcp:'))
|
|
623
|
+
return {
|
|
624
|
+
sessions: list.length,
|
|
625
|
+
totalTokens: total,
|
|
626
|
+
cacheRate: inputTotal > 0 ? cacheRead / inputTotal : 0,
|
|
627
|
+
outputShare: total > 0 ? output / total : 0,
|
|
628
|
+
reasoningShare: output > 0 ? reasoning / output : 0,
|
|
629
|
+
topMcp: topMcp ? topMcp.tool : null,
|
|
630
|
+
topMcpShare: topMcp && toolTotal > 0 ? topMcp.tok / toolTotal : 0,
|
|
631
|
+
bloatedSessions: bloated,
|
|
632
|
+
}
|
|
633
|
+
}
|
package/package.json
CHANGED