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
This commit is contained in:
drexkooo 2026-03-17 13:53:18 +01:00
parent af4ea96f67
commit db23e717c7
2 changed files with 20 additions and 5 deletions

View file

@ -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)

View file

@ -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