* Studio: longest-prefix pricing match + accept chat-style usage keys Two P1 / High follow-ups from PR 5690 review feedback: 1. Pricing prefix lookup returned the first key it iterated, so dated snapshots like ``gpt-5.4-mini-2026-04-23`` collided with the shorter ``gpt-5.4`` entry and overbilled by 3x+. Sort the table keys longest-first so the most specific entry wins. 2. ``calculate_cost`` only read ``input_tokens`` / ``output_tokens``, but Studio's OpenAI-Chat-style usage envelope re-emits ``prompt_tokens`` / ``completion_tokens`` (the OpenAI Chat Completions vocabulary). Callers handing in the chat-style shape silently got a zeroed bill. Accept either pair so the calculator works against both raw upstream usage and the Studio-translated envelope. Tests (4 new in test_pricing.py): dated mini/pro snapshots inherit the right rate; chat-style usage keys price correctly; raw key wins when both shapes are present. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: dedupe cache buckets when costing chat-style Anthropic usage When the caller hands in Studio's chat-style envelope (``prompt_tokens`` emitted by ``_build_usage_chunk``) for Anthropic, that value already folds ``cache_creation_input_tokens`` + ``cache_read_input_tokens`` into the total. The previous follow-up accepted the chat-style key but then re-added both cache buckets in ``billable_input_tokens`` and ``input_usd``, double-counting cache tokens on every Anthropic chat-style call. Detect which envelope landed (``input_tokens`` present = raw upstream; absent + ``prompt_tokens`` present = Studio chat-style) and peel the cache buckets off for Anthropic before the downstream math so both envelopes produce identical costs. OpenAI: ``input_tokens`` and Studio's ``prompt_tokens`` both already include ``cache_read`` and exclude any notional ``cache_creation``, so the OpenAI path stays a straight passthrough. Tests (2 new): both envelopes match for Anthropic on a triple (uncached + cache_creation + cache_read); OpenAI envelopes match on a cached-tokens fixture. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: prefer raw output_tokens over chat-style completion_tokens Codex flagged that the previous fallback chain 'usage.get("output_tokens") or usage.get("completion_tokens")' treats an explicit 0 as missing -- a mixed-envelope payload where 'output_tokens' is 0 but 'completion_tokens' is non-zero (or stale) bills the wrong amount. Mirror the has_input_tokens precedence pattern: when the raw key is present we use it even at 0; otherwise fall back to completion_tokens. * 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. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: tighten pricing prefix match + clamp corrupt usage Three follow-ups on the longest-prefix pricing match landed in this PR: - Prefix match now requires a dash boundary or end-of-string. The longest-key sort alone still falsely landed "claude-opus-4-15" on the "claude-opus-4-1" row, and "gpt-5.5-prod" on the "gpt-5.5-pro" row (a 6x overcharge). Demanding the next character be "-" rules out the lookalikes while keeping dated snapshots ("gpt-5.4-mini-2026-04-23", "claude-opus-4-7-20260414") landing on their canonical row. - Clamp every token count to >= 0. A corrupted upstream payload (negative cached count, off-by-one in a fixture) could previously produce a negative bill that masked real spend in the session total tooltip. - Tolerate a non-dict "cache_creation" (e.g. an upstream proxy folded the field down to a single int). The current code raised AttributeError mid-turn; now it falls back to the 5m-default bucket so the rest of the cost calculation still runs. Adds tests/test_pricing_edge.py with 20 adversarial cases covering the boundary check, negative / None / zero token values across both envelopes, cache_read > prompt corruption, the OpenAI long-context threshold crossover on cache-inflated billable input, malformed sub-objects, and unknown-provider degradation. Combined suite is 51 tests, all green. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * 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. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add Anthropic fast_mode pricing multiplier PR 5715 wires the fast-mode-2026-02-01 beta header + speed:"fast" field through to Anthropic, but the cost calculator never learnt about the matching 6x premium documented at https://platform.claude.com/docs/en/build-with-claude/fast-mode (Opus 4.7 standard $5/$25 per MTok, fast $30/$150). This adds: - ANTHROPIC_FAST_MODE_MULT = 6.0 constant. - calculate_cost(..., fast_mode=True) applies the 6x to base input AND output rates before any cache multipliers (cache mults stack on top of fast per Anthropic docs). - Provider+model gate: silently no-op on every model that is not claude-opus-4-6 / claude-opus-4-7 so a stray fast_mode=True on Sonnet/Haiku can never over-charge. - model_priced label tagged "(fast)" so the cost tooltip can surface which rate fired. - pricing_snapshot now exposes fast_mode_mult so the frontend cost panel doesn't have to hard-code 6. 7 new edge tests pin the math; existing 55 still pass. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Honor explicit zero cache_read_input_tokens on Anthropic envelopes The previous follow-up fell back to ``prompt_tokens_details.cached_tokens`` whenever the native ``cache_read_input_tokens`` was missing OR equal to 0, even though the commit message stated the native key always wins when present. A proxy that forwards a stale ``prompt_tokens_details`` block alongside an authoritative ``cache_read_input_tokens: 0`` would then inflate cache_read past the real native count, posting a false cache_read line and bumping billable_input_tokens. Switch the gate to native-key presence so an explicit zero stays authoritative; the mirror only kicks in when the native key is absent. Add a regression test pinning the explicit-zero precedence. * Move fast_mode pricing back to #5715 The fast_mode 6x multiplier landed in two places at once -- here (f66df7ba) and on #5715 (4f1afdb5) -- since both audits ran in parallel. Drop the duplicate from this branch so the change lives in its natural home (#5715, which introduces fast_mode itself); this PR stays focused on the cache-read fallback + 1h breakdown. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Shorten pricing comments for PR #5722 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
475 lines
16 KiB
Python
475 lines
16 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Adversarial edge cases for ``calculate_cost`` / ``_lookup``: prefix
|
|
boundary, negative tokens, chat vs raw parity, long-context crossover
|
|
on billable count, and malformed sub-objects."""
|
|
|
|
import math
|
|
|
|
from core.inference.pricing import (
|
|
ANTHROPIC_CACHE_5M_WRITE_MULT,
|
|
ANTHROPIC_CACHE_READ_MULT,
|
|
ANTHROPIC_PRICING,
|
|
OPENAI_CACHE_READ_MULT,
|
|
OPENAI_PRICING,
|
|
_lookup,
|
|
calculate_cost,
|
|
)
|
|
|
|
|
|
def _isclose(a, b, tol = 1e-6):
|
|
return math.isclose(a, b, rel_tol = tol, abs_tol = tol)
|
|
|
|
|
|
# ── prefix-match boundary checks ────────────────────────────────────
|
|
|
|
|
|
def test_prefix_match_requires_dash_boundary_opus_variant():
|
|
# `claude-opus-4-15` must not inherit `claude-opus-4-1` pricing;
|
|
# next char must be `-` or end-of-string.
|
|
assert _lookup("anthropic", "claude-opus-4-15") is None
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-15",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is False
|
|
assert out["total_usd"] == 0.0
|
|
|
|
|
|
def test_prefix_match_requires_dash_boundary_gpt_variant():
|
|
# Same dash-boundary invariant for OpenAI ids.
|
|
assert _lookup("openai", "gpt-5.55") is None
|
|
assert _lookup("openai", "gpt-5.55-2026-04-23") is None
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.55-2026-04-23",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is False
|
|
|
|
|
|
def test_prefix_match_requires_dash_boundary_pro_lookalike():
|
|
# `gpt-5.5-prod` must fall through `gpt-5.5-pro` (6x overcharge)
|
|
# and land on the canonical `gpt-5.5` row.
|
|
prices = _lookup("openai", "gpt-5.5-prod")
|
|
assert prices is not None
|
|
assert (
|
|
prices["input_per_mtok"] == OPENAI_PRICING["gpt-5.5"]["input_per_mtok"]
|
|
), "expected fallback to gpt-5.5 base ($5), not gpt-5.5-pro ($30)"
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5-prod",
|
|
{"input_tokens": 100_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
assert _isclose(out["input_usd"], 100_000 / 1_000_000.0 * 5.0)
|
|
|
|
|
|
def test_prefix_match_still_resolves_legit_dated_snapshots():
|
|
# Boundary fix must not regress legit dated snapshots.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4-mini-2026-04-23",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
assert _isclose(out["input_usd"], 0.75)
|
|
|
|
# And Anthropic dated snapshot still resolves to canonical row.
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7-20260414",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
assert _isclose(out["input_usd"], 5.0)
|
|
|
|
|
|
# ── precedence: input_tokens wins over prompt_tokens (and 0 is real) ──
|
|
|
|
|
|
def test_explicit_zero_input_tokens_wins_over_stale_prompt_tokens():
|
|
# Input-side mirror of the output zero precedence test.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 0,
|
|
"prompt_tokens": 1_000_000, # stale chat-style mirror
|
|
"output_tokens": 100,
|
|
},
|
|
)
|
|
assert out["billable_input_tokens"] == 0
|
|
assert out["input_usd"] == 0.0
|
|
|
|
|
|
def test_none_input_tokens_falls_through_to_prompt_tokens():
|
|
# `None` is "key present but unset"; chat-style mirror wins.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": None,
|
|
"prompt_tokens": 200_000,
|
|
"output_tokens": None,
|
|
"completion_tokens": 5_000,
|
|
},
|
|
)
|
|
assert out["billable_input_tokens"] == 200_000
|
|
assert out["billable_output_tokens"] == 5_000
|
|
assert _isclose(out["input_usd"], 200_000 / 1_000_000.0 * 5.0)
|
|
assert _isclose(out["output_usd"], 5_000 / 1_000_000.0 * 30.0)
|
|
|
|
|
|
# ── negative / corrupted upstream values clamp to zero ──────────────
|
|
|
|
|
|
def test_negative_tokens_clamp_to_zero_no_negative_bill():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": -100, "output_tokens": -50},
|
|
)
|
|
assert out["billable_input_tokens"] == 0
|
|
assert out["billable_output_tokens"] == 0
|
|
assert out["input_usd"] == 0.0
|
|
assert out["output_usd"] == 0.0
|
|
assert out["total_usd"] == 0.0
|
|
|
|
|
|
def test_negative_cache_buckets_clamp_to_zero():
|
|
# Negative cache_read on Anthropic would otherwise refund the bill.
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 1_000,
|
|
"output_tokens": 0,
|
|
"cache_creation_input_tokens": -500,
|
|
"cache_read_input_tokens": -1_000,
|
|
},
|
|
)
|
|
assert out["cache_write_usd"] == 0.0
|
|
assert out["cache_read_usd"] == 0.0
|
|
assert out["billable_input_tokens"] == 1_000
|
|
assert out["total_usd"] >= 0.0
|
|
|
|
|
|
def test_negative_prompt_tokens_chat_style_clamp():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4-mini",
|
|
{"prompt_tokens": -100, "completion_tokens": -50},
|
|
)
|
|
assert out["billable_input_tokens"] == 0
|
|
assert out["billable_output_tokens"] == 0
|
|
assert out["total_usd"] == 0.0
|
|
|
|
|
|
# ── cache_read > prompt_tokens corruption: no negative billable ─────
|
|
|
|
|
|
def test_anthropic_chat_cache_read_exceeds_prompt_no_negative_billable():
|
|
# cache_read > prompt_tokens clamps uncached_input at 0; billable
|
|
# still reflects cache buckets (we charge for what we got).
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"prompt_tokens": 100,
|
|
"cache_creation_input_tokens": 0,
|
|
"cache_read_input_tokens": 500,
|
|
"completion_tokens": 0,
|
|
},
|
|
)
|
|
assert out["input_usd"] == 0.0 # uncached clamped to 0
|
|
assert out["billable_input_tokens"] == 500 # 0 uncached + 500 cache_read
|
|
# cache_read still priced at the discount rate.
|
|
base = ANTHROPIC_PRICING["claude-opus-4-7"]["input_per_mtok"]
|
|
assert _isclose(
|
|
out["cache_read_usd"], 500 / 1_000_000.0 * base * ANTHROPIC_CACHE_READ_MULT
|
|
)
|
|
|
|
|
|
def test_openai_raw_cached_tokens_exceeds_input_clamp_non_cached():
|
|
# OpenAI variant: cached > input must not produce negative input_usd.
|
|
base = OPENAI_PRICING["gpt-5.5"]["input_per_mtok"]
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 100,
|
|
"output_tokens": 0,
|
|
"input_tokens_details": {"cached_tokens": 500},
|
|
},
|
|
)
|
|
assert out["input_usd"] == 0.0
|
|
# Cache read still priced (the 0.1x bucket).
|
|
assert _isclose(
|
|
out["cache_read_usd"], 500 / 1_000_000.0 * base * OPENAI_CACHE_READ_MULT
|
|
)
|
|
|
|
|
|
# ── long-context tier crosses on billable, including cache_creation ──
|
|
|
|
|
|
def test_openai_long_context_triggers_on_cache_creation_inflated_billable():
|
|
# cache_creation pushes billable past 272k -> long-context tier
|
|
# must fire to avoid undercounting.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 250_000,
|
|
"cache_creation_input_tokens": 50_000,
|
|
"output_tokens": 1_000,
|
|
},
|
|
)
|
|
assert out["billable_input_tokens"] == 300_000
|
|
assert "long-context" in out["model_priced"]
|
|
assert _isclose(out["input_usd"], 250_000 / 1_000_000.0 * 10.0)
|
|
assert _isclose(out["output_usd"], 1_000 / 1_000_000.0 * 45.0)
|
|
|
|
|
|
def test_openai_long_context_threshold_boundary_inclusive():
|
|
# Threshold is inclusive (>=).
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 272_000, "output_tokens": 1_000},
|
|
)
|
|
assert "long-context" in out["model_priced"]
|
|
out_lo = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 271_999, "output_tokens": 1_000},
|
|
)
|
|
assert "long-context" not in out_lo["model_priced"]
|
|
|
|
|
|
# ── chat-style vs raw envelope parity at OpenAI long-context tier ──
|
|
|
|
|
|
def test_openai_chat_envelope_long_context_parity_with_raw():
|
|
raw = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 300_000, "output_tokens": 10_000},
|
|
)
|
|
chat = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"prompt_tokens": 300_000, "completion_tokens": 10_000},
|
|
)
|
|
assert _isclose(chat["total_usd"], raw["total_usd"])
|
|
assert "long-context" in chat["model_priced"]
|
|
assert "long-context" in raw["model_priced"]
|
|
|
|
|
|
# ── malformed sub-objects: no crash, no false bill ──────────────────
|
|
|
|
|
|
def test_cache_creation_as_int_does_not_crash():
|
|
# Proxies sometimes fold cache_creation to an int; tolerate it
|
|
# and fall back to the 5m default.
|
|
base = ANTHROPIC_PRICING["claude-opus-4-7"]["input_per_mtok"]
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"cache_creation_input_tokens": 1_000_000,
|
|
"cache_creation": 12345, # malformed; must not raise
|
|
},
|
|
)
|
|
# Falls back to 5m default for the whole bucket.
|
|
assert _isclose(
|
|
out["cache_write_usd"],
|
|
1_000_000 / 1_000_000.0 * base * ANTHROPIC_CACHE_5M_WRITE_MULT,
|
|
)
|
|
|
|
|
|
def test_non_dict_server_tool_use_is_ignored():
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{"input_tokens": 100, "output_tokens": 100, "server_tool_use": "garbage"},
|
|
)
|
|
assert out["server_tools_usd"] == 0.0
|
|
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 100, "output_tokens": 100, "openai_tool_use": [1, 2, 3]},
|
|
)
|
|
assert out["server_tools_usd"] == 0.0
|
|
|
|
|
|
def test_non_dict_input_tokens_details_is_ignored():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 100,
|
|
"output_tokens": 0,
|
|
"input_tokens_details": "nope",
|
|
"prompt_tokens_details": [1, 2, 3],
|
|
},
|
|
)
|
|
# No cached_tokens recovered -> no discount.
|
|
assert out["cache_read_usd"] == 0.0
|
|
|
|
|
|
# ── unknown provider degrades gracefully ────────────────────────────
|
|
|
|
|
|
def test_unknown_provider_priced_false_zero_bill():
|
|
out = calculate_cost(
|
|
"gemini",
|
|
"gemini-pro",
|
|
{"input_tokens": 1_000_000, "output_tokens": 1_000_000},
|
|
)
|
|
assert out["priced"] is False
|
|
assert out["total_usd"] == 0.0
|
|
# Tokens still report for the UI.
|
|
assert out["billable_input_tokens"] == 1_000_000
|
|
assert out["billable_output_tokens"] == 1_000_000
|
|
|
|
|
|
def test_anthropic_provider_with_openai_model_priced_false():
|
|
# OpenAI id against Anthropic table must not falsely match.
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"gpt-5.5",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is False
|
|
|
|
|
|
# ── all-zero / empty usage stays at zero ────────────────────────────
|
|
|
|
|
|
def test_empty_usage_dict_zero_bill():
|
|
out = calculate_cost("openai", "gpt-5.5", {})
|
|
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():
|
|
"""Chat-style envelope without `cache_read_input_tokens` but with
|
|
mirrored `prompt_tokens_details.cached_tokens` should still apply
|
|
the cache_read discount."""
|
|
r = calculate_cost(
|
|
provider = "anthropic",
|
|
model = "claude-opus-4-7",
|
|
usage = {
|
|
"prompt_tokens": 1_000_000,
|
|
"completion_tokens": 0,
|
|
# Only the mirrored shape (no native key).
|
|
"prompt_tokens_details": {"cached_tokens": 1_000_000},
|
|
"cache_creation_input_tokens": 0,
|
|
},
|
|
)
|
|
assert r["billable_input_tokens"] == 1_000_000, r
|
|
# 1M cached at 0.1x of $5 (opus 4.7) = $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 native and mirrored cache-read fields are present,
|
|
the native Anthropic field wins (mirror is fallback-only)."""
|
|
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 = uncached_input + cache_creation + cache_read
|
|
# = (1M - 0 - 800k) + 0 + 800k = 1M
|
|
assert r["billable_input_tokens"] == 1_000_000, r
|
|
# cache_read uses 800k (native), not 1M (mirrored).
|
|
assert math.isclose(r["cache_read_usd"], 0.4, rel_tol = 1e-3), r
|
|
|
|
|
|
def test_anthropic_native_zero_takes_precedence_over_mirrored():
|
|
"""Explicit `cache_read_input_tokens: 0` is authoritative; a stale
|
|
mirrored block from a proxy must not inflate cache_read past it."""
|
|
r = calculate_cost(
|
|
provider = "anthropic",
|
|
model = "claude-opus-4-7",
|
|
usage = {
|
|
"input_tokens": 1_000_000,
|
|
"output_tokens": 0,
|
|
"cache_read_input_tokens": 0,
|
|
# Stale mirror from a proxy; must be ignored (native present).
|
|
"prompt_tokens_details": {"cached_tokens": 1_000_000},
|
|
},
|
|
)
|
|
# Native is 0 -> cache_read stays 0.
|
|
assert r["cache_read_usd"] == 0.0, r
|
|
# billable = input + cache_creation + cache_read = 1M + 0 + 0
|
|
assert r["billable_input_tokens"] == 1_000_000, r
|
|
# 1M uncached at $5/M (no discount).
|
|
assert math.isclose(r["input_usd"], 5.0, rel_tol = 1e-3), r
|
|
assert math.isclose(r["total_usd"], 5.0, rel_tol = 1e-3), r
|
|
|
|
|
|
# ── _build_usage_chunk preserves cache_creation breakdown ──
|
|
|
|
|
|
def test_build_usage_chunk_forwards_anthropic_cache_creation_breakdown():
|
|
"""Chat-style envelope must carry the 5m/1h cache-write breakdown
|
|
so downstream cost calc applies 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 at 1h-premium (2x of $5 = $10); 5m baseline would be $6.25.
|
|
assert math.isclose(r["cache_write_usd"], 10.0, rel_tol = 1e-2), r
|