unsloth/studio/backend/tests/test_pricing_edge.py
Daniel Han 542d74370f
Studio: pricing follow-up to #5690 (longest-prefix match + chat-style usage keys) (#5722)
* 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>
2026-05-25 23:39:58 -07:00

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