Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/agents/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,13 @@ def add(self, other: Usage) -> None:
)

# Automatically preserve request_usage_entries.
# If the other Usage represents a single request with tokens, record it.
if other.requests == 1 and other.total_tokens > 0:
# If the other Usage already has individual request breakdowns, merge them
# (this preserves nested token details that would otherwise be discarded
# when synthesizing an entry from only the top-level fields).
if other.request_usage_entries:
self.request_usage_entries.extend(other.request_usage_entries)
elif other.requests == 1 and other.total_tokens > 0:
# Otherwise, if the other Usage represents a single request with tokens, record it.
input_details = other.input_tokens_details or InputTokensDetails(cached_tokens=0)
output_details = other.output_tokens_details or OutputTokensDetails(reasoning_tokens=0)
request_usage = RequestUsage(
Expand All @@ -208,9 +213,6 @@ def add(self, other: Usage) -> None:
output_tokens_details=output_details,
)
self.request_usage_entries.append(request_usage)
elif other.request_usage_entries:
# If the other Usage already has individual request breakdowns, merge them.
self.request_usage_entries.extend(other.request_usage_entries)


def _serialize_usage_details(details: Any, default: dict[str, int]) -> dict[str, Any]:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,39 @@ def test_usage_add_with_pre_existing_request_usage_entries():
assert u1.request_usage_entries[1].input_tokens == 50


def test_usage_add_preserves_existing_entries_when_top_level_also_set():
"""When `other` has both top-level single-request fields AND pre-populated
`request_usage_entries`, the existing entries (which carry the authoritative
nested token details) must not be discarded in favor of a synthesized entry
built from only the top-level fields.
"""
u1 = Usage()
u2 = Usage(
requests=1,
input_tokens=100,
output_tokens=50,
total_tokens=150,
request_usage_entries=[
RequestUsage(
input_tokens=100,
output_tokens=50,
total_tokens=150,
input_tokens_details=InputTokensDetails(cached_tokens=10),
output_tokens_details=OutputTokensDetails(reasoning_tokens=5),
)
],
)

u1.add(u2)

# The pre-populated entry must be preserved — including its nested details —
# rather than being replaced by a synthesized entry with zeroed-out details.
assert len(u1.request_usage_entries) == 1
entry = u1.request_usage_entries[0]
assert entry.input_tokens_details.cached_tokens == 10
assert entry.output_tokens_details.reasoning_tokens == 5


def test_usage_request_usage_entries_default_empty():
"""Test that request_usage_entries defaults to an empty list."""
u = Usage()
Expand Down
Loading