Skip to content

feat: track and display LLM token usage across CLI, reports, and extension - #224

Open
arunSunnyKVS wants to merge 1 commit into
masterfrom
feat/token-usage-tracking
Open

feat: track and display LLM token usage across CLI, reports, and extension#224
arunSunnyKVS wants to merge 1 commit into
masterfrom
feat/token-usage-tracking

Conversation

@arunSunnyKVS

@arunSunnyKVS arunSunnyKVS commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a TokenTracker class (core/src/execute/tokenTracker.ts) that meters every LLM call (attacker generation, adaptive follow-ups, judge) and aggregates per-evaluator and per-run totals
  • Threads token tracking through the full execution pipeline: runAllevaluatorLoop → attack drivers → withRetry / generateText / chatCompletionJsonContent
  • Displays token usage in CLI output, HTML/JSON reports, and the browser extension (popup UI + downloadable report)

What changed

Core engine

  • New TokenTracker with parent-child hierarchy — root tracker per run, child tracker per evaluator
  • withRetry wrapper automatically captures usage from generateText / generateObject results
  • Direct generateText and chatCompletionJsonContent call sites explicitly record usage
  • TokenUsage type added to execute/types.ts and report/types.ts
  • EvaluatorResult.tokenUsage and UnifiedRunReport.summary.tokenUsage carry the data through

CLI

  • Prints a summary line after run results: Token usage: 51,323 input / 6,057 output (57,380 total)

HTML report (core render.ts)

  • Token Usage stat card in the executive summary
  • Per-evaluator token counts in detail section headers (e.g. "14.5K tokens")

JSON report

  • summary.tokenUsage and per-evaluator tokenUsage fields for CI consumption

Browser extension

  • orchestrator.js: propagates tokenUsage from runAllBrowser result to popup
  • popup.js / popup.html: Tokens stat on Done screen with input/output breakdown; per-evaluator counts in result rows and downloaded HTML report
  • Fixed pruneRawForHistory to preserve tokenUsage (was being stripped by the allowlist)
  • Fixed # Details placeholder → proper styled "5 Detailed Results" section header

Docs

  • docs/cli.md: new "Token usage tracking" section
  • docs/browser-extension.md: note about token display
  • AGENTS.md: tokenTracker.ts in key files table + tracking mechanism description

Test plan

  • TokenTracker unit tests (core/tests/tokenTracker.test.ts) — parent-child aggregation, record(), child(), usage getter
  • Run opfor run --config tests/e2e/agents/customer-support/opfor.config.json — verify CLI prints token summary and HTML/JSON reports include token data
  • Run browser extension against a live chat — verify Done screen shows Tokens stat, result rows show per-evaluator counts, downloaded HTML report shows per-evaluator tokens in detail headers and total in executive summary
  • Verify no regressions: runs without token data (e.g. older cached results) gracefully hide the token UI elements

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added token usage tracking across scans, including input, output, and total token counts.
    • CLI results, browser extension screens, JSON reports, and HTML reports now display token usage.
    • Per-evaluator token usage is included in reports and downloadable results.
  • Documentation

    • Added guidance on token tracking and where usage metrics appear.

…nsion

Add a TokenTracker class that meters every LLM call (attacker generation,
adaptive follow-ups, judge) and aggregates per-evaluator and per-run totals.

- Core: thread TokenTracker through runAll → evaluatorLoop → attack drivers;
  withRetry, generateText, and chatCompletionJsonContent auto-record usage
- CLI: print token summary line after run results
- Reports: add Token Usage stat card to HTML executive summary, per-evaluator
  counts in detail headers, and tokenUsage fields in JSON output
- Extension: propagate tokenUsage from orchestrator to popup; show Tokens stat
  on Done screen with in/out breakdown; per-evaluator counts in result rows
  and downloaded HTML report; fix pruneRawForHistory to preserve tokenUsage;
  fix "# Details" → proper section 5 header in HTML template
- Docs: update cli.md, browser-extension.md, AGENTS.md

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds nested token tracking across LLM generation and judging, attaches usage to evaluator and run results, and displays token counts in CLI, JSON, HTML, and browser-extension outputs.

Changes

Token usage tracking

Layer / File(s) Summary
Token usage accumulator and contracts
core/src/execute/tokenTracker.ts, core/src/execute/types.ts, core/tests/tokenTracker.test.ts
Adds nested TokenTracker aggregation, token usage result fields, and tests for accumulation and child isolation.
LLM usage capture
core/src/lib/llmRetry.ts, core/src/llm/openaiCompatible.ts, core/src/generate/*, core/src/evaluators/judge.ts, core/src/run/judge.ts
Records provider usage from retry-wrapped generation, adaptive turns, MCP turns, and judge calls.
Execution pipeline propagation
core/src/execute/evaluatorLoop.ts, core/src/execute/*AttackDriver.ts, core/src/execute/runAll*.ts
Creates run/evaluator trackers, passes them through attack execution, and attaches evaluator and run totals to reports.
Report and UI surfaces
core/src/report/*, runners/cli/src/commands/run.ts, runners/extension/*, docs/*, AGENTS.md
Displays token totals in reports and interfaces, preserves extension history data, and documents the tracking behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, but it doesn't follow the required template and is missing Problem, Solution, Changes, Issue, How to test, and Screenshots sections. Rewrite the PR description using the repo template and add the missing sections, including Issue and Screenshots or N/A where appropriate.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: end-to-end token usage tracking and display across CLI, reports, and extension.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/token-usage-tracking

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arunSunnyKVS
arunSunnyKVS marked this pull request as ready for review July 28, 2026 11:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
core/tests/tokenTracker.test.ts (1)

10-22: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the totalTokens branch.

These tests only verify totals derived from input/output, so they would pass while explicit provider totals are silently ignored. Add a total-only case and a case where totalTokens differs from the component sum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/tests/tokenTracker.test.ts` around lines 10 - 22, Add tests in the
TokenTracker record suite for an input containing only explicit totalTokens, and
for input where totalTokens differs from inputTokens plus outputTokens; assert
that totals preserve the provider-supplied total in both cases while retaining
the expected component values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/execute/evaluatorLoop.ts`:
- Around line 243-245: Centralize evaluator-result decoration so partial results
preserve per-evaluator token usage: in core/src/execute/evaluatorLoop.ts lines
243-245, ensure the early-stop return applies evalTracker.totals to
toEvaluatorResult output; in core/src/execute/runAllBrowser.ts lines 232-242,
apply the same decoration in both attack error/stop branches. Reuse the existing
evaluator result and evalTracker symbols without changing normal completion
behavior.

In `@core/src/execute/tokenTracker.ts`:
- Around line 26-38: Preserve explicit total token counts end to end: update
TokenTracker.record in core/src/execute/tokenTracker.ts to accumulate supplied
totalTokens and use input/output sums only when totalTokens is absent; update
the OpenAI-compatible adapter in core/src/llm/openaiCompatible.ts to pass
data.usage.total_tokens; extend core/tests/tokenTracker.test.ts with total-only
and mismatched-total regression cases.

In `@core/src/lib/llmRetry.ts`:
- Around line 149-152: Validate the final LLM usage object with the existing Zod
validation approach before calling TokenTracker.record(), replacing unchecked
casts/property access. Apply this at core/src/lib/llmRetry.ts:149-152,
core/src/llm/openaiCompatible.ts:149-157, and both
core/src/generate/generateNextTurn.ts:154-155 and :301-302 call sites; only
record usage when it matches the expected inputTokens/outputTokens shape.

In `@docs/cli.md`:
- Around line 220-224: Update the fenced code block in the CLI results example
by specifying an appropriate language, such as text, immediately after its
opening fence to satisfy markdownlint MD040; leave the example content
unchanged.

---

Nitpick comments:
In `@core/tests/tokenTracker.test.ts`:
- Around line 10-22: Add tests in the TokenTracker record suite for an input
containing only explicit totalTokens, and for input where totalTokens differs
from inputTokens plus outputTokens; assert that totals preserve the
provider-supplied total in both cases while retaining the expected component
values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a0f354c-09be-4e06-b271-c5d6a75e74b6

📥 Commits

Reviewing files that changed from the base of the PR and between 094687c and 3601c33.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (24)
  • AGENTS.md
  • core/src/evaluators/judge.ts
  • core/src/execute/agentAttackDriver.ts
  • core/src/execute/evaluatorLoop.ts
  • core/src/execute/mcpAttackDriver.ts
  • core/src/execute/runAll.ts
  • core/src/execute/runAllBrowser.ts
  • core/src/execute/tokenTracker.ts
  • core/src/execute/types.ts
  • core/src/generate/generateAttacks.ts
  • core/src/generate/generateNextTurn.ts
  • core/src/lib/llmRetry.ts
  • core/src/llm/openaiCompatible.ts
  • core/src/report/buildReport.ts
  • core/src/report/render.ts
  • core/src/report/types.ts
  • core/src/run/judge.ts
  • core/tests/tokenTracker.test.ts
  • docs/browser-extension.md
  • docs/cli.md
  • runners/cli/src/commands/run.ts
  • runners/extension/orchestrator.js
  • runners/extension/popup.html
  • runners/extension/popup.js

Comment on lines +243 to +245
const evResult = toEvaluatorResult(evaluatorMeta, attackResults);
if (evalTracker) evResult.tokenUsage = evalTracker.totals;
evaluatorResults.push(evResult);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve per-evaluator usage in partial results.

The normal completion paths add tokenUsage, but stop/error branches push toEvaluatorResult(...) directly. Partial reports therefore retain run totals while omitting the affected evaluator’s usage.

  • core/src/execute/evaluatorLoop.ts#L243-L245: centralize evaluator-result decoration so the early stop return also receives evalTracker.totals.
  • core/src/execute/runAllBrowser.ts#L232-L242: apply the same decoration in both attack error/stop branches.
📍 Affects 2 files
  • core/src/execute/evaluatorLoop.ts#L243-L245 (this comment)
  • core/src/execute/runAllBrowser.ts#L232-L242
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/evaluatorLoop.ts` around lines 243 - 245, Centralize
evaluator-result decoration so partial results preserve per-evaluator token
usage: in core/src/execute/evaluatorLoop.ts lines 243-245, ensure the early-stop
return applies evalTracker.totals to toEvaluatorResult output; in
core/src/execute/runAllBrowser.ts lines 232-242, apply the same decoration in
both attack error/stop branches. Reuse the existing evaluator result and
evalTracker symbols without changing normal completion behavior.

Comment on lines +26 to +38
/** Record usage from a single LLM call. Safe to call with undefined/partial usage. */
record(usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number }): void {
if (!usage) return;
this.input += usage.inputTokens ?? 0;
this.output += usage.outputTokens ?? 0;
}

get totals(): TokenUsage {
return {
inputTokens: this.input,
outputTokens: this.output,
totalTokens: this.input + this.output,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve totalTokens end to end. The tracker discards explicit totals, the OpenAI-compatible adapter does not forward total_tokens, and the tests do not cover this contract.

  • core/src/execute/tokenTracker.ts#L26-L38: accumulate supplied totals and fall back to component sums only when absent.
  • core/src/llm/openaiCompatible.ts#L151-L157: pass data.usage.total_tokens into TokenTracker.record().
  • core/tests/tokenTracker.test.ts#L10-L22: add total-only and mismatched-total regression cases.
📍 Affects 3 files
  • core/src/execute/tokenTracker.ts#L26-L38 (this comment)
  • core/src/llm/openaiCompatible.ts#L151-L157
  • core/tests/tokenTracker.test.ts#L10-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/tokenTracker.ts` around lines 26 - 38, Preserve explicit
total token counts end to end: update TokenTracker.record in
core/src/execute/tokenTracker.ts to accumulate supplied totalTokens and use
input/output sums only when totalTokens is absent; update the OpenAI-compatible
adapter in core/src/llm/openaiCompatible.ts to pass data.usage.total_tokens;
extend core/tests/tokenTracker.test.ts with total-only and mismatched-total
regression cases.

Comment thread core/src/lib/llmRetry.ts
Comment on lines +149 to +152
if (tokenTracker && result && typeof result === "object" && "usage" in result) {
tokenTracker.record(
(result as { usage?: { inputTokens?: number; outputTokens?: number } }).usage
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)llmRetry\.ts$|openaiCompatible\.ts$|generateNextTurn\.ts$|zod' || true

echo
echo "Relevant snippets:"
for f in core/src/lib/llmRetry.ts core/src/llm/openaiCompatible.ts core/src/generate/generateNextTurn.ts; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    rg -n "tokenTracker|result\.usage|array.*result|generateText|usage" "$f" -C 4 || true
  fi
done

echo
echo "Search tokenTracker implementations/usages:"
rg -n "class .*TokenTracker|record\\(|tokenTracker|Usage|inputTokens|outputTokens" core/src -g '*.ts' || true

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 14939


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Dependencies and existing Zod usage:"
if [ -f package.json ]; then cat package.json | sed -n '1,220p'; fi

echo
rg -n "\bzod\b|from \"zod\"|from 'zod'" -g '*.ts' -g '*.tsx' | head -200 || true

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 2738


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "tokenTracker implementation:"
cat -n core/src/execute/tokenTracker.ts

echo
echo "openaiCompatible relevant section:"
sed -n '130,170p' core/src/llm/openaiCompatible.ts | cat -n -v

echo
echo "generateNextTurn relevant sections:"
sed -n '130,165p' core/src/generate/generateNextTurn.ts | cat -n -v
sed -n '285,310p' core/src/generate/generateNextTurn.ts | cat -n -v

echo
echo "LLM retry full relevant section:"
sed -n '130,160p' core/src/lib/llmRetry.ts | cat -n -v

echo
echo "Package lock zod versions if present:"
for f in package-lock.json core/package.json core/package-lock.json; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    rg -n '"zod"|"`@hono/node-server`"|node_modules/zod|zod' "$f" | head -60 || true
  fi
done

echo
echo "Behavioral probe: TokenTracker accepts numeric/non-numeric and non-object values with optional inputs:"
node - <<'JS'
const inputs = [
  undefined,
  null,
  123,
  "12",
  {},
  { inputTokens: "9", outputTokens: "11" },
  { inputTokens: NaN, outputTokens: null },
  { inputTokens: true, outputTokens: "2" },
];
let input = 0, output = 0;
function record(usage) {
  input += usage.inputTokens ?? 0;
  output += usage.outputTokens ?? 0;
}
for (const usage of inputs) {
  try {
    record(usage);
    console.log(JSON.stringify(usage) + " -> " + input + "/" + output);
  } catch (e) {
    console.log(JSON.stringify(usage) + " threw " + e.name + ": " + e.message);
  }
}
JS

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 10171


Validate LLM usage before recording. These boundaries pass provider/SDK response objects through casts or unchecked properties into TokenTracker.record(), which can coerce malformed values before persisting token totals. Validate the final usage shape with Zod before recording at llmRetry.ts, openaiCompatible.ts, and both generateNextTurn.ts call sites.

📍 Affects 3 files
  • core/src/lib/llmRetry.ts#L149-L152 (this comment)
  • core/src/llm/openaiCompatible.ts#L149-L157
  • core/src/generate/generateNextTurn.ts#L154-L155
  • core/src/generate/generateNextTurn.ts#L301-L302
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/lib/llmRetry.ts` around lines 149 - 152, Validate the final LLM
usage object with the existing Zod validation approach before calling
TokenTracker.record(), replacing unchecked casts/property access. Apply this at
core/src/lib/llmRetry.ts:149-152, core/src/llm/openaiCompatible.ts:149-157, and
both core/src/generate/generateNextTurn.ts:154-155 and :301-302 call sites; only
record usage when it matches the expected inputTokens/outputTokens shape.

Source: Coding guidelines

Comment thread docs/cli.md
Comment on lines +220 to +224
```
Results: 5 passed, 2 failed, 0 errors
Safety score: 71%
Token usage: 51,323 input / 6,057 output (57,380 total)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify the fenced block language.

Add text (or another appropriate language) after the opening fence to satisfy markdownlint MD040.

-```
+```text
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 220-220: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/cli.md` around lines 220 - 224, Update the fenced code block in the CLI
results example by specifying an appropriate language, such as text, immediately
after its opening fence to satisfy markdownlint MD040; leave the example content
unchanged.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant