From cc37cdd54bc402329348eafbc05b93d796b63afe Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 22 May 2026 17:41:50 +0000 Subject: [PATCH] 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. --- studio/backend/core/inference/pricing.py | 16 ++++++++-- studio/backend/tests/test_pricing.py | 37 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/studio/backend/core/inference/pricing.py b/studio/backend/core/inference/pricing.py index d9033e73ea..9f344059fd 100644 --- a/studio/backend/core/inference/pricing.py +++ b/studio/backend/core/inference/pricing.py @@ -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: diff --git a/studio/backend/tests/test_pricing.py b/studio/backend/tests/test_pricing.py index 662866187f..f5f25c1aa3 100644 --- a/studio/backend/tests/test_pricing.py +++ b/studio/backend/tests/test_pricing.py @@ -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``.