Surface Anthropic cache-read fallback and forward 1h breakdown
Two correctness gaps surfaced on the chat-style usage envelope: 1) Anthropic cache_read fell through to "uncached input" pricing when the envelope arrived without the native ``cache_read_input_tokens`` key (e.g. via a proxy that only emits the mirrored ``prompt_tokens_details.cached_tokens`` block). Studio's canonical ``_build_usage_chunk`` always sets both so production traffic was never affected, but the calculator should accept either as a defense-in-depth measure. Add a fallback to read the mirrored field when the native one is missing or zero; the native key still wins when both are present so the math stays deterministic. 2) ``_build_usage_chunk`` dropped the ``cache_creation`` 5m / 1h breakdown. Downstream ``calculate_cost`` then could not apply the 2x 1h premium and silently fell back to the 5m default, underbilling 1h cache writes by 2x on chat-style traffic. Forward the breakdown verbatim when the upstream usage carries it. Tests grow by 4 (20 -> 24): two for the prompt_tokens_details fallback (with native-precedence pin), one for the chunk shape, one for the end-to-end pricing parity check at 1h.
This commit is contained in:
parent
da82a68174
commit
6a939b4754
3 changed files with 123 additions and 0 deletions
|
|
@ -3939,6 +3939,12 @@ def _build_usage_chunk(
|
|||
"cache_creation_input_tokens": cache_creation,
|
||||
"cache_read_input_tokens": cache_read,
|
||||
}
|
||||
# Forward the 5m / 1h cache-write breakdown so downstream cost
|
||||
# calculation can apply the 2x 1h premium instead of silently
|
||||
# falling back to 5m default pricing on chat-style envelopes.
|
||||
cc_breakdown = last_usage.get("cache_creation")
|
||||
if isinstance(cc_breakdown, dict) and cc_breakdown:
|
||||
usage_block["cache_creation"] = cc_breakdown
|
||||
else:
|
||||
prompt_tokens = last_usage.get("input_tokens") or 0
|
||||
cached = 0
|
||||
|
|
|
|||
|
|
@ -220,6 +220,18 @@ def calculate_cost(
|
|||
# session total tooltip.
|
||||
cache_creation = max(0, int(usage.get("cache_creation_input_tokens") or 0))
|
||||
cache_read = max(0, int(usage.get("cache_read_input_tokens") or 0))
|
||||
# Defense in depth for Anthropic: a chat-style envelope arriving
|
||||
# without the native ``cache_read_input_tokens`` key (e.g. via a
|
||||
# proxy that only emits the mirrored ``prompt_tokens_details``
|
||||
# block) would otherwise be billed as uncached input. Pull the
|
||||
# cache-read count from the mirrored field when the native one is
|
||||
# missing or zero. Studio's own ``_build_usage_chunk`` emits both
|
||||
# so this never fires on canonical traffic; it just hardens the
|
||||
# path against off-spec callers.
|
||||
if cache_read == 0:
|
||||
details = usage.get("prompt_tokens_details") or {}
|
||||
if isinstance(details, dict):
|
||||
cache_read = max(0, int(details.get("cached_tokens") or 0))
|
||||
has_input_tokens = "input_tokens" in usage and usage.get("input_tokens") is not None
|
||||
if has_input_tokens:
|
||||
# Raw upstream envelope.
|
||||
|
|
|
|||
|
|
@ -395,3 +395,108 @@ def test_empty_usage_dict_zero_bill():
|
|||
assert out["priced"] is True # model is in the table
|
||||
assert out["billable_input_tokens"] == 0
|
||||
assert out["total_usd"] == 0.0
|
||||
|
||||
|
||||
# ── Defense-in-depth: Anthropic prompt_tokens_details.cached_tokens ──
|
||||
|
||||
|
||||
def test_anthropic_prompt_tokens_details_fallback_when_native_key_missing():
|
||||
"""A chat-style envelope without ``cache_read_input_tokens`` but
|
||||
with the mirrored ``prompt_tokens_details.cached_tokens`` should
|
||||
still apply the cache_read discount instead of billing as full
|
||||
uncached input.
|
||||
"""
|
||||
r = calculate_cost(
|
||||
provider = "anthropic",
|
||||
model = "claude-opus-4-7",
|
||||
usage = {
|
||||
"prompt_tokens": 1_000_000,
|
||||
"completion_tokens": 0,
|
||||
# No cache_read_input_tokens; only the mirrored shape.
|
||||
"prompt_tokens_details": {"cached_tokens": 1_000_000},
|
||||
"cache_creation_input_tokens": 0,
|
||||
},
|
||||
)
|
||||
assert r["billable_input_tokens"] == 1_000_000, r
|
||||
# 1M cached tokens at 0.1x of $5 (opus 4.7 input rate) = $0.50
|
||||
assert math.isclose(r["cache_read_usd"], 0.5, rel_tol = 1e-3), r
|
||||
|
||||
|
||||
def test_anthropic_native_key_takes_precedence_over_mirrored():
|
||||
"""When both ``cache_read_input_tokens`` and
|
||||
``prompt_tokens_details.cached_tokens`` are present, the native
|
||||
Anthropic field wins (the mirror is only a fallback). Studio's
|
||||
canonical envelope always sets both to the same value, so there is
|
||||
no observable difference in production; the precedence rule just
|
||||
keeps the math deterministic on off-spec inputs.
|
||||
"""
|
||||
r = calculate_cost(
|
||||
provider = "anthropic",
|
||||
model = "claude-opus-4-7",
|
||||
usage = {
|
||||
"prompt_tokens": 1_000_000,
|
||||
"cache_read_input_tokens": 800_000,
|
||||
"prompt_tokens_details": {"cached_tokens": 1_000_000},
|
||||
"cache_creation_input_tokens": 0,
|
||||
},
|
||||
)
|
||||
# billable_input_tokens = uncached_input + cache_creation + cache_read
|
||||
# uncached_input = prompt_tokens - cache_creation - cache_read
|
||||
# = 1_000_000 - 0 - 800_000 = 200_000
|
||||
# billable = 200_000 + 0 + 800_000 = 1_000_000
|
||||
assert r["billable_input_tokens"] == 1_000_000, r
|
||||
# cache_read_usd uses 800_000 not 1_000_000 (native wins).
|
||||
assert math.isclose(r["cache_read_usd"], 0.4, rel_tol = 1e-3), r
|
||||
|
||||
|
||||
# ── _build_usage_chunk preserves cache_creation breakdown ──
|
||||
|
||||
|
||||
def test_build_usage_chunk_forwards_anthropic_cache_creation_breakdown():
|
||||
"""Studio chat-style envelope must carry the 5m / 1h cache-write
|
||||
breakdown so downstream cost calc can apply the 2x 1h premium.
|
||||
"""
|
||||
import json
|
||||
from core.inference.external_provider import _build_usage_chunk
|
||||
|
||||
chunk = _build_usage_chunk(
|
||||
completion_id = "cmpl-x",
|
||||
provider = "anthropic",
|
||||
last_usage = {
|
||||
"input_tokens": 10,
|
||||
"output_tokens": 5,
|
||||
"cache_creation_input_tokens": 1_000_000,
|
||||
"cache_read_input_tokens": 0,
|
||||
"cache_creation": {
|
||||
"ephemeral_5m_input_tokens": 250_000,
|
||||
"ephemeral_1h_input_tokens": 750_000,
|
||||
},
|
||||
},
|
||||
)
|
||||
assert chunk is not None
|
||||
payload = json.loads(chunk.split("data: ", 1)[1])
|
||||
cc = payload["usage"]["cache_creation"]
|
||||
assert cc["ephemeral_1h_input_tokens"] == 750_000, cc
|
||||
assert cc["ephemeral_5m_input_tokens"] == 250_000, cc
|
||||
|
||||
|
||||
def test_calculate_cost_uses_forwarded_cache_creation_for_1h_premium():
|
||||
"""Re-emitted chat envelope must price 1h cache writes at 2x base.
|
||||
"""
|
||||
r = calculate_cost(
|
||||
provider = "anthropic",
|
||||
model = "claude-opus-4-7",
|
||||
usage = {
|
||||
"prompt_tokens": 1_000_010,
|
||||
"completion_tokens": 0,
|
||||
"cache_creation_input_tokens": 1_000_000,
|
||||
"cache_read_input_tokens": 0,
|
||||
"cache_creation": {
|
||||
"ephemeral_5m_input_tokens": 0,
|
||||
"ephemeral_1h_input_tokens": 1_000_000,
|
||||
},
|
||||
},
|
||||
)
|
||||
# 1M tokens at 1h-premium (2x of $5 = $10/M = $10). 5m baseline
|
||||
# would be 1.25x ($6.25). 2x means cache_write_usd ~= $10.
|
||||
assert math.isclose(r["cache_write_usd"], 10.0, rel_tol = 1e-2), r
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue