Skip to content

feat: token usage tracking - #10

Merged
brucearctor merged 5 commits into
mainfrom
feat/token-tracking
Jul 9, 2026
Merged

feat: token usage tracking#10
brucearctor merged 5 commits into
mainfrom
feat/token-tracking

Conversation

@brucearctor

@brucearctor brucearctor commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds input/output/total token tracking from Vertex AI responses. Zero external deps.

Where it shows up

Terminal (in the summary box):

┌─────────────────────────────────────────────────────┐
│  Code Review: 3 finding(s)                          │
│  🔴 1 critical  🟡 2 medium                         │
│  Tokens: 12847 in / 1203 out / 14050 total          │
└─────────────────────────────────────────────────────┘

JSON (--json):

{"usage": {"input_tokens": 12847, "output_tokens": 1203, "total_tokens": 14050}}

CI logs (via slog):

level=INFO msg="token usage" input=12847 output=1203 total=14050

Changes

File What
internal/model/provider.go TokenUsage struct, capture from UsageMetadata
internal/model/multi.go Aggregate across models in mergeResults
internal/reviewer/reviewer.go Aggregate across chunks, slog output
internal/reviewer/terminal.go Token line in summary box
internal/reviewer/output_test.go 2 new tests

147 tests, all passing.

Summary by CodeRabbit

  • New Features
    • Review results now expose optional model token usage (input/output/total) when available.
    • Terminal output shows token counts in the summary header (colored and plain text), including cases with no findings.
  • Bug Fixes
    • Token usage is now correctly aggregated across reviewed chunks and merged results, and is only displayed when totals are present.
  • Tests
    • Added/updated coverage for token-usage rendering and for merge behavior when usage is present or absent.

Captures input/output/total token counts from Vertex AI responses:
- Provider.Review() reads UsageMetadata from genai response
- Reviewer aggregates across chunks
- MultiProvider mergeResults aggregates across models
- Terminal summary box shows: 'Tokens: 1500 in / 200 out / 1700 total'
- JSON output includes 'usage' field
- slog.Info logs usage for CI job visibility

Tests:
- TestColorTerminalOutput_TokenUsage: verifies counts in output
- TestColorTerminalOutput_NoTokenUsage: hidden when zero
- 147 tests total
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c2da17a-0aaf-4a95-9068-daf4bc7a25a6

📥 Commits

Reviewing files that changed from the base of the PR and between c200648 and 99ccb43.

📒 Files selected for processing (1)
  • internal/reviewer/output_test.go

📝 Walkthrough

Walkthrough

This PR adds token usage tracking from provider responses through result aggregation and terminal rendering. It also adds tests for merged usage counts and output formatting with and without token data.

Changes

Token Usage Tracking

Layer / File(s) Summary
TokenUsage type and provider capture
internal/model/provider.go
Adds TokenUsage, extends ReviewResult with Usage, and fills it from Vertex usage metadata in Provider.Review.
Aggregation across models and chunks
internal/model/multi.go, internal/model/multi_test.go, internal/reviewer/reviewer.go
Aggregates token counts in merged model results and reviewer chunk processing, then verifies summed and nil-usage cases in tests.
Terminal rendering and tests
internal/reviewer/terminal.go, internal/reviewer/output.go, internal/reviewer/output_test.go
Adds token-usage output in colored and plain terminal renderers and covers presence/absence cases in tests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Provider
  participant Reviewer
  participant mergeResults
  participant Terminal

  Provider->>Provider: read UsageMetadata
  Provider->>Reviewer: ReviewResult with Usage
  Reviewer->>Reviewer: accumulate totalUsage per chunk
  Reviewer->>mergeResults: per-model ReviewResults
  mergeResults->>mergeResults: sum InputTokens, OutputTokens, TotalTokens
  mergeResults-->>Reviewer: merged ReviewResult with Usage
  Reviewer->>Terminal: ReviewResult with aggregated Usage
  Terminal->>Terminal: render token usage row if TotalTokens > 0
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding token usage tracking.
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-tracking

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@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.

🧹 Nitpick comments (1)
internal/model/provider.go (1)

21-26: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

omitempty on struct field Usage has no effect.

Go's encoding/json never treats a non-pointer struct as "empty" for omitempty purposes — only false, 0, nil pointers/interfaces, and empty arrays/slices/maps/strings qualify. TokenUsage is a plain struct, so Usage will always be serialized in JSON output, even when all counts are zero, despite the omitempty tag suggesting otherwise.

If the intent is to hide the usage block when zero (matching the terminal's conditional behavior), use a pointer (*TokenUsage) and only set it when TotalTokens > 0, or use Go 1.24's omitzero tag if the module's Go version supports it.

Proposed fix using a pointer field
 type ReviewResult struct {
 	Summary  string     `json:"summary"`
 	Findings []Finding  `json:"findings"`
-	Usage    TokenUsage `json:"usage,omitempty"`
+	Usage    *TokenUsage `json:"usage,omitempty"`
 }
🤖 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 `@internal/model/provider.go` around lines 21 - 26, The `ReviewResult` struct’s
`Usage` field uses `omitempty` on a non-pointer `TokenUsage`, so it will still
always appear in JSON. Update `ReviewResult` in `provider.go` so `Usage` can
actually be omitted when empty, preferably by changing it to a pointer
`*TokenUsage` and only populating it when `TotalTokens > 0` in the code that
builds `ReviewResult`; alternatively, if the project targets Go 1.24+, use
`omitzero` instead of `omitempty`.
🤖 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.

Nitpick comments:
In `@internal/model/provider.go`:
- Around line 21-26: The `ReviewResult` struct’s `Usage` field uses `omitempty`
on a non-pointer `TokenUsage`, so it will still always appear in JSON. Update
`ReviewResult` in `provider.go` so `Usage` can actually be omitted when empty,
preferably by changing it to a pointer `*TokenUsage` and only populating it when
`TotalTokens > 0` in the code that builds `ReviewResult`; alternatively, if the
project targets Go 1.24+, use `omitzero` instead of `omitempty`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 58d073c0-c5b6-4673-8482-761120c4dcce

📥 Commits

Reviewing files that changed from the base of the PR and between ff7b0ac and cb1213b.

📒 Files selected for processing (5)
  • internal/model/multi.go
  • internal/model/provider.go
  • internal/reviewer/output_test.go
  • internal/reviewer/reviewer.go
  • internal/reviewer/terminal.go

Fixes from deep review:
- TokenUsage fields: int32 → int64 (defensive for multi-chunk × multi-model)
- Usage field: TokenUsage → *TokenUsage (omitempty now properly suppresses
  zero-value JSON output instead of emitting {0,0,0})
- All nil-pointer checks added (terminal, reviewer, multi aggregation)
- Plain-text TerminalOutput() now shows token usage (was missing)

Tests added:
- TestMergeResults_UsageAggregation: verifies summing across models
- TestMergeResults_UsageNilWhenNoUsage: nil when no results have usage
- TestTerminalOutput_PlainText_TokenUsage: plain-text path coverage
- TestTerminalOutput_PlainText_NoTokenUsage: hidden when nil
- 151 tests total

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/reviewer/output.go (1)

16-54: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Token usage line unreachable when there are no findings.

The early return at line 21-24 (len(result.Findings) == 0) exits before the token usage block at lines 50-54 is reached, so plain-text TerminalOutput never prints tokens when there are no findings. This contradicts ColorTerminalOutput, which places its equivalent usage block before its own no-findings return, so it does show tokens even with zero findings. This breaks parity between the two renderers and doesn't match the PR's goal of surfacing token usage in terminal output regardless of finding count.

🐛 Proposed fix
 	if len(result.Findings) == 0 {
 		sb.WriteString("✅ No issues found. Code looks clean and ready to merge.\n")
+		if result.Usage != nil && result.Usage.TotalTokens > 0 {
+			fmt.Fprintf(&sb, "---\nTokens: %d in / %d out / %d total\n",
+				result.Usage.InputTokens, result.Usage.OutputTokens, result.Usage.TotalTokens)
+		}
 		return sb.String()
 	}
🤖 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 `@internal/reviewer/output.go` around lines 16 - 54, Move the token usage
rendering in TerminalOutput so it runs before the len(result.Findings) == 0
early return, matching the behavior of ColorTerminalOutput. Keep the no-findings
message, but ensure the usage block in TerminalOutput is emitted whenever
result.Usage.TotalTokens is set, even when there are no findings. Use
TerminalOutput and the existing result.Usage block as the main touchpoints.
internal/reviewer/output_test.go (1)

235-270: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing coverage for "usage present, no findings" case.

None of the added tests cover TerminalOutput/ColorTerminalOutput with a non-nil Usage and zero findings, which is exactly the scenario where plain-text output currently drops the token line (see comment on output.go). Adding this case would have caught the early-return bug.

Want me to add a test like TestTerminalOutput_PlainText_TokenUsage_NoFindings asserting the tokens line still appears when Findings is empty but Usage is set?

🤖 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 `@internal/reviewer/output_test.go` around lines 235 - 270, Add coverage for
the case where ReviewResult.Usage is set but Findings is empty, since
TerminalOutput and ColorTerminalOutput currently skip the token line in that
path. Update output_test.go by adding a focused test around TerminalOutput
and/or ColorTerminalOutput that uses non-nil Usage with zero findings and
asserts the token counts still appear. Keep the test aligned with the existing
TerminalOutput_PlainText_TokenUsage and TestColorTerminalOutput_NoTokenUsage
patterns so it exercises the early-return behavior in the output helpers.
🤖 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.

Outside diff comments:
In `@internal/reviewer/output_test.go`:
- Around line 235-270: Add coverage for the case where ReviewResult.Usage is set
but Findings is empty, since TerminalOutput and ColorTerminalOutput currently
skip the token line in that path. Update output_test.go by adding a focused test
around TerminalOutput and/or ColorTerminalOutput that uses non-nil Usage with
zero findings and asserts the token counts still appear. Keep the test aligned
with the existing TerminalOutput_PlainText_TokenUsage and
TestColorTerminalOutput_NoTokenUsage patterns so it exercises the early-return
behavior in the output helpers.

In `@internal/reviewer/output.go`:
- Around line 16-54: Move the token usage rendering in TerminalOutput so it runs
before the len(result.Findings) == 0 early return, matching the behavior of
ColorTerminalOutput. Keep the no-findings message, but ensure the usage block in
TerminalOutput is emitted whenever result.Usage.TotalTokens is set, even when
there are no findings. Use TerminalOutput and the existing result.Usage block as
the main touchpoints.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a117cd97-c879-4d33-8d80-4fb24a60060d

📥 Commits

Reviewing files that changed from the base of the PR and between cb1213b and d6f816d.

📒 Files selected for processing (7)
  • internal/model/multi.go
  • internal/model/multi_test.go
  • internal/model/provider.go
  • internal/reviewer/output.go
  • internal/reviewer/output_test.go
  • internal/reviewer/reviewer.go
  • internal/reviewer/terminal.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/reviewer/terminal.go
  • internal/model/provider.go
  • internal/reviewer/reviewer.go
  • internal/model/multi.go

…odeRabbit)

TerminalOutput() early-returned before reaching the usage block when
Findings was empty. Added usage rendering before the early return,
matching ColorTerminalOutput's behavior.

Tests:
- TestTerminalOutput_PlainText_UsageWithNoFindings
- TestColorTerminalOutput_UsageWithNoFindings
- 153 tests total

@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.

🧹 Nitpick comments (1)
internal/reviewer/output_test.go (1)

272-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider table-driven tests for these usage-rendering cases.

These two tests (and the similarly-shaped ones at lines 216-270) could be consolidated into a single table-driven test over {usage *model.TokenUsage, findings []model.Finding, wantContains []string} cases, covering plain-text/color and with/without findings. Given the existing file already uses this standalone-function style throughout, this is optional and can be deferred.

As per path instructions, "**/*_test.go: Verify table-driven tests, proper cleanup, and race condition safety."

🤖 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 `@internal/reviewer/output_test.go` around lines 272 - 300, The usage-rendering
tests in TerminalOutput and ColorTerminalOutput are duplicated across plain-text
and colored cases, so consolidate them into one table-driven test that iterates
over combinations of ReviewResult.Usage and Findings and checks the expected
substrings for both output functions. Keep the existing assertions for token
totals and “no findings” behavior, but drive them from a shared cases table
keyed by TerminalOutput, ColorTerminalOutput, and model.TokenUsage to reduce
repetition and make future cases easier to add.

Source: Path instructions

🤖 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.

Nitpick comments:
In `@internal/reviewer/output_test.go`:
- Around line 272-300: The usage-rendering tests in TerminalOutput and
ColorTerminalOutput are duplicated across plain-text and colored cases, so
consolidate them into one table-driven test that iterates over combinations of
ReviewResult.Usage and Findings and checks the expected substrings for both
output functions. Keep the existing assertions for token totals and “no
findings” behavior, but drive them from a shared cases table keyed by
TerminalOutput, ColorTerminalOutput, and model.TokenUsage to reduce repetition
and make future cases easier to add.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 695a3259-8c8d-4115-8e04-1d86a943335e

📥 Commits

Reviewing files that changed from the base of the PR and between d6f816d and e784195.

📒 Files selected for processing (2)
  • internal/reviewer/output.go
  • internal/reviewer/output_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/reviewer/output.go

Replaces 6 standalone token usage tests with 1 table-driven test
covering 3 cases × 2 renderers = 6 subtests. Easier to extend.

@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.

🧹 Nitpick comments (1)
internal/reviewer/output_test.go (1)

226-245: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test case: findings present + nil usage.

TerminalOutput (output.go) has two separate result.Usage != nil checks guarding the "Tokens:" line — one in the no-findings early-return branch, one after the by-file loop for the has-findings branch. The current table only exercises the has-findings branch with usage present (case 1) and the no-findings branch with usage absent (case 2). There's no case verifying that the has-findings branch correctly omits "Tokens:" when Usage is nil, so a regression dropping that nil-check in the second location would go undetected.

♻️ Suggested additional test case
 		{
 			name:         "no findings but usage shown",
 			findings:     nil,
 			usage:        &model.TokenUsage{InputTokens: 3000, OutputTokens: 100, TotalTokens: 3100},
 			wantContains: []string{"3000", "3100"},
 		},
+		{
+			name:       "findings present but nil usage hidden",
+			findings:   []model.Finding{finding},
+			usage:      nil,
+			wantAbsent: []string{"Tokens:"},
+		},
 	}

As per path instructions for **/*_test.go: "Verify table-driven tests, proper cleanup, and race condition safety."

🤖 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 `@internal/reviewer/output_test.go` around lines 226 - 245, Add a table-driven
case for TerminalOutput that covers findings present with nil Usage so the
has-findings path is exercised without printing "Tokens:". Update the existing
output tests in output_test.go alongside the current TerminalOutput scenarios,
and assert that the output contains the findings but omits the token line when
result.Usage is nil. This will verify the second result.Usage != nil guard in
TerminalOutput and prevent regressions in the by-file branch.

Source: Path instructions

🤖 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.

Nitpick comments:
In `@internal/reviewer/output_test.go`:
- Around line 226-245: Add a table-driven case for TerminalOutput that covers
findings present with nil Usage so the has-findings path is exercised without
printing "Tokens:". Update the existing output tests in output_test.go alongside
the current TerminalOutput scenarios, and assert that the output contains the
findings but omits the token line when result.Usage is nil. This will verify the
second result.Usage != nil guard in TerminalOutput and prevent regressions in
the by-file branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f4e92e7-ebac-4247-bac8-e680f4063dcd

📥 Commits

Reviewing files that changed from the base of the PR and between e784195 and c200648.

📒 Files selected for processing (1)
  • internal/reviewer/output_test.go

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