@@ -169,6 +169,24 @@ def test_gemini_openai_compat_format(self):
169169 assert usage .output_tokens == 9
170170 assert usage .reasoning_tokens == 26
171171
172+ def test_gemini_total_tokens_includes_thinking (self ):
173+ """Gemini total_tokens includes thoughts_tokens (differs from input+output)."""
174+ usage = build_token_usage (
175+ {
176+ "prompt_tokens" : 7 ,
177+ "completion_tokens" : 10 ,
178+ "thoughts_tokens" : 21 ,
179+ "total_tokens" : 38 ,
180+ },
181+ [],
182+ "" ,
183+ "gemini-2.5-flash" ,
184+ )
185+ assert usage .input_tokens == 7
186+ assert usage .output_tokens == 10
187+ assert usage .reasoning_tokens == 21
188+ assert usage .total_tokens == 38 # API total includes thinking
189+
172190 def test_gemini_native_format (self ):
173191 """Gemini native: camelCase fields from usageMetadata."""
174192 usage = build_token_usage (
@@ -203,7 +221,7 @@ def test_doubao_format(self):
203221 assert usage .output_tokens == 27
204222
205223 def test_cached_tokens (self ):
206- """Extract cached_tokens from prompt_tokens_details."""
224+ """Extract cached_input_tokens from prompt_tokens_details."""
207225 usage = build_token_usage (
208226 {
209227 "prompt_tokens" : 500 ,
@@ -214,7 +232,59 @@ def test_cached_tokens(self):
214232 "" ,
215233 "gpt-4o" ,
216234 )
217- assert usage .cached_tokens == 300
235+ assert usage .cached_input_tokens == 300
236+ assert usage .cache_creation_input_tokens == 0
237+
238+ def test_anthropic_format (self ):
239+ """Anthropic: top-level cache_read/cache_creation, no prompt_tokens_details."""
240+ usage = build_token_usage (
241+ {
242+ "input_tokens" : 50 ,
243+ "output_tokens" : 100 ,
244+ "cache_read_input_tokens" : 100000 ,
245+ "cache_creation_input_tokens" : 248 ,
246+ },
247+ [],
248+ "" ,
249+ "claude-sonnet-4-20250514" ,
250+ )
251+ assert usage .input_tokens == 50
252+ assert usage .output_tokens == 100
253+ assert usage .cached_input_tokens == 100000
254+ assert usage .cache_creation_input_tokens == 248
255+
256+ def test_deepseek_cache_format (self ):
257+ """DeepSeek: top-level prompt_cache_hit_tokens."""
258+ usage = build_token_usage (
259+ {
260+ "prompt_tokens" : 405 ,
261+ "total_tokens" : 415 ,
262+ "completion_tokens" : 10 ,
263+ "prompt_tokens_details" : None ,
264+ "prompt_cache_hit_tokens" : 380 ,
265+ "reasoning_tokens" : 42 ,
266+ },
267+ [],
268+ "" ,
269+ "deepseek-r1" ,
270+ )
271+ assert usage .cached_input_tokens == 380
272+ assert usage .cache_creation_input_tokens == 0
273+
274+ def test_gemini_native_cache_format (self ):
275+ """Gemini native: cachedContentTokenCount。"""
276+ usage = build_token_usage (
277+ {
278+ "promptTokenCount" : 200 ,
279+ "candidatesTokenCount" : 15 ,
280+ "thoughtsTokenCount" : 30 ,
281+ "cachedContentTokenCount" : 150 ,
282+ },
283+ [],
284+ "" ,
285+ "gemini-2.5-flash" ,
286+ )
287+ assert usage .cached_input_tokens == 150
218288
219289 def test_fallback_to_estimator (self ):
220290 usage = build_token_usage (
@@ -284,30 +354,38 @@ def test_multi_model_accumulation(self):
284354
285355 def test_cached_and_reasoning_accumulation (self ):
286356 req = MagicMock (shared_data = {})
287- aggregate_token_usage (
288- req ,
289- TokenUsage (
290- input_tokens = 100 ,
291- output_tokens = 50 ,
292- cached_tokens = 60 ,
293- reasoning_tokens = 20 ,
294- model_name = "deepseek-r1" ,
295- ),
357+ u1 = build_token_usage (
358+ {
359+ "prompt_tokens" : 100 ,
360+ "completion_tokens" : 50 ,
361+ "prompt_tokens_details" : {"cached_tokens" : 60 },
362+ "cache_creation_input_tokens" : 10 ,
363+ "reasoning_tokens" : 20 ,
364+ },
365+ [],
366+ "" ,
367+ "deepseek-r1" ,
296368 )
297- aggregate_token_usage (
298- req ,
299- TokenUsage (
300- input_tokens = 80 ,
301- output_tokens = 30 ,
302- cached_tokens = 40 ,
303- reasoning_tokens = 10 ,
304- model_name = "deepseek-r1" ,
305- ),
369+ u2 = build_token_usage (
370+ {
371+ "prompt_tokens" : 80 ,
372+ "completion_tokens" : 30 ,
373+ "prompt_tokens_details" : {"cached_tokens" : 40 },
374+ "cache_creation_input_tokens" : 5 ,
375+ "reasoning_tokens" : 10 ,
376+ },
377+ [],
378+ "" ,
379+ "deepseek-r1" ,
306380 )
381+ aggregate_token_usage (req , u1 )
382+ aggregate_token_usage (req , u2 )
307383 m = req .shared_data ["_metrics" ]["token_usage" ]
308- assert m ["cached_tokens" ] == 100
384+ assert m ["cached_input_tokens" ] == 100
385+ assert m ["cache_creation_input_tokens" ] == 15
309386 assert m ["reasoning_tokens" ] == 30
310- assert m ["by_model" ]["deepseek-r1" ]["cached_tokens" ] == 100
387+ assert m ["by_model" ]["deepseek-r1" ]["cached_input_tokens" ] == 100
388+ assert m ["by_model" ]["deepseek-r1" ]["cache_creation_input_tokens" ] == 15
311389 assert m ["by_model" ]["deepseek-r1" ]["reasoning_tokens" ] == 30
312390
313391 def test_disabled (self ):
@@ -347,3 +425,28 @@ async def test_aggregates_token_usage(self):
347425 assert isinstance (resp .extra ["usage" ], dict )
348426 assert resp .extra ["usage" ]["total_tokens" ] == 15
349427 assert req .shared_data ["_metrics" ]["token_usage" ]["total_tokens" ] == 15
428+
429+
430+ # ---------------------------------------------------------------------------
431+ # model_dump includes new cache fields
432+ # ---------------------------------------------------------------------------
433+
434+
435+ class TestModelDump :
436+ def test_includes_cached_input_tokens (self ):
437+ usage = TokenUsage (
438+ input_tokens = 100 ,
439+ output_tokens = 50 ,
440+ cached_input_tokens = 80 ,
441+ cache_creation_input_tokens = 10 ,
442+ model_name = "gpt-4o" ,
443+ )
444+ data = usage .model_dump ()
445+ assert data ["cached_input_tokens" ] == 80
446+ assert data ["cache_creation_input_tokens" ] == 10
447+
448+ def test_defaults_zero (self ):
449+ usage = TokenUsage (input_tokens = 100 , output_tokens = 50 )
450+ data = usage .model_dump ()
451+ assert data ["cached_input_tokens" ] == 0
452+ assert data ["cache_creation_input_tokens" ] == 0
0 commit comments