@@ -92,9 +92,18 @@ def __post_init__(self) -> None:
9292 # (cached_tokens, reasoning_tokens), and the OpenAI SDK's generated
9393 # code can bypass Pydantic validation (e.g., via model_construct),
9494 # allowing None values. We normalize these to 0 to prevent TypeErrors.
95- if self .input_tokens_details .cached_tokens is None :
95+ input_details_none = self .input_tokens_details is None
96+ input_cached_none = (
97+ not input_details_none and self .input_tokens_details .cached_tokens is None
98+ )
99+ if input_details_none or input_cached_none :
96100 self .input_tokens_details = InputTokensDetails (cached_tokens = 0 )
97- if self .output_tokens_details .reasoning_tokens is None :
101+
102+ output_details_none = self .output_tokens_details is None
103+ output_reasoning_none = (
104+ not output_details_none and self .output_tokens_details .reasoning_tokens is None
105+ )
106+ if output_details_none or output_reasoning_none :
98107 self .output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 )
99108
100109 def add (self , other : Usage ) -> None :
@@ -109,25 +118,46 @@ def add(self, other: Usage) -> None:
109118 self .input_tokens += other .input_tokens if other .input_tokens else 0
110119 self .output_tokens += other .output_tokens if other .output_tokens else 0
111120 self .total_tokens += other .total_tokens if other .total_tokens else 0
112- self .input_tokens_details = InputTokensDetails (
113- cached_tokens = self .input_tokens_details .cached_tokens
114- + other .input_tokens_details .cached_tokens
121+
122+ # Null guards for nested token details (other may bypass validation via model_construct)
123+ other_cached = (
124+ other .input_tokens_details .cached_tokens
125+ if other .input_tokens_details and other .input_tokens_details .cached_tokens
126+ else 0
115127 )
128+ other_reasoning = (
129+ other .output_tokens_details .reasoning_tokens
130+ if other .output_tokens_details and other .output_tokens_details .reasoning_tokens
131+ else 0
132+ )
133+ self_cached = (
134+ self .input_tokens_details .cached_tokens
135+ if self .input_tokens_details and self .input_tokens_details .cached_tokens
136+ else 0
137+ )
138+ self_reasoning = (
139+ self .output_tokens_details .reasoning_tokens
140+ if self .output_tokens_details and self .output_tokens_details .reasoning_tokens
141+ else 0
142+ )
143+
144+ self .input_tokens_details = InputTokensDetails (cached_tokens = self_cached + other_cached )
116145
117146 self .output_tokens_details = OutputTokensDetails (
118- reasoning_tokens = self .output_tokens_details .reasoning_tokens
119- + other .output_tokens_details .reasoning_tokens
147+ reasoning_tokens = self_reasoning + other_reasoning
120148 )
121149
122150 # Automatically preserve request_usage_entries.
123151 # If the other Usage represents a single request with tokens, record it.
124152 if other .requests == 1 and other .total_tokens > 0 :
153+ input_details = other .input_tokens_details or InputTokensDetails (cached_tokens = 0 )
154+ output_details = other .output_tokens_details or OutputTokensDetails (reasoning_tokens = 0 )
125155 request_usage = RequestUsage (
126156 input_tokens = other .input_tokens ,
127157 output_tokens = other .output_tokens ,
128158 total_tokens = other .total_tokens ,
129- input_tokens_details = other . input_tokens_details ,
130- output_tokens_details = other . output_tokens_details ,
159+ input_tokens_details = input_details ,
160+ output_tokens_details = output_details ,
131161 )
132162 self .request_usage_entries .append (request_usage )
133163 elif other .request_usage_entries :
0 commit comments