From 376530d64a23b5461b7033c4c3bacb3f73f022a9 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Tue, 4 Nov 2025 09:47:05 +0800 Subject: [PATCH] fix(usage): Normalize None token details on Usage initialization Some providers don't populate optional token detail fields, resulting in None values that bypass Pydantic validation. This fix adds a __post_init__ method to normalize None to 0 for cached_tokens and reasoning_tokens on any Usage object creation. This defensive approach handles the issue at the boundary we control, regardless of how providers construct their response objects. Signed-off-by: Adrian Cole --- src/agents/usage.py | 10 ++++++++++ tests/test_usage.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/agents/usage.py b/src/agents/usage.py index a2b41529e..a10778123 100644 --- a/src/agents/usage.py +++ b/src/agents/usage.py @@ -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. diff --git a/tests/test_usage.py b/tests/test_usage.py index d0e674111..9d89cc750 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -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