Skip to content

Commit b5480cb

Browse files
author
Aditya Singh
committed
fix(usage): copy RequestUsage entries instead of mutating other in place
The elif branch in Usage.add() was mutating agent_name/model_name directly on the RequestUsage objects inside other.request_usage_entries, bypassing the non-overwrite guard and causing silent mis-attribution when the same Usage instance was added to multiple aggregators. Fix: create a new RequestUsage copy for each entry with the annotation applied, leaving the originals unchanged. Adds a regression test that verifies the original entries are not mutated.
1 parent 9d73f17 commit b5480cb

File tree

2 files changed

+54
-6
lines changed

2 files changed

+54
-6
lines changed

src/agents/usage.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,20 @@ def add(
238238
self.request_usage_entries.append(request_usage)
239239
elif other.request_usage_entries:
240240
# If the other Usage already has individual request breakdowns, merge them.
241-
# Apply agent_name/model_name to entries that don't already have them set.
241+
# Apply agent_name/model_name to entries that don't already have them set,
242+
# but copy each entry rather than mutating the original objects in place
243+
# to avoid silent mis-attribution when the same Usage is added multiple times.
242244
for entry in other.request_usage_entries:
243-
if agent_name is not None and entry.agent_name is None:
244-
entry.agent_name = agent_name
245-
if model_name is not None and entry.model_name is None:
246-
entry.model_name = model_name
247-
self.request_usage_entries.extend(other.request_usage_entries)
245+
annotated_entry = RequestUsage(
246+
input_tokens=entry.input_tokens,
247+
output_tokens=entry.output_tokens,
248+
total_tokens=entry.total_tokens,
249+
input_tokens_details=entry.input_tokens_details,
250+
output_tokens_details=entry.output_tokens_details,
251+
agent_name=agent_name if (agent_name is not None and entry.agent_name is None) else entry.agent_name,
252+
model_name=model_name if (model_name is not None and entry.model_name is None) else entry.model_name,
253+
)
254+
self.request_usage_entries.append(annotated_entry)
248255

249256

250257
def _serialize_usage_details(details: Any, default: dict[str, int]) -> dict[str, Any]:

tests/test_usage.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,44 @@ async def test_multi_agent_run_attributes_usage_to_correct_agents():
610610

611611
specialist_entry = next(e for e in all_entries if e.agent_name == "Specialist Agent")
612612
assert specialist_entry.input_tokens == 200
613+
614+
615+
def test_add_does_not_mutate_other_entries() -> None:
616+
"""Adding a Usage with existing request_usage_entries must not mutate the original entries.
617+
618+
Previously, the elif branch in Usage.add() called entry.agent_name = ... directly on
619+
the objects inside other.request_usage_entries, causing silent mis-attribution when the
620+
same Usage object was re-used or added to multiple aggregators.
621+
"""
622+
from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
623+
624+
source_entry = RequestUsage(
625+
input_tokens=50,
626+
output_tokens=25,
627+
total_tokens=75,
628+
input_tokens_details=InputTokensDetails(cached_tokens=0),
629+
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
630+
agent_name=None,
631+
model_name=None,
632+
)
633+
634+
# Build a Usage that already has request_usage_entries (requests != 1 path)
635+
other = Usage(
636+
requests=2,
637+
input_tokens=50,
638+
output_tokens=25,
639+
total_tokens=75,
640+
request_usage_entries=[source_entry],
641+
)
642+
643+
agg = Usage()
644+
agg.add(other, agent_name="MyAgent", model_name="gpt-4o")
645+
646+
# The aggregator should have a copy with the annotation applied
647+
assert len(agg.request_usage_entries) == 1
648+
assert agg.request_usage_entries[0].agent_name == "MyAgent"
649+
assert agg.request_usage_entries[0].model_name == "gpt-4o"
650+
651+
# The original entry must NOT be mutated
652+
assert source_entry.agent_name is None, "Original entry was mutated!"
653+
assert source_entry.model_name is None, "Original entry was mutated!"

0 commit comments

Comments
 (0)