Skip to content

feat: report complete and observed model usage - #442

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

mnajafian-nv wants to merge 32 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 expose token counts with different cache conventions and different coverage. Some fill missing fields with zero or omit failed attempts. Adding those counters without shared rules can make incomplete runs appear cheaper.

Add a shared model-usage contract to NVIDIA NeMo Fabric and connect Deep Agents as its first collector. Results distinguish complete totals from available observations and explain which counts remain unknown. Users do not need recording or Relay to read this accounting.

Dependency: Merge after #476, which supplies the Relay startup compatibility changes. The diff against main includes unmerged prerequisites.

Details

Python and TypeScript helpers preserve reported fields, model identity and gaps. Core validates and normalizes that evidence into RunResult.model_usage. Input includes cache tokens; output includes reasoning tokens. Omitted values remain unknown, while a reported zero remains zero. Complete counts, observations and provider-reported cost remain separate. Per-model entries retain identities established by the source. An exhaustive empty model list cannot accompany positive token counts. Catalog pricing is added separately in #478.

Deep Agents combines model callbacks with passive HTTP-attempt observations. It covers adapter-created ChatOpenAI clients, including summarization and in-process subagents using the observed clients. Custom model overrides, unsupported clients, actual remote dispatch and unfinished work leave the relevant coverage unverified. Graph-message subtotals remain labeled diagnostics in output.usage.

Each model call stays with the invocation that started it. Accounting freezes when that invocation returns. Later reports cannot move into another invocation. Observations are not guaranteed lower bounds. A non-model failure preserves settled usage. Empty source coverage does not establish a measured zero. If validly shaped evidence cannot be normalized, retain the execution result and raw evidence with an accounting diagnostic; schema-invalid extensions still fail validation.

Consumers with older strict result decoders must upgrade before reading the new model_usage field.

Validation

The TypeScript adapter checks remain blocked by the shared dependency audit tracked in #484.

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

Where should the reviewer start?

Start with crates/fabric-core/src/model_usage.rs and usage_normalization.rs for complete versus observed values. Then read Deep Agents usage.py and the shared Python, Rust and TypeScript contract vectors.

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

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in 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

Walkthrough

The change adds invocation-scoped model usage accounting across adapters, shared contracts, Fabric Core, and SDK results. It preserves unknown counts, records gaps, freezes evidence at invocation return, and exposes normalized usage through RunResult.model_usage. Relay readiness and OpenShell launcher argument handling also change.

Changes

Usage accounting

Layer / File(s) Summary
Evidence contracts and reducers
adapter-contract/python/..., adapter-contract/typescript/..., schemas/extensions/usage-accounting.schema.json, tests/fixtures/usage_accounting/*
Adds typed usage evidence and Python and TypeScript reducers. The reducers separate observed from complete totals, retain reported zeros, and represent missing or unverified evidence with gaps.
Deep Agents collection
adapters/python/deepagents/src/nemo_fabric_adapters/deepagents/{adapter.py,usage.py}, adapters/python/deepagents/deepagents.fabric-adapter.json
Records model callbacks and HTTP attempts within an invocation, including cache, remote activity, retries, streaming, and failures. Adapter results use accounted usage; graph-message usage remains a separate diagnostic.
Core and SDK usage results
crates/fabric-core/src/{usage_accounting.rs,usage_normalization.rs,model_usage.rs,runtime.rs}, sdk/python/nemo-fabric-runtime/src/nemo_fabric/{types.py,accounting.py}
Validates and normalizes adapter evidence, adds optional RunResult.model_usage, and provides summarize_usage against an explicit invocation inventory.
Schemas, documentation, and validation
schemas/*, adapter-contract/typescript/*, docs/*, skills/*, tests/*
Adds usage schemas, API references, adapter guidance, shared conformance fixtures, and tests for accounting, normalization, runtime behavior, and SDK summaries.

Runtime operations

Layer / File(s) Summary
Relay readiness
adapters/python/common/src/nemo_fabric_adapters/common/relay_gateway.py, tests/adapters/test_adapters_common_relay_gateway.py, adapters/python/{claude,codex,common}/README.md
Changes the default readiness budget from 10 to 30 seconds. Each health request and sleep is bounded by the remaining deadline.
OpenShell argument handling
examples/openshell/run-demo.sh, tests/scripts/test_openshell_launcher.py
Guards empty environment-argument arrays under Bash 3.2 with set -u and tests supplied and default argument combinations.

Priority: Normal

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

Merge Risk: Low · up to 63082

Some consumers may lose top-level token counts, and valid SDK results can raise errors when optional metadata is accessed. These are bounded compatibility risks requiring owner awareness.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 21.03% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 233 functions across 39 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title "feat: report complete and observed model usage" follows Conventional Commits (type feat, lowercase, imperative, no trailing period, 46 characters). It matches the main change: shared invoca…
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.
Description check Passed The description covers the overview, implementation details, validation, reviewer starting points, and the related-issues and checklist items. It explains the shared model-usage contract, the Deep Age…
Full details: Title check

Explanation

The title "feat: report complete and observed model usage" follows Conventional Commits (type feat, lowercase, imperative, no trailing period, 46 characters). It matches the main change: shared invocation accounting that separates complete token counts from observations, surfaced as RunResult.model_usage.

Full details: Description check

Explanation

The description covers the overview, implementation details, validation, reviewer starting points, and the related-issues and checklist items. It explains the shared model-usage contract, the Deep Agents collector, the Core normalization into RunResult.model_usage, and the cache and reasoning conventions. It also states the compatibility impact for strict older decoders and the dependency on #476. The Related Issues line reads 'Relates to: none', which does not follow the template's 'Closes / Fixes / Resolves / Relates to #' format. The Validation section names test coverage, but it does not clearly state which checks passed. The objectives text reports the validation results, so the description is mostly complete.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
🧪 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.

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

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>
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 ready for review October 23, 2026 09:28
@mnajafian-nv
mnajafian-nv requested a review from a team as a code owner October 23, 2026 09:28
@mnajafian-nv
mnajafian-nv marked this pull request as draft October 23, 2026 15:53
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
@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>

This branch was successfully deployed

1 active deployment
fern — a1d55279 Deployed Oct 26, 2026 by copy-pr-bot[bot] via Preview docs #2491
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