Skip to content

Commit a415801

Browse files
committed
feat(metrics): add token usage tracking and cost estimation
- Add totalInputTokens, totalOutputTokens, totalCostUsd to Metrics interface - Add SessionStats interface to track tokens, tool calls, files modified - Implement recordTokenUsage() to accumulate token counts and costs - Add formatTokenCount() helper (1.2M, 500K format) - Add formatCost() helper (/usr/bin/zsh.01 format) - Add tests for token tracking, formatting, and cost calculation Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent ae66286 commit a415801

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

src/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,28 @@ export interface Metrics {
137137
firstRunTime?: string
138138
/** ISO timestamp of last activity */
139139
lastActivityTime: string
140+
/** Total input tokens used */
141+
totalInputTokens: number
142+
/** Total output tokens used */
143+
totalOutputTokens: number
144+
/** Total estimated cost in USD */
145+
totalCostUsd: number
146+
}
147+
148+
/** Session statistics for a single build/plan operation */
149+
export interface SessionStats {
150+
/** Number of tool calls made */
151+
toolCalls: number
152+
/** Input tokens used */
153+
inputTokens: number
154+
/** Output tokens used */
155+
outputTokens: number
156+
/** Estimated cost in USD */
157+
costUsd: number
158+
/** Files modified */
159+
filesModified: string[]
160+
/** Start time */
161+
startTime: number
140162
}
141163

142164
/** Result of task build */
@@ -181,4 +203,5 @@ export interface CliOptions {
181203
autoPush?: boolean
182204
commitSignoff?: boolean
183205
status?: boolean
206+
metricsReset?: boolean
184207
}

tests/metrics.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
66
import { existsSync, mkdirSync, rmSync } from "node:fs"
77
import { join } from "node:path"
88
import {
9+
formatCost,
910
formatDuration,
1011
formatMetricsSummary,
12+
formatTokenCount,
1113
getAverageCycleDuration,
1214
getTaskSuccessRate,
1315
loadMetrics,
@@ -18,6 +20,7 @@ import {
1820
recordTaskCompleted,
1921
recordTaskFailed,
2022
recordTaskSkipped,
23+
recordTokenUsage,
2124
resetMetrics,
2225
saveMetrics,
2326
} from "../src/metrics.ts"
@@ -37,6 +40,9 @@ function createTestMetrics(overrides?: Partial<Metrics>): Metrics {
3740
ideasProcessed: 0,
3841
totalCycleDurationMs: 0,
3942
lastActivityTime: new Date().toISOString(),
43+
totalInputTokens: 0,
44+
totalOutputTokens: 0,
45+
totalCostUsd: 0,
4046
...overrides,
4147
}
4248
}
@@ -326,8 +332,71 @@ describe("metrics", () => {
326332

327333
expect(metrics.cyclesCompleted).toBe(0)
328334
expect(metrics.tasksCompleted).toBe(0)
335+
expect(metrics.totalInputTokens).toBe(0)
336+
expect(metrics.totalOutputTokens).toBe(0)
337+
expect(metrics.totalCostUsd).toBe(0)
329338
expect(metrics.firstRunTime).toBeTruthy()
330339
expect(metrics.lastActivityTime).toBeTruthy()
331340
})
332341
})
342+
343+
describe("recordTokenUsage", () => {
344+
test("accumulates token counts and cost", () => {
345+
let metrics = createTestMetrics()
346+
347+
metrics = recordTokenUsage(metrics, 1000, 500, 0.05)
348+
expect(metrics.totalInputTokens).toBe(1000)
349+
expect(metrics.totalOutputTokens).toBe(500)
350+
expect(metrics.totalCostUsd).toBeCloseTo(0.05, 4)
351+
352+
metrics = recordTokenUsage(metrics, 2000, 1000, 0.1)
353+
expect(metrics.totalInputTokens).toBe(3000)
354+
expect(metrics.totalOutputTokens).toBe(1500)
355+
expect(metrics.totalCostUsd).toBeCloseTo(0.15, 4)
356+
})
357+
358+
test("handles zero values", () => {
359+
let metrics = createTestMetrics()
360+
361+
metrics = recordTokenUsage(metrics, 0, 0, 0)
362+
expect(metrics.totalInputTokens).toBe(0)
363+
expect(metrics.totalOutputTokens).toBe(0)
364+
expect(metrics.totalCostUsd).toBe(0)
365+
})
366+
})
367+
368+
describe("formatTokenCount", () => {
369+
test("formats small numbers", () => {
370+
expect(formatTokenCount(0)).toBe("0")
371+
expect(formatTokenCount(123)).toBe("123")
372+
expect(formatTokenCount(999)).toBe("999")
373+
})
374+
375+
test("formats thousands as K", () => {
376+
expect(formatTokenCount(1000)).toBe("1.0K")
377+
expect(formatTokenCount(1500)).toBe("1.5K")
378+
expect(formatTokenCount(50000)).toBe("50.0K")
379+
expect(formatTokenCount(999999)).toBe("1000.0K")
380+
})
381+
382+
test("formats millions as M", () => {
383+
expect(formatTokenCount(1000000)).toBe("1.0M")
384+
expect(formatTokenCount(1500000)).toBe("1.5M")
385+
expect(formatTokenCount(10000000)).toBe("10.0M")
386+
})
387+
})
388+
389+
describe("formatCost", () => {
390+
test("formats small costs with 4 decimals", () => {
391+
expect(formatCost(0.001)).toBe("$0.0010")
392+
expect(formatCost(0.0099)).toBe("$0.0099")
393+
})
394+
395+
test("formats normal costs with 2 decimals", () => {
396+
expect(formatCost(0.01)).toBe("$0.01")
397+
expect(formatCost(0.1)).toBe("$0.10")
398+
expect(formatCost(1.0)).toBe("$1.00")
399+
expect(formatCost(10.5)).toBe("$10.50")
400+
})
401+
})
333402
})

0 commit comments

Comments
 (0)