From db23e717c75d2c3d9a5a17ce9d40df808af401b5 Mon Sep 17 00:00:00 2001 From: drexkooo Date: Tue, 17 Mar 2026 13:53:18 +0100 Subject: [PATCH] fix: populate cache_creation_tokens for cost-calc path (OpenRouter) The previous fix correctly set _cache_creation_input_tokens (private field) from prompt_tokens_details.cache_write_tokens, which unblocked the streaming adapter and Langfuse logging paths. However the cost calculator in llm_cost_calc/utils.py (_parse_prompt_tokens_details, line 442) reads prompt_tokens_details.cache_creation_tokens (the public wrapper field), not the private attribute. The Anthropic native mapping already sets both fields; the OpenRouter block was inconsistent in only setting the private one. This commit adds _ptd.cache_creation_tokens = _writes so both paths see the correct value, and extends the unit test to assert the public field is populated (covering the cost-calculation path that was previously untested). Made-with: Cursor --- litellm/types/utils.py | 15 +++++++++++---- tests/test_litellm/types/test_types_utils.py | 10 +++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 200e108538e..e2c04fa4f2f 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1662,10 +1662,13 @@ class Usage(SafeAttributeModel, CompletionUsage): ## OPENROUTER / OPENAI FORMAT MAPPING ## # OpenRouter (and other OpenAI-compatible providers) return cache token counts # in prompt_tokens_details rather than as top-level Anthropic fields. - # Populate the private Anthropic-style fields from prompt_tokens_details when - # the explicit Anthropic params (cache_creation_input_tokens, - # cache_read_input_tokens) were not provided, so that downstream consumers - # (cost calculators, streaming adapters, logging hooks) see the correct values. + # Populate both the private Anthropic-style fields AND the public + # prompt_tokens_details fields from prompt_tokens_details when the explicit + # Anthropic params (cache_creation_input_tokens, cache_read_input_tokens) + # were not provided, so that ALL downstream consumers see the correct values: + # - streaming adapter / Langfuse: reads _cache_*_input_tokens (private) + # - cost calculator (_parse_prompt_tokens_details): reads + # prompt_tokens_details.cache_creation_tokens (public field on wrapper) _ptd = getattr(self, "prompt_tokens_details", None) if _ptd is not None: if not self._cache_read_input_tokens: @@ -1676,6 +1679,10 @@ class Usage(SafeAttributeModel, CompletionUsage): _writes = getattr(_ptd, "cache_write_tokens", 0) or 0 if _writes > 0: self._cache_creation_input_tokens = _writes + # Also populate the public field that the cost calculator reads + # (_parse_prompt_tokens_details reads cache_creation_tokens, not + # the private _cache_creation_input_tokens attribute) + _ptd.cache_creation_tokens = _writes for k, v in params.items(): setattr(self, k, v) diff --git a/tests/test_litellm/types/test_types_utils.py b/tests/test_litellm/types/test_types_utils.py index 105126ea9df..e63942b7555 100644 --- a/tests/test_litellm/types/test_types_utils.py +++ b/tests/test_litellm/types/test_types_utils.py @@ -323,10 +323,16 @@ def test_usage_openrouter_cache_tokens_from_prompt_tokens_details(): ), ) - # Private Anthropic-style fields must be populated from prompt_tokens_details + # Private Anthropic-style fields (streaming adapter / Langfuse path) assert usage._cache_read_input_tokens == 17000 assert usage._cache_creation_input_tokens == 400 + # Public prompt_tokens_details fields (cost-calculator path). + # _parse_prompt_tokens_details in llm_cost_calc/utils.py reads + # prompt_tokens_details.cache_creation_tokens — it must be populated too. + assert usage.prompt_tokens_details is not None + assert usage.prompt_tokens_details.cache_creation_tokens == 400 # cost-calc path + # When Anthropic native params are provided they must take precedence over # prompt_tokens_details so existing callers are not broken. usage_native = Usage( @@ -353,3 +359,5 @@ def test_usage_openrouter_cache_tokens_from_prompt_tokens_details(): dumped = usage_no_writes.model_dump() ptd = dumped.get("prompt_tokens_details", {}) assert "cache_write_tokens" not in ptd + # cache_creation_tokens should also be absent when no writes were reported + assert "cache_creation_tokens" not in ptd