diff --git a/studio/backend/core/inference/pricing.py b/studio/backend/core/inference/pricing.py index 3db8882623..9f0892f616 100644 --- a/studio/backend/core/inference/pricing.py +++ b/studio/backend/core/inference/pricing.py @@ -106,6 +106,15 @@ ANTHROPIC_CACHE_5M_WRITE_MULT = 1.25 ANTHROPIC_CACHE_1H_WRITE_MULT = 2.0 ANTHROPIC_CACHE_READ_MULT = 0.1 +# Anthropic fast_mode (Opus 4.6/4.7 only) bills BOTH input AND output +# at 6x standard rates per +# https://platform.claude.com/docs/en/build-with-claude/fast-mode +# (Opus 4.7 standard: $5/$25 per MTok; fast: $30/$150 per MTok). +# Cache multipliers stack on top of the fast premium. Exposed via +# pricing_snapshot so the frontend cost panel can apply it when +# inferenceParams.fastMode was set on the request (see #5715). +ANTHROPIC_FAST_MODE_MULT = 6.0 + # OpenAI: cache reads are 0.1x base input, cache writes are not billed # separately (the first prefix-write request just pays normal input). OPENAI_CACHE_READ_MULT = 0.1 @@ -165,6 +174,8 @@ def calculate_cost( provider: str, model: str, usage: dict[str, Any], + *, + fast_mode: bool = False, ) -> dict[str, float]: """Return a per-turn USD cost breakdown. @@ -296,6 +307,23 @@ def calculate_cost( base = prices["input_per_mtok"] out_per = prices["output_per_mtok"] + # Anthropic fast_mode bills BOTH input AND output at 6x; the + # premium stacks on top of the cache multipliers (Anthropic docs: + # "Prompt caching multipliers apply on top of fast mode pricing"). + # Restricted to claude-opus-4-6 / 4-7 -- silently no-op on every + # other model so a stray ``fast_mode=True`` on a Sonnet can't + # over-charge the user. + if ( + fast_mode + and provider == "anthropic" + and (model.startswith("claude-opus-4-6") or model.startswith("claude-opus-4-7")) + ): + base = base * ANTHROPIC_FAST_MODE_MULT + out_per = out_per * ANTHROPIC_FAST_MODE_MULT + out["model_priced"] = ( + f"{out['model_priced'] or model} (fast)" + ) + out["input_usd"] = (input_tokens / 1_000_000.0) * base out["output_usd"] = (output_tokens / 1_000_000.0) * out_per @@ -379,6 +407,7 @@ def pricing_snapshot() -> dict[str, Any]: "cache_5m_write_mult": ANTHROPIC_CACHE_5M_WRITE_MULT, "cache_1h_write_mult": ANTHROPIC_CACHE_1H_WRITE_MULT, "cache_read_mult": ANTHROPIC_CACHE_READ_MULT, + "fast_mode_mult": ANTHROPIC_FAST_MODE_MULT, "web_search_usd_per_1k": ANTHROPIC_WEB_SEARCH_USD_PER_1K, "code_execution_usd_per_hour": ANTHROPIC_CODE_EXEC_USD_PER_HOUR, }, diff --git a/studio/backend/tests/test_pricing_edge.py b/studio/backend/tests/test_pricing_edge.py index 1ca62e6894..1680bd70dd 100644 --- a/studio/backend/tests/test_pricing_edge.py +++ b/studio/backend/tests/test_pricing_edge.py @@ -32,11 +32,13 @@ import math from core.inference.pricing import ( ANTHROPIC_CACHE_5M_WRITE_MULT, ANTHROPIC_CACHE_READ_MULT, + ANTHROPIC_FAST_MODE_MULT, ANTHROPIC_PRICING, OPENAI_CACHE_READ_MULT, OPENAI_PRICING, _lookup, calculate_cost, + pricing_snapshot, ) @@ -499,3 +501,82 @@ def test_calculate_cost_uses_forwarded_cache_creation_for_1h_premium(): # 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 + + +# ── Anthropic fast_mode pricing ─────────────────────────────────── + + +def test_fast_mode_bills_input_and_output_at_6x_on_opus_47(): + """Opus 4.7 standard: $5/$25; fast: $30/$150.""" + standard = calculate_cost( + "anthropic", "claude-opus-4-7", + {"input_tokens": 1_000_000, "output_tokens": 1_000_000}, + ) + fast = calculate_cost( + "anthropic", "claude-opus-4-7", + {"input_tokens": 1_000_000, "output_tokens": 1_000_000}, + fast_mode = True, + ) + assert math.isclose(standard["input_usd"], 5.0) + assert math.isclose(standard["output_usd"], 25.0) + assert math.isclose(fast["input_usd"], 30.0) + assert math.isclose(fast["output_usd"], 150.0) + assert "fast" in fast["model_priced"] + + +def test_fast_mode_applies_to_opus_46_too(): + fast = calculate_cost( + "anthropic", "claude-opus-4-6", + {"input_tokens": 1_000_000, "output_tokens": 0}, + fast_mode = True, + ) + assert math.isclose(fast["input_usd"], 30.0), fast + + +def test_fast_mode_silently_dropped_on_non_opus_46_47(): + """Stray fast_mode=True on Sonnet/Haiku must not over-charge.""" + for model in ("claude-sonnet-4-5", "claude-sonnet-4-6", "claude-haiku-4-5"): + std = calculate_cost( + "anthropic", model, + {"input_tokens": 1_000_000, "output_tokens": 0}, + ) + fast = calculate_cost( + "anthropic", model, + {"input_tokens": 1_000_000, "output_tokens": 0}, + fast_mode = True, + ) + assert math.isclose(std["input_usd"], fast["input_usd"]), (model, std, fast) + + +def test_fast_mode_ignored_on_openai(): + """Provider gate: fast_mode is Anthropic-only.""" + std = calculate_cost( + "openai", "gpt-5.4", + {"input_tokens": 1_000_000, "output_tokens": 0}, + ) + fast = calculate_cost( + "openai", "gpt-5.4", + {"input_tokens": 1_000_000, "output_tokens": 0}, + fast_mode = True, + ) + assert math.isclose(std["input_usd"], fast["input_usd"]), (std, fast) + + +def test_fast_mode_stacks_with_cache_read_discount(): + """Cache reads stay at 0.1x base * fast_mult per Anthropic docs.""" + r = calculate_cost( + "anthropic", "claude-opus-4-7", + { + "input_tokens": 0, + "output_tokens": 0, + "cache_read_input_tokens": 1_000_000, + }, + fast_mode = True, + ) + # 1M cache_read at 0.1x base (where base = $5 * 6 = $30) = $3. + assert math.isclose(r["cache_read_usd"], 3.0, rel_tol = 1e-3), r + + +def test_pricing_snapshot_exposes_fast_mode_mult(): + snap = pricing_snapshot() + assert snap["anthropic"]["fast_mode_mult"] == 6.0