Skip to content

fix: report per-invocation cost from the Claude adapter - #417

Merged
AjayThorve merged 3 commits into
NVIDIA:mainfrom
mnajafian-nv:fix/claude-invocation-cost
Oct 19, 2026
Merged

AjayThorve merged 3 commits into
NVIDIA:mainfrom
mnajafian-nv:fix/claude-invocation-cost

Conversation

@mnajafian-nv

@mnajafian-nv mnajafian-nv commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Overview

The Claude adapter currently reports Claude Code's cumulative cost as the cost of each invocation. Because a runtime reuses the same Claude Code process, later invocations include earlier spend. The adapter now reports the difference between successive totals.

For example, in a live Claude Haiku 4.5 run, the second invocation reported 0.0395142 USD, but only 0.0039867 USD belonged to that invocation. Token counts are unchanged.

Details

Calculating each invocation's cost:

ClaudeRuntime keeps the last observed total_cost_usd, starting at zero for each new client. It reports the difference in both output["cost_usd"] and usage.cost_usd. A failed terminal result can still report cost when the difference is known.

Each reported cost, including the first, is rounded to 10 decimal places to remove floating-point subtraction noise. The saved total remains unrounded. The conversion happens in ClaudeRuntime._normalize_invocation; the module-level normalize_result function is unchanged.

When cost stays unknown:

  • If a total is missing, non-finite, negative, or otherwise invalid, the adapter clears the saved total. The next valid observation establishes a baseline but still reports unknown cost. A subsequent valid, nondecreasing total allows reporting to resume, provided no reset occurs.
  • If a valid total decreases, or a conversation reset is reported, that invocation's cost is unknown. Its valid total becomes the new baseline for the next invocation.

The lifecycle host processes invocations one at a time. SDK failures, timeouts, and missing terminal results already make the runtime unusable, so a later invocation cannot compare against a stale total after one of those failures.

Session validation is unchanged. A reset before the first successful invocation can establish a new session. Once a successful invocation has established the session, a later successful result with a different session ID fails with claude_session_mismatch; subsequent invocations return claude_runtime_unavailable.

Compatibility: there are no API or configuration changes. Reported Claude costs now belong to individual invocations rather than accumulating earlier spend. All reported costs use the same rounding, and cost remains unknown when a reliable difference cannot be calculated.

Validation

  • pytest tests/adapters/test_claude_adapter.py: 75 passed. Coverage includes cumulative totals, zero additional spend, decreasing totals, missing/negative/NaN totals followed by recovery, failed terminal results between successes, both reset/session cases, and fresh clients.
  • Live Claude Haiku 4.5 run with two invocations on one runtime: before the fix, 0.0355275 then 0.0395142 USD; in a separate run with the fix, 0.0072985 then 0.0029729 USD. The latter matches the second invocation's recorded usage at Haiku 4.5 list rates. This run predates the negative-total check; that check does not alter valid-total calculations.
  • just no_uv=true test-python: 2168 passed, 106 skipped, 11 failed. Ten failures are Relay end-to-end tests with gateway-start failures or KeyError: 'scope_category'. The adapter test test_relay_initializes_v4_atof_atif_config fails because inherited system Relay configuration adds unexpected diagnostics. The Relay end-to-end path remains unverified on this machine.
  • uv run pre-commit run --all-files with cargo-about 0.9.0: passed.

Where should the reviewer start?

Start with ClaudeRuntime._invocation_cost in adapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py, then test_claude_runtime_reports_cost_per_invocation in tests/adapters/test_claude_adapter.py.

The main thing to check is what happens after each observation: whether the total can be used for the next comparison, and whether the current invocation's cost is known. The tests distinguish an invalid total from a valid total that decreased or arrived after a reset.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • Relates to: none

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.

  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Summary by CodeRabbit

  • New Features
    • Claude usage now reports the cost for each invocation based on changes in cumulative session cost. Costs are also reported for failed invocations when available.
    • Costs are marked unknown when a reliable baseline is unavailable, such as after a conversation reset or when totals are invalid or decrease.

A Claude runtime sends every invocation to one connected Claude Code
process, and that process reports total_cost_usd as a running total for
the whole session. The adapter copied that total into cost_usd, so every
invocation after the first reported the cost of all earlier invocations
too. Fabric defines cost_usd as the invocation's cost, and the adapter
contract asks for the difference between successive observations of a
cumulative counter.

Track the last total per runtime and report the difference. The baseline
starts at zero when the runtime connects its Claude Code process. A
missing, decreasing, or reset total leaves the cost unknown until the
next total gives a new baseline. The lifecycle host serves one request
at a time, so successive totals belong to successive invocations.

Token counts stay as they are because ResultMessage.usage already covers
only the current invocation.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
A negative total_cost_usd was stored as the baseline, so the next
invocation reported its total plus the negative amount. Treat a negative
total like a missing or non-finite one: report no cost and leave no
baseline, so the next known total starts a new one.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Pin the per-invocation cost of a failed result between two successful
ones. Give the reset tests the new session id that Claude Code sends
after a reset: a reset in the first invocation keeps the runtime usable,
and a reset after the session is established fails the session check.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/NeMo-Fabric/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Enterprise
  • Run ID: 43c0393a-f932-49bc-b8c3-75f619e9605d
📥 Commits

Reviewing files that changed from the base of the PR and between be4f4d2 and d3b852e.

📒 Files selected for processing: 2
  • adapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py
  • tests/adapters/test_claude_adapter.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout: 28
  • GitHub Check: Preview docs
  • GitHub Check: Test (Python 3.11, macos-arm64)
  • GitHub Check: Test adapters (Node 24)
  • GitHub Check: Test (Python 3.14, windows-amd64)
  • GitHub Check: Test (Python 3.14, linux-amd64)
  • GitHub Check: Test (Python 3.12, windows-amd64)
  • GitHub Check: Test (Node 20.18.3)
  • GitHub Check: Test (Python 3.13, linux-amd64)
  • GitHub Check: Test (Python 3.11, windows-amd64)
  • GitHub Check: Test (Python 3.13, linux-arm64)
  • GitHub Check: Test (Python 3.14, macos-arm64)
  • GitHub Check: Test (Python 3.13, windows-amd64)
  • GitHub Check: Test (Python 3.12, macos-arm64)
  • GitHub Check: Test (Python 3.12, linux-amd64)
  • GitHub Check: Test (Python 3.14, linux-arm64)
  • GitHub Check: Test (Python 3.12, linux-arm64)
  • GitHub Check: Test (Python 3.11, linux-arm64)
  • GitHub Check: Test (Python 3.11, linux-amd64)
  • GitHub Check: Test adapters (Node 22.19.0)
  • GitHub Check: OpenCode E2E
  • GitHub Check: Relay 0.9 (Python 3.12, linux-amd64)
  • GitHub Check: Cline E2E
  • GitHub Check: Test (Python 3.13, macos-arm64)
  • GitHub Check: Qwen Code E2E
  • GitHub Check: Pre-commit
  • GitHub Check: Test (arm64)
  • GitHub Check: Hermes adapter (upstream Relay 0.10, Python 3.14)
  • GitHub Check: Test (x86_64)
🧰 Additional context used
📚 Code guidelines: 1
.agents/skills/contribute-adapter/SKILL.md — unknown
📓 Path-based instructions: 3
Review adapter and example changes for command correctness, config/schema consistency, artifact handling, and compatibility with the public NeMo Fabric contracts.

⚙️ CodeRabbit configuration file

Files:

  • adapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py
Tests should cover the behavior promised by the changed API surface, including error paths, lifecycle cleanup, and SDK/native parity where relevant.

⚙️ CodeRabbit configuration file

Files:

  • tests/adapters/test_claude_adapter.py
Source excerpt: Follow these repository-specific requirements after applying the public skill: Place a Python adapter under `adapters/python//` with `LICENSE -> ../../../LICENSE`, `README.md`, `.fabric-adapter.json`, Python pack...

📄 CodeRabbit inference engine (.agents/skills/contribute-adapter/SKILL.md)

Files:

  • adapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py
🔇 Additional comments: 2
adapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py: 1

29-29: LGTM!

Also applies to: 1017-1017, 1063-1064, 1196-1196, 1210-1241

tests/adapters/test_claude_adapter.py: 1

26-26: LGTM!

Also applies to: 1053-1208

Walkthrough

ClaudeRuntime now tracks Claude Code’s cumulative session cost and reports the difference as each invocation’s cost. Tests cover invalid totals, conversation resets, error results, and separate client sessions.

Changes

Claude cost tracking

Layer / File(s) Summary
Track cumulative cost
adapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py
ClaudeRuntime initializes a cost baseline after connecting and calculates invocation cost from cumulative totals. Invalid totals, missing baselines, resets, and decreasing totals produce unknown cost.
Test invocation cost handling
tests/adapters/test_claude_adapter.py
Tests cover cost deltas, unusable totals, error results, conversation resets, and fresh baselines for separate clients.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to d3b85

No identified issue blocks merging the Claude invocation-cost change after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
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.
Title check Passed The title follows Conventional Commits format with the lowercase type "fix", an imperative summary, no trailing period, and 55 characters.
Description check Passed The description is complete and relevant. It includes an overview, implementation details, validation results, reviewer guidance, and completed contribution checks. The related-issues section states "…
Full details: Description check

Explanation

The description is complete and relevant. It includes an overview, implementation details, validation results, reviewer guidance, and completed contribution checks. The related-issues section states "Relates to: none" instead of providing an issue reference, but this is a minor non-critical omission.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions

Copy link
Copy Markdown

@mnajafian-nv mnajafian-nv self-assigned this Oct 16, 2026
@AjayThorve
AjayThorve merged commit 40e5229 into NVIDIA:main Oct 19, 2026
43 checks passed

This branch was previously deployed

1 inactive deployment
fern — d3b852e5 Deployed Oct 19, 2026 by AjayThorve via Clean up docs preview #2129
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.

2 participants