@@ -41,8 +41,14 @@ def _make_completion(
4141 finish_reason : str = "stop" ,
4242 prompt_tokens : int = 10 ,
4343 completion_tokens : int = 20 ,
44+ cached_tokens : int | None = None ,
4445) -> ChatCompletion :
4546 """Create a mock ChatCompletion response."""
47+ prompt_tokens_details = None
48+ if cached_tokens is not None :
49+ from openai .types .completion_usage import PromptTokensDetails
50+
51+ prompt_tokens_details = PromptTokensDetails (cached_tokens = cached_tokens )
4652 return ChatCompletion (
4753 id = "chatcmpl-test" ,
4854 choices = [
@@ -63,6 +69,7 @@ def _make_completion(
6369 prompt_tokens = prompt_tokens ,
6470 completion_tokens = completion_tokens ,
6571 total_tokens = prompt_tokens + completion_tokens ,
72+ prompt_tokens_details = prompt_tokens_details ,
6673 ),
6774 )
6875
@@ -456,6 +463,31 @@ def test_usage_captured(self) -> None:
456463 assert result .usage .input_tokens == 50
457464 assert result .usage .output_tokens == 100
458465
466+ def test_cached_tokens_are_subtracted_from_input (self ) -> None :
467+ """OpenAI reports cached tokens as a SUBSET of prompt_tokens;
468+ `UsageParam` consumers expect disjoint fields (Anthropic style).
469+ Without the subtraction, cached tokens are counted and billed twice
470+ (statistics callback, reporting)."""
471+ completion = _make_completion (
472+ content = "ok" ,
473+ prompt_tokens = 1000 ,
474+ completion_tokens = 50 ,
475+ cached_tokens = 400 ,
476+ )
477+ result = _from_openai_response (completion )
478+ assert result .usage is not None
479+ assert result .usage .input_tokens == 600 # 1000 - 400 cached
480+ assert result .usage .cache_read_input_tokens == 400
481+ assert result .usage .output_tokens == 50
482+
483+ def test_cached_tokens_never_drive_input_negative (self ) -> None :
484+ completion = _make_completion (
485+ content = "ok" , prompt_tokens = 100 , completion_tokens = 5 , cached_tokens = 150
486+ )
487+ result = _from_openai_response (completion )
488+ assert result .usage is not None
489+ assert result .usage .input_tokens == 0
490+
459491
460492class TestOpenAIMessagesApi :
461493 def test_create_message_delegates_to_client (self ) -> None :
0 commit comments