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
10 changes: 10 additions & 0 deletions src/agents/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ class Usage:
cost calculation or context window management.
"""

def __post_init__(self) -> None:
# Some providers don't populate optional token detail fields
# (cached_tokens, reasoning_tokens), and the OpenAI SDK's generated
# code can bypass Pydantic validation (e.g., via model_construct),
# allowing None values. We normalize these to 0 to prevent TypeErrors.
if self.input_tokens_details.cached_tokens is None:
self.input_tokens_details = InputTokensDetails(cached_tokens=0)
if self.output_tokens_details.reasoning_tokens is None:
self.output_tokens_details = OutputTokensDetails(reasoning_tokens=0)

def add(self, other: "Usage") -> None:
"""Add another Usage object to this one, aggregating all fields.

Expand Down
22 changes: 22 additions & 0 deletions tests/test_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,25 @@ def test_anthropic_cost_calculation_scenario():
for req in usage.request_usage_entries:
assert req.input_tokens < 200_000
assert req.output_tokens < 200_000


def test_usage_normalizes_none_token_details():
# Some providers don't populate optional fields, resulting in None values
input_details = InputTokensDetails(cached_tokens=0)
input_details.__dict__["cached_tokens"] = None

output_details = OutputTokensDetails(reasoning_tokens=0)
output_details.__dict__["reasoning_tokens"] = None

usage = Usage(
requests=1,
input_tokens=100,
input_tokens_details=input_details,
output_tokens=50,
output_tokens_details=output_details,
total_tokens=150,
)

# __post_init__ should normalize None to 0
assert usage.input_tokens_details.cached_tokens == 0
assert usage.output_tokens_details.reasoning_tokens == 0