Repository navigation
fix: report per-invocation cost from the Claude adapter - #417
Merged
Merged
Conversation
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>
|
Fern docs preview: https://nvidia-preview-pull-request-417.docs.buildwithfern.com/nemo/fabric |
AjayThorve
approved these changes
Oct 19, 2026
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
ClaudeRuntimekeeps the last observedtotal_cost_usd, starting at zero for each new client. It reports the difference in bothoutput["cost_usd"]andusage.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-levelnormalize_resultfunction is unchanged.When cost stays unknown:
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 returnclaude_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.just no_uv=true test-python: 2168 passed, 106 skipped, 11 failed. Ten failures are Relay end-to-end tests with gateway-start failures orKeyError: 'scope_category'. The adapter testtest_relay_initializes_v4_atof_atif_configfails 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-fileswith cargo-about 0.9.0: passed.Where should the reviewer start?
Start with
ClaudeRuntime._invocation_costinadapters/python/claude/src/nemo_fabric_adapters/claude/adapter.py, thentest_claude_runtime_reports_cost_per_invocationintests/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