e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
625 lines
21 KiB
Python
625 lines
21 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
|
|
|
|
"""Unit tests for the per-session cost calculator: math against
|
|
``core/inference/pricing.py`` plus graceful degradation."""
|
|
|
|
import math
|
|
|
|
from core.inference.pricing import (
|
|
ANTHROPIC_CACHE_5M_WRITE_MULT,
|
|
ANTHROPIC_CACHE_1H_WRITE_MULT,
|
|
ANTHROPIC_CACHE_READ_MULT,
|
|
ANTHROPIC_FAST_MODE_MULT,
|
|
ANTHROPIC_PRICING,
|
|
OPENAI_CACHE_READ_MULT,
|
|
OPENAI_CONTAINER_USD_PER_HOUR,
|
|
OPENAI_PRICING,
|
|
OPENAI_WEB_SEARCH_USD_PER_1K,
|
|
calculate_cost,
|
|
pricing_snapshot,
|
|
)
|
|
|
|
|
|
def _isclose(
|
|
a,
|
|
b,
|
|
tol = 1e-6,
|
|
):
|
|
return math.isclose(a, b, rel_tol = tol, abs_tol = tol)
|
|
|
|
|
|
# ── unknown model -> priced=False, totals zero, tokens still report ──
|
|
|
|
|
|
def test_unknown_model_priced_false():
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"made-up-model-9000",
|
|
{"input_tokens": 100, "output_tokens": 50},
|
|
)
|
|
assert out["priced"] is False
|
|
assert out["total_usd"] == 0.0
|
|
assert out["billable_input_tokens"] == 100
|
|
assert out["billable_output_tokens"] == 50
|
|
|
|
|
|
# ── Anthropic base math (Opus 4.7: 5/25 per MTok) ────────────────────
|
|
|
|
|
|
def test_anthropic_opus_4_7_input_and_output_math():
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{"input_tokens": 1_000_000, "output_tokens": 1_000_000},
|
|
)
|
|
assert _isclose(out["input_usd"], 5.0)
|
|
assert _isclose(out["output_usd"], 25.0)
|
|
assert _isclose(out["total_usd"], 30.0)
|
|
|
|
|
|
# ── Anthropic fast-mode 6x multiplier (Opus 4.6 / 4.7 only) ─────────
|
|
|
|
|
|
def test_anthropic_fast_mode_charges_6x_standard_opus():
|
|
"""6x on input + output when ``usage.speed == "fast"``.
|
|
https://platform.claude.com/docs/en/build-with-claude/fast-mode"""
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 1_000_000,
|
|
"output_tokens": 1_000_000,
|
|
"speed": "fast",
|
|
},
|
|
)
|
|
assert _isclose(out["input_usd"], 5.0 * ANTHROPIC_FAST_MODE_MULT)
|
|
assert _isclose(out["output_usd"], 25.0 * ANTHROPIC_FAST_MODE_MULT)
|
|
assert _isclose(out["total_usd"], 30.0 * ANTHROPIC_FAST_MODE_MULT)
|
|
assert "(fast)" in out["model_priced"], out["model_priced"]
|
|
|
|
|
|
def test_anthropic_fast_mode_does_not_affect_standard_speed():
|
|
"""``speed: "standard"`` (or missing) keeps the base rates."""
|
|
out_standard = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 1_000_000,
|
|
"output_tokens": 1_000_000,
|
|
"speed": "standard",
|
|
},
|
|
)
|
|
out_missing = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{"input_tokens": 1_000_000, "output_tokens": 1_000_000},
|
|
)
|
|
assert _isclose(out_standard["total_usd"], out_missing["total_usd"])
|
|
assert _isclose(out_standard["total_usd"], 30.0)
|
|
|
|
|
|
def test_anthropic_fast_mode_stacks_with_cache_read_multiplier():
|
|
"""Cache multipliers apply on top of fast-mode (per docs)."""
|
|
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_read_input_tokens": 1_000_000,
|
|
"speed": "fast",
|
|
},
|
|
)
|
|
expected = base * ANTHROPIC_FAST_MODE_MULT * ANTHROPIC_CACHE_READ_MULT
|
|
assert _isclose(out["cache_read_usd"], expected)
|
|
|
|
|
|
# ── Anthropic cache write 5m + read multipliers ──────────────────────
|
|
|
|
|
|
def test_anthropic_cache_5m_and_read_use_correct_multipliers():
|
|
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_read_input_tokens": 1_000_000,
|
|
"cache_creation": {
|
|
"ephemeral_5m_input_tokens": 1_000_000,
|
|
"ephemeral_1h_input_tokens": 0,
|
|
},
|
|
},
|
|
)
|
|
assert _isclose(out["cache_write_usd"], base * ANTHROPIC_CACHE_5M_WRITE_MULT)
|
|
assert _isclose(out["cache_read_usd"], base * ANTHROPIC_CACHE_READ_MULT)
|
|
# billable_input_tokens = input + cache_create + cache_read
|
|
assert out["billable_input_tokens"] == 2_000_000
|
|
|
|
|
|
def test_anthropic_cache_1h_write_uses_2x_multiplier():
|
|
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_read_input_tokens": 0,
|
|
"cache_creation": {
|
|
"ephemeral_5m_input_tokens": 0,
|
|
"ephemeral_1h_input_tokens": 1_000_000,
|
|
},
|
|
},
|
|
)
|
|
assert _isclose(out["cache_write_usd"], base * ANTHROPIC_CACHE_1H_WRITE_MULT)
|
|
|
|
|
|
def test_anthropic_cache_5m_default_when_no_breakdown():
|
|
# No 5m/1h split surfaced -> assume the default 5m pool.
|
|
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": 500_000,
|
|
},
|
|
)
|
|
expected = 0.5 * base * ANTHROPIC_CACHE_5M_WRITE_MULT
|
|
assert _isclose(out["cache_write_usd"], expected)
|
|
|
|
|
|
# ── Anthropic server-tool surcharges ────────────────────────────────
|
|
|
|
|
|
def test_anthropic_web_search_charged_per_thousand():
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"server_tool_use": {"web_search_requests": 250},
|
|
},
|
|
)
|
|
assert _isclose(out["server_tools_usd"], 2.5) # $10/1000 * 250
|
|
|
|
|
|
def test_anthropic_code_exec_charged_per_hour():
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"server_tool_use": {"code_execution_hours": 2.0},
|
|
},
|
|
)
|
|
assert _isclose(out["server_tools_usd"], 0.10) # $0.05/hr * 2
|
|
|
|
|
|
def test_anthropic_dated_id_falls_back_to_canonical_prefix():
|
|
# Dated snapshot inherits canonical pricing via prefix-match.
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7-20260712",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
assert _isclose(out["input_usd"], 5.0)
|
|
|
|
|
|
# ── OpenAI base math (gpt-5.5: 5/30 per MTok) ────────────────────────
|
|
|
|
|
|
def test_openai_gpt55_input_output_math():
|
|
# Sub-272k stays in short-context tier ($5/$30).
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 200_000, "output_tokens": 50_000},
|
|
)
|
|
assert _isclose(out["input_usd"], 200_000 / 1_000_000.0 * 5.0)
|
|
assert _isclose(out["output_usd"], 50_000 / 1_000_000.0 * 30.0)
|
|
assert _isclose(out["total_usd"], 1.0 + 1.5)
|
|
|
|
|
|
def test_openai_cache_read_subtracted_from_input_at_discount():
|
|
# OpenAI folds cached into input_tokens; subtract and re-bill at 0.1x.
|
|
base = OPENAI_PRICING["gpt-5.5"]["input_per_mtok"]
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 100_000,
|
|
"output_tokens": 0,
|
|
"input_tokens_details": {"cached_tokens": 80_000},
|
|
},
|
|
)
|
|
# 20k charged at full price, 80k charged at 0.1x
|
|
assert _isclose(out["input_usd"], 20_000 / 1_000_000.0 * base)
|
|
assert _isclose(out["cache_read_usd"], 80_000 / 1_000_000.0 * base * OPENAI_CACHE_READ_MULT)
|
|
|
|
|
|
def test_openai_billable_input_tokens_does_not_double_count_cache_read():
|
|
# input_tokens already includes cached; don't double-count.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 100_000,
|
|
"output_tokens": 0,
|
|
"input_tokens_details": {"cached_tokens": 80_000},
|
|
},
|
|
)
|
|
assert out["billable_input_tokens"] == 100_000
|
|
|
|
|
|
def test_openai_dated_snapshot_inherits_canonical_pricing():
|
|
# Dated snapshot inherits gpt-5.5 pricing via prefix-match.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5-2026-04-23",
|
|
{"input_tokens": 200_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
assert _isclose(out["input_usd"], 200_000 / 1_000_000.0 * 5.0)
|
|
|
|
|
|
def test_openai_gpt54_family_uses_verified_prices():
|
|
# Spot-check lower-tier rows that previously underbilled.
|
|
cases = {
|
|
# (input_tokens, expected_input_usd, expected_output_usd)
|
|
"gpt-5.4": (200_000, 200_000 / 1_000_000.0 * 2.5, 200_000 / 1_000_000.0 * 15.0),
|
|
"gpt-5.4-mini": (1_000_000, 0.75, 4.5),
|
|
"gpt-5.4-nano": (1_000_000, 0.20, 1.25),
|
|
"gpt-5.3-codex": (1_000_000, 1.75, 14.0),
|
|
}
|
|
for model, (in_tokens, exp_in, exp_out) in cases.items():
|
|
out = calculate_cost(
|
|
"openai",
|
|
model,
|
|
{"input_tokens": in_tokens, "output_tokens": in_tokens},
|
|
)
|
|
assert out["priced"] is True, model
|
|
assert _isclose(out["input_usd"], exp_in), model
|
|
assert _isclose(out["output_usd"], exp_out), model
|
|
|
|
|
|
def test_openai_unlisted_model_priced_false_not_zero_default():
|
|
# o-series / gpt-4.5 are off the pricing page; drop rather than $0.
|
|
for model in ("o3", "o4-mini", "gpt-4.5", "gpt-4.5-preview"):
|
|
out = calculate_cost(
|
|
"openai",
|
|
model,
|
|
{"input_tokens": 1_000_000, "output_tokens": 1_000_000},
|
|
)
|
|
assert out["priced"] is False, model
|
|
assert out["total_usd"] == 0.0, model
|
|
# Token counts still report so the UI can render usage.
|
|
assert out["billable_input_tokens"] == 1_000_000, model
|
|
assert out["billable_output_tokens"] == 1_000_000, model
|
|
|
|
|
|
# ── canonical Anthropic 4.5 ids now resolve to a price ─────────────
|
|
|
|
|
|
def test_anthropic_canonical_4_5_ids_are_priced():
|
|
# Pin the bare-id aliases (backend defaults reference these).
|
|
cases = {
|
|
"claude-opus-4-5": (5.0, 25.0),
|
|
"claude-sonnet-4-5": (3.0, 15.0),
|
|
"claude-haiku-4-5": (1.0, 5.0),
|
|
# Opus 4.1 has the same problem.
|
|
"claude-opus-4-1": (15.0, 75.0),
|
|
}
|
|
for model, (inp, outp) in cases.items():
|
|
out = calculate_cost(
|
|
"anthropic",
|
|
model,
|
|
{"input_tokens": 1_000_000, "output_tokens": 1_000_000},
|
|
)
|
|
assert out["priced"] is True, model
|
|
assert _isclose(out["input_usd"], inp), model
|
|
assert _isclose(out["output_usd"], outp), model
|
|
|
|
|
|
# ── OpenAI long-context tier crossover ──────────────────────────────
|
|
|
|
|
|
def test_openai_gpt55_short_context_under_272k_uses_base_rates():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 100_000, "output_tokens": 5_000},
|
|
)
|
|
assert _isclose(out["input_usd"], 100_000 / 1_000_000.0 * 5.0)
|
|
assert _isclose(out["output_usd"], 5_000 / 1_000_000.0 * 30.0)
|
|
# No long-context marker on the model id when we stayed under.
|
|
assert "long-context" not in out["model_priced"], out["model_priced"]
|
|
|
|
|
|
def test_openai_gpt55_long_context_crossover_uses_higher_rates():
|
|
# >272k billable -> long-context tier on the whole turn.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{"input_tokens": 300_000, "output_tokens": 10_000},
|
|
)
|
|
assert _isclose(out["input_usd"], 300_000 / 1_000_000.0 * 10.0)
|
|
assert _isclose(out["output_usd"], 10_000 / 1_000_000.0 * 45.0)
|
|
assert "long-context" in out["model_priced"], out["model_priced"]
|
|
|
|
|
|
def test_openai_gpt54_long_context_crossover():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4",
|
|
{"input_tokens": 500_000, "output_tokens": 20_000},
|
|
)
|
|
assert _isclose(out["input_usd"], 500_000 / 1_000_000.0 * 5.0)
|
|
assert _isclose(out["output_usd"], 20_000 / 1_000_000.0 * 22.5)
|
|
|
|
|
|
def test_openai_gpt54_mini_has_no_long_context_tier():
|
|
# Mini/nano/codex have no long-context tier; base rate always applies.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4-mini",
|
|
{"input_tokens": 500_000, "output_tokens": 0},
|
|
)
|
|
assert _isclose(out["input_usd"], 500_000 / 1_000_000.0 * 0.75)
|
|
assert "long-context" not in out["model_priced"], out["model_priced"]
|
|
|
|
|
|
# ── OpenAI server-tool surcharges ──────────────────────────────────
|
|
|
|
|
|
def test_openai_web_search_charged_per_thousand():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"openai_tool_use": {"web_search_requests": 250},
|
|
},
|
|
)
|
|
assert _isclose(out["server_tools_usd"], 250 / 1_000.0 * OPENAI_WEB_SEARCH_USD_PER_1K)
|
|
assert _isclose(out["total_usd"], 250 / 1_000.0 * OPENAI_WEB_SEARCH_USD_PER_1K)
|
|
|
|
|
|
def test_openai_container_hours_charged():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"openai_tool_use": {"container_hours": 1.5},
|
|
},
|
|
)
|
|
assert _isclose(out["server_tools_usd"], 1.5 * OPENAI_CONTAINER_USD_PER_HOUR)
|
|
|
|
|
|
def test_openai_tool_surcharges_added_to_total():
|
|
# End-to-end: total must sum input + output + web_search + container.
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 100_000,
|
|
"output_tokens": 5_000,
|
|
"openai_tool_use": {
|
|
"web_search_requests": 3,
|
|
"container_hours": 0.25,
|
|
},
|
|
},
|
|
)
|
|
expected_input = 100_000 / 1_000_000.0 * 5.0
|
|
expected_output = 5_000 / 1_000_000.0 * 30.0
|
|
expected_tools = (
|
|
3 / 1_000.0 * OPENAI_WEB_SEARCH_USD_PER_1K + 0.25 * OPENAI_CONTAINER_USD_PER_HOUR
|
|
)
|
|
assert _isclose(
|
|
out["total_usd"],
|
|
round(expected_input + expected_output + expected_tools, 6),
|
|
)
|
|
|
|
|
|
# ── snapshot endpoint includes the multipliers ───────────────────────
|
|
|
|
|
|
def test_snapshot_contains_provider_buckets_and_multipliers():
|
|
snap = pricing_snapshot()
|
|
assert set(snap.keys()) == {"anthropic", "openai"}
|
|
a = snap["anthropic"]
|
|
o = snap["openai"]
|
|
assert "models" in a and "claude-opus-4-7" in a["models"]
|
|
assert a["cache_5m_write_mult"] == ANTHROPIC_CACHE_5M_WRITE_MULT
|
|
assert a["cache_1h_write_mult"] == ANTHROPIC_CACHE_1H_WRITE_MULT
|
|
assert a["cache_read_mult"] == ANTHROPIC_CACHE_READ_MULT
|
|
assert a["fast_mode_mult"] == ANTHROPIC_FAST_MODE_MULT
|
|
assert "web_search_usd_per_1k" in a
|
|
assert "code_execution_usd_per_hour" in a
|
|
assert "models" in o and "gpt-5.5" in o["models"]
|
|
assert o["cache_read_mult"] == OPENAI_CACHE_READ_MULT
|
|
# OpenAI tool surcharge constants are exposed for the frontend.
|
|
assert o["web_search_usd_per_1k"] == OPENAI_WEB_SEARCH_USD_PER_1K
|
|
assert o["container_usd_per_hour"] == OPENAI_CONTAINER_USD_PER_HOUR
|
|
# Long-context tier metadata travels with the model row.
|
|
gpt55 = o["models"]["gpt-5.5"]
|
|
assert gpt55["long_context_threshold"] == 272_000
|
|
assert gpt55["long_context_input_per_mtok"] == 10.0
|
|
assert gpt55["long_context_output_per_mtok"] == 45.0
|
|
|
|
|
|
# ── longest-prefix match: dated mini variant must not collide with the
|
|
# shorter family prefix ──
|
|
|
|
|
|
def test_longest_prefix_match_wins_for_dated_mini_snapshot():
|
|
"""`gpt-5.4-mini-2026-...` inherits the mini rate, not the shorter
|
|
`gpt-5.4` rate (longest prefix wins)."""
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4-mini-2026-04-23",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
# mini = 0.75/MTok, shorter gpt-5.4 = 2.5/MTok (>3x overcharge).
|
|
assert _isclose(out["input_usd"], 0.75), out
|
|
|
|
|
|
def test_longest_prefix_match_wins_for_dated_pro_snapshot():
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5-pro-2026-04-23",
|
|
{"input_tokens": 1_000_000, "output_tokens": 0},
|
|
)
|
|
assert out["priced"] is True
|
|
# gpt-5.5-pro = 30/MTok vs gpt-5.5 = 5/MTok; longest wins.
|
|
assert _isclose(out["input_usd"], 30.0), out
|
|
|
|
|
|
# ── accept both chat-style and Responses envelope shapes. ──
|
|
|
|
|
|
def test_openai_chat_style_usage_keys_priced_correctly():
|
|
"""Chat-style envelope (`prompt_tokens`/`completion_tokens`) must
|
|
produce a non-zero cost (previously silently zeroed)."""
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4-mini",
|
|
{"prompt_tokens": 1_000_000, "completion_tokens": 1_000_000},
|
|
)
|
|
# gpt-5.4-mini: 0.75 input + 4.5 output per MTok.
|
|
assert _isclose(out["input_usd"], 0.75), out
|
|
assert _isclose(out["output_usd"], 4.5), out
|
|
|
|
|
|
def test_input_tokens_preferred_when_both_keys_present():
|
|
"""Raw key wins when both envelope shapes are present."""
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-5.4-mini",
|
|
{
|
|
"input_tokens": 2_000_000,
|
|
"prompt_tokens": 5_000_000,
|
|
"output_tokens": 0,
|
|
},
|
|
)
|
|
# input_tokens=2M wins -> 2 * 0.75 = 1.50.
|
|
assert _isclose(out["input_usd"], 1.50), out
|
|
|
|
|
|
def test_anthropic_chat_style_prompt_tokens_dedupes_cache_buckets():
|
|
"""Anthropic chat-style prompt_tokens already folds cache buckets;
|
|
don't double-count billable input."""
|
|
# 1M uncached + 200K cache_creation + 500K cache_read -> 1.7M folded.
|
|
raw = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"input_tokens": 1_000_000,
|
|
"cache_creation_input_tokens": 200_000,
|
|
"cache_read_input_tokens": 500_000,
|
|
"output_tokens": 0,
|
|
},
|
|
)
|
|
chat = calculate_cost(
|
|
"anthropic",
|
|
"claude-opus-4-7",
|
|
{
|
|
"prompt_tokens": 1_700_000,
|
|
"cache_creation_input_tokens": 200_000,
|
|
"cache_read_input_tokens": 500_000,
|
|
"completion_tokens": 0,
|
|
},
|
|
)
|
|
# Both envelopes must price the same.
|
|
assert _isclose(chat["input_usd"], raw["input_usd"]), (chat, raw)
|
|
assert _isclose(chat["cache_write_usd"], raw["cache_write_usd"]), (chat, raw)
|
|
assert _isclose(chat["cache_read_usd"], raw["cache_read_usd"]), (chat, raw)
|
|
assert _isclose(chat["total_usd"], raw["total_usd"]), (chat, raw)
|
|
assert chat["billable_input_tokens"] == raw["billable_input_tokens"], (chat, raw)
|
|
|
|
|
|
def test_openai_chat_style_prompt_tokens_keeps_cache_read_semantics():
|
|
"""OpenAI prompt_tokens includes cache_read like raw input_tokens."""
|
|
raw = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"input_tokens": 1_000_000,
|
|
"input_tokens_details": {"cached_tokens": 200_000},
|
|
"output_tokens": 100_000,
|
|
},
|
|
)
|
|
chat = calculate_cost(
|
|
"openai",
|
|
"gpt-5.5",
|
|
{
|
|
"prompt_tokens": 1_000_000,
|
|
"cache_read_input_tokens": 200_000,
|
|
"completion_tokens": 100_000,
|
|
},
|
|
)
|
|
assert _isclose(chat["total_usd"], raw["total_usd"]), (chat, raw)
|
|
|
|
|
|
def test_openai_chat_style_envelope_reads_cache_from_prompt_tokens_details():
|
|
"""Chat-style envelope ships cached under prompt_tokens_details;
|
|
calculator must honour both that and input_tokens_details."""
|
|
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)
|
|
# 80k 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():
|
|
"""Explicit ``output_tokens: 0`` beats a stale ``completion_tokens``;
|
|
the previous `or` fallback treated 0 as missing."""
|
|
out = calculate_cost(
|
|
"openai",
|
|
"gpt-4o-mini",
|
|
{
|
|
"input_tokens": 100,
|
|
"output_tokens": 0,
|
|
# Stale chat-style mirror; must not bill against it.
|
|
"completion_tokens": 50,
|
|
},
|
|
)
|
|
assert out["billable_output_tokens"] == 0, out
|
|
assert out["output_usd"] == 0.0, out
|