Skip to content

feat: add shared model usage accounting - #442

Closed
mnajafian-nv wants to merge 37 commits into
NVIDIA:mainfrom
mnajafian-nv:fix/deepagents-token-usage
Closed

mnajafian-nv wants to merge 37 commits into
NVIDIA:mainfrom
mnajafian-nv:fix/deepagents-token-usage

Conversation

@mnajafian-nv

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

Copy link
Copy Markdown
Contributor

Overview

Harnesses report token counts differently. Some include cached tokens in input; others report them separately. Missing usage can also appear as zero. Comparing these numbers directly can make incomplete runs look cheaper.

Add shared accounting rules to NVIDIA NeMo Fabric, with the existing Deep Agents adapter as the first collector. SDK users get normalized token counts and reasons for unknown values through RunResult.model_usage, without enabling recording or Relay.

Fabric returns a total only when the usage evidence accounts for the relevant work. When it cannot establish a total, that field is null. Available counts remain separately accessible in observed, with reasons in gaps. A reported zero stays zero.

Details

Normalize input to include cache reads and writes, output to include reasoning tokens, and total to equal input plus output. Each field can be unknown independently. A per-model breakdown is included when the source establishes which model performed the work.

Model work, including subagent work, belongs to the Fabric invocation that started it. Counts are finalized when that invocation returns. Late usage cannot change its result or move into a later invocation. A failed invocation can still have known usage. Missing usage or unfinished model work leaves the affected totals unknown. Values in observed are not guaranteed lower bounds.

The Deep Agents collector combines LangChain callbacks with HTTP-attempt hooks on adapter-created ChatOpenAI clients. This includes summarization and local subagents using those clients. Unmanaged model overrides and dispatched remote Agent Protocol work leave the affected totals unknown. For clients whose usage is available only through LangGraph messages, existing RunResult.usage.input_tokens and RunResult.usage.output_tokens can now be null. Their graph-message subtotals remain in RunResult.output.usage as diagnostics and must not be treated as complete invocation totals.

The contract keeps provider-reported cost separate from estimates. The Deep Agents collector does not collect provider-reported cost. Catalog estimates follow in #478. The run journal and readable report are added separately in #423.

If structurally valid accounting cannot be normalized, Fabric keeps the execution outcome and raw usage and adds an accounting diagnostic. Extensions that violate the adapter schema still fail validation.

Consumers with older strict result decoders must upgrade before reading model_usage. Rust struct literals that list every RunResult field must also add model_usage.

Validation

The Rust workspace tests, native Python build and focused Python accounting checks passed. Shared fixtures check normalization across Rust, Python and TypeScript. Regression cases cover independent dimensions, reported zero, omissions, invalid evidence and schema-copy parity. Deep Agents cases cover retries, summaries, parallel in-process subagents, concurrent runtimes and late callbacks.

With the checkout built and Python dependencies installed, run the focused checks from the repository root:

python -m pytest -p no:cacheprovider tests/python/test_model_usage.py tests/python/test_usage_reducer_parity.py tests/adapters/test_deepagents_accounting.py -q

Where should the reviewer start?

Review the shared contract first:

  1. Check crates/fabric-core/src/model_usage.rs, usage_normalization.rs, and schemas/extensions/usage-accounting.schema.json for when totals are known, when they stay null, and which observations remain available. Compare those rules with the shared conformance fixtures and Python and TypeScript reducers.
  2. Check adapters/python/deepagents/src/nemo_fabric_adapters/deepagents/usage.py and tests/adapters/test_deepagents_accounting.py for retries, subagents, and usage that arrives after an invocation returns.

The files under docs/reference/api/ are generated API references. Spot-check them against the public types after reviewing the source.

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.

The Deep Agents adapter summed usage_metadata from the AI messages that
reached graph state through the updates stream. Three cases came out
wrong:

- Deep Agents adds SummarizationMiddleware to every agent and subagent.
  Its summary call returns only text, so the summary model's message
  and usage never enter graph state, and an invocation that summarized
  under-reported input, output and total tokens.
- When the invocation raised, including GraphRecursionError, the
  adapter returned no messages, so usage was None even though model
  calls had finished.
- A count was summed over the calls that reported it and returned as
  known. ChatOpenAI also turns an omitted Chat Completions count into 0
  and derives a missing total, so a partial provider report came out as
  measured zeros.

The adapter now records usage from LangChain's chat-model callbacks,
which fire for every chat-model call that runs in the adapter process:
the main agent, declarative subagents and summarization. Agent Protocol
subagents run remotely and are not included. A new recorder is created
for each invocation, so earlier turns are never counted again. It
outlives an exception, so a failed invocation still reports the fields
that every counted call reported.

Each token count and the cost are known only when every counted call
in the invocation reported them, and an explicit zero stays a known
zero. A request the provider rejects with a 4xx status (other than 408)
contributes no usage. A 5xx response, timeout, cancellation or
unfinished call leaves the invocation's counts and cost unknown,
because the provider may have done work without reporting usage.
OpenAI SDK retries happen inside one LangChain call, so a 5xx the SDK
retries to success is not visible to the recorder, and only the final
response's reported usage counts. When
a response carries OpenAI-shaped raw usage, the raw counts decide what
was reported: an omitted count stays unknown, the provider's total is
used when present, and a derived total counts only when input and
output were both reported.

The graph tests run the real Deep Agents graph and ChatOpenAI against
a scripted Chat Completions endpoint for summarization, parallel task
subagents, 400, 408, 503 and timeout failures, the recursion limit,
partial reports and explicit zeros. Separate ChatOpenAI.with_retry
tests cover 429 then success and 503 then success. The existing usage tests
now produce their messages through a LangChain chat-model call so the
callbacks fire.

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

This comment was marked as outdated.

@mnajafian-nv mnajafian-nv self-assigned this Oct 19, 2026
coderabbitai[bot]

This comment was marked as resolved.

@github-actions

Copy link
Copy Markdown

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
LangChain carries cache reads per model call in
usage_metadata.input_token_details.cache_read, and OpenAI raw usage
carries them in prompt_tokens_details.cached_tokens, but the adapter
never reported cached_input_tokens.

Sum cached input per call with the same rule as the other counts: one
call without a cache count leaves the sum unknown, and a reported 0
stays 0. Raw OpenAI usage is read first because ChatOpenAI renames the
cache key for priority and flex service tiers.

Report input_tokens_include_cache=True whenever input tokens are known.
LangChain's UsageMetadata.input_tokens counts every input token type;
langchain-anthropic adds cache reads and writes back into it, and
OpenAI's prompt_tokens already includes cached tokens.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
The adapter drops a model call that the provider rejects with a 4xx
status other than 408, because the provider spent no tokens on it. When
every call was rejected, or no call started, the call list was empty and
the usage was reported as unknown, although the invocation provably
spent no tokens. The adapter now reports 0 for every token count and the
cost in that case.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Deep Agents routes a subagent spec with graph_id to its async subagent
middleware, which starts and updates tasks on a remote Agent Protocol
server. Those model calls never reach the adapter's callbacks, so the
reported counts left them out while appearing complete. The adapter
blocked such subagents only when a tools policy was configured.

When the configured subagents include one with graph_id, the adapter now
reports the invocation's token counts and cost as unknown. The local
sums stay in output.usage.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
The usage notes said that after a 5xx response, timeout, or cancelled
call the provider may have done work. The reason the counts stay unknown
is narrower: the provider may have billed the call without reporting
usage. The README, the recorder docstring, and a test comment now say
that.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Apply ruff format to the check added in the previous commit.

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

The adapter reports 0 when its callbacks observed no billed model call.
That 0 holds for the invocation only when every model call runs through
those callbacks. Deep Agents runs declarative subagents and summarization
inside the graph, where the run config's callbacks reach them, but an
Agent Protocol subagent (graph_id) runs on a remote server that the
callbacks never see. The adapter already reports unknown usage for a
runtime with such a subagent; this test pins that for an invocation
whose only observed call was rejected, and the docstring and README
state the condition.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
@mnajafian-nv
mnajafian-nv marked this pull request as draft October 20, 2026 19:53
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Separate complete invocation totals from available observations and preserve accounting gaps. Add the first collector in Deep Agents.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
@mnajafian-nv mnajafian-nv changed the title fix: count every Deep Agents model call in token usage feat: report complete and observed model usage Oct 22, 2026
coderabbitai[bot]

This comment was marked as resolved.

@mnajafian-nv
mnajafian-nv marked this pull request as ready for review October 23, 2026 16:23
Use the system Bash path so macOS tests exercise Bash 3.2 even when PATH contains a newer Bash. This keeps the empty-array regression meaningful in CI.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
@mnajafian-nv mnajafian-nv changed the title feat: report complete and observed model usage feat: add shared model usage accounting Oct 26, 2026
@mnajafian-nv

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
@mnajafian-nv
mnajafian-nv marked this pull request as draft October 27, 2026 03:21

@AjayThorve AjayThorve left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the shared contract, independent normalization dimensions, schema parity, runtime failure preservation and Deep Agents invocation ownership at 1007bcb. Local validation: Rust workspace 316 passed; native Python build; 307 focused accounting/Deep Agents/contract checks; Python suite 2,621 passed, 274 skipped with tests/e2e excluded; TypeScript contract/package tests passed. Current product CI and required DCO are green. The NVSkills request job lacks validation evidence and still needs maintainer follow-through. Draft status is unchanged. This is accounting approval, not a B200 task qualification: the retained Deep Agents filesystem/timeout failures remain separate.

@mnajafian-nv

Copy link
Copy Markdown
Contributor Author

The focused Harbor fixes are merged, and the broader usage needs are documented for team review. I’m closing this contract branch so future implementation can follow a shared design decision.

This branch was previously deployed

1 inactive deployment
fern — 1007bcb6 Deployed Oct 27, 2026 by copy-pr-bot[bot] via Preview docs #2614
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