modelmeter-collect 0.3.0 → 0.4.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/collect.mjs +71 -16
- package/package.json +1 -1
package/collect.mjs
CHANGED
|
@@ -86,6 +86,9 @@ function recentFiles(dir, limit = Infinity) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const events = []
|
|
89
|
+
// Codex tool attribution is computed per session (its token accounting is periodic,
|
|
90
|
+
// not per-turn), so it is collected here and folded into the tool aggregation later.
|
|
91
|
+
const codexToolDeltas = []
|
|
89
92
|
|
|
90
93
|
// --- Claude Code: assistant turns carry message.usage; dedup by message uuid.
|
|
91
94
|
function scanClaude(files) {
|
|
@@ -143,14 +146,30 @@ function findLastTokenCount(obj) {
|
|
|
143
146
|
}
|
|
144
147
|
return last
|
|
145
148
|
}
|
|
149
|
+
// Codex tool names are plain (exec_command, apply_patch, ...); MCP calls carry an
|
|
150
|
+
// invocation with a server name. Returns a group key, or null if not a tool call.
|
|
151
|
+
function codexToolFromEvent(payload, ptype) {
|
|
152
|
+
if (ptype === 'function_call' || ptype === 'custom_tool_call') {
|
|
153
|
+
return typeof payload.name === 'string' && payload.name ? payload.name : null
|
|
154
|
+
}
|
|
155
|
+
if (ptype === 'mcp_tool_call_end' || ptype === 'mcp_tool_call_begin') {
|
|
156
|
+
const inv = payload.invocation || {}
|
|
157
|
+
const server = inv.server || inv.server_name
|
|
158
|
+
return server ? `mcp:${server}` : 'mcp'
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
function scanCodex(files) {
|
|
147
164
|
for (const file of files) {
|
|
148
165
|
const m = file.match(/rollout-(\d{4}-\d{2}-\d{2})T[\d-]+-([0-9a-f-]+)\.jsonl$/)
|
|
149
166
|
if (!m) continue
|
|
150
|
-
const date = m[1]
|
|
151
167
|
const sessionId = m[2]
|
|
152
168
|
let totals = null
|
|
169
|
+
let totalsTs = null // timestamp of the latest token_count event, for the 5-hour window
|
|
170
|
+
let maxTs = ''
|
|
153
171
|
let model = 'gpt-5'
|
|
172
|
+
const toolCalls = [] // { ts, group }
|
|
154
173
|
let text = ''
|
|
155
174
|
try {
|
|
156
175
|
text = readFileSync(file, 'utf8')
|
|
@@ -165,34 +184,63 @@ function scanCodex(files) {
|
|
|
165
184
|
} catch {
|
|
166
185
|
continue
|
|
167
186
|
}
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
187
|
+
const ts = typeof o.timestamp === 'string' ? o.timestamp : null
|
|
188
|
+
if (ts && ts > maxTs) maxTs = ts
|
|
189
|
+
const p = o.payload || o
|
|
190
|
+
const ptype = p.type || o.type
|
|
170
191
|
if (typeof o.model === 'string') model = o.model
|
|
171
|
-
else if (typeof
|
|
192
|
+
else if (typeof p.model === 'string') model = p.model
|
|
193
|
+
const tc = findLastTokenCount(o)
|
|
194
|
+
if (tc) {
|
|
195
|
+
totals = tc
|
|
196
|
+
if (ts) totalsTs = ts
|
|
197
|
+
}
|
|
198
|
+
const g = codexToolFromEvent(p, ptype)
|
|
199
|
+
if (g) toolCalls.push({ ts: ts || totalsTs || '', group: g })
|
|
172
200
|
}
|
|
173
201
|
if (!totals) continue
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
202
|
+
const stored = state.codex[sessionId]
|
|
203
|
+
const prevTotal =
|
|
204
|
+
(stored && stored.total) ||
|
|
205
|
+
(stored && stored.input_tokens != null ? stored : null) || {
|
|
206
|
+
input_tokens: 0,
|
|
207
|
+
cached_input_tokens: 0,
|
|
208
|
+
output_tokens: 0,
|
|
209
|
+
reasoning_output_tokens: 0,
|
|
210
|
+
}
|
|
211
|
+
const prevLastTs = (stored && stored.lastTs) || ''
|
|
212
|
+
const dInput = Math.max(0, (totals.input_tokens || 0) - prevTotal.input_tokens)
|
|
213
|
+
const dCached = Math.max(0, (totals.cached_input_tokens || 0) - prevTotal.cached_input_tokens)
|
|
214
|
+
const dOut = Math.max(0, (totals.output_tokens || 0) - prevTotal.output_tokens)
|
|
215
|
+
const dReason = Math.max(0, (totals.reasoning_output_tokens || 0) - prevTotal.reasoning_output_tokens)
|
|
216
|
+
const newTotalTokens = Math.max(0, dInput - dCached) + dCached + (dOut + dReason)
|
|
184
217
|
if (dInput + dCached + dOut + dReason > 0) {
|
|
218
|
+
const occurredOn = (totalsTs || '').slice(0, 10) || m[1]
|
|
185
219
|
events.push({
|
|
186
220
|
provider: 'openai',
|
|
187
221
|
model,
|
|
188
|
-
occurredOn
|
|
222
|
+
occurredOn,
|
|
223
|
+
occurredAt: totalsTs || undefined, // enables the 5-hour window for Codex
|
|
189
224
|
uncachedInputTokens: Math.max(0, dInput - dCached),
|
|
190
225
|
cacheReadInputTokens: dCached,
|
|
191
226
|
cacheCreationInputTokens: 0,
|
|
192
227
|
outputTokens: dOut + dReason, // reasoning tokens bill as output
|
|
193
228
|
numRequests: 1,
|
|
194
229
|
})
|
|
195
|
-
|
|
230
|
+
// Tool calls new since the last run; even-split this run's new tokens across them.
|
|
231
|
+
const newCalls = toolCalls.filter((c) => c.ts && c.ts > prevLastTs)
|
|
232
|
+
if (newCalls.length > 0 && newTotalTokens > 0) {
|
|
233
|
+
const share = Math.round(newTotalTokens / newCalls.length)
|
|
234
|
+
for (const c of newCalls) {
|
|
235
|
+
codexToolDeltas.push({
|
|
236
|
+
tool: c.group,
|
|
237
|
+
bucketDate: (c.ts || '').slice(0, 10) || occurredOn,
|
|
238
|
+
calls: 1,
|
|
239
|
+
tokens: share,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
state.codex[sessionId] = { total: totals, lastTs: maxTs }
|
|
196
244
|
}
|
|
197
245
|
}
|
|
198
246
|
}
|
|
@@ -299,6 +347,13 @@ for (const e of events) {
|
|
|
299
347
|
byTool.set(key, cur)
|
|
300
348
|
}
|
|
301
349
|
}
|
|
350
|
+
for (const d of codexToolDeltas) {
|
|
351
|
+
const key = `${d.tool}|${d.bucketDate}`
|
|
352
|
+
const cur = byTool.get(key) || { tool: d.tool, bucketDate: d.bucketDate, calls: 0, tokens: 0 }
|
|
353
|
+
cur.calls += d.calls
|
|
354
|
+
cur.tokens += d.tokens
|
|
355
|
+
byTool.set(key, cur)
|
|
356
|
+
}
|
|
302
357
|
const toolsPayload = [...byTool.values()]
|
|
303
358
|
|
|
304
359
|
if (payload.length === 0) {
|
package/package.json
CHANGED