Studio: read OpenAI cached tokens from prompt_tokens_details too

Codex flagged that the chat-style OpenAI envelope Studio re-emits
via _build_usage_chunk surfaces cached prompt tokens under
prompt_tokens_details.cached_tokens, not input_tokens_details. The
OpenAI branch only checked input_tokens_details, so a cache-heavy
chat-style turn billed every cached token at the full input rate
instead of the 0.1x cache_read discount.

Walk both keys when discovering the cached count. New regression
test pins that the two envelopes price identically for a turn with
80k of 100k tokens cached.
This commit is contained in:
Daniel Han 2026-05-22 17:41:50 +00:00 committed by danielhanchen
commit cc37cdd54b
2 changed files with 50 additions and 3 deletions

View file

@ -230,9 +230,19 @@ def calculate_cost(
else:
output_tokens = int(usage.get("completion_tokens") or 0)
if provider == "openai":
details = usage.get("input_tokens_details") or {}
if isinstance(details, dict):
cache_read = max(cache_read, int(details.get("cached_tokens") or 0))
# Cached prompt tokens live on different sub-objects depending on
# which envelope landed here. Raw OpenAI Responses usage uses
# ``input_tokens_details.cached_tokens``; the OpenAI Chat
# Completions envelope Studio re-emits via ``_build_usage_chunk``
# uses ``prompt_tokens_details.cached_tokens``. Check both so
# cache-heavy chat-style turns get the 0.1x cache_read_mult
# discount instead of full input pricing.
for key in ("input_tokens_details", "prompt_tokens_details"):
details = usage.get(key) or {}
if isinstance(details, dict):
cache_read = max(
cache_read, int(details.get("cached_tokens") or 0)
)
# OpenAI: cache_read already counted inside input_tokens.
out["billable_input_tokens"] = input_tokens + cache_creation
else:

View file

@ -554,6 +554,43 @@ def test_openai_chat_style_prompt_tokens_keeps_cache_read_semantics():
assert _isclose(chat["total_usd"], raw["total_usd"]), (chat, raw)
def test_openai_chat_style_envelope_reads_cache_from_prompt_tokens_details():
"""``_build_usage_chunk`` emits cached tokens under
``prompt_tokens_details.cached_tokens`` (chat-style), not
``input_tokens_details``. The cost calculator must honour
both so cache-heavy chat-style turns get the discounted rate."""
base = OPENAI_PRICING["gpt-5.5"]["input_per_mtok"]
raw = calculate_cost(
"openai",
"gpt-5.5",
{
"input_tokens": 100_000,
"input_tokens_details": {"cached_tokens": 80_000},
"output_tokens": 0,
},
)
chat_style = calculate_cost(
"openai",
"gpt-5.5",
{
"prompt_tokens": 100_000,
"prompt_tokens_details": {"cached_tokens": 80_000},
"completion_tokens": 0,
},
)
# Both envelopes must price identically.
assert _isclose(chat_style["input_usd"], raw["input_usd"]), (chat_style, raw)
assert _isclose(chat_style["cache_read_usd"], raw["cache_read_usd"]), (
chat_style,
raw,
)
# And the discount is real: 80k charged at 0.1x base, 20k at full.
assert _isclose(
chat_style["cache_read_usd"],
80_000 / 1_000_000.0 * base * OPENAI_CACHE_READ_MULT,
)
def test_explicit_zero_output_tokens_wins_over_stale_completion_tokens():
"""``output_tokens: 0`` must beat a stale ``completion_tokens: 50``.