fix: cleanup cache_write_tokens when None + add unit test

- PromptTokensDetailsWrapper.__init__: delete cache_write_tokens when
  None, matching the cleanup pattern used by all other optional extension
  fields (character_count, image_count, web_search_requests, etc.) so
  it doesn't appear as null in model_dump() for non-OpenRouter callers
- tests/test_litellm/types/test_types_utils.py: add
  test_usage_openrouter_cache_tokens_from_prompt_tokens_details covering
  the happy path (cached_tokens + cache_write_tokens → private fields),
  Anthropic-native precedence, and absence of cache_write_tokens in
  serialised output when not provided

Made-with: Cursor
This commit is contained in:
drexkooo 2026-03-17 02:36:00 +01:00
parent e44128c68c
commit af4ea96f67
2 changed files with 51 additions and 0 deletions

View file

@ -1493,6 +1493,8 @@ class PromptTokensDetailsWrapper(
del self.cache_creation_tokens
if self.cache_creation_token_details is None:
del self.cache_creation_token_details
if self.cache_write_tokens is None:
del self.cache_write_tokens
class ServerToolUse(BaseModel):

View file

@ -304,3 +304,52 @@ def test_delta_maps_reasoning_to_reasoning_content():
# When neither is present, reasoning_content is not set (OpenAI spec)
delta4 = Delta(content="hello")
assert not hasattr(delta4, "reasoning_content")
def test_usage_openrouter_cache_tokens_from_prompt_tokens_details():
"""OpenRouter returns cache token counts in prompt_tokens_details (OpenAI format).
Usage.__init__ must map them to the Anthropic-style private fields so that cost
calculations and the Anthropic pass-through streaming adapter see correct values."""
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
# Simulate what OpenRouter sends in the final streaming chunk
usage = Usage(
prompt_tokens=18500,
completion_tokens=120,
total_tokens=18620,
prompt_tokens_details=PromptTokensDetailsWrapper(
cached_tokens=17000,
cache_write_tokens=400,
),
)
# Private Anthropic-style fields must be populated from prompt_tokens_details
assert usage._cache_read_input_tokens == 17000
assert usage._cache_creation_input_tokens == 400
# When Anthropic native params are provided they must take precedence over
# prompt_tokens_details so existing callers are not broken.
usage_native = Usage(
prompt_tokens=18500,
completion_tokens=120,
total_tokens=18620,
cache_read_input_tokens=999,
cache_creation_input_tokens=888,
prompt_tokens_details=PromptTokensDetailsWrapper(
cached_tokens=17000,
cache_write_tokens=400,
),
)
assert usage_native._cache_read_input_tokens == 999
assert usage_native._cache_creation_input_tokens == 888
# cache_write_tokens must not appear in serialised output when absent
usage_no_writes = Usage(
prompt_tokens=100,
completion_tokens=10,
total_tokens=110,
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=50),
)
dumped = usage_no_writes.model_dump()
ptd = dumped.get("prompt_tokens_details", {})
assert "cache_write_tokens" not in ptd