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:
parent
8ac670d77a
commit
cc37cdd54b
2 changed files with 50 additions and 3 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue