mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
[Fix] UI - Usage: Cached tokens always showing zero for OpenAI models
OpenAI stores cached tokens under prompt_tokens_details.cached_tokens, but the daily spend writer only checked the top-level cache_read_input_tokens field (used by Anthropic/DeepSeek). Added fallback to read from prompt_tokens_details when the top-level field is absent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e21b06265a
commit
3203cd9895
2 changed files with 87 additions and 6 deletions
|
|
@ -1852,6 +1852,38 @@ class DBSpendUpdateWriter:
|
|||
unique_constraint_name="tag_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_cache_read_input_tokens(usage_obj: dict) -> int:
|
||||
"""
|
||||
Extract cache read input tokens from a usage object dict.
|
||||
|
||||
Anthropic/DeepSeek store this as a top-level `cache_read_input_tokens` field.
|
||||
OpenAI stores it nested under `prompt_tokens_details.cached_tokens`.
|
||||
"""
|
||||
value = usage_obj.get("cache_read_input_tokens", 0) or 0
|
||||
if value:
|
||||
return value
|
||||
prompt_details = usage_obj.get("prompt_tokens_details") or {}
|
||||
if isinstance(prompt_details, dict):
|
||||
return prompt_details.get("cached_tokens", 0) or 0
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _get_cache_creation_input_tokens(usage_obj: dict) -> int:
|
||||
"""
|
||||
Extract cache creation input tokens from a usage object dict.
|
||||
|
||||
Anthropic stores this as a top-level `cache_creation_input_tokens` field.
|
||||
It may also appear under `prompt_tokens_details.cache_creation_tokens`.
|
||||
"""
|
||||
value = usage_obj.get("cache_creation_input_tokens", 0) or 0
|
||||
if value:
|
||||
return value
|
||||
prompt_details = usage_obj.get("prompt_tokens_details") or {}
|
||||
if isinstance(prompt_details, dict):
|
||||
return prompt_details.get("cache_creation_tokens", 0) or 0
|
||||
return 0
|
||||
|
||||
async def _common_add_spend_log_transaction_to_daily_transaction(
|
||||
self,
|
||||
payload: Union[dict, SpendLogsPayload],
|
||||
|
|
@ -1932,12 +1964,12 @@ class DBSpendUpdateWriter:
|
|||
api_requests=1,
|
||||
successful_requests=1 if request_status == "success" else 0,
|
||||
failed_requests=1 if request_status != "success" else 0,
|
||||
cache_read_input_tokens=usage_obj.get("cache_read_input_tokens", 0)
|
||||
or 0,
|
||||
cache_creation_input_tokens=usage_obj.get(
|
||||
"cache_creation_input_tokens", 0
|
||||
)
|
||||
or 0,
|
||||
cache_read_input_tokens=self._get_cache_read_input_tokens(
|
||||
usage_obj
|
||||
),
|
||||
cache_creation_input_tokens=self._get_cache_creation_input_tokens(
|
||||
usage_obj
|
||||
),
|
||||
)
|
||||
return daily_transaction
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1428,3 +1428,52 @@ async def test_commit_spend_updates_uses_pipeline():
|
|||
mock_redis_update_buffer.get_all_daily_end_user_spend_update_transactions_from_redis_buffer.assert_not_called()
|
||||
mock_redis_update_buffer.get_all_daily_agent_spend_update_transactions_from_redis_buffer.assert_not_called()
|
||||
mock_redis_update_buffer.get_all_daily_tag_spend_update_transactions_from_redis_buffer.assert_not_called()
|
||||
|
||||
|
||||
class TestGetCacheTokenHelpers:
|
||||
"""Tests for _get_cache_read_input_tokens and _get_cache_creation_input_tokens.
|
||||
|
||||
These helpers extract cache token counts from usage objects. Anthropic/DeepSeek
|
||||
store them as top-level fields, while OpenAI nests them under prompt_tokens_details.
|
||||
"""
|
||||
|
||||
def test_cache_read_from_top_level_field(self):
|
||||
"""Anthropic-style: cache_read_input_tokens at top level."""
|
||||
usage = {"cache_read_input_tokens": 500}
|
||||
assert DBSpendUpdateWriter._get_cache_read_input_tokens(usage) == 500
|
||||
|
||||
def test_cache_read_from_prompt_tokens_details(self):
|
||||
"""OpenAI-style: cached_tokens nested in prompt_tokens_details."""
|
||||
usage = {"prompt_tokens_details": {"cached_tokens": 300}}
|
||||
assert DBSpendUpdateWriter._get_cache_read_input_tokens(usage) == 300
|
||||
|
||||
def test_cache_read_top_level_takes_precedence(self):
|
||||
"""When both are present, top-level field wins."""
|
||||
usage = {
|
||||
"cache_read_input_tokens": 500,
|
||||
"prompt_tokens_details": {"cached_tokens": 300},
|
||||
}
|
||||
assert DBSpendUpdateWriter._get_cache_read_input_tokens(usage) == 500
|
||||
|
||||
def test_cache_read_empty_usage(self):
|
||||
assert DBSpendUpdateWriter._get_cache_read_input_tokens({}) == 0
|
||||
|
||||
def test_cache_read_none_values(self):
|
||||
usage = {
|
||||
"cache_read_input_tokens": None,
|
||||
"prompt_tokens_details": None,
|
||||
}
|
||||
assert DBSpendUpdateWriter._get_cache_read_input_tokens(usage) == 0
|
||||
|
||||
def test_cache_creation_from_top_level_field(self):
|
||||
"""Anthropic-style: cache_creation_input_tokens at top level."""
|
||||
usage = {"cache_creation_input_tokens": 200}
|
||||
assert DBSpendUpdateWriter._get_cache_creation_input_tokens(usage) == 200
|
||||
|
||||
def test_cache_creation_from_prompt_tokens_details(self):
|
||||
"""cache_creation_tokens nested in prompt_tokens_details."""
|
||||
usage = {"prompt_tokens_details": {"cache_creation_tokens": 150}}
|
||||
assert DBSpendUpdateWriter._get_cache_creation_input_tokens(usage) == 150
|
||||
|
||||
def test_cache_creation_empty_usage(self):
|
||||
assert DBSpendUpdateWriter._get_cache_creation_input_tokens({}) == 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue