[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:
yuneng-jiang 2026-03-11 20:10:42 -07:00
parent e21b06265a
commit 3203cd9895
2 changed files with 87 additions and 6 deletions

View file

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

View file

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