Repository navigation
fix(aviary): sum token usage across every model call in an episode - #4716
Merged
Merged
Conversation
AviaryAgent.responses() built its returned response from the last model call, so response.usage (and the rollout input/output/total token fields reward_profile copies from it) counted only the final call of a multi-step episode. Accumulate usage with accumulate_response_usage over every validated model response, including the post-terminal final-response call. toolsandbox_agent copied the same loop and had the same defect; apply the same accumulator there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: qiaochuz <qiaochuz@nvidia.com>
Contributor
Author
|
/ok to test e64106a |
This was referenced Oct 19, 2026
cmunley1
approved these changes
Oct 20, 2026
Contributor
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39393435484 |
Contributor
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39393828662 |
zyzhou5
pushed a commit
to zyzhou5/Gym
that referenced
this pull request
Oct 22, 2026
…ts (NVIDIA-NeMo#4753) proof_refinement_agent and three conversational_tool_use generation agents (domain, policy_tool, scenario) returned a rollout response whose usage came from one model call: the last correction turn, the last domain phase, the final tools completion, or the one completion chosen as the scenario artifact. reward_profile copies response.usage into the rollout token fields, so those under-counted every multi-call rollout. Sum every policy-model call's usage with accumulate_response_usage, as NVIDIA-NeMo#4716 does for aviary_agent and toolsandbox_agent. Chat agents convert each completion's usage with a new chat_completion_usage_to_response_usage helper, which chat_completion_to_response now uses too. Scenario generation also counts completions whose text failed to parse; policy_tool_generation leaves its judge calls out, as rollout usage covers the policy model. proof_refinement_agent correction turns also copied max_tokens, which Responses requests don't have, so the caller's max_output_tokens cap was dropped on every turn after the first. Copy max_output_tokens instead. Follow-up to NVBug 6900411. Signed-off-by: qiaochuz <qiaochuz@nvidia.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
What does this PR do?
Makes
aviary_agentandtoolsandbox_agentreport the token usage of the whole episode. Before this change,response.usageheld only the last model call's counts. Every multi-step rollout therefore under-reportedinput_tokens/output_tokens/total_tokens, and so did every aggregate built from them.Fixes NVBug 6900411.
Closes #4504
Cause
AviaryAgent.responses()loops model call →/step→ model call. It reassignsmodel_responseeach turn andreturns
AviaryNeMoGymResponse.model_validate(model_response.model_dump() | output_overrides).output_overridesnever set
usage, so the last call's usage wins. This construction dates from the original integration (#55); #590moved the overrides into
output_overridesand keptmodel_response.model_dump()as the base. #3805 added apost-terminal model call (
generate_final_response_after_done), and that call was dropped the same way.toolsandbox_agent(#1949) copied the same loop and has the same defect.nemo_gym/reward_profile.pycopiesresponse.usageinto each rollout's numeric fields, so the per-rollout tokencounts and the
mean/,median/,max/...token metrics were all under-counted. They are real counts from onecall, so nothing looked wrong. The
rollout_token_count_mismatchhealth check would flag the mismatch, but onlywhen model-call capture is on.
Fix
accumulate_response_usage(usage, model_response.usage)after each validated modelresponse and put
"usage": usageinoutput_overrides. The rest of the returned response (id, status, model)still comes from the last call.
simple_agentandgymnasium_agentalready use. A call that fails or fails validationbreaks before it is counted, as before. A call with nousage(a provider that omits it,responses_converter.pyleaves itNone) is skipped, which is the helper's policy.critpt_agentinsteadreports
nullwhen any call lacks usage. I kept the shared behaviour, to match the other loop agents.test_responses_multi_step, including cached and reasoning details, and onboth calls of the parametrized post-terminal test (aviary). A new two-turn test for toolsandbox. Each fails on
mainwith the last call's numbers.Before the fix
Gym
b3103ea8a(currentmain):Live, with model-call capture on (QA
model_call_capture_agent_backends, aviary_gsm8k, forced multi-call episode,h100).
response.usageequals the last captured call alone:After the fix
Gym
e64106a7a(this branch), same commands:Validation
gym env test +entrypoint=responses_api_agents/aviary_agent +should_validate_data=true: 13 passed;same for
toolsandbox_agent: 14 passed. Both new assertions fail onmain.pre-commit run --all-files: passed.model_call_capture_agent_backends(h100, GPU lane, inference provider + local Qwen3-8B vLLM), witha QA-side assertion that
response.usageequals the sum of the captured per-call counts:main0/2 → 2/2.mainand fix). Toolsandbox: itsregistered QA case is blocked on README-model access, so I ran a scratch smoke of the shipped toolsandbox config
on
Nemotron-3-Nano-30B-A3Bwith capture on,--limit 2. It compared usage with the agent's captured Responsescalls; the user-simulator's chat calls are excluded. Toolsandbox has no committed integration check beyond its
unit tests.
Compatibility
input_tokens/output_tokens/total_tokensand their aggregates now cover every model call. Single-step episodes are unchanged. Earliernumbers for these environments under-count and are not comparable.
rollout-evidence.mdx: usage is the sum over model responses thatfinished). It also removes a false
rollout_token_count_mismatchfor multi-call rollouts when capture is on.response.usagefrom Gym rollouts, so training is unaffected.aviary_agent/app.py; neither touches usage.🤖 Generated with Claude Code